diff --git a/.gitignore b/.gitignore index a9d37c5..d4f917d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +*.swp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..23d4db7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: rust +sudo: false +rust: + - stable diff --git a/Cargo.toml b/Cargo.toml index 05a3495..fbdef95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "cldap" -version = "0.1.0" -authors = ["Ross Delinger "] +name = "openldap" +version = "1.2.2" +authors = ["Josh Leverette ", "Ross Delinger ", "Stephen Holsapple ", "Yong Wen Chua "] license = "MIT" readme = "README.md" -repository = "https://github.com/rossdylan/rust-cldap" -homepage = "https://github.com/rossdylan/rust-cldap" -description = "Rust bindings to the C ldap library." +repository = "https://github.com/coder543/rust-cldap" +homepage = "https://github.com/coder543/rust-cldap" +documentation = "https://docs.rs/openldap/" +description = "Straightforward Rust bindings to the C openldap library. This is a fork of cldap that has been methodically fixed, extended, and made to be more compliant with openldap. It should be relatively robust and production ready at this point. Not heavily maintained, but feel free to send PRs if you see something missing." [dependencies] libc = "0.2.10" diff --git a/README.md b/README.md index 23c9e24..60b1d6e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,73 @@ -cldap -===== +# openldap -Rust bindings for the C LDAP library. Provides a simple public API for querying -ldap. +Rust bindings for the native OpenLDAP library with a few convenient +abstractions for connecting, binding, configuring, and querying your LDAP +server. + +## usage + +Using openldap is as easy as the following. + +```rust +extern crate openldap; + +use openldap::*; +use openldap::errors::*; + +fn some_ldap_function(ldap_uri: &str, ldap_user: &str, ldap_pass: &str) -> Result<(), LDAPError> { + let ldap = RustLDAP::new(ldap_uri).unwrap(); + + ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, + &codes::versions::LDAP_VERSION3); + + ldap.set_option(codes::options::LDAP_OPT_X_TLS_REQUIRE_CERT, + &codes::options::LDAP_OPT_X_TLS_DEMAND); + + ldap.simple_bind(ldap_user, ldap_pass).unwrap(); + + // Returns a LDAPResponse, a.k.a. Vec>>. + let _ = ldap.simple_search("CN=Stephen,OU=People,DC=Earth", + codes::scopes::LDAP_SCOPE_BASE) + .unwrap(); + + Ok(()) +} + +fn main() { + let ldap_uri = "ldaps://localhost:636"; + let ldap_user = "user"; + let ldap_pass = "pass"; + some_ldap_function(ldap_uri, ldap_user, ldap_pass).unwrap(); +} +``` + +### Security + +You should use *start_tls* before calling bind to avoid sending credentials in plain text over an untrusted +network. See https://linux.die.net/man/3/ldap_start_tls_s for more information + +```rust +fn some_ldap_function(ldap_uri: &str, ldap_user: &str, ldap_pass: &str) -> Result<(), LDAPError> { + let ldap = RustLDAP::new(ldap_uri).unwrap(); + + ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, + &codes::versions::LDAP_VERSION3); + + ldap.set_option(codes::options::LDAP_OPT_X_TLS_REQUIRE_CERT, + &codes::options::LDAP_OPT_X_TLS_DEMAND); + ldap.set_option(openldap::codes::options::LDAP_OPT_X_TLS_NEWCTX, &0); + + ldap.start_tls(None, None); + + ldap.simple_bind(ldap_user, ldap_pass).unwrap(); + + Ok(()) +} + +``` +On failure, an `openldap::errors::LDAPError` will be returned that includes a detailed +message from the native OpenLDAP library. + +## contributing + +I'm happy to accept contributions. If you have work you want to be merged back into `master`, send me a pull request and I will be happy to look at it. I prefer changes which don't break the API, of course, but I'm willing to consider breaking changes. diff --git a/src/codes.rs b/src/codes.rs index 7d54fd4..3cd232c 100644 --- a/src/codes.rs +++ b/src/codes.rs @@ -1,3 +1,6 @@ +//! LDAP codes from transcribed from ldap.h. +//! + /// Re-export protocol result codes from ldap.h pub mod results { pub static LDAP_SUCCESS: i32 = 0x00; @@ -148,4 +151,17 @@ pub mod options { pub static LDAP_OPT_SECURITY_CONTEXT: i32 = 0x0099; pub static LDAP_OPT_API_EXTENSION_BASE: i32 = 0x4000; + + pub static LDAP_OPT_X_TLS_CACERTDIR: i32 = 0x6003; + pub static LDAP_OPT_X_TLS_CACERTFILE: i32 = 0x6002; + pub static LDAP_OPT_X_TLS_CERTFILE: i32 = 0x6004; + pub static LDAP_OPT_X_TLS_KEYFILE: i32 = 0x6005; + pub static LDAP_OPT_X_TLS_NEWCTX: i32 = 0x600f; + pub static LDAP_OPT_X_TLS_REQUIRE_CERT: i32 = 0x6006; + + pub static LDAP_OPT_X_TLS_NEVER: i32 = 0x0000; + pub static LDAP_OPT_X_TLS_HARD: i32 = 0x0001; + pub static LDAP_OPT_X_TLS_DEMAND: i32 = 0x0002; + pub static LDAP_OPT_X_TLS_ALLOW: i32 = 0x0003; + pub static LDAP_OPT_X_TLS_TRY: i32 = 0x0004; } diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..65cdb99 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,52 @@ +//! Errors and trait implementations. +//! +use std::fmt; +use std::error; +use std::convert; + + +/// A LDAP error. +/// +/// LDAP errors occur when an underlying function returns with an error code. Currently, there is +/// only one type of error raised: `LDAPError::NativeError`. A `LDAPError::NativeError` includes a +/// string field describing the error in more detail. +/// +/// A `LDAPError` implements necessary traits (i.e., `std::fmt::Display`, `std::error::Error`, and +/// `std::convert::From`) to do proper error handling using the `try!` macro. +/// +#[derive(Debug, PartialEq)] +pub enum LDAPError { + NativeError(String), +} + +impl fmt::Display for LDAPError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + LDAPError::NativeError(ref err) => write!(f, "LDAP error: {}", err), + } + } +} + +impl error::Error for LDAPError { + /// Get the description of this error. + /// + fn description(&self) -> &str { + match *self { + LDAPError::NativeError(ref err) => err, + } + } + + /// Get the cause of this error. + /// + /// Note, currently this method always return `None` as we do not know the root cause of the + /// error. + fn cause(&self) -> Option<&error::Error> { + None + } +} + +impl convert::From for LDAPError { + fn from(err: String) -> LDAPError { + LDAPError::NativeError(err) + } +} diff --git a/src/lib.rs b/src/lib.rs index ca32e5e..d0b1406 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,408 +1,723 @@ +//! Objects for connecting and querying LDAP servers using `OpenLDAP`. +//! +//! Current support includes connection, initializing, binding, configuring, and search against an +//! LDAP directory. +//! extern crate libc; use libc::{c_int, c_char, c_void, timeval}; -use std::ptr; -use std::ffi::{CStr, CString}; use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::ptr; use std::slice; +use std::boxed; +use std::ptr::null_mut; pub mod codes; +pub mod errors; -// Define the structs used by the ldap c library. #[repr(C)] struct LDAP; + #[repr(C)] struct LDAPMessage; + #[repr(C)] pub struct LDAPControl; + #[repr(C)] struct BerElement; +unsafe impl Sync for LDAP {} +unsafe impl Send for LDAP {} + #[link(name = "lber")] #[allow(improper_ctypes)] -extern { - fn ber_free(ber: *const BerElement, freebuf: c_int); +extern "C" { + fn ber_free(ber: *const BerElement, freebuf: c_int); } -#[link(name = "ldap")] +#[link(name = "ldap_r")] #[allow(improper_ctypes)] -extern { - fn ldap_initialize(ldap: *mut *mut LDAP, uri: *const c_char) -> c_int; - fn ldap_memfree(p: *mut c_void); - fn ldap_msgfree(msg: *mut LDAPMessage) -> c_int; - fn ldap_err2string(err: c_int) -> *const c_char; - fn ldap_first_entry(ldap: *mut LDAP, result: *mut LDAPMessage) -> *mut LDAPMessage; - fn ldap_next_entry(ldap: *mut LDAP, entry: *mut LDAPMessage) -> *mut LDAPMessage; - fn ldap_get_values(ldap: *mut LDAP, entry: *mut LDAPMessage, attr: *const c_char) -> *const *const c_char; - fn ldap_count_values(vals: *const *const c_char) -> c_int; - fn ldap_value_free(vals: *const *const c_char); - - fn ldap_simple_bind_s(ldap: *mut LDAP, who: *const c_char, pass: *const c_char) -> c_int; - fn ldap_first_attribute(ldap: *mut LDAP, entry: *mut LDAPMessage, berptr: *mut *mut BerElement) -> *const c_char; - fn ldap_next_attribute(ldap: *mut LDAP, entry: *mut LDAPMessage, berptr: *mut BerElement) -> *const c_char; - fn ldap_search_ext_s(ldap: *mut LDAP, base: *const c_char, scope: c_int, - filter: *const c_char, attrs: *const *const c_char, - attrsonly: c_int, serverctrls: *mut *mut LDAPControl, - clientctrls: *mut *mut LDAPControl, timeout: *mut timeval, - sizelimit: c_int, res: *mut *mut LDAPMessage) -> c_int; - - fn ldap_unbind_ext_s(ldap: *mut LDAP, sctrls: *mut *mut LDAPControl, cctrls: *mut *mut LDAPControl) -> c_int; +extern "C" { + static ber_pvt_opt_on: c_char; + fn ldap_initialize(ldap: *mut *mut LDAP, uri: *const c_char) -> c_int; + fn ldap_memfree(p: *mut c_void); + fn ldap_msgfree(msg: *mut LDAPMessage) -> c_int; + fn ldap_err2string(err: c_int) -> *const c_char; + fn ldap_first_entry(ldap: *mut LDAP, result: *mut LDAPMessage) -> *mut LDAPMessage; + fn ldap_next_entry(ldap: *mut LDAP, entry: *mut LDAPMessage) -> *mut LDAPMessage; + fn ldap_get_dn(ldap: *mut LDAP, entry: *mut LDAPMessage) -> *const c_char; + fn ldap_get_values(ldap: *mut LDAP, + entry: *mut LDAPMessage, + attr: *const c_char) + -> *const *const c_char; + fn ldap_count_values(vals: *const *const c_char) -> c_int; + fn ldap_value_free(vals: *const *const c_char); + fn ldap_set_option(ldap: *const LDAP, option: c_int, invalue: *const c_void) -> c_int; + fn ldap_simple_bind_s(ldap: *mut LDAP, who: *const c_char, pass: *const c_char) -> c_int; + fn ldap_first_attribute(ldap: *mut LDAP, + entry: *mut LDAPMessage, + berptr: *mut *mut BerElement) + -> *const c_char; + fn ldap_next_attribute(ldap: *mut LDAP, + entry: *mut LDAPMessage, + berptr: *mut BerElement) + -> *const c_char; + fn ldap_search_ext_s(ldap: *mut LDAP, + base: *const c_char, + scope: c_int, + filter: *const c_char, + attrs: *const *const c_char, + attrsonly: c_int, + serverctrls: *mut *mut LDAPControl, + clientctrls: *mut *mut LDAPControl, + timeout: *mut timeval, + sizelimit: c_int, + res: *mut *mut LDAPMessage) + -> c_int; + fn ldap_unbind_ext_s(ldap: *mut LDAP, + sctrls: *mut *mut LDAPControl, + cctrls: *mut *mut LDAPControl) + -> c_int; + fn ldap_start_tls_s(ldap: *mut LDAP, + scrtrls: *mut *mut LDAPControl, + cctrls: *mut *mut LDAPControl) -> c_int; } +/// A typedef for an `LDAPResponse` type. +/// +/// LDAP responses are organized as vectors of mached entities. Typically, each entity is +/// represented as a map of attributes to list of values. +/// +pub type LDAPResponse = Vec>>; + + +/// A high level abstraction over the raw `OpenLDAP` functions. +/// +/// A `RustLDAP` object hides raw `OpenLDAP` complexities and exposes a simple object that is +/// created, configured, and queried. Methods that call underlying `OpenLDAP` calls that can fail +/// will raise an `errors::LDAPError` with additional details. +/// +/// Using a `RustLDAP` object is easy! +/// pub struct RustLDAP { - // Have the raw pointer to it so we can pass it into internal functions - ldap_ptr: *mut LDAP + /// A pointer to the underlying `OpenLDAP` object. + ldap_ptr: *mut LDAP, } -impl Drop for RustLDAP { - fn drop(&mut self){ +unsafe impl Sync for RustLDAP {} +unsafe impl Send for RustLDAP {} - //unbind the LDAP connection, making the C library free the LDAP* - let rc = unsafe { ldap_unbind_ext_s(self.ldap_ptr, ptr::null_mut(), ptr::null_mut()) }; +impl Drop for RustLDAP { + fn drop(&mut self) { + // Unbind the LDAP connection, making the C library free the LDAP*. + let rc = unsafe { ldap_unbind_ext_s(self.ldap_ptr, ptr::null_mut(), ptr::null_mut()) }; + + // Make sure it actually happened. + if rc != codes::results::LDAP_SUCCESS { + unsafe { + // Hopefully this never happens. + let raw_estr = ldap_err2string(rc as c_int); + panic!(CStr::from_ptr(raw_estr).to_owned().into_string().unwrap()); + } + } + } +} - //make sure it actually happened - if rc != codes::results::LDAP_SUCCESS { - unsafe { //hopefully this never happens - let raw_estr = ldap_err2string(rc as c_int); - panic!(CStr::from_ptr(raw_estr).to_owned().into_string().unwrap()); - } +/// A trait for types that can be passed as LDAP option values. +/// +/// Underlying `OpenLDAP` implementation calls for option values to be passed in as `*const c_void`, +/// while allowing values to be `i32` or `String`. Using traits, we implement function overloading to +/// handle `i32` and `String` option value types. +/// +/// This trait allocates memory that a caller must free using `std::boxed::Box::from_raw`. This +/// helps guarantee that there is not a use after free bug (in Rust) while providing the appearance +/// of opaque memory to `OpenLDAP` (in C). In pure C, we would've accomplished this by casting a +/// local variable to a `const void *`. In Rust, we must do this on the heap to ensure Rust's +/// ownership system does not free the memory used to store the option value between now and when +/// the option is actually set. +/// +pub trait LDAPOptionValue { + fn as_cvoid_ptr(&self) -> *const c_void; +} - } +impl LDAPOptionValue for str { + fn as_cvoid_ptr(&self) -> *const c_void { + let string = CString::new(self).unwrap(); + string.into_raw() as *const c_void + } +} - } +impl LDAPOptionValue for i32 { + fn as_cvoid_ptr(&self) -> *const c_void { + let mem = boxed::Box::new(*self); + boxed::Box::into_raw(mem) as *const c_void + } +} +impl LDAPOptionValue for bool { + fn as_cvoid_ptr(&self) -> *const c_void { + if *self { + let mem = unsafe { boxed::Box::new(&ber_pvt_opt_on) }; + boxed::Box::into_raw(mem) as *const c_void + } else { + let mem = boxed::Box::new(0); + boxed::Box::into_raw(mem) as *const c_void + } + } } impl RustLDAP { - /// Create a new RustLDAP struct and use an ffi call to ldap_initialize to - /// allocate and init a C LDAP struct. All of that is hidden inside of RustLDAP. - pub fn new(uri: &str) -> Result { - - //attempt to convert the URI string into a C-string - let uri_cstring = CString::new(uri).unwrap(); - - //Create some space for the LDAP* - let mut cldap = ptr::null_mut(); - - unsafe { - //call ldap_initialize and check for errors - let res = ldap_initialize(&mut cldap, uri_cstring.as_ptr()); - if res != codes::results::LDAP_SUCCESS { - let raw_estr = ldap_err2string(res as c_int); - return Err(CStr::from_ptr(raw_estr).to_owned().into_string().unwrap()); - } - - } - - //create and return a new instance - let new_ldap = RustLDAP { ldap_ptr: cldap }; - return Ok(new_ldap); - } - - /// Perform a synchronos simple bind (ldap_simple_bind_s). The result is - /// either Ok(LDAP_SUCCESS) or Err(ldap_err2string). - pub fn simple_bind(&self, who: &str, pass: &str) -> Result { - - //convert arguments to C-strings - let who_cstr = CString::new(who).unwrap(); - let pass_cstr = CString::new(pass).unwrap(); - let who_ptr = who_cstr.as_ptr(); - let pass_ptr = pass_cstr.as_ptr(); - - //call ldap_bind and check for errors - unsafe { - let res = ldap_simple_bind_s(self.ldap_ptr, who_ptr, pass_ptr); - if res < 0 { - let raw_estr = ldap_err2string(res as c_int); - return Err(CStr::from_ptr(raw_estr).to_owned().into_string().unwrap()); - } - return Ok(res); - } - } - - /// Perform a simple search with only the base, returning all attributes found - pub fn simple_search(&self, base: &str, scope: i32) -> Result>>, String> { - return self.ldap_search(base, scope, None, None, false, None, None, ptr::null_mut(), -1); - } - - /// Expose a not very 'rust-y' api for ldap_search_ext_s. Ideally this will - /// be used mainly internally and a simpler api is exposed to users. - pub fn ldap_search(&self, base: &str, scope: i32, filter: Option<&str>, attrs: Option>, attrsonly: bool, - serverctrls: Option<*mut *mut LDAPControl>, clientctrls: Option<*mut *mut LDAPControl>, - timeout: *mut timeval, sizelimit: i32) -> Result>>, String> { - - //Make room for the LDAPMessage, being sure to delete this before we return - let mut ldap_msg = ptr::null_mut();; - - //Convert the passed in filter sting to either a C-string or null if one is not passed - let filter_cstr: CString; - let r_filter = match filter { - Some(fs) => { - filter_cstr = CString::new(fs).unwrap(); - filter_cstr.as_ptr() - }, - None => ptr::null() - }; - - //Convert the vec of attributes into the null-terminated array that the library expects - //We also copy the strings into C-strings - let mut r_attrs: *const *const c_char = ptr::null(); - let mut c_strs: Vec = Vec::new(); - let mut r_attrs_ptrs: Vec<*const c_char> = Vec::new(); - - if let Some(strs) = attrs { - for string in strs { - - //create new CString and take ownership of it in c_strs - c_strs.push(CString::new(string).unwrap()); - - //create a pointer to that CString's raw data and store it in r_attrs - r_attrs_ptrs.push(c_strs[c_strs.len() - 1].as_ptr()); - } - r_attrs_ptrs.push(ptr::null()); //ensure that there is a null value at the end of the vec - r_attrs = r_attrs_ptrs.as_ptr(); - } - - //PAss in either the controlls or a null if none are specified - let r_serverctrls = match serverctrls { - Some(sc) => sc, - None => ptr::null_mut() - }; - let r_clientctrls = match clientctrls { - Some(cc) => cc, - None => ptr::null_mut() - }; - - //Copy the search base into a C-string - let base = CString::new(base).unwrap(); - - //call into the C library and check for error - unsafe { - let res: i32 = ldap_search_ext_s(self.ldap_ptr, base.as_ptr(), scope as c_int, r_filter, r_attrs, attrsonly as c_int, - r_serverctrls, r_clientctrls, timeout, sizelimit as c_int, &mut ldap_msg); - if res != codes::results::LDAP_SUCCESS { - let raw_estr = ldap_err2string(res as c_int); - return Err(CStr::from_ptr(raw_estr).to_owned().into_string().unwrap()); - } - } - - //We now have to parse the results, copying the C-strings into Rust ones - //making sure to free the C-strings afterwards - let mut resvec: Vec>> = vec![]; - let mut entry = unsafe { ldap_first_entry(self.ldap_ptr, ldap_msg) }; - - while !entry.is_null() { - - //Make the map holding the attribute : value pairs - //as well as the BerElement that keeps track of what position we're in - let mut map: HashMap> = HashMap::new(); - let mut ber: *mut BerElement = ptr::null_mut(); - unsafe { - let mut attr: *const c_char = ldap_first_attribute(self.ldap_ptr, entry, &mut ber); - - while !attr.is_null() { - - //convert the attribute into a Rust string - let key = CStr::from_ptr(attr).to_owned().into_string().unwrap(); - - //get the attribute values from LDAP - let raw_vals: *const *const c_char = ldap_get_values(self.ldap_ptr, entry, attr); - let raw_vals_len = ldap_count_values(raw_vals) as usize; - let val_slice: &[*const c_char] = slice::from_raw_parts(raw_vals, raw_vals_len); - - //map these into a vec of Strings - let values: Vec = val_slice.iter().map(|ptr| { - CStr::from_ptr(*ptr).to_owned().into_string().unwrap() - }).collect(); - - //insert newly constructed Rust key-value strings - map.insert(key, values); - - //free the attr and value, then get next attr - ldap_value_free(raw_vals); - ldap_memfree(attr as *mut c_void); - attr = ldap_next_attribute(self.ldap_ptr, entry, ber) - - } - - //free the BerElement and advance to the next entry - ber_free(ber, 0); - entry = ldap_next_entry(self.ldap_ptr, entry); - - } - - //push this entry into the vec - resvec.push(map); - - } - - //make sure we free the message and return the parsed results - unsafe { ldap_msgfree(ldap_msg) }; - return Ok(resvec); - } + /// Create a new `RustLDAP`. + /// + /// Creates a new `RustLDAP` and initializes underlying `OpenLDAP` library. Upon creation, a + /// subsequent calls to `set_option` and `simple_bind` are possible. Before calling a search + /// related function, one must bind to the server by calling `simple_bind`. See module usage + /// information for more details on using a `RustLDAP` object. + /// + /// # Parameters + /// + /// * uri - URI of the LDAP server to connect to. E.g., ldaps://localhost:636. + /// + pub fn new(uri: &str) -> Result { + + // Create some space for the LDAP pointer. + let mut cldap = ptr::null_mut(); + + let uri_cstring = CString::new(uri).unwrap(); + + unsafe { + let res = ldap_initialize(&mut cldap, uri_cstring.as_ptr()); + if res != codes::results::LDAP_SUCCESS { + let raw_estr = ldap_err2string(res as c_int); + return Err(errors::LDAPError::NativeError(CStr::from_ptr(raw_estr) + .to_owned() + .into_string() + .unwrap())); + } + + } + + Ok(RustLDAP { ldap_ptr: cldap }) + } + + /// Sets an option on the LDAP connection. + /// + /// When setting an option to _ON_ or _OFF_ one may use the boolean values `true` or `false`, + /// respectively. + /// + /// # Parameters + /// + /// * option - An option identifier from `cldap::codes`. + /// * value - The value to set for the option. + /// + pub fn set_option(&self, option: i32, value: &T) -> bool { + let ptr: *const c_void = value.as_cvoid_ptr(); + unsafe { + let res: i32; + res = ldap_set_option(self.ldap_ptr, option, ptr); + // Allows for memory to be dropped when this binding goes away. + let _ = boxed::Box::from_raw(ptr as *mut c_void); + res == 0 + } + } + + /// Bind to the LDAP server. + /// + /// If you wish to configure options on the LDAP server, be sure to set required options using + ///`set_option` _before_ binding to the LDAP server. In some advanced cases, it may be required + /// to set multiple options for an option to be made available. Refer to the `OpenLDAP` + /// documentation for information on available options and how to use them. + /// + /// # Parameters + /// + /// * who - The user's name to bind with. + /// * pass - The user's password to bind with. + /// + pub fn simple_bind(&self, who: &str, pass: &str) -> Result { + let who_cstr = CString::new(who).unwrap(); + let pass_cstr = CString::new(pass).unwrap(); + let who_ptr = who_cstr.as_ptr(); + let pass_ptr = pass_cstr.as_ptr(); + unsafe { + let res = ldap_simple_bind_s(self.ldap_ptr, who_ptr, pass_ptr); + if res < 0 { + let raw_estr = ldap_err2string(res as c_int); + return Err(errors::LDAPError::NativeError(CStr::from_ptr(raw_estr) + .to_owned() + .into_string() + .unwrap())); + } + Ok(res) + } + } + + /// Simple synchronous search. + /// + /// Performs a simple search with only the base, returning all attributes found. + /// + /// # Parameters + /// + /// * base - The LDAP base. + /// * scope - The search scope. See `cldap::codes::scopes`. + /// + pub fn simple_search(&self, base: &str, scope: i32) -> Result { + self.ldap_search(base, + scope, + None, + None, + false, + None, + None, + ptr::null_mut(), + -1) + } + + /// Installs TLS handlers on the session + /// + /// # Examples + /// + /// ```should_panic + /// use openldap::RustLDAP; + /// let ldap = RustLDAP::new(&"ldaps://myserver:636").unwrap(); + /// + /// ldap.set_option( + /// openldap::codes::options::LDAP_OPT_PROTOCOL_VERSION, + /// &openldap::codes::versions::LDAP_VERSION3, + /// ); + /// + /// ldap.set_option( + /// openldap::codes::options::LDAP_OPT_X_TLS_REQUIRE_CERT, + /// &openldap::codes::options::LDAP_OPT_X_TLS_ALLOW, + /// ); + /// + /// ldap.set_option(openldap::codes::options::LDAP_OPT_X_TLS_NEWCTX, &0); + /// + /// ldap.start_tls(None, None); + /// ldap.simple_bind("some-dn", "some-password").unwrap(); + /// ``` + pub fn start_tls(&self, serverctrls: Option<*mut *mut LDAPControl>, clientctrls: Option<*mut *mut LDAPControl>) -> Result { + let r_serverctrls = match serverctrls { + Some(sc) => sc, + None => ptr::null_mut(), + }; + + let r_clientctrls = match clientctrls { + Some(cc) => cc, + None => ptr::null_mut(), + }; + + unsafe { + let res = ldap_start_tls_s(self.ldap_ptr, r_serverctrls, r_clientctrls); + + if res < 0 { + let raw_estr = ldap_err2string(res as c_int); + return Err(errors::LDAPError::NativeError(CStr::from_ptr(raw_estr) + .to_owned() + .into_string() + .unwrap())); + } + + Ok(res) + } + } + + /// Advanced synchronous search. + /// + /// Exposes a raw API around the underlying `ldap_search_ext_s` function from `OpenLDAP`. + /// Wherever possible, use provided wrappers. + /// + /// # Parameters + /// + /// * base - The base domain. + /// * scope - The search scope. See `cldap::codes::scopes`. + /// * filter - An optional filter. + /// * attrs - An optional set of attrs. + /// * attrsonly - True if should return only the attrs specified in `attrs`. + /// * serverctrls - Optional sever controls. + /// * clientctrls - Optional client controls. + /// * timeout - A timeout. + /// * sizelimit - The maximum number of entities to return, or -1 for no limit. + /// + pub fn ldap_search(&self, + base: &str, + scope: i32, + filter: Option<&str>, + attrs: Option>, + attrsonly: bool, + serverctrls: Option<*mut *mut LDAPControl>, + clientctrls: Option<*mut *mut LDAPControl>, + timeout: *mut timeval, + sizelimit: i32) + -> Result { + + // Make room for the LDAPMessage, being sure to delete this before we return. + let mut ldap_msg = ptr::null_mut(); + + // Convert the passed in filter sting to either a C-string or null if one is not passed. + let filter_cstr: CString; + let r_filter = match filter { + Some(fs) => { + filter_cstr = CString::new(fs).unwrap(); + filter_cstr.as_ptr() + } + None => ptr::null(), + }; + + // Convert the vec of attributes into the null-terminated array that the library expects. + let mut r_attrs: *const *const c_char = ptr::null(); + let mut c_strs: Vec = Vec::new(); + let mut r_attrs_ptrs: Vec<*const c_char> = Vec::new(); + + if let Some(strs) = attrs { + for string in strs { + // Create new CString and take ownership of it in c_strs. + c_strs.push(CString::new(string).unwrap()); + // Create a pointer to that CString's raw data and store it in r_attrs. + r_attrs_ptrs.push(c_strs[c_strs.len() - 1].as_ptr()); + } + // Ensure that there is a null value at the end of the vector. + r_attrs_ptrs.push(ptr::null()); + r_attrs = r_attrs_ptrs.as_ptr(); + } + + let r_serverctrls = match serverctrls { + Some(sc) => sc, + None => ptr::null_mut(), + }; + + let r_clientctrls = match clientctrls { + Some(cc) => cc, + None => ptr::null_mut(), + }; + + let base = CString::new(base).unwrap(); + + unsafe { + let res: i32 = ldap_search_ext_s(self.ldap_ptr, + base.as_ptr(), + scope as c_int, + r_filter, + r_attrs, + attrsonly as c_int, + r_serverctrls, + r_clientctrls, + timeout, + sizelimit as c_int, + &mut ldap_msg); + if res != codes::results::LDAP_SUCCESS { + let raw_estr = ldap_err2string(res as c_int); + return Err(errors::LDAPError::NativeError(CStr::from_ptr(raw_estr) + .to_owned() + .into_string() + .unwrap())); + } + } + + // We now have to parse the results, copying the C-strings into Rust ones making sure to + // free the C-strings afterwards + let mut resvec: Vec>> = vec![]; + let mut entry = unsafe { ldap_first_entry(self.ldap_ptr, ldap_msg) }; + + while !entry.is_null() { + + // Make the map holding the attribute : value pairs as well as the BerElement that keeps + // track of what position we're in + let mut map: HashMap> = HashMap::new(); + let mut ber: *mut BerElement = ptr::null_mut(); + unsafe { + // Populate the "DN" of the user + let raw_dn = ldap_get_dn(self.ldap_ptr, entry); + let mut dn: Vec = Vec::new(); + dn.push(CStr::from_ptr(raw_dn) + .to_owned() + .into_string() + .unwrap_or("".to_string())); + map.insert("dn".to_string(), dn); + ldap_memfree(raw_dn as *mut c_void); + + let mut attr: *const c_char = ldap_first_attribute(self.ldap_ptr, entry, &mut ber); + + while !attr.is_null() { + + // Convert the attribute into a Rust string. + let key = CStr::from_ptr(attr).to_owned().into_string().unwrap(); + + // Get the attribute values from LDAP. + let raw_vals: *const *const c_char = + ldap_get_values(self.ldap_ptr, entry, attr); + let raw_vals_len = ldap_count_values(raw_vals) as usize; + let val_slice: &[*const c_char] = slice::from_raw_parts(raw_vals, raw_vals_len); + + // Map these into a vector of Strings. + let values: Vec = val_slice.iter() + .map(|ptr| { + // TODO(sholsapp): If this contains binary data this will fail. + CStr::from_ptr(*ptr) + .to_owned() + .into_string() + .unwrap_or("".to_string()) + }) + .collect(); + + // Insert newly constructed Rust key-value strings. + map.insert(key, values); + + // Free the attr and value, then get next attr. + ldap_value_free(raw_vals); + ldap_memfree(attr as *mut c_void); + attr = ldap_next_attribute(self.ldap_ptr, entry, ber) + } + + // Free the BerElement and advance to the next entry. + ber_free(ber, 0); + entry = ldap_next_entry(self.ldap_ptr, entry); + + } + + // Push this entry into the vector. + resvec.push(map); + + } + + // Make sure we free the message and return the parsed results. + unsafe { ldap_msgfree(ldap_msg) }; + + Ok(resvec) + } } #[cfg(test)] mod tests { - use std::ptr; - use codes; - - const TEST_ADDRESS: &'static str = "ldap://ldap.forumsys.com"; - const TEST_BIND_DN: &'static str = "cn=read-only-admin,dc=example,dc=com"; - const TEST_BIND_PASS: &'static str = "password"; - const TEST_SIMPLE_SEARCH_QUERY: &'static str = "uid=tesla,dc=example,dc=com"; - const TEST_SEARCH_BASE: &'static str = "dc=example,dc=com"; - const TEST_SEARCH_FILTER: &'static str = "(uid=euler)"; - const TEST_SEARCH_INVALID_FILTER: &'static str = "(uid=INVALID)"; - - /// Test creating a RustLDAP struct with a valid uri. - #[test] - fn test_ldap_new(){ - - let _ = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - - } - - /// Test creating a RustLDAP struct with an invalid uri. - #[test] - fn test_invalid_ldap_new(){ - - if let Err(e) = super::RustLDAP::new("lda://localhost"){ - - assert_eq!("Bad parameter to an ldap routine", e); - - } else { - - assert!(false); - - } - - } - - #[test] - #[should_panic] - fn test_invalid_cstring_ldap_new(){ - - let _ = super::RustLDAP::new("INVALID\0CSTRING").unwrap(); - - } - - #[test] - fn test_simple_bind(){ - - let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); - println!("Bind result: {:?}", res); - - } - - #[test] - fn test_simple_search(){ - - println!("Testing simple search"); - let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - let _ = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); - let search_res = ldap.simple_search(TEST_SIMPLE_SEARCH_QUERY, codes::scopes::LDAP_SCOPE_BASE).unwrap(); - - //make sure we got something back - assert!(search_res.len() == 1); - - for result in search_res { - println!("simple search result: {:?}", result); - for (key, value) in result { - println!("- key: {:?}", key); - for res_val in value { - println!("- - res_val: {:?}", res_val); - } - } - } - - } - - #[test] - fn test_search(){ - - println!("Testing search"); - let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - let _ = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); - let search_res = ldap.ldap_search(TEST_SEARCH_BASE, codes::scopes::LDAP_SCOPE_SUB, Some(TEST_SEARCH_FILTER), - None, false, None, None, ptr::null_mut(), -1).unwrap(); - - //make sure we got something back - assert!(search_res.len() == 1); - - for result in search_res { - println!("search result: {:?}", result); - for (key, value) in result { - println!("- key: {:?}", key); - for res_val in value { - println!("- - res_val: {:?}", res_val); - } - } - } - - } - - #[test] - fn test_invalid_search(){ - - println!("Testing invalid search"); - let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - let _ = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); - let search_res = ldap.ldap_search(TEST_SEARCH_BASE, codes::scopes::LDAP_SCOPE_SUB, Some(TEST_SEARCH_INVALID_FILTER), - None, false, None, None, ptr::null_mut(), -1).unwrap(); - - //make sure we got something back - assert!(search_res.len() == 0); - - } - - #[test] - fn test_search_attrs(){ - - println!("Testing search with attrs"); - let test_search_attrs_vec = vec!["cn", "sn", "mail"]; - let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - let _ = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); - let search_res = ldap.ldap_search(TEST_SEARCH_BASE, codes::scopes::LDAP_SCOPE_SUB, Some(TEST_SEARCH_FILTER), - Some(test_search_attrs_vec), false, None, None, ptr::null_mut(), -1).unwrap(); - - //make sure we got something back - assert!(search_res.len() == 1); - - for result in search_res { - println!("attrs search result: {:?}", result); - for (key, value) in result { - println!("- key: {:?}", key); - for res_val in value { - println!("- - res_val: {:?}", res_val); - } - } - } - - } - - #[test] - fn test_search_invalid_attrs(){ - - println!("Testing search with invalid attrs"); - let test_search_attrs_vec = vec!["cn", "sn", "mail", "INVALID"]; - let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); - let _ = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); - let search_res = ldap.ldap_search(TEST_SEARCH_BASE, codes::scopes::LDAP_SCOPE_SUB, Some(TEST_SEARCH_FILTER), - Some(test_search_attrs_vec), false, None, None, ptr::null_mut(), -1).unwrap(); - - for result in search_res { - println!("attrs search result: {:?}", result); - for (key, value) in result { - println!("- key: {:?}", key); - for res_val in value { - println!("- - res_val: {:?}", res_val); - } - } - } - - } + use std::ptr; + use codes; + + const TEST_ADDRESS: &'static str = "ldap://ldap.forumsys.com"; + const TEST_BIND_DN: &'static str = "cn=read-only-admin,dc=example,dc=com"; + const TEST_BIND_PASS: &'static str = "password"; + const TEST_SIMPLE_SEARCH_QUERY: &'static str = "uid=tesla,dc=example,dc=com"; + const TEST_SEARCH_BASE: &'static str = "dc=example,dc=com"; + const TEST_SEARCH_FILTER: &'static str = "(uid=euler)"; + const TEST_SEARCH_INVALID_FILTER: &'static str = "(uid=INVALID)"; + + /// Test creating a RustLDAP struct with a valid uri. + #[test] + fn test_ldap_new() { + let _ = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + } + + /// Test creating a RustLDAP struct with an invalid uri. + #[test] + fn test_invalid_ldap_new() { + if let Err(e) = super::RustLDAP::new("lda://localhost") { + assert_eq!(super::errors::LDAPError::NativeError("Bad parameter to an ldap routine" + .to_string()), + e); + } else { + assert!(false); + } + } + + #[test] + #[should_panic] + fn test_invalid_cstring_ldap_new() { + let _ = super::RustLDAP::new("INVALID\0CSTRING").unwrap(); + } + + #[test] + fn test_simple_bind() { + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + println!("Bind result: {:?}", res); + + } + + #[test] + #[should_panic] // the TEST_ADDRESS being used doesn't support LDAPS, only LDAP + fn test_simple_bind_with_start_tls() { + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + ldap.start_tls(None, None); + + ldap.set_option( + codes::options::LDAP_OPT_PROTOCOL_VERSION, + &codes::versions::LDAP_VERSION3, + ); + + ldap.set_option( + codes::options::LDAP_OPT_X_TLS_REQUIRE_CERT, + &codes::options::LDAP_OPT_X_TLS_ALLOW, + ); + + ldap.set_option(codes::options::LDAP_OPT_X_TLS_NEWCTX, &0); + + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + println!("Bind result: {:?}", res); + + } + + + #[test] + fn test_simple_search() { + + println!("Testing simple search"); + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + let search_res = + ldap.simple_search(TEST_SIMPLE_SEARCH_QUERY, codes::scopes::LDAP_SCOPE_BASE).unwrap(); + + //make sure we got something back + assert!(search_res.len() == 1); + + // make sure the DN is searched and returned correctly + assert_eq!(search_res[0]["dn"][0], "uid=tesla,dc=example,dc=com"); + + for result in search_res { + println!("simple search result: {:?}", result); + for (key, value) in result { + println!("- key: {:?}", key); + for res_val in value { + println!("- - res_val: {:?}", res_val); + } + } + } + + } + + #[test] + fn test_search() { + + println!("Testing search"); + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + let search_res = ldap.ldap_search(TEST_SEARCH_BASE, + codes::scopes::LDAP_SCOPE_SUB, + Some(TEST_SEARCH_FILTER), + None, + false, + None, + None, + ptr::null_mut(), + -1) + .unwrap(); + + //make sure we got something back + assert!(search_res.len() == 1); + + // make sure the DN is searched and returned correctly + assert_eq!(search_res[0]["dn"][0], "uid=euler,dc=example,dc=com"); + + for result in search_res { + println!("search result: {:?}", result); + for (key, value) in result { + println!("- key: {:?}", key); + for res_val in value { + println!("- - res_val: {:?}", res_val); + } + } + } + + } + + #[test] + fn test_invalid_search() { + + println!("Testing invalid search"); + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + let search_res = ldap.ldap_search(TEST_SEARCH_BASE, + codes::scopes::LDAP_SCOPE_SUB, + Some(TEST_SEARCH_INVALID_FILTER), + None, + false, + None, + None, + ptr::null_mut(), + -1) + .unwrap(); + + //make sure we got something back + assert!(search_res.len() == 0); + + } + + #[test] + fn test_search_attrs() { + + println!("Testing search with attrs"); + let test_search_attrs_vec = vec!["cn", "sn", "mail"]; + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + let search_res = ldap.ldap_search(TEST_SEARCH_BASE, + codes::scopes::LDAP_SCOPE_SUB, + Some(TEST_SEARCH_FILTER), + Some(test_search_attrs_vec), + false, + None, + None, + ptr::null_mut(), + -1) + .unwrap(); + + //make sure we got something back + assert!(search_res.len() == 1); + + for result in search_res { + println!("attrs search result: {:?}", result); + for (key, value) in result { + println!("- key: {:?}", key); + for res_val in value { + println!("- - res_val: {:?}", res_val); + } + } + } + + } + + #[test] + fn test_search_invalid_attrs() { + + println!("Testing search with invalid attrs"); + let test_search_attrs_vec = vec!["cn", "sn", "mail", "INVALID"]; + let ldap = super::RustLDAP::new(TEST_ADDRESS).unwrap(); + assert!(ldap.set_option(codes::options::LDAP_OPT_PROTOCOL_VERSION, &3)); + let res = ldap.simple_bind(TEST_BIND_DN, TEST_BIND_PASS).unwrap(); + assert_eq!(codes::results::LDAP_SUCCESS, res); + let search_res = ldap.ldap_search(TEST_SEARCH_BASE, + codes::scopes::LDAP_SCOPE_SUB, + Some(TEST_SEARCH_FILTER), + Some(test_search_attrs_vec), + false, + None, + None, + ptr::null_mut(), + -1) + .unwrap(); + + for result in search_res { + println!("attrs search result: {:?}", result); + for (key, value) in result { + println!("- key: {:?}", key); + for res_val in value { + println!("- - res_val: {:?}", res_val); + } + } + } + + } }