diff --git a/Cargo.lock b/Cargo.lock index 1ab38d7..696a646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,10 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "dn_escape" +version = "0.1.0" + [[package]] name = "futures-core" version = "0.3.34" @@ -100,6 +104,7 @@ checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", + "futures-sink", "futures-task", "pin-project-lite", "slab", @@ -172,6 +177,7 @@ version = "0.1.0" dependencies = [ "argh", "camino", + "dn_escape", "futures-util", "ldap3_proto", "serde", diff --git a/Cargo.toml b/Cargo.toml index b44a4b7..0ea53c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,8 @@ edition = "2024" [dependencies] argh = "0.1.19" camino = "1.2.5" -futures-util = "0.3.34" +dn_escape = { path = "vendor/dn_escape" } +futures-util = { version = "0.3.34", features = ["sink"] } ldap3_proto = "0.8.1" serde = { version = "1.0.229", features = ["derive"] } tokio = { version = "1.53.1", features = ["macros", "net", "rt", "time", "sync"] } diff --git a/src/db/common.rs b/src/db/common.rs index fd6d16a..094b0e5 100644 --- a/src/db/common.rs +++ b/src/db/common.rs @@ -26,11 +26,13 @@ impl Database { /// 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 } } diff --git a/src/db/interface.rs b/src/db/interface.rs index ef4517b..3bcd7a3 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -1,4 +1,4 @@ -use crate::db::error::*; +use crate::db::error::UserAlreadyExists; use crate::db::{Database, User, UserRef}; impl DatabaseInterface for Database { diff --git a/src/db/memory.rs b/src/db/memory.rs index f151a8e..cc88068 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -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)] diff --git a/src/ldap/client_state.rs b/src/ldap/client_state.rs new file mode 100644 index 0000000..6ee2d21 --- /dev/null +++ b/src/ldap/client_state.rs @@ -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) + } +} diff --git a/src/ldap/dn.rs b/src/ldap/dn.rs new file mode 100644 index 0000000..d431572 --- /dev/null +++ b/src/ldap/dn.rs @@ -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)>, +} + +impl VecMap { + pub fn new() -> Self { + Self { inner: vec![] } + } + + pub fn get(&self, key: &str) -> Option<&Vec> { + 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, 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 { + 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 { + 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 = + host.split('.').map(|x| dn_escape(x).to_string()).collect(); + + self.keys.insert("dc", domain_components, force); + } + + pub fn to_user_ref(&self) -> Result { + 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"); + } +} diff --git a/src/ldap/handler.rs b/src/ldap/handler.rs index d8bd7cc..e1a8710 100644 --- a/src/ldap/handler.rs +++ b/src/ldap/handler.rs @@ -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(mut s: LdapStream, db: Database) { +#[tracing::instrument(name = "ldap", skip(stream, db), fields(session = %stream.session))] +pub async fn ldap_handler(mut stream: LdapStream, mut db: Database) { 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(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( + stream: &mut LdapStream, + msg: LdapMsg, + client_state: &mut LdapClientState, + db: &mut Database, +) -> Result { 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) + } + } } diff --git a/src/ldap/mod.rs b/src/ldap/mod.rs index a829da5..c26dcf9 100644 --- a/src/ldap/mod.rs +++ b/src/ldap/mod.rs @@ -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; diff --git a/src/ldap/op/bind.rs b/src/ldap/op/bind.rs new file mode 100644 index 0000000..ee5464d --- /dev/null +++ b/src/ldap/op/bind.rs @@ -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 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( + stream: &mut LdapStream, + db: &Database, + req: LdapBindRequest, + msgid: i32, +) -> Result, 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) + } +} diff --git a/src/ldap/op/ext.rs b/src/ldap/op/ext.rs new file mode 100644 index 0000000..908030d --- /dev/null +++ b/src/ldap/op/ext.rs @@ -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(()) +} diff --git a/src/ldap/op/mod.rs b/src/ldap/op/mod.rs new file mode 100644 index 0000000..07acdea --- /dev/null +++ b/src/ldap/op/mod.rs @@ -0,0 +1,2 @@ +pub mod bind; +pub mod ext; diff --git a/src/ldap/return_error.rs b/src/ldap/return_error.rs new file mode 100644 index 0000000..b8e6f3e --- /dev/null +++ b/src/ldap/return_error.rs @@ -0,0 +1,6 @@ +use ldap3_proto::LdapResultCode; + +pub trait LdapReturnError { + fn code(&self) -> LdapResultCode; + fn message(&self) -> String; +} diff --git a/src/ldap/stream.rs b/src/ldap/stream.rs index 3975e81..0d4c94f 100644 --- a/src/ldap/stream.rs +++ b/src/ldap/stream.rs @@ -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, @@ -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) + } } diff --git a/src/ldap/stream_error.rs b/src/ldap/stream_error.rs new file mode 100644 index 0000000..12bb036 --- /dev/null +++ b/src/ldap/stream_error.rs @@ -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 {} diff --git a/vendor/dn_escape/Cargo.toml b/vendor/dn_escape/Cargo.toml new file mode 100644 index 0000000..9457b64 --- /dev/null +++ b/vendor/dn_escape/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "dn_escape" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/vendor/dn_escape/MIT_LICENSE b/vendor/dn_escape/MIT_LICENSE new file mode 100644 index 0000000..62db836 --- /dev/null +++ b/vendor/dn_escape/MIT_LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 Ivan Nejgebauer +Copyright (c) 2014-2017 Gregor Reitzenstein + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/dn_escape/README.md b/vendor/dn_escape/README.md new file mode 100644 index 0000000..1f9f216 --- /dev/null +++ b/vendor/dn_escape/README.md @@ -0,0 +1,4 @@ +# dn_escape + +Borrowed from the ldap3 project at version v0.12.1, under MIT license. A copy +of the license can be found in [MIT_LICENSE](MIT_LICENSE). diff --git a/vendor/dn_escape/src/lib.rs b/vendor/dn_escape/src/lib.rs new file mode 100644 index 0000000..58d37db --- /dev/null +++ b/vendor/dn_escape/src/lib.rs @@ -0,0 +1,69 @@ +// CODE DEVELOPED BY THE LDAP3 PROJECT, UNDER MIT LICENSE. + +use std::borrow::Cow; + +/// Escape an attribute value in a relative distinguished name (RDN). +/// +/// When a literal string is used to represent an attribute value in an RDN, +/// some of its characters might need to be escaped according to the rules +/// of [RFC 4514](https://tools.ietf.org/html/rfc4514). +/// +/// The function is named `dn_escape()` instead of `rdn_escape()` because of +/// a long-standing association of its intended use with the handling of DNs. +/// +/// The argument, `val`, can be owned or borrowed. The function doesn't +/// allocate the return value unless there's need to escape the input. +pub fn dn_escape<'a, S: Into>>(val: S) -> Cow<'a, str> { + #[inline] + fn always_escape(c: u8) -> bool { + c == b'"' + || c == b'+' + || c == b',' + || c == b';' + || c == b'<' + || c == b'=' + || c == b'>' + || c == b'\\' + || c == 0 + } + + #[inline] + fn escape_leading(c: u8) -> bool { + c == b' ' || c == b'#' + } + + #[inline] + fn escape_trailing(c: u8) -> bool { + c == b' ' + } + + #[inline] + fn xdigit(c: u8) -> u8 { + c + if c < 10 { b'0' } else { b'a' - 10 } + } + + let val = val.into(); + let mut output = None; + for (i, &c) in val.as_bytes().iter().enumerate() { + if always_escape(c) + || i == 0 && escape_leading(c) + || i + 1 == val.len() && escape_trailing(c) + { + if output.is_none() { + output = Some(Vec::with_capacity(val.len() + 12)); // guess: up to 4 escaped chars + output.as_mut().unwrap().extend(val[..i].as_bytes()); + } + let output = output.as_mut().unwrap(); + output.push(b'\\'); + output.push(xdigit(c >> 4)); + output.push(xdigit(c & 0xF)); + } else if let Some(ref mut output) = output { + output.push(c); + } + } + if let Some(output) = output { + Cow::Owned(String::from_utf8(output).expect("dn escaped")) + } else { + val + } +}