feat: Implement LDAP bind/whoami
This commit is contained in:
parent
bdb5008914
commit
b4330b37fa
19 changed files with 664 additions and 37 deletions
|
|
@ -26,11 +26,13 @@ impl<D: DatabaseInterface> Database<D> {
|
|||
/// TODO: we may want to make sure the method runs in constant time
|
||||
/// to avoid leaking information about existing users...
|
||||
/// or maybe we do not care.
|
||||
async fn check_password(&self, user: &UserRef, password: &str) -> bool {
|
||||
pub async fn check_password(&self, user: &UserRef, password: &str) -> bool {
|
||||
let Some(user) = self.get_user(user).await else {
|
||||
tracing::debug!("check_password: User not found {user}");
|
||||
return false;
|
||||
};
|
||||
|
||||
tracing::debug!("Comparing {} and {}", user.password, password);
|
||||
user.password == password
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::db::error::*;
|
||||
use crate::db::error::UserAlreadyExists;
|
||||
use crate::db::{Database, User, UserRef};
|
||||
|
||||
impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::future::{Future, ready};
|
||||
|
||||
use crate::db::error::*;
|
||||
use crate::db::error::UserAlreadyExists;
|
||||
use crate::db::{Database, DatabaseInterface, Group, User, UserRef};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
|
|||
35
src/ldap/client_state.rs
Normal file
35
src/ldap/client_state.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::ldap::Dn;
|
||||
|
||||
/// Whether a client is successfully logged in (bound).
|
||||
///
|
||||
/// As it is, this is currently only used for the whoami operation.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LdapClientState {
|
||||
Unbound,
|
||||
Bound(Dn),
|
||||
}
|
||||
|
||||
impl LdapClientState {
|
||||
pub fn new() -> Self {
|
||||
Self::Unbound
|
||||
}
|
||||
|
||||
pub fn unbind(&mut self) {
|
||||
*self = Self::Unbound;
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, dn: Dn) {
|
||||
*self = Self::Bound(dn);
|
||||
}
|
||||
|
||||
pub fn bound_dn(&self) -> Option<&Dn> {
|
||||
match self {
|
||||
Self::Unbound => None,
|
||||
Self::Bound(dn) => Some(dn),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bound_dn_string(&self) -> String {
|
||||
self.bound_dn().map_or(String::new(), Dn::to_dn_string)
|
||||
}
|
||||
}
|
||||
222
src/ldap/dn.rs
Normal file
222
src/ldap/dn.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
use dn_escape::dn_escape;
|
||||
use ldap3_proto::LdapResultCode;
|
||||
|
||||
use crate::db::UserRef;
|
||||
use crate::ldap::LdapReturnError;
|
||||
|
||||
/// A simple multi-value Map powered by a vector, to respect
|
||||
/// order of first appearance of keys.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VecMap {
|
||||
inner: Vec<(String, Vec<String>)>,
|
||||
}
|
||||
|
||||
impl VecMap {
|
||||
pub fn new() -> Self {
|
||||
Self { inner: vec![] }
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
|
||||
for (prev_key, prev_values) in &self.inner {
|
||||
if key == prev_key {
|
||||
return Some(prev_values);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn insert_or_append(&mut self, key: &str, value: &str) {
|
||||
for (prev_key, prev_values) in &mut self.inner {
|
||||
if key == prev_key {
|
||||
prev_values.push(value.to_string());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.push((key.to_string(), vec![value.to_string()]));
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: &str, values: Vec<String>, overwrite: bool) {
|
||||
for (prev_key, prev_values) in &mut self.inner {
|
||||
if key == prev_key {
|
||||
if overwrite {
|
||||
*prev_values = values;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.push((key.to_string(), values));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InvalidDnError(pub String);
|
||||
|
||||
impl LdapReturnError for InvalidDnError {
|
||||
fn code(&self) -> LdapResultCode {
|
||||
LdapResultCode::InvalidDNSyntax
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
format!("Invalid dn: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NotUserDnError(pub String);
|
||||
|
||||
impl LdapReturnError for NotUserDnError {
|
||||
fn code(&self) -> LdapResultCode {
|
||||
LdapResultCode::InvalidDNSyntax
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
format!("Not a user dn containing uid/dc: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Dn is a key-value mapping which can contain the same key several times.
|
||||
///
|
||||
/// This implementation is not fully RFC compliant and will only parse simple DNs for
|
||||
/// basic attribute manipulation.
|
||||
///
|
||||
/// Keys are lowercased, but scrambled keys in a broken order will be reordered. For example,
|
||||
/// `dc=example,ou=people,dc=com` will become `dc=example,dc=com,ou=people`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Dn {
|
||||
pub keys: VecMap,
|
||||
}
|
||||
|
||||
impl Dn {
|
||||
/// Parse a DN string. Here parsing escaped characters is not critical, if a
|
||||
/// client sends us funny characters, the domain name simply won't match
|
||||
/// and their request won't go anywhere.
|
||||
///
|
||||
/// However, if we find a really funny request such as `dc=foo=bar`, then
|
||||
/// we return an error to the client.
|
||||
pub fn from_dn_str(input: &str) -> Result<Self, InvalidDnError> {
|
||||
let mut keys = VecMap::new();
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok(Self { keys });
|
||||
}
|
||||
|
||||
for query in input.split(',') {
|
||||
let mut query_parts = query.split('=');
|
||||
// Here we have key=val pairs
|
||||
if query_parts.clone().count() != 2 {
|
||||
// Bad request
|
||||
return Err(InvalidDnError(input.to_string()));
|
||||
}
|
||||
|
||||
let key = query_parts.next().unwrap();
|
||||
let val = query_parts.next().unwrap();
|
||||
keys.insert_or_append(key, val);
|
||||
}
|
||||
|
||||
Ok(Self { keys })
|
||||
}
|
||||
|
||||
pub fn to_dn_string(&self) -> String {
|
||||
let mut s = String::new();
|
||||
let mut first = true;
|
||||
for (key, values) in &self.keys.inner {
|
||||
for value in values {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
s.push(',');
|
||||
}
|
||||
s.push_str(key);
|
||||
s.push('=');
|
||||
s.push_str(value);
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
/// Gets the hostname defined in the `dc` fields of the DN.
|
||||
///
|
||||
/// For example, `dc=example,dc=com` becomes `Some(example.com)`.
|
||||
///
|
||||
/// The returned domain is not normalized and may require casing treatment
|
||||
/// to compare meaningfully.
|
||||
pub fn get_hostname(&self) -> Option<String> {
|
||||
let domain_components = self.keys.get("dc")?;
|
||||
|
||||
// We don't populate the dc key if there was no value at all, so
|
||||
// we have at least one component.
|
||||
let mut domain_components = domain_components.iter();
|
||||
let mut s = String::from(domain_components.next().unwrap());
|
||||
for domain_component in domain_components {
|
||||
s.push('.');
|
||||
s.push_str(domain_component);
|
||||
}
|
||||
|
||||
Some(s)
|
||||
}
|
||||
|
||||
/// Overrides the DN hostname (`dc` fields) with the provided host.
|
||||
///
|
||||
/// When no `dc` fields are present, they are only added when `force` is true.
|
||||
pub fn set_hostname(&mut self, host: &str, force: bool) {
|
||||
let domain_components: Vec<String> =
|
||||
host.split('.').map(|x| dn_escape(x).to_string()).collect();
|
||||
|
||||
self.keys.insert("dc", domain_components, force);
|
||||
}
|
||||
|
||||
pub fn to_user_ref(&self) -> Result<UserRef, NotUserDnError> {
|
||||
let Some(username_vals) = self.keys.get("uid") else {
|
||||
return Err(NotUserDnError(self.to_dn_string()));
|
||||
};
|
||||
if username_vals.len() != 1 {
|
||||
return Err(NotUserDnError(self.to_dn_string()));
|
||||
}
|
||||
let username = username_vals[0].clone();
|
||||
|
||||
let Some(domain_vals) = self.keys.get("dc") else {
|
||||
return Err(NotUserDnError(self.to_dn_string()));
|
||||
};
|
||||
let domain = domain_vals.join(".");
|
||||
|
||||
Ok(UserRef { username, domain })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic_dn_str() {
|
||||
let s = "cn=admin,ou=people,dc=example,dc=com";
|
||||
let mut dn = Dn::from_dn_str(s).unwrap();
|
||||
|
||||
assert_eq!(&dn.to_dn_string(), s);
|
||||
assert_eq!(dn.get_hostname().as_deref(), Some("example.com"));
|
||||
dn.set_hostname("a.localhost", false);
|
||||
assert_eq!(dn.get_hostname().as_deref(), Some("a.localhost"));
|
||||
assert_eq!(&dn.to_dn_string(), "cn=admin,ou=people,dc=a,dc=localhost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_dn_str() {
|
||||
let s = "";
|
||||
let mut dn = Dn::from_dn_str(s).unwrap();
|
||||
|
||||
assert_eq!(&dn.to_dn_string(), s);
|
||||
assert!(dn.get_hostname().is_none());
|
||||
|
||||
dn.set_hostname("a.localhost", false);
|
||||
assert_eq!(dn.get_hostname().as_deref(), None);
|
||||
assert_eq!(&dn.to_dn_string(), s);
|
||||
|
||||
dn.set_hostname("a.localhost", true);
|
||||
assert_eq!(dn.get_hostname().as_deref(), Some("a.localhost"));
|
||||
assert_eq!(&dn.to_dn_string(), "dc=a,dc=localhost");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,35 @@
|
|||
use ldap3_proto::LdapMsg;
|
||||
use ldap3_proto::proto::LdapOp;
|
||||
|
||||
use crate::db::{Database, DatabaseInterface};
|
||||
use crate::ldap::{LdapStream, LdapStreamError};
|
||||
use crate::ldap::{LdapClientState, LdapStream, LdapStreamError, op_bind, op_ext};
|
||||
|
||||
#[tracing::instrument(name = "ldap", skip(s, db), fields(session = %s.session))]
|
||||
pub async fn ldap_handler<D: DatabaseInterface>(mut s: LdapStream, db: Database<D>) {
|
||||
#[tracing::instrument(name = "ldap", skip(stream, db), fields(session = %stream.session))]
|
||||
pub async fn ldap_handler<D: DatabaseInterface>(mut stream: LdapStream, mut db: Database<D>) {
|
||||
tracing::info! {
|
||||
remote_addr = ?s.remote_addr,
|
||||
remote_addr = ?stream.remote_addr,
|
||||
"New client connection"
|
||||
};
|
||||
|
||||
let mut state = LdapClientState::new();
|
||||
|
||||
loop {
|
||||
match s.next().await {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = ldap_handler_inner(msg).await {
|
||||
match stream.next().await {
|
||||
Ok(msg) => match ldap_handler_inner(&mut stream, msg, &mut state, &mut db).await {
|
||||
Ok(should_keep_alive) => {
|
||||
if !should_keep_alive {
|
||||
tracing::debug!("Finished connection");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
reason = ?e,
|
||||
"Failed to respond"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
reason = ?e,
|
||||
|
|
@ -32,7 +41,53 @@ pub async fn ldap_handler<D: DatabaseInterface>(mut s: LdapStream, db: Database<
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn ldap_handler_inner(msg: LdapMsg) -> Result<(), LdapStreamError> {
|
||||
/// Return true to keep the connection going, false to close it.
|
||||
#[tracing::instrument(name = "ldap-handler", skip(client_state, db, stream))]
|
||||
pub async fn ldap_handler_inner<D: DatabaseInterface>(
|
||||
stream: &mut LdapStream,
|
||||
msg: LdapMsg,
|
||||
client_state: &mut LdapClientState,
|
||||
db: &mut Database<D>,
|
||||
) -> Result<bool, LdapStreamError> {
|
||||
tracing::debug!(msg = ?msg, "Received LDAP message");
|
||||
Ok(())
|
||||
match msg {
|
||||
// Disconnect
|
||||
LdapMsg {
|
||||
msgid: _,
|
||||
op: LdapOp::UnbindRequest,
|
||||
ctrl: _,
|
||||
} => {
|
||||
client_state.unbind();
|
||||
// TODO: keep the connection open?
|
||||
Ok(true)
|
||||
}
|
||||
LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::ExtendedRequest(ler),
|
||||
ctrl: _,
|
||||
} => {
|
||||
op_ext(stream, ler, msgid, client_state).await?;
|
||||
Ok(true)
|
||||
}
|
||||
LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::BindRequest(lbr),
|
||||
ctrl: _,
|
||||
} => {
|
||||
if let Some(bound_dn) = op_bind(stream, db, lbr, msgid).await? {
|
||||
tracing::debug!("Successful bind");
|
||||
client_state.bind(bound_dn);
|
||||
Ok(true)
|
||||
} else {
|
||||
tracing::debug!("Unsuccessful bind");
|
||||
// TODO: abort connection here?
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
// Unsupported message
|
||||
_ => {
|
||||
tracing::warn!("Unsupported client message, closing connection");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
mod client_state;
|
||||
pub use client_state::LdapClientState;
|
||||
mod dn;
|
||||
pub use dn::{Dn, InvalidDnError, NotUserDnError};
|
||||
mod handler;
|
||||
mod op;
|
||||
pub use handler::ldap_handler;
|
||||
pub use op::bind::op_bind;
|
||||
pub use op::ext::op_ext;
|
||||
mod return_error;
|
||||
pub use return_error::LdapReturnError;
|
||||
mod stream;
|
||||
pub use stream::{LdapStream, LdapStreamError};
|
||||
pub use stream::LdapStream;
|
||||
mod stream_error;
|
||||
pub use stream_error::LdapStreamError;
|
||||
|
|
|
|||
129
src/ldap/op/bind.rs
Normal file
129
src/ldap/op/bind.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapBindResponse, LdapOp, LdapResult};
|
||||
use ldap3_proto::{LdapMsg, LdapResultCode};
|
||||
|
||||
use crate::db::{Database, DatabaseInterface};
|
||||
use crate::ldap::{
|
||||
Dn, InvalidDnError, LdapReturnError, LdapStream, LdapStreamError, NotUserDnError,
|
||||
};
|
||||
|
||||
pub enum BindError {
|
||||
// TODO: DB Error
|
||||
InvalidCredentials,
|
||||
InvalidDn(InvalidDnError),
|
||||
NotUserDn(NotUserDnError),
|
||||
UnsupportedSASL,
|
||||
}
|
||||
|
||||
impl BindError {
|
||||
pub async fn error_message(
|
||||
&self,
|
||||
stream: &mut LdapStream,
|
||||
msgid: i32,
|
||||
) -> Result<(), LdapStreamError> {
|
||||
let resp_msg = LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::BindResponse(LdapBindResponse {
|
||||
res: LdapResult {
|
||||
code: self.code(),
|
||||
matcheddn: String::new(),
|
||||
message: self.message(),
|
||||
referral: vec![],
|
||||
},
|
||||
saslcreds: None,
|
||||
}),
|
||||
ctrl: vec![],
|
||||
};
|
||||
|
||||
stream.send(resp_msg).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidDnError> for BindError {
|
||||
fn from(e: InvalidDnError) -> Self {
|
||||
Self::InvalidDn(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl LdapReturnError for BindError {
|
||||
fn code(&self) -> LdapResultCode {
|
||||
match self {
|
||||
Self::InvalidCredentials => LdapResultCode::InvalidCredentials,
|
||||
Self::InvalidDn(e) => e.code(),
|
||||
Self::NotUserDn(e) => e.code(),
|
||||
Self::UnsupportedSASL => LdapResultCode::OperationsError,
|
||||
}
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
match self {
|
||||
Self::InvalidCredentials => "Wrong username or password".to_string(),
|
||||
Self::InvalidDn(e) => e.message(),
|
||||
Self::NotUserDn(e) => e.message(),
|
||||
Self::UnsupportedSASL => "SASL login is not supported".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn bind_success(stream: &mut LdapStream, msgid: i32) -> Result<(), LdapStreamError> {
|
||||
let resp_msg = LdapMsg {
|
||||
msgid,
|
||||
op: LdapOp::BindResponse(LdapBindResponse {
|
||||
res: LdapResult {
|
||||
code: LdapResultCode::Success,
|
||||
matcheddn: String::new(),
|
||||
message: String::new(),
|
||||
referral: vec![],
|
||||
},
|
||||
saslcreds: None,
|
||||
}),
|
||||
ctrl: vec![],
|
||||
};
|
||||
|
||||
stream.send(resp_msg).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to bind the user.
|
||||
///
|
||||
/// On success, returns `Ok(Some(bound_dn))`. `Ok(None)` means credentials failed,
|
||||
/// either because the account does not exist, or the password is wrong.
|
||||
pub async fn op_bind<D: DatabaseInterface>(
|
||||
stream: &mut LdapStream,
|
||||
db: &Database<D>,
|
||||
req: LdapBindRequest,
|
||||
msgid: i32,
|
||||
) -> Result<Option<Dn>, LdapStreamError> {
|
||||
let dn = match Dn::from_dn_str(&req.dn) {
|
||||
Ok(dn) => dn,
|
||||
Err(e) => {
|
||||
BindError::InvalidDn(e).error_message(stream, msgid).await?;
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let LdapBindCred::Simple(password) = req.cred else {
|
||||
BindError::UnsupportedSASL
|
||||
.error_message(stream, msgid)
|
||||
.await?;
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let user_ref = match dn.to_user_ref() {
|
||||
Ok(user_ref) => user_ref,
|
||||
Err(e) => {
|
||||
BindError::NotUserDn(e).error_message(stream, msgid).await?;
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
if db.check_password(&user_ref, &password).await {
|
||||
bind_success(stream, msgid).await?;
|
||||
Ok(Some(dn))
|
||||
} else {
|
||||
BindError::InvalidCredentials
|
||||
.error_message(stream, msgid)
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
43
src/ldap/op/ext.rs
Normal file
43
src/ldap/op/ext.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use ldap3_proto::proto::{LdapExtendedRequest, LdapExtendedResponse, LdapOp, LdapResult};
|
||||
use ldap3_proto::{LdapMsg, LdapResultCode};
|
||||
|
||||
use crate::ldap::{LdapClientState, LdapStream, LdapStreamError};
|
||||
|
||||
pub async fn op_ext(
|
||||
stream: &mut LdapStream,
|
||||
ler: LdapExtendedRequest,
|
||||
msgid: i32,
|
||||
client_state: &LdapClientState,
|
||||
) -> Result<(), LdapStreamError> {
|
||||
let response = match ler.name.as_str() {
|
||||
"1.3.6.1.4.1.4203.1.11.3" => LdapOp::ExtendedResponse(LdapExtendedResponse {
|
||||
res: LdapResult {
|
||||
code: LdapResultCode::Success,
|
||||
matcheddn: String::new(),
|
||||
message: String::new(),
|
||||
referral: vec![],
|
||||
},
|
||||
name: None,
|
||||
value: Some(Vec::from(client_state.bound_dn_string())),
|
||||
}),
|
||||
_ => LdapOp::ExtendedResponse(LdapExtendedResponse {
|
||||
res: LdapResult {
|
||||
code: LdapResultCode::OperationsError,
|
||||
matcheddn: String::new(),
|
||||
message: "Unsupported extended operation".to_string(),
|
||||
referral: vec![],
|
||||
},
|
||||
name: None,
|
||||
value: None,
|
||||
}),
|
||||
};
|
||||
|
||||
stream
|
||||
.send(LdapMsg {
|
||||
msgid,
|
||||
op: response,
|
||||
ctrl: vec![],
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
2
src/ldap/op/mod.rs
Normal file
2
src/ldap/op/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod bind;
|
||||
pub mod ext;
|
||||
6
src/ldap/return_error.rs
Normal file
6
src/ldap/return_error.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use ldap3_proto::LdapResultCode;
|
||||
|
||||
pub trait LdapReturnError {
|
||||
fn code(&self) -> LdapResultCode;
|
||||
fn message(&self) -> String;
|
||||
}
|
||||
|
|
@ -1,32 +1,12 @@
|
|||
use futures_util::StreamExt;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use ldap3_proto::{LdapCodec, LdapMsg};
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tokio_util::codec::Framed;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::ldap::LdapStreamError;
|
||||
use crate::stream::{AbstractSocketAddr, AbstractStreamKind};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LdapStreamError {
|
||||
ClientClosed,
|
||||
Timeout(Duration),
|
||||
LdapError(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for LdapStreamError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ClientClosed => write!(f, "Client closed the connection"),
|
||||
Self::Timeout(d) => write!(f, "Client timeout after {}ms", d.as_millis()),
|
||||
Self::LdapError(e) => write!(f, "Failed to parse LDAP message: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LdapStreamError {}
|
||||
|
||||
pub struct LdapStream {
|
||||
pub remote_addr: AbstractSocketAddr,
|
||||
pub inner: Framed<AbstractStreamKind, LdapCodec>,
|
||||
|
|
@ -66,4 +46,12 @@ impl LdapStream {
|
|||
// because of transient IO error, or any other error condition.
|
||||
msg.map_err(LdapStreamError::LdapError)
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, msg: LdapMsg) -> Result<(), LdapStreamError> {
|
||||
tracing::debug!(msg=?msg, "Sending to client");
|
||||
self.inner
|
||||
.send(msg)
|
||||
.await
|
||||
.map_err(LdapStreamError::ClientSend)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
src/ldap/stream_error.rs
Normal file
26
src/ldap/stream_error.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A non-normal condition to close the LDAP stream.
|
||||
///
|
||||
/// For an error result that does not abort the connection, see [`LdapReturnError`](crate::ldap::LdapReturnError).
|
||||
#[derive(Debug)]
|
||||
pub enum LdapStreamError {
|
||||
ClientClosed,
|
||||
Timeout(Duration),
|
||||
LdapError(std::io::Error),
|
||||
ClientSend(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for LdapStreamError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ClientClosed => write!(f, "Client closed the connection"),
|
||||
Self::Timeout(d) => write!(f, "Client timeout after {}ms", d.as_millis()),
|
||||
Self::LdapError(e) => write!(f, "Failed to parse LDAP message: {e:?}"),
|
||||
Self::ClientSend(e) => write!(f, "Failed to send message to the client: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LdapStreamError {}
|
||||
Loading…
Reference in a new issue