use crate::ber::{self, Decoder, Encoder};
use rustlavel_core::{Error, Result};
pub const VERSION: i64 = 3;
pub const START_TLS_OID: &str = "1.3.6.1.4.1.1466.20037";
pub mod tags {
use crate::ber::application;
pub const BIND_REQUEST: u8 = application(0, true);
pub const BIND_RESPONSE: u8 = application(1, true);
pub const UNBIND_REQUEST: u8 = application(2, false);
pub const SEARCH_REQUEST: u8 = application(3, true);
pub const SEARCH_RESULT_ENTRY: u8 = application(4, true);
pub const SEARCH_RESULT_DONE: u8 = application(5, true);
pub const SEARCH_RESULT_REFERENCE: u8 = application(19, true);
pub const EXTENDED_REQUEST: u8 = application(23, true);
pub const EXTENDED_RESPONSE: u8 = application(24, true);
pub const INTERMEDIATE_RESPONSE: u8 = application(25, true);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResultCode {
Success,
OperationsError,
ProtocolError,
TimeLimitExceeded,
SizeLimitExceeded,
CompareFalse,
CompareTrue,
AuthMethodNotSupported,
StrongerAuthRequired,
Referral,
AdminLimitExceeded,
UnavailableCriticalExtension,
ConfidentialityRequired,
SaslBindInProgress,
NoSuchAttribute,
UndefinedAttributeType,
InappropriateMatching,
ConstraintViolation,
AttributeOrValueExists,
InvalidAttributeSyntax,
NoSuchObject,
AliasProblem,
InvalidDnSyntax,
AliasDereferencingProblem,
InappropriateAuthentication,
InvalidCredentials,
InsufficientAccessRights,
Busy,
Unavailable,
UnwillingToPerform,
LoopDetect,
NamingViolation,
ObjectClassViolation,
NotAllowedOnNonLeaf,
NotAllowedOnRdn,
EntryAlreadyExists,
ObjectClassModsProhibited,
AffectsMultipleDsas,
Other,
Unknown(i64),
}
impl ResultCode {
pub fn from_i64(code: i64) -> ResultCode {
use ResultCode::*;
match code {
0 => Success,
1 => OperationsError,
2 => ProtocolError,
3 => TimeLimitExceeded,
4 => SizeLimitExceeded,
5 => CompareFalse,
6 => CompareTrue,
7 => AuthMethodNotSupported,
8 => StrongerAuthRequired,
10 => Referral,
11 => AdminLimitExceeded,
12 => UnavailableCriticalExtension,
13 => ConfidentialityRequired,
14 => SaslBindInProgress,
16 => NoSuchAttribute,
17 => UndefinedAttributeType,
18 => InappropriateMatching,
19 => ConstraintViolation,
20 => AttributeOrValueExists,
21 => InvalidAttributeSyntax,
32 => NoSuchObject,
33 => AliasProblem,
34 => InvalidDnSyntax,
36 => AliasDereferencingProblem,
48 => InappropriateAuthentication,
49 => InvalidCredentials,
50 => InsufficientAccessRights,
51 => Busy,
52 => Unavailable,
53 => UnwillingToPerform,
54 => LoopDetect,
64 => NamingViolation,
65 => ObjectClassViolation,
66 => NotAllowedOnNonLeaf,
67 => NotAllowedOnRdn,
68 => EntryAlreadyExists,
69 => ObjectClassModsProhibited,
71 => AffectsMultipleDsas,
80 => Other,
other => Unknown(other),
}
}
pub fn as_i64(self) -> i64 {
use ResultCode::*;
match self {
Success => 0,
OperationsError => 1,
ProtocolError => 2,
TimeLimitExceeded => 3,
SizeLimitExceeded => 4,
CompareFalse => 5,
CompareTrue => 6,
AuthMethodNotSupported => 7,
StrongerAuthRequired => 8,
Referral => 10,
AdminLimitExceeded => 11,
UnavailableCriticalExtension => 12,
ConfidentialityRequired => 13,
SaslBindInProgress => 14,
NoSuchAttribute => 16,
UndefinedAttributeType => 17,
InappropriateMatching => 18,
ConstraintViolation => 19,
AttributeOrValueExists => 20,
InvalidAttributeSyntax => 21,
NoSuchObject => 32,
AliasProblem => 33,
InvalidDnSyntax => 34,
AliasDereferencingProblem => 36,
InappropriateAuthentication => 48,
InvalidCredentials => 49,
InsufficientAccessRights => 50,
Busy => 51,
Unavailable => 52,
UnwillingToPerform => 53,
LoopDetect => 54,
NamingViolation => 64,
ObjectClassViolation => 65,
NotAllowedOnNonLeaf => 66,
NotAllowedOnRdn => 67,
EntryAlreadyExists => 68,
ObjectClassModsProhibited => 69,
AffectsMultipleDsas => 71,
Other => 80,
Unknown(code) => code,
}
}
pub fn name(self) -> &'static str {
use ResultCode::*;
match self {
Success => "success",
OperationsError => "operationsError",
ProtocolError => "protocolError",
TimeLimitExceeded => "timeLimitExceeded",
SizeLimitExceeded => "sizeLimitExceeded",
CompareFalse => "compareFalse",
CompareTrue => "compareTrue",
AuthMethodNotSupported => "authMethodNotSupported",
StrongerAuthRequired => "strongerAuthRequired",
Referral => "referral",
AdminLimitExceeded => "adminLimitExceeded",
UnavailableCriticalExtension => "unavailableCriticalExtension",
ConfidentialityRequired => "confidentialityRequired",
SaslBindInProgress => "saslBindInProgress",
NoSuchAttribute => "noSuchAttribute",
UndefinedAttributeType => "undefinedAttributeType",
InappropriateMatching => "inappropriateMatching",
ConstraintViolation => "constraintViolation",
AttributeOrValueExists => "attributeOrValueExists",
InvalidAttributeSyntax => "invalidAttributeSyntax",
NoSuchObject => "noSuchObject",
AliasProblem => "aliasProblem",
InvalidDnSyntax => "invalidDNSyntax",
AliasDereferencingProblem => "aliasDereferencingProblem",
InappropriateAuthentication => "inappropriateAuthentication",
InvalidCredentials => "invalidCredentials",
InsufficientAccessRights => "insufficientAccessRights",
Busy => "busy",
Unavailable => "unavailable",
UnwillingToPerform => "unwillingToPerform",
LoopDetect => "loopDetect",
NamingViolation => "namingViolation",
ObjectClassViolation => "objectClassViolation",
NotAllowedOnNonLeaf => "notAllowedOnNonLeaf",
NotAllowedOnRdn => "notAllowedOnRDN",
EntryAlreadyExists => "entryAlreadyExists",
ObjectClassModsProhibited => "objectClassModsProhibited",
AffectsMultipleDsas => "affectsMultipleDSAs",
Other => "other",
Unknown(_) => "an unrecognised result code",
}
}
pub fn is_success(self) -> bool {
matches!(self, ResultCode::Success)
}
}
impl std::fmt::Display for ResultCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.name(), self.as_i64())
}
}
#[derive(Debug, Clone)]
pub struct LdapResult {
pub code: ResultCode,
pub matched_dn: String,
pub diagnostic: String,
pub referrals: Vec<String>,
}
impl LdapResult {
pub fn is_success(&self) -> bool {
self.code.is_success()
}
pub fn into_result(self, operation: &str) -> Result<LdapResult> {
if self.is_success() {
return Ok(self);
}
let mut message = format!("the directory refused the {operation}: {}", self.code);
if !self.diagnostic.is_empty() {
message.push_str(&format!(" — {}", self.diagnostic));
}
if !self.matched_dn.is_empty() {
message.push_str(&format!(" (it did find `{}`)", self.matched_dn));
}
Err(Error::msg(message))
}
fn decode(decoder: &mut Decoder<'_>) -> Result<LdapResult> {
let code = ResultCode::from_i64(decoder.enumerated()?);
let matched_dn = decoder.string()?;
let diagnostic = decoder.string()?;
let mut referrals = Vec::new();
if decoder.has_tag(ber::context(3, true)) {
let mut list = decoder.nested(ber::context(3, true))?;
while !list.is_empty() {
referrals.push(list.string()?);
}
}
Ok(LdapResult { code, matched_dn, diagnostic, referrals })
}
}
#[derive(Debug, Clone)]
pub struct Control {
pub oid: String,
pub critical: bool,
pub value: Option<Vec<u8>>,
}
impl Control {
pub fn new(oid: impl Into<String>) -> Control {
Control { oid: oid.into(), critical: false, value: None }
}
pub fn critical(mut self, critical: bool) -> Control {
self.critical = critical;
self
}
pub fn value(mut self, value: impl Into<Vec<u8>>) -> Control {
self.value = Some(value.into());
self
}
fn encode(&self, encoder: &mut Encoder) {
encoder.sequence(|body| {
body.string(&self.oid);
if self.critical {
body.boolean(true);
}
if let Some(value) = &self.value {
body.octet_string(value);
}
});
}
fn decode(decoder: &mut Decoder<'_>) -> Result<Control> {
let mut fields = decoder.sequence()?;
let oid = fields.string()?;
let mut critical = false;
let mut value = None;
if fields.has_tag(ber::BOOLEAN) {
critical = fields.boolean()?;
}
if fields.has_tag(ber::OCTET_STRING) {
value = Some(fields.octet_string()?.to_vec());
}
Ok(Control { oid, critical, value })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Scope {
Base,
OneLevel,
#[default]
Subtree,
}
impl Scope {
pub fn as_i64(self) -> i64 {
match self {
Scope::Base => 0,
Scope::OneLevel => 1,
Scope::Subtree => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DerefAliases {
#[default]
Never,
InSearching,
FindingBaseObject,
Always,
}
impl DerefAliases {
pub fn as_i64(self) -> i64 {
match self {
DerefAliases::Never => 0,
DerefAliases::InSearching => 1,
DerefAliases::FindingBaseObject => 2,
DerefAliases::Always => 3,
}
}
}
#[derive(Debug, Clone)]
pub struct SearchRequest {
pub base: String,
pub scope: Scope,
pub deref: DerefAliases,
pub size_limit: i64,
pub time_limit: i64,
pub types_only: bool,
pub filter: Filter,
pub attributes: Vec<String>,
}
impl SearchRequest {
pub fn new(base: impl Into<String>, filter: Filter) -> SearchRequest {
SearchRequest {
base: base.into(),
scope: Scope::default(),
deref: DerefAliases::default(),
size_limit: 0,
time_limit: 0,
types_only: false,
filter,
attributes: Vec::new(),
}
}
pub fn scope(mut self, scope: Scope) -> SearchRequest {
self.scope = scope;
self
}
pub fn deref(mut self, deref: DerefAliases) -> SearchRequest {
self.deref = deref;
self
}
pub fn size_limit(mut self, limit: i64) -> SearchRequest {
self.size_limit = limit;
self
}
pub fn time_limit(mut self, seconds: i64) -> SearchRequest {
self.time_limit = seconds;
self
}
pub fn types_only(mut self, types_only: bool) -> SearchRequest {
self.types_only = types_only;
self
}
pub fn attributes<S: Into<String>>(
mut self,
attributes: impl IntoIterator<Item = S>,
) -> SearchRequest {
self.attributes = attributes.into_iter().map(Into::into).collect();
self
}
fn encode(&self, encoder: &mut Encoder) {
encoder.constructed(tags::SEARCH_REQUEST, |body| {
body.string(&self.base);
body.enumerated(self.scope.as_i64());
body.enumerated(self.deref.as_i64());
body.integer(self.size_limit);
body.integer(self.time_limit);
body.boolean(self.types_only);
self.filter.encode(body);
body.sequence(|list| {
for attribute in &self.attributes {
list.string(attribute);
}
});
});
}
}
#[derive(Debug, Clone)]
pub struct Attribute {
pub name: String,
pub values: Vec<Vec<u8>>,
}
impl Attribute {
pub fn first(&self) -> Option<&str> {
self.values.first().and_then(|value| std::str::from_utf8(value).ok())
}
}
#[derive(Debug, Clone)]
pub struct SearchEntry {
pub dn: String,
pub attributes: Vec<Attribute>,
}
impl SearchEntry {
pub fn attribute(&self, name: &str) -> Option<&Attribute> {
self.attributes.iter().find(|attribute| attribute.name.eq_ignore_ascii_case(name))
}
pub fn value(&self, name: &str) -> Option<&str> {
self.attribute(name).and_then(Attribute::first)
}
pub fn values(&self, name: &str) -> Vec<&str> {
self.attribute(name)
.map(|attribute| {
attribute
.values
.iter()
.filter_map(|value| std::str::from_utf8(value).ok())
.collect()
})
.unwrap_or_default()
}
fn decode(decoder: &mut Decoder<'_>) -> Result<SearchEntry> {
let dn = decoder.string()?;
let mut attributes = Vec::new();
let mut list = decoder.sequence()?;
while !list.is_empty() {
let mut partial = list.sequence()?;
let name = partial.string()?;
let mut values = Vec::new();
let mut set = partial.set()?;
while !set.is_empty() {
values.push(set.octet_string()?.to_vec());
}
attributes.push(Attribute { name, values });
}
Ok(SearchEntry { dn, attributes })
}
}
#[derive(Debug, Clone)]
pub struct SearchOutcome {
pub entries: Vec<SearchEntry>,
pub referrals: Vec<Vec<String>>,
pub result: LdapResult,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Filter {
And(Vec<Filter>),
Or(Vec<Filter>),
Not(Box<Filter>),
Equality { attribute: String, value: String },
GreaterOrEqual { attribute: String, value: String },
LessOrEqual { attribute: String, value: String },
Approximate { attribute: String, value: String },
Present { attribute: String },
Substrings {
attribute: String,
initial: Option<String>,
any: Vec<String>,
last: Option<String>,
},
}
impl Filter {
pub fn equals(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
Filter::Equality { attribute: attribute.into(), value: value.into() }
}
pub fn greater_or_equal(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
Filter::GreaterOrEqual { attribute: attribute.into(), value: value.into() }
}
pub fn less_or_equal(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
Filter::LessOrEqual { attribute: attribute.into(), value: value.into() }
}
pub fn approximately(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
Filter::Approximate { attribute: attribute.into(), value: value.into() }
}
pub fn present(attribute: impl Into<String>) -> Filter {
Filter::Present { attribute: attribute.into() }
}
pub fn starts_with(attribute: impl Into<String>, prefix: impl Into<String>) -> Filter {
Filter::Substrings {
attribute: attribute.into(),
initial: Some(prefix.into()),
any: Vec::new(),
last: None,
}
}
pub fn ends_with(attribute: impl Into<String>, suffix: impl Into<String>) -> Filter {
Filter::Substrings {
attribute: attribute.into(),
initial: None,
any: Vec::new(),
last: Some(suffix.into()),
}
}
pub fn contains(attribute: impl Into<String>, fragment: impl Into<String>) -> Filter {
Filter::Substrings {
attribute: attribute.into(),
initial: None,
any: vec![fragment.into()],
last: None,
}
}
pub fn and(filters: impl IntoIterator<Item = Filter>) -> Filter {
Filter::And(filters.into_iter().collect())
}
pub fn or(filters: impl IntoIterator<Item = Filter>) -> Filter {
Filter::Or(filters.into_iter().collect())
}
pub fn negate(self) -> Filter {
Filter::Not(Box::new(self))
}
pub fn encode(&self, encoder: &mut Encoder) {
match self {
Filter::And(filters) => {
encoder.constructed(ber::context(0, true), |body| {
for filter in filters {
filter.encode(body);
}
});
}
Filter::Or(filters) => {
encoder.constructed(ber::context(1, true), |body| {
for filter in filters {
filter.encode(body);
}
});
}
Filter::Not(inner) => {
encoder.constructed(ber::context(2, true), |body| inner.encode(body));
}
Filter::Equality { attribute, value } => {
assertion(encoder, ber::context(3, true), attribute, value);
}
Filter::Substrings { attribute, initial, any, last } => {
encoder.constructed(ber::context(4, true), |body| {
body.string(attribute);
body.sequence(|parts| {
if let Some(initial) = initial {
parts.tagged_string(ber::context(0, false), initial);
}
for fragment in any {
parts.tagged_string(ber::context(1, false), fragment);
}
if let Some(last) = last {
parts.tagged_string(ber::context(2, false), last);
}
});
});
}
Filter::GreaterOrEqual { attribute, value } => {
assertion(encoder, ber::context(5, true), attribute, value);
}
Filter::LessOrEqual { attribute, value } => {
assertion(encoder, ber::context(6, true), attribute, value);
}
Filter::Present { attribute } => {
encoder.tagged_string(ber::context(7, false), attribute);
}
Filter::Approximate { attribute, value } => {
assertion(encoder, ber::context(8, true), attribute, value);
}
}
}
}
impl std::ops::Not for Filter {
type Output = Filter;
fn not(self) -> Filter {
Filter::Not(Box::new(self))
}
}
fn assertion(encoder: &mut Encoder, tag: u8, attribute: &str, value: &str) {
encoder.constructed(tag, |body| {
body.string(attribute);
body.string(value);
});
}
impl std::fmt::Display for Filter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Filter::And(filters) => {
f.write_str("(&")?;
for filter in filters {
write!(f, "{filter}")?;
}
f.write_str(")")
}
Filter::Or(filters) => {
f.write_str("(|")?;
for filter in filters {
write!(f, "{filter}")?;
}
f.write_str(")")
}
Filter::Not(inner) => write!(f, "(!{inner})"),
Filter::Equality { attribute, value } => {
write!(f, "({}={})", escape_filter_value(attribute), escape_filter_value(value))
}
Filter::GreaterOrEqual { attribute, value } => {
write!(f, "({}>={})", escape_filter_value(attribute), escape_filter_value(value))
}
Filter::LessOrEqual { attribute, value } => {
write!(f, "({}<={})", escape_filter_value(attribute), escape_filter_value(value))
}
Filter::Approximate { attribute, value } => {
write!(f, "({}~={})", escape_filter_value(attribute), escape_filter_value(value))
}
Filter::Present { attribute } => write!(f, "({}=*)", escape_filter_value(attribute)),
Filter::Substrings { attribute, initial, any, last } => {
write!(f, "({}=", escape_filter_value(attribute))?;
if let Some(initial) = initial {
f.write_str(&escape_filter_value(initial))?;
}
f.write_str("*")?;
for fragment in any {
f.write_str(&escape_filter_value(fragment))?;
f.write_str("*")?;
}
if let Some(last) = last {
f.write_str(&escape_filter_value(last))?;
}
f.write_str(")")
}
}
}
}
pub fn escape_filter_value(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'\\' => escaped.push_str("\\5c"),
'*' => escaped.push_str("\\2a"),
'(' => escaped.push_str("\\28"),
')' => escaped.push_str("\\29"),
'\0' => escaped.push_str("\\00"),
other => escaped.push(other),
}
}
escaped
}
pub fn escape_dn_value(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
let characters: Vec<char> = value.chars().collect();
for (index, &character) in characters.iter().enumerate() {
let first = index == 0;
let last = index + 1 == characters.len();
match character {
'\0' => escaped.push_str("\\00"),
'"' | '+' | ',' | ';' | '<' | '>' | '\\' | '=' => {
escaped.push('\\');
escaped.push(character);
}
'#' if first => escaped.push_str("\\#"),
' ' if first || last => escaped.push_str("\\ "),
other => escaped.push(other),
}
}
escaped
}
pub struct Operation {
kind: &'static str,
op: Vec<u8>,
controls: Vec<Control>,
carries_password: bool,
}
impl Operation {
pub fn simple_bind(dn: &str, password: &str) -> Result<Operation> {
if password.is_empty() {
return Err(Error::msg(
"refusing to send a simple bind with an empty password. LDAP treats that as an \
anonymous bind and answers `success`, so a blank password field would report a \
successful login. If an anonymous bind is what you meant, ask for one by name \
with `bind_anonymous`.",
));
}
Ok(Operation {
kind: "bind",
op: bind_bytes(dn, password),
controls: Vec::new(),
carries_password: true,
})
}
pub fn anonymous_bind() -> Operation {
Operation {
kind: "anonymous bind",
op: bind_bytes("", ""),
controls: Vec::new(),
carries_password: false,
}
}
pub fn search(request: &SearchRequest) -> Operation {
let mut encoder = Encoder::new();
request.encode(&mut encoder);
Operation {
kind: "search",
op: encoder.into_bytes(),
controls: Vec::new(),
carries_password: false,
}
}
pub fn unbind() -> Operation {
let mut encoder = Encoder::new();
encoder.element(tags::UNBIND_REQUEST, &[]);
Operation {
kind: "unbind",
op: encoder.into_bytes(),
controls: Vec::new(),
carries_password: false,
}
}
pub fn extended(oid: &str, value: Option<&[u8]>) -> Operation {
let mut encoder = Encoder::new();
encoder.constructed(tags::EXTENDED_REQUEST, |body| {
body.tagged_string(ber::context(0, false), oid);
if let Some(value) = value {
body.element(ber::context(1, false), value);
}
});
Operation {
kind: "extended request",
op: encoder.into_bytes(),
controls: Vec::new(),
carries_password: false,
}
}
pub fn start_tls() -> Operation {
let mut operation = Operation::extended(START_TLS_OID, None);
operation.kind = "StartTLS request";
operation
}
pub fn with_control(mut self, control: Control) -> Operation {
self.controls.push(control);
self
}
pub fn kind(&self) -> &'static str {
self.kind
}
pub fn carries_password(&self) -> bool {
self.carries_password
}
pub fn encode(&self, message_id: i64) -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(message_id);
body.raw(&self.op);
if !self.controls.is_empty() {
body.constructed(ber::context(0, true), |list| {
for control in &self.controls {
control.encode(list);
}
});
}
});
encoder.into_bytes()
}
}
fn bind_bytes(dn: &str, password: &str) -> Vec<u8> {
let mut encoder = Encoder::new();
encoder.constructed(tags::BIND_REQUEST, |body| {
body.integer(VERSION);
body.string(dn);
body.tagged_string(ber::context(0, false), password);
});
encoder.into_bytes()
}
#[derive(Debug, Clone)]
pub enum ProtocolOp {
BindResponse { result: LdapResult, server_sasl_credentials: Option<Vec<u8>> },
SearchResultEntry(SearchEntry),
SearchResultReference(Vec<String>),
SearchResultDone(LdapResult),
ExtendedResponse { result: LdapResult, name: Option<String>, value: Option<Vec<u8>> },
IntermediateResponse,
Unrecognised { tag: u8 },
}
impl ProtocolOp {
pub fn result(&self) -> Option<&LdapResult> {
match self {
ProtocolOp::BindResponse { result, .. }
| ProtocolOp::SearchResultDone(result)
| ProtocolOp::ExtendedResponse { result, .. } => Some(result),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
ProtocolOp::BindResponse { .. } => "a bind response",
ProtocolOp::SearchResultEntry(_) => "a search result entry",
ProtocolOp::SearchResultReference(_) => "a search result reference",
ProtocolOp::SearchResultDone(_) => "a search result",
ProtocolOp::ExtendedResponse { .. } => "an extended response",
ProtocolOp::IntermediateResponse => "an intermediate response",
ProtocolOp::Unrecognised { .. } => "an operation this client does not implement",
}
}
}
#[derive(Debug, Clone)]
pub struct LdapMessage {
pub id: i64,
pub op: ProtocolOp,
pub controls: Vec<Control>,
}
impl LdapMessage {
pub fn parse(bytes: &[u8]) -> Result<LdapMessage> {
let mut outer = Decoder::new(bytes);
let mut message = outer.sequence()?;
outer.finish("an LDAP message")?;
let id = message.integer()?;
if id < 0 {
return Err(Error::msg(format!(
"the directory sent message id {id}; RFC 4511 says a message id is a positive \
integer or zero"
)));
}
let tag = message
.peek_tag()
.ok_or_else(|| Error::msg("an LDAP message carried an id but no operation"))?;
let op = match tag {
tags::BIND_RESPONSE => {
let mut fields = message.nested(tag)?;
let result = LdapResult::decode(&mut fields)?;
let credentials = optional_octets(&mut fields, ber::context(7, false))?;
ProtocolOp::BindResponse { result, server_sasl_credentials: credentials }
}
tags::SEARCH_RESULT_ENTRY => {
let mut fields = message.nested(tag)?;
ProtocolOp::SearchResultEntry(SearchEntry::decode(&mut fields)?)
}
tags::SEARCH_RESULT_DONE => {
let mut fields = message.nested(tag)?;
ProtocolOp::SearchResultDone(LdapResult::decode(&mut fields)?)
}
tags::SEARCH_RESULT_REFERENCE => {
let mut fields = message.nested(tag)?;
let mut uris = Vec::new();
while !fields.is_empty() {
uris.push(fields.string()?);
}
ProtocolOp::SearchResultReference(uris)
}
tags::EXTENDED_RESPONSE => {
let mut fields = message.nested(tag)?;
let result = LdapResult::decode(&mut fields)?;
let name = match fields.has_tag(ber::context(10, false)) {
true => Some(fields.tagged_string(ber::context(10, false))?),
false => None,
};
let value = optional_octets(&mut fields, ber::context(11, false))?;
ProtocolOp::ExtendedResponse { result, name, value }
}
tags::INTERMEDIATE_RESPONSE => {
message.skip()?;
ProtocolOp::IntermediateResponse
}
other => {
message.skip()?;
ProtocolOp::Unrecognised { tag: other }
}
};
let mut controls = Vec::new();
if message.has_tag(ber::context(0, true)) {
let mut list = message.nested(ber::context(0, true))?;
while !list.is_empty() {
controls.push(Control::decode(&mut list)?);
}
}
message.finish("an LDAP message")?;
Ok(LdapMessage { id, op, controls })
}
}
fn optional_octets(decoder: &mut Decoder<'_>, tag: u8) -> Result<Option<Vec<u8>>> {
if decoder.has_tag(tag) {
return Ok(Some(decoder.expect(tag)?.to_vec()));
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
fn filter_bytes(filter: &Filter) -> Vec<u8> {
let mut encoder = Encoder::new();
filter.encode(&mut encoder);
encoder.into_bytes()
}
#[test]
fn an_anonymous_bind_is_the_message_every_reference_shows() {
assert_eq!(
Operation::anonymous_bind().encode(1),
vec![
0x30, 0x0c, 0x02, 0x01, 0x01, 0x60, 0x07, 0x02, 0x01, 0x03, 0x04, 0x00, 0x80, 0x00, ]
);
}
#[test]
fn a_simple_bind_carries_the_dn_and_the_password_unwrapped() {
let bytes = Operation::simple_bind("cn=root", "hunter2").unwrap().encode(2);
assert_eq!(
bytes,
vec![
0x30, 0x1a, 0x02, 0x01, 0x02, 0x60, 0x15, 0x02, 0x01, 0x03, 0x04, 0x07, b'c', b'n', b'=', b'r', b'o', b'o', b't', 0x80, 0x07, b'h', b'u', b'n', b't', b'e', b'r', b'2',
]
);
assert!(bytes.windows(7).any(|window| window == b"hunter2"));
assert!(Operation::simple_bind("cn=root", "hunter2").unwrap().carries_password());
assert!(!Operation::anonymous_bind().carries_password());
}
#[test]
fn an_empty_password_is_refused_before_it_reaches_the_wire() {
let error = match Operation::simple_bind("uid=alice,dc=example,dc=test", "") {
Ok(_) => panic!("an empty password must never build a bind"),
Err(error) => error.to_string(),
};
assert!(error.contains("anonymous"), "the error must say what an empty password becomes");
assert!(error.contains("bind_anonymous"), "and name the deliberate alternative: {error}");
assert!(Operation::simple_bind("uid=alice,dc=example,dc=test", " ").is_ok());
}
#[test]
fn unbind_is_a_primitive_null_with_no_response() {
assert_eq!(Operation::unbind().encode(3), vec![0x30, 0x05, 0x02, 0x01, 0x03, 0x42, 0x00]);
}
#[test]
fn start_tls_names_the_oid_from_rfc_4511() {
let bytes = Operation::start_tls().encode(1);
let oid = START_TLS_OID.as_bytes();
assert_eq!(&bytes[..5], &[0x30, 0x1d, 0x02, 0x01, 0x01]);
assert_eq!(bytes[5], 0x77, "[APPLICATION 23] ExtendedRequest");
assert_eq!(bytes[7], 0x80, "requestName [0], primitive");
assert_eq!(bytes[8] as usize, oid.len());
assert_eq!(&bytes[9..], oid);
}
#[test]
fn an_equality_filter_puts_the_value_in_a_length_delimited_string() {
assert_eq!(
filter_bytes(&Filter::equals("uid", "alice")),
vec![
0xa3, 0x0c, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x05, b'a', b'l', b'i', b'c', b'e',
]
);
}
#[test]
fn an_asterisk_in_an_equality_value_stays_a_literal_asterisk() {
assert_eq!(
filter_bytes(&Filter::equals("uid", "*")),
vec![0xa3, 0x08, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x01, 0x2a]
);
assert_eq!(
filter_bytes(&Filter::present("uid")),
vec![0x87, 0x03, b'u', b'i', b'd']
);
let injected = Filter::equals("uid", "alice)(uid=admin");
let bytes = filter_bytes(&injected);
assert_eq!(bytes[0], 0xa3, "still one equality assertion");
assert_eq!(bytes.iter().filter(|&&byte| byte == 0xa3).count(), 1);
}
#[test]
fn a_not_filter_wraps_a_whole_inner_filter_because_the_tag_is_explicit() {
assert_eq!(
filter_bytes(&!Filter::equals("cn", "x")),
vec![0xa2, 0x09, 0xa3, 0x07, 0x04, 0x02, b'c', b'n', 0x04, 0x01, b'x']
);
}
#[test]
fn and_and_or_replace_the_set_tag_rather_than_wrapping_it() {
let bytes = filter_bytes(&Filter::and([Filter::present("a"), Filter::present("b")]));
assert_eq!(bytes, vec![0xa0, 0x06, 0x87, 0x01, b'a', 0x87, 0x01, b'b']);
let bytes = filter_bytes(&Filter::or([Filter::present("a")]));
assert_eq!(bytes, vec![0xa1, 0x03, 0x87, 0x01, b'a']);
}
#[test]
fn a_substring_filter_tags_each_part_by_position() {
let filter = Filter::Substrings {
attribute: "o".into(),
initial: Some("univ".into()),
any: vec!["of".into(), "mich".into()],
last: None,
};
assert_eq!(
filter_bytes(&filter),
vec![
0xa4, 0x15, 0x04, 0x01, b'o', 0x30, 0x10, 0x80, 0x04, b'u', b'n', b'i', b'v', 0x81, 0x02, b'o', b'f', 0x81, 0x04, b'm', b'i', b'c', b'h', ]
);
assert_eq!(filter.to_string(), "(o=univ*of*mich*)");
}
#[test]
fn filters_print_the_examples_from_rfc_4515() {
assert_eq!(Filter::equals("cn", "Babs Jensen").to_string(), "(cn=Babs Jensen)");
assert_eq!(Filter::equals("cn", "Tim Howes").negate().to_string(), "(!(cn=Tim Howes))");
assert_eq!(!Filter::equals("cn", "Tim Howes"), Filter::equals("cn", "Tim Howes").negate());
assert_eq!(
Filter::and([
Filter::equals("objectClass", "Person"),
Filter::or([
Filter::equals("sn", "Jensen"),
Filter::starts_with("cn", "Babs J"),
]),
])
.to_string(),
"(&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))"
);
assert_eq!(Filter::equals("seeAlso", "").to_string(), "(seeAlso=)");
assert_eq!(Filter::present("objectClass").to_string(), "(objectClass=*)");
assert_eq!(Filter::and([]).to_string(), "(&)");
assert_eq!(Filter::or([]).to_string(), "(|)");
}
#[test]
fn escaping_matches_rfc_4515_s_worked_examples() {
assert_eq!(
Filter::equals("o", "Parens R Us (for all your parenthetical needs)").to_string(),
"(o=Parens R Us \\28for all your parenthetical needs\\29)"
);
assert_eq!(Filter::contains("cn", "*").to_string(), "(cn=*\\2a*)");
assert_eq!(
Filter::equals("filename", "C:\\MyFile").to_string(),
"(filename=C:\\5cMyFile)"
);
assert_eq!(escape_filter_value("\0\0\0\u{4}"), "\\00\\00\\00\u{4}");
}
#[test]
fn an_unescaped_asterisk_would_have_matched_everybody() {
let username = "*";
let naive = format!("(uid={username})");
assert_eq!(naive, "(uid=*)", "which is a presence filter — every account matches");
let escaped = format!("(uid={})", escape_filter_value(username));
assert_eq!(escaped, "(uid=\\2a)", "which matches an account literally named *");
assert_eq!(
escape_filter_value("alice)(uid=admin"),
"alice\\29\\28uid=admin",
"the parentheses have to go, or the filter grows a second clause"
);
}
#[test]
fn dn_escaping_covers_what_rfc_4514_requires() {
assert_eq!(escape_dn_value("Smith, John"), "Smith\\, John");
assert_eq!(escape_dn_value("a+b"), "a\\+b");
assert_eq!(escape_dn_value("back\\slash"), "back\\\\slash");
assert_eq!(escape_dn_value(" alice "), "\\ alice\\ ");
assert_eq!(escape_dn_value("#alice"), "\\#alice");
assert_eq!(escape_dn_value("mid#dle"), "mid#dle");
assert_eq!(
escape_dn_value("alice,ou=admins"),
"alice\\,ou\\=admins"
);
}
#[test]
fn a_search_request_encodes_every_field_in_order() {
let request = SearchRequest::new("dc=example,dc=test", Filter::equals("uid", "alice"))
.scope(Scope::Subtree)
.size_limit(2)
.attributes(["cn"]);
assert_eq!(
Operation::search(&request).encode(4),
vec![
0x30, 0x3c, 0x02, 0x01, 0x04, 0x63, 0x37, 0x04, 0x12, b'd', b'c', b'=', b'e', b'x', b'a', b'm', b'p', b'l', b'e', b',',
b'd', b'c', b'=', b't', b'e', b's', b't', 0x0a, 0x01, 0x02, 0x0a, 0x01, 0x00, 0x02, 0x01, 0x02, 0x02, 0x01, 0x00, 0x01, 0x01, 0x00, 0xa3, 0x0c, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x05, b'a', b'l', b'i', b'c',
b'e', 0x30, 0x04, 0x04, 0x02, b'c', b'n', ]
);
}
#[test]
fn a_bind_response_decodes_its_code_and_the_directory_s_own_words() {
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(2);
body.constructed(tags::BIND_RESPONSE, |fields| {
fields.enumerated(49);
fields.string("");
fields.string("80090308: LdapErr: DSID-0C0903A9, comment: ..., data 533, v2580");
});
});
let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
assert_eq!(message.id, 2);
let ProtocolOp::BindResponse { result, .. } = &message.op else {
panic!("expected a bind response, got {}", message.op.name());
};
assert_eq!(result.code, ResultCode::InvalidCredentials);
assert!(result.diagnostic.contains("data 533"));
assert!(!result.is_success());
}
#[test]
fn a_search_result_entry_decodes_its_dn_and_multi_valued_attributes() {
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(5);
body.constructed(tags::SEARCH_RESULT_ENTRY, |entry| {
entry.string("uid=alice,ou=people,dc=example,dc=test");
entry.sequence(|attributes| {
attributes.sequence(|attribute| {
attribute.string("cn");
attribute.set(|values| {
values.octet_string(b"Alice Liddell");
});
});
attributes.sequence(|attribute| {
attribute.string("objectClass");
attribute.set(|values| {
values.octet_string(b"top");
values.octet_string(b"inetOrgPerson");
});
});
});
});
});
let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
let ProtocolOp::SearchResultEntry(entry) = &message.op else {
panic!("expected an entry, got {}", message.op.name());
};
assert_eq!(entry.dn, "uid=alice,ou=people,dc=example,dc=test");
assert_eq!(entry.value("cn"), Some("Alice Liddell"));
assert_eq!(entry.value("CN"), Some("Alice Liddell"));
assert_eq!(entry.values("objectclass"), vec!["top", "inetOrgPerson"]);
assert_eq!(entry.value("mail"), None);
}
#[test]
fn a_referral_in_a_result_is_read_and_not_followed() {
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(6);
body.constructed(tags::SEARCH_RESULT_DONE, |fields| {
fields.enumerated(10);
fields.string("dc=example,dc=test");
fields.string("");
fields.constructed(ber::context(3, true), |referral| {
referral.string("ldap://other.example.test/dc=example,dc=test");
});
});
});
let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
let ProtocolOp::SearchResultDone(result) = &message.op else {
panic!("expected a search result");
};
assert_eq!(result.code, ResultCode::Referral);
assert_eq!(result.referrals, vec!["ldap://other.example.test/dc=example,dc=test"]);
assert_eq!(result.matched_dn, "dc=example,dc=test");
}
#[test]
fn controls_round_trip_on_a_request_and_a_response() {
let operation = Operation::search(&SearchRequest::new("", Filter::present("objectClass")))
.with_control(Control::new("1.2.840.113556.1.4.319").critical(true).value(vec![1, 2]));
let bytes = operation.encode(9);
let parsed = LdapMessage::parse(&bytes).unwrap();
assert_eq!(parsed.id, 9);
assert!(matches!(parsed.op, ProtocolOp::Unrecognised { .. }));
assert_eq!(parsed.controls.len(), 1);
assert_eq!(parsed.controls[0].oid, "1.2.840.113556.1.4.319");
assert!(parsed.controls[0].critical);
assert_eq!(parsed.controls[0].value, Some(vec![1, 2]));
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(9);
body.constructed(tags::SEARCH_RESULT_DONE, |fields| {
fields.enumerated(0);
fields.string("");
fields.string("");
});
body.constructed(ber::context(0, true), |list| {
list.sequence(|control| {
control.string("1.2.840.113556.1.4.319");
control.octet_string(&[7]);
});
});
});
let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
assert_eq!(message.controls.len(), 1);
assert_eq!(message.controls[0].oid, "1.2.840.113556.1.4.319");
assert!(!message.controls[0].critical, "criticality DEFAULT FALSE when absent");
assert_eq!(message.controls[0].value, Some(vec![7]));
}
#[test]
fn an_operation_this_client_never_asked_for_is_named_not_guessed_at() {
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(1);
body.constructed(ber::application(7, true), |fields| {
fields.enumerated(0);
fields.string("");
fields.string("");
});
});
let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
assert!(matches!(message.op, ProtocolOp::Unrecognised { tag: 0x67 }));
assert!(message.op.result().is_none());
}
#[test]
fn a_truncated_or_corrupt_message_is_an_error_and_never_a_panic() {
let mut encoder = Encoder::new();
encoder.sequence(|body| {
body.integer(1);
body.constructed(tags::BIND_RESPONSE, |fields| {
fields.enumerated(0);
fields.string("");
fields.string("");
});
});
let complete = encoder.into_bytes();
for length in 0..complete.len() {
assert!(
LdapMessage::parse(&complete[..length]).is_err(),
"a {length} byte prefix should not parse"
);
}
assert!(LdapMessage::parse(&complete).is_ok());
let mut extra = complete.clone();
extra.push(0x00);
assert!(LdapMessage::parse(&extra).is_err());
}
#[test]
fn result_codes_round_trip_and_name_themselves() {
for code in [0i64, 1, 32, 49, 50, 53, 80] {
assert_eq!(ResultCode::from_i64(code).as_i64(), code);
}
assert_eq!(ResultCode::from_i64(49), ResultCode::InvalidCredentials);
assert_eq!(ResultCode::from_i64(32), ResultCode::NoSuchObject);
assert_eq!(ResultCode::from_i64(4242), ResultCode::Unknown(4242));
assert_eq!(ResultCode::InvalidCredentials.to_string(), "invalidCredentials (49)");
assert_eq!(ResultCode::Unknown(4242).as_i64(), 4242);
assert!(ResultCode::Success.is_success());
}
#[test]
fn a_failed_result_explains_itself_in_words() {
let result = LdapResult {
code: ResultCode::NoSuchObject,
matched_dn: "dc=example,dc=test".into(),
diagnostic: "no such object".into(),
referrals: Vec::new(),
};
let error = result.into_result("search").unwrap_err().to_string();
assert!(error.contains("noSuchObject (32)"), "got {error}");
assert!(error.contains("no such object"), "got {error}");
assert!(error.contains("dc=example,dc=test"), "the matched DN is the clue: {error}");
}
}