From 2697ae1e35c126d92828cfabbcb3cdd482a51b42 Mon Sep 17 00:00:00 2001 From: Mateusz Kondej Date: Tue, 6 Aug 2024 17:05:37 +0200 Subject: [PATCH 1/3] Add support for creating LDAP clients from existing `TcpStream` and `UnixStream`. That's allows to create LDAP client from file descriptor and handle connection (with ssl) in sandboxed process. --- examples/bind_sync.rs | 2 +- examples/bind_sync_from_exiting_tcp_stream.rs | 20 +++++++ src/conn.rs | 57 +++++++++++++++++-- src/sync.rs | 50 ++++++++++++++++ 4 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 examples/bind_sync_from_exiting_tcp_stream.rs diff --git a/examples/bind_sync.rs b/examples/bind_sync.rs index 782bac7..6df68d1 100644 --- a/examples/bind_sync.rs +++ b/examples/bind_sync.rs @@ -1,5 +1,5 @@ // Demonstrates synchronously connecting, binding to, -// and disconnectiong from the server. +// and disconnection from the server. use ldap3::result::Result; use ldap3::LdapConn; diff --git a/examples/bind_sync_from_exiting_tcp_stream.rs b/examples/bind_sync_from_exiting_tcp_stream.rs new file mode 100644 index 0000000..fdc111e --- /dev/null +++ b/examples/bind_sync_from_exiting_tcp_stream.rs @@ -0,0 +1,20 @@ +// Demonstrates synchronously connecting, binding to, +// and disconnection from the exiting tcp stream. + +use ldap3::result::Result; +use ldap3::{LdapConn, LdapConnSettings}; +use std::net::TcpStream; +use url::Url; + +fn main() -> Result<()> { + let stream = TcpStream::connect("localhost:2389")?; + + // ... go into capsicum/seccomp mode, so process is not able to open new file descriptors ... + + let url = Url::parse("ldap://localhost:2389").unwrap(); + let mut ldap = LdapConn::from_tcp_stream(stream, LdapConnSettings::new(), &url)?; + let _res = ldap + .simple_bind("cn=Manager,dc=example,dc=org", "secret")? + .success()?; + Ok(ldap.unbind()?) +} diff --git a/src/conn.rs b/src/conn.rs index 4bee54e..d171d64 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -396,6 +396,38 @@ impl LdapConnAsync { Self::from_url_with_settings(LdapConnSettings::new(), url).await } + /// Create a connection to an LDAP server from existing UnixStream. + #[cfg(unix)] + pub fn from_unix_stream(stream: UnixStream) -> (Self, Ldap) { + Self::conn_pair(ConnType::Unix(stream)) + } + + /// Create a connection to an LDAP server from existing UnixStream. + #[cfg(not(unix))] + pub fn from_unix_stream(stream: UnixStream) -> (Self, Ldap) { + unimplemented!("no Unix domain sockets on non-Unix platforms"); + } + + /// Create a connection to an LDAP server from existing TcpStream, + /// specified by an already parsed `Url`, using + /// `settings` to specify additional parameters. + pub async fn from_tcp_stream( + stream: TcpStream, + settings: LdapConnSettings, + url: &Url, + ) -> Result<(Self, Ldap)> { + let mut settings = settings; + let timeout = settings.conn_timeout.take(); + + let (scheme, hostname, _host_port) = Self::extract_scheme_host_port(url, &settings)?; + let conn_future = Self::new_tcp_stream(stream, scheme, hostname, settings); + if let Some(timeout) = timeout { + time::timeout(timeout, conn_future).await? + } else { + conn_future.await + } + } + #[cfg(unix)] async fn new_unix(url: &Url, _settings: LdapConnSettings) -> Result<(Self, Ldap)> { let path = url.host_str().unwrap_or(""); @@ -415,8 +447,10 @@ impl LdapConnAsync { unimplemented!("no Unix domain sockets on non-Unix platforms"); } - #[allow(unused_mut)] - async fn new_tcp(url: &Url, mut settings: LdapConnSettings) -> Result<(Self, Ldap)> { + fn extract_scheme_host_port<'a>( + url: &'a Url, + settings: &LdapConnSettings, + ) -> Result<(&'a str, &'a str, String)> { let mut port = 389; let scheme = match url.scheme() { s @ "ldap" => { @@ -428,7 +462,6 @@ impl LdapConnAsync { } #[cfg(any(feature = "tls-native", feature = "tls-rustls"))] s @ "ldaps" => { - settings = settings.set_starttls(false); port = 636; s } @@ -437,12 +470,26 @@ impl LdapConnAsync { if let Some(url_port) = url.port() { port = url_port; } - let (_hostname, host_port) = match url.host_str() { + let (hostname, host_port) = match url.host_str() { Some(h) if !h.is_empty() => (h, format!("{}:{}", h, port)), Some(h) if !h.is_empty() => ("localhost", format!("localhost:{}", port)), _ => panic!("unexpected None from url.host_str()"), }; + Ok((scheme, hostname, host_port)) + } + + async fn new_tcp(url: &Url, settings: LdapConnSettings) -> Result<(Self, Ldap)> { + let (scheme, hostname, host_port) = Self::extract_scheme_host_port(url, &settings)?; let stream = TcpStream::connect(host_port.as_str()).await?; + Self::new_tcp_stream(stream, scheme, hostname, settings).await + } + + async fn new_tcp_stream( + stream: TcpStream, + scheme: &str, + hostname: &str, + settings: LdapConnSettings, + ) -> Result<(Self, Ldap)> { let (mut conn, mut ldap) = Self::conn_pair(ConnType::Tcp(stream)); match scheme { "ldap" => (), @@ -465,7 +512,7 @@ impl LdapConnAsync { } let parts = conn.stream.into_parts(); let tls_stream = if let ConnType::Tcp(stream) = parts.io { - LdapConnAsync::create_tls_stream(settings, _hostname, stream).await? + LdapConnAsync::create_tls_stream(settings, hostname, stream).await? } else { panic!("underlying stream not TCP"); }; diff --git a/src/sync.rs b/src/sync.rs index 0e53cc0..9ec0c28 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,5 +1,8 @@ use std::collections::HashSet; use std::hash::Hash; +use std::net::TcpStream; +#[cfg(unix)] +use std::os::unix::net::UnixStream; use std::time::Duration; use crate::adapters::IntoAdapterVec; @@ -69,6 +72,53 @@ impl LdapConn { Ok(LdapConn { ldap, rt }) } + /// Create a connection to an LDAP server from existing UnixStream. + #[cfg(unix)] + pub fn from_unix_stream(stream: UnixStream) -> Result { + let stream = tokio::net::UnixStream::from_std(stream)?; + let rt = runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let ldap = rt.block_on(async move { + let (conn, ldap) = LdapConnAsync::from_unix_stream(stream); + super::drive!(conn); + ldap + }); + Ok(LdapConn { ldap, rt }) + } + + /// Create a connection to an LDAP server from existing UnixStream. + #[cfg(not(unix))] + pub fn from_unix_stream(stream: UnixStream) -> Result { + unimplemented!("no Unix domain sockets on non-Unix platforms"); + } + + /// Create a connection to an LDAP server from existing TcpStream, + /// specified by an already parsed `Url`, using + /// `settings` to specify additional parameters. + pub fn from_tcp_stream( + stream: TcpStream, + settings: LdapConnSettings, + url: &Url, + ) -> Result { + let rt = runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let ldap = rt.block_on(async move { + stream.set_nonblocking(true)?; + let stream = tokio::net::TcpStream::from_std(stream)?; + let (conn, ldap) = match LdapConnAsync::from_tcp_stream(stream, settings, url).await { + Ok((conn, ldap)) => (conn, ldap), + Err(e) => return Err(e), + }; + super::drive!(conn); + Ok(ldap) + })?; + Ok(LdapConn { ldap, rt }) + } + /// See [`Ldap::with_search_options()`](struct.Ldap.html#method.with_search_options). pub fn with_search_options(&mut self, opts: SearchOptions) -> &mut Self { self.ldap.search_opts = Some(opts); From d3147ff1d01f057473d97fdba0923a390f7e514f Mon Sep 17 00:00:00 2001 From: Ivan Nejgebauer Date: Wed, 23 Oct 2024 21:38:50 +0200 Subject: [PATCH 2/3] Revert previous commit Per issue #132 discussion: the approach in 2697ae complicates the API, the solution will be via LdapConnSettings, but I want to record the contribution since the basic idea is sound and useful. --- examples/bind_sync.rs | 2 +- examples/bind_sync_from_exiting_tcp_stream.rs | 20 ------- src/conn.rs | 57 ++----------------- src/sync.rs | 50 ---------------- 4 files changed, 6 insertions(+), 123 deletions(-) delete mode 100644 examples/bind_sync_from_exiting_tcp_stream.rs diff --git a/examples/bind_sync.rs b/examples/bind_sync.rs index 6df68d1..782bac7 100644 --- a/examples/bind_sync.rs +++ b/examples/bind_sync.rs @@ -1,5 +1,5 @@ // Demonstrates synchronously connecting, binding to, -// and disconnection from the server. +// and disconnectiong from the server. use ldap3::result::Result; use ldap3::LdapConn; diff --git a/examples/bind_sync_from_exiting_tcp_stream.rs b/examples/bind_sync_from_exiting_tcp_stream.rs deleted file mode 100644 index fdc111e..0000000 --- a/examples/bind_sync_from_exiting_tcp_stream.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Demonstrates synchronously connecting, binding to, -// and disconnection from the exiting tcp stream. - -use ldap3::result::Result; -use ldap3::{LdapConn, LdapConnSettings}; -use std::net::TcpStream; -use url::Url; - -fn main() -> Result<()> { - let stream = TcpStream::connect("localhost:2389")?; - - // ... go into capsicum/seccomp mode, so process is not able to open new file descriptors ... - - let url = Url::parse("ldap://localhost:2389").unwrap(); - let mut ldap = LdapConn::from_tcp_stream(stream, LdapConnSettings::new(), &url)?; - let _res = ldap - .simple_bind("cn=Manager,dc=example,dc=org", "secret")? - .success()?; - Ok(ldap.unbind()?) -} diff --git a/src/conn.rs b/src/conn.rs index d171d64..4bee54e 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -396,38 +396,6 @@ impl LdapConnAsync { Self::from_url_with_settings(LdapConnSettings::new(), url).await } - /// Create a connection to an LDAP server from existing UnixStream. - #[cfg(unix)] - pub fn from_unix_stream(stream: UnixStream) -> (Self, Ldap) { - Self::conn_pair(ConnType::Unix(stream)) - } - - /// Create a connection to an LDAP server from existing UnixStream. - #[cfg(not(unix))] - pub fn from_unix_stream(stream: UnixStream) -> (Self, Ldap) { - unimplemented!("no Unix domain sockets on non-Unix platforms"); - } - - /// Create a connection to an LDAP server from existing TcpStream, - /// specified by an already parsed `Url`, using - /// `settings` to specify additional parameters. - pub async fn from_tcp_stream( - stream: TcpStream, - settings: LdapConnSettings, - url: &Url, - ) -> Result<(Self, Ldap)> { - let mut settings = settings; - let timeout = settings.conn_timeout.take(); - - let (scheme, hostname, _host_port) = Self::extract_scheme_host_port(url, &settings)?; - let conn_future = Self::new_tcp_stream(stream, scheme, hostname, settings); - if let Some(timeout) = timeout { - time::timeout(timeout, conn_future).await? - } else { - conn_future.await - } - } - #[cfg(unix)] async fn new_unix(url: &Url, _settings: LdapConnSettings) -> Result<(Self, Ldap)> { let path = url.host_str().unwrap_or(""); @@ -447,10 +415,8 @@ impl LdapConnAsync { unimplemented!("no Unix domain sockets on non-Unix platforms"); } - fn extract_scheme_host_port<'a>( - url: &'a Url, - settings: &LdapConnSettings, - ) -> Result<(&'a str, &'a str, String)> { + #[allow(unused_mut)] + async fn new_tcp(url: &Url, mut settings: LdapConnSettings) -> Result<(Self, Ldap)> { let mut port = 389; let scheme = match url.scheme() { s @ "ldap" => { @@ -462,6 +428,7 @@ impl LdapConnAsync { } #[cfg(any(feature = "tls-native", feature = "tls-rustls"))] s @ "ldaps" => { + settings = settings.set_starttls(false); port = 636; s } @@ -470,26 +437,12 @@ impl LdapConnAsync { if let Some(url_port) = url.port() { port = url_port; } - let (hostname, host_port) = match url.host_str() { + let (_hostname, host_port) = match url.host_str() { Some(h) if !h.is_empty() => (h, format!("{}:{}", h, port)), Some(h) if !h.is_empty() => ("localhost", format!("localhost:{}", port)), _ => panic!("unexpected None from url.host_str()"), }; - Ok((scheme, hostname, host_port)) - } - - async fn new_tcp(url: &Url, settings: LdapConnSettings) -> Result<(Self, Ldap)> { - let (scheme, hostname, host_port) = Self::extract_scheme_host_port(url, &settings)?; let stream = TcpStream::connect(host_port.as_str()).await?; - Self::new_tcp_stream(stream, scheme, hostname, settings).await - } - - async fn new_tcp_stream( - stream: TcpStream, - scheme: &str, - hostname: &str, - settings: LdapConnSettings, - ) -> Result<(Self, Ldap)> { let (mut conn, mut ldap) = Self::conn_pair(ConnType::Tcp(stream)); match scheme { "ldap" => (), @@ -512,7 +465,7 @@ impl LdapConnAsync { } let parts = conn.stream.into_parts(); let tls_stream = if let ConnType::Tcp(stream) = parts.io { - LdapConnAsync::create_tls_stream(settings, hostname, stream).await? + LdapConnAsync::create_tls_stream(settings, _hostname, stream).await? } else { panic!("underlying stream not TCP"); }; diff --git a/src/sync.rs b/src/sync.rs index 9ec0c28..0e53cc0 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,8 +1,5 @@ use std::collections::HashSet; use std::hash::Hash; -use std::net::TcpStream; -#[cfg(unix)] -use std::os::unix::net::UnixStream; use std::time::Duration; use crate::adapters::IntoAdapterVec; @@ -72,53 +69,6 @@ impl LdapConn { Ok(LdapConn { ldap, rt }) } - /// Create a connection to an LDAP server from existing UnixStream. - #[cfg(unix)] - pub fn from_unix_stream(stream: UnixStream) -> Result { - let stream = tokio::net::UnixStream::from_std(stream)?; - let rt = runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - let ldap = rt.block_on(async move { - let (conn, ldap) = LdapConnAsync::from_unix_stream(stream); - super::drive!(conn); - ldap - }); - Ok(LdapConn { ldap, rt }) - } - - /// Create a connection to an LDAP server from existing UnixStream. - #[cfg(not(unix))] - pub fn from_unix_stream(stream: UnixStream) -> Result { - unimplemented!("no Unix domain sockets on non-Unix platforms"); - } - - /// Create a connection to an LDAP server from existing TcpStream, - /// specified by an already parsed `Url`, using - /// `settings` to specify additional parameters. - pub fn from_tcp_stream( - stream: TcpStream, - settings: LdapConnSettings, - url: &Url, - ) -> Result { - let rt = runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - let ldap = rt.block_on(async move { - stream.set_nonblocking(true)?; - let stream = tokio::net::TcpStream::from_std(stream)?; - let (conn, ldap) = match LdapConnAsync::from_tcp_stream(stream, settings, url).await { - Ok((conn, ldap)) => (conn, ldap), - Err(e) => return Err(e), - }; - super::drive!(conn); - Ok(ldap) - })?; - Ok(LdapConn { ldap, rt }) - } - /// See [`Ldap::with_search_options()`](struct.Ldap.html#method.with_search_options). pub fn with_search_options(&mut self, opts: SearchOptions) -> &mut Self { self.ldap.search_opts = Some(opts); From 504d45af36064995c51c24d2a2466afd2e1dcb30 Mon Sep 17 00:00:00 2001 From: Ivan Nejgebauer Date: Wed, 23 Oct 2024 23:22:34 +0200 Subject: [PATCH 3/3] Add support for opening a connection from an existing stream Rework of an earlier PR, so that a stdlib stream can be placed in LdapConnSettings and used to initialize the connection. To keep the settings cloneable, the stream-bearing enum is cloned to an invalid variant, since the streams themselves can't be cloned. The provided stream is automatically set to nonblocking mode. --- examples/bind_sync_tcp_stream.rs | 17 +++++++ src/conn.rs | 83 +++++++++++++++++++++++++++----- src/lib.rs | 2 +- src/result.rs | 4 ++ 4 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 examples/bind_sync_tcp_stream.rs diff --git a/examples/bind_sync_tcp_stream.rs b/examples/bind_sync_tcp_stream.rs new file mode 100644 index 0000000..4aa4e33 --- /dev/null +++ b/examples/bind_sync_tcp_stream.rs @@ -0,0 +1,17 @@ +// Demonstrates synchronously connecting, binding to, +// and disconnecting from the exiting tcp stream. + +use std::net::TcpStream; + +use ldap3::result::Result; +use ldap3::{LdapConn, LdapConnSettings, StdStream}; + +fn main() -> Result<()> { + let stream = TcpStream::connect("localhost:2389")?; + let settings = LdapConnSettings::new().set_std_stream(StdStream::Tcp(stream)); + let mut ldap = LdapConn::with_settings(settings, "ldap://localhost:2389")?; + let _res = ldap + .simple_bind("cn=Manager,dc=example,dc=org", "secret")? + .success()?; + Ok(ldap.unbind()?) +} diff --git a/src/conn.rs b/src/conn.rs index 4bee54e..80881d9 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -187,6 +187,29 @@ impl AsyncWrite for ConnType { } } +/// Existing stream from which a connection can be created. +/// +/// A connection may be created from a previously opened TCP or Unix +/// stream (the latter only if Unix domain sockets are supported) by +/// placing an instance of this structure in `LdapConnSettings`. +/// +/// Since the stdlib streams can't be cloned, and `LdapConnSettings` +/// derives `Clone`, cloning the enum will produce the `Invalid` +/// variant. Thus, the settings should not be cloned if they +/// contain an existing stream. +pub enum StdStream { + Tcp(std::net::TcpStream), + #[cfg(unix)] + Unix(std::os::unix::net::UnixStream), + Invalid, +} + +impl Clone for StdStream { + fn clone(&self) -> StdStream { + StdStream::Invalid + } +} + /// Additional settings for an LDAP connection. /// /// The structure is opaque for better extensibility. An instance with @@ -204,6 +227,7 @@ pub struct LdapConnSettings { starttls: bool, #[cfg(any(feature = "tls-native", feature = "tls-rustls"))] no_tls_verify: bool, + std_stream: Option, } impl LdapConnSettings { @@ -272,6 +296,21 @@ impl LdapConnSettings { self.no_tls_verify = no_tls_verify; self } + + /// Create an LDAP connection using a previously opened standard library + /// stream (TCP or Unix, if applicable.) The full URL must still be provided + /// in order to select connection details, such as TLS establishment or + /// Unix domain socket operation. + /// + /// For Unix streams, the URL can be __ldapi:///__, since the path won't + /// be used. + /// + /// If the provided stream doesn't match the URL (e.g., a Unix stream is + /// given with the __ldap__ or __ldaps__ URL), an error will be returned. + pub fn set_std_stream(mut self, stream: StdStream) -> Self { + self.std_stream = Some(stream); + self + } } enum LoopMode { @@ -397,16 +436,27 @@ impl LdapConnAsync { } #[cfg(unix)] - async fn new_unix(url: &Url, _settings: LdapConnSettings) -> Result<(Self, Ldap)> { - let path = url.host_str().unwrap_or(""); - if path.is_empty() { - return Err(LdapError::EmptyUnixPath); - } - if path.contains(':') { - return Err(LdapError::PortInUnixPath); - } - let dec_path = percent_decode(path.as_bytes()).decode_utf8_lossy(); - let stream = UnixStream::connect(dec_path.as_ref()).await?; + async fn new_unix(url: &Url, settings: LdapConnSettings) -> Result<(Self, Ldap)> { + let stream = match settings.std_stream { + None => { + let path = url.host_str().unwrap_or(""); + if path.is_empty() { + return Err(LdapError::EmptyUnixPath); + } + if path.contains(':') { + return Err(LdapError::PortInUnixPath); + } + let dec_path = percent_decode(path.as_bytes()).decode_utf8_lossy(); + UnixStream::connect(dec_path.as_ref()).await? + } + Some(StdStream::Unix(stream)) => { + stream.set_nonblocking(true)?; + UnixStream::from_std(stream)? + } + Some(StdStream::Tcp(_)) | Some(StdStream::Invalid) => { + return Err(LdapError::MismatchedStreamType) + } + }; Ok(Self::conn_pair(ConnType::Unix(stream))) } @@ -442,7 +492,18 @@ impl LdapConnAsync { Some(h) if !h.is_empty() => ("localhost", format!("localhost:{}", port)), _ => panic!("unexpected None from url.host_str()"), }; - let stream = TcpStream::connect(host_port.as_str()).await?; + let stream = match settings.std_stream { + None => TcpStream::connect(host_port.as_str()).await?, + Some(StdStream::Tcp(_)) => { + let stream = match settings.std_stream.take().expect("StdStream") { + StdStream::Tcp(stream) => stream, + _ => panic!("non-tcp stream in enum"), + }; + stream.set_nonblocking(true)?; + TcpStream::from_std(stream)? + } + Some(_) => return Err(LdapError::MismatchedStreamType), + }; let (mut conn, mut ldap) = Self::conn_pair(ConnType::Tcp(stream)); match scheme { "ldap" => (), diff --git a/src/lib.rs b/src/lib.rs index 2075710..79d850e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,7 +228,7 @@ mod search; mod sync; mod util; -pub use conn::{LdapConnAsync, LdapConnSettings}; +pub use conn::{LdapConnAsync, LdapConnSettings, StdStream}; pub use filter::parse as parse_filter; pub use ldap::{Ldap, Mod}; pub use result::{LdapError, LdapResult, SearchResult}; diff --git a/src/result.rs b/src/result.rs index 4709742..8833a5c 100644 --- a/src/result.rs +++ b/src/result.rs @@ -42,6 +42,10 @@ pub enum LdapError { #[error("the port must be empty in the ldapi scheme")] PortInUnixPath, + /// The existing stream in `LdapConnectionSettings` doesn't match the URL. + #[error("the stream type in LdapConnSettings does not match the URL")] + MismatchedStreamType, + /// Encapsulated I/O error. #[error("I/O error: {source}")] Io {