Skip to main content

ldap_client_proto/
message.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::fmt;
4
5use ldap_client_ber::tag::Tag;
6use ldap_client_ber::{BerReader, BerWriter, Class};
7use zeroize::Zeroizing;
8
9use crate::ProtoError;
10use crate::controls::Control;
11use crate::filter::Filter;
12use crate::result_code::ResultCode;
13
14/// LDAP message IDs are positive i32 values. 0 is reserved for unsolicited notifications.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct MessageId(pub i32);
17
18/// Top-level LDAP PDU (RFC 4511 ยง4.1.1).
19#[derive(Debug, Clone)]
20pub struct LdapMessage {
21    pub message_id: MessageId,
22    pub operation: LdapOperation,
23    pub controls: Vec<Control>,
24}
25
26#[derive(Debug, Clone)]
27pub enum LdapOperation {
28    BindRequest(BindRequest),
29    BindResponse(BindResponse),
30    UnbindRequest,
31    SearchRequest(SearchRequest),
32    SearchResultEntry(SearchResultEntry),
33    SearchResultDone(LdapResult),
34    SearchResultReference(Vec<String>),
35    ModifyRequest(ModifyRequest),
36    ModifyResponse(LdapResult),
37    AddRequest(AddRequest),
38    AddResponse(LdapResult),
39    DeleteRequest(String),
40    DeleteResponse(LdapResult),
41    ModifyDnRequest(ModifyDnRequest),
42    ModifyDnResponse(LdapResult),
43    CompareRequest(CompareRequest),
44    CompareResponse(LdapResult),
45    AbandonRequest(MessageId),
46    ExtendedRequest(ExtendedRequest),
47    ExtendedResponse(ExtendedResponse),
48    IntermediateResponse(IntermediateResponse),
49}
50
51// Application tags from RFC 4511.
52const APP_BIND_REQUEST: u32 = 0;
53const APP_BIND_RESPONSE: u32 = 1;
54const APP_UNBIND_REQUEST: u32 = 2;
55const APP_SEARCH_REQUEST: u32 = 3;
56const APP_SEARCH_RESULT_ENTRY: u32 = 4;
57const APP_SEARCH_RESULT_DONE: u32 = 5;
58const APP_MODIFY_REQUEST: u32 = 6;
59const APP_MODIFY_RESPONSE: u32 = 7;
60const APP_ADD_REQUEST: u32 = 8;
61const APP_ADD_RESPONSE: u32 = 9;
62const APP_DEL_REQUEST: u32 = 10;
63const APP_DEL_RESPONSE: u32 = 11;
64const APP_MODIFY_DN_REQUEST: u32 = 12;
65const APP_MODIFY_DN_RESPONSE: u32 = 13;
66const APP_COMPARE_REQUEST: u32 = 14;
67const APP_COMPARE_RESPONSE: u32 = 15;
68const APP_ABANDON_REQUEST: u32 = 16;
69const APP_SEARCH_RESULT_REFERENCE: u32 = 19;
70const APP_EXTENDED_REQUEST: u32 = 23;
71const APP_EXTENDED_RESPONSE: u32 = 24;
72const APP_INTERMEDIATE_RESPONSE: u32 = 25;
73
74// --- Request/Response types ---
75
76#[derive(Debug, Clone)]
77pub struct BindRequest {
78    pub version: i64,
79    pub name: String,
80    pub authentication: BindAuthentication,
81}
82
83#[derive(Clone)]
84pub enum BindAuthentication {
85    Simple(Zeroizing<Vec<u8>>),
86    Sasl {
87        mechanism: String,
88        credentials: Option<Zeroizing<Vec<u8>>>,
89    },
90}
91
92impl fmt::Debug for BindAuthentication {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::Simple(_) => f.debug_tuple("Simple").field(&"[REDACTED]").finish(),
96            Self::Sasl { mechanism, .. } => f
97                .debug_struct("Sasl")
98                .field("mechanism", mechanism)
99                .field("credentials", &"[REDACTED]")
100                .finish(),
101        }
102    }
103}
104
105#[derive(Debug, Clone)]
106pub struct BindResponse {
107    pub result: LdapResult,
108    pub server_sasl_creds: Option<Vec<u8>>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct LdapResult {
113    pub code: ResultCode,
114    pub matched_dn: String,
115    pub diagnostic_message: String,
116    pub referral: Vec<String>,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum SearchScope {
121    BaseObject = 0,
122    SingleLevel = 1,
123    WholeSubtree = 2,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum DerefAliases {
128    NeverDerefAliases = 0,
129    DerefInSearching = 1,
130    DerefFindingBaseObj = 2,
131    DerefAlways = 3,
132}
133
134#[derive(Debug, Clone)]
135pub struct SearchRequest {
136    pub base_dn: String,
137    pub scope: SearchScope,
138    pub deref_aliases: DerefAliases,
139    pub size_limit: i32,
140    pub time_limit: i32,
141    pub types_only: bool,
142    pub filter: Filter,
143    pub attributes: Vec<String>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct SearchResultEntry {
148    pub dn: String,
149    pub attributes: Vec<PartialAttribute>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct PartialAttribute {
154    pub name: String,
155    pub values: Vec<Vec<u8>>,
156}
157
158impl PartialAttribute {
159    pub fn string_values(&self) -> Vec<&str> {
160        self.values
161            .iter()
162            .filter_map(|v| std::str::from_utf8(v).ok())
163            .collect()
164    }
165
166    pub fn first_string_value(&self) -> Option<&str> {
167        self.values
168            .first()
169            .and_then(|v| std::str::from_utf8(v).ok())
170    }
171
172    pub fn first_value(&self) -> Option<&[u8]> {
173        self.values.first().map(|v| v.as_slice())
174    }
175}
176
177impl SearchResultEntry {
178    pub fn attr(&self, name: &str) -> Option<&PartialAttribute> {
179        self.attributes
180            .iter()
181            .find(|a| a.name.eq_ignore_ascii_case(name))
182    }
183
184    pub fn first_value(&self, attr: &str) -> Option<&[u8]> {
185        self.attr(attr).and_then(|a| a.first_value())
186    }
187
188    pub fn first_string_value(&self, attr: &str) -> Option<&str> {
189        self.attr(attr).and_then(|a| a.first_string_value())
190    }
191}
192
193#[derive(Debug, Clone)]
194pub struct CompareRequest {
195    pub dn: String,
196    pub attr: String,
197    pub value: Vec<u8>,
198}
199
200#[derive(Debug, Clone)]
201pub struct AddRequest {
202    pub dn: String,
203    pub attributes: Vec<PartialAttribute>,
204}
205
206#[derive(Debug, Clone)]
207pub struct ModifyRequest {
208    pub dn: String,
209    pub changes: Vec<Modification>,
210}
211
212#[derive(Debug, Clone)]
213pub struct Modification {
214    pub operation: ModifyOperation,
215    pub attribute: PartialAttribute,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum ModifyOperation {
220    Add = 0,
221    Delete = 1,
222    Replace = 2,
223    Increment = 3,
224}
225
226#[derive(Debug, Clone)]
227pub struct ModifyDnRequest {
228    pub dn: String,
229    pub new_rdn: String,
230    pub delete_old_rdn: bool,
231    pub new_superior: Option<String>,
232}
233
234#[derive(Debug, Clone)]
235pub struct ExtendedRequest {
236    pub oid: String,
237    pub value: Option<Vec<u8>>,
238}
239
240#[derive(Debug, Clone)]
241pub struct ExtendedResponse {
242    pub result: LdapResult,
243    pub oid: Option<String>,
244    pub value: Option<Vec<u8>>,
245}
246
247#[derive(Debug, Clone)]
248pub struct IntermediateResponse {
249    pub oid: Option<String>,
250    pub value: Option<Vec<u8>>,
251}
252
253pub trait HasLdapResult {
254    fn result(&self) -> &LdapResult;
255
256    fn is_success(&self) -> bool {
257        self.result().code.is_success()
258    }
259
260    fn is_referral(&self) -> bool {
261        self.result().code.is_referral()
262    }
263
264    fn referral_urls(&self) -> &[String] {
265        &self.result().referral
266    }
267}
268
269impl HasLdapResult for LdapResult {
270    fn result(&self) -> &LdapResult {
271        self
272    }
273}
274
275impl HasLdapResult for BindResponse {
276    fn result(&self) -> &LdapResult {
277        &self.result
278    }
279}
280
281impl HasLdapResult for ExtendedResponse {
282    fn result(&self) -> &LdapResult {
283        &self.result
284    }
285}
286
287// --- Encoding ---
288
289impl LdapMessage {
290    pub fn encode(&self) -> Vec<u8> {
291        let mut w = BerWriter::new();
292        w.write_sequence(Tag::sequence(), |msg| {
293            msg.write_integer(self.message_id.0 as i64);
294            encode_operation(msg, &self.operation);
295            if !self.controls.is_empty() {
296                crate::controls::encode_controls(msg, &self.controls);
297            }
298        });
299        w.into_bytes()
300    }
301
302    pub fn decode(data: &[u8]) -> Result<Self, ProtoError> {
303        let mut r = BerReader::new(data);
304        let (message_id, op_tag, op_value, controls) =
305            r.read_sequence_lax(Tag::sequence(), |msg| {
306                let raw_id = msg.read_integer()?;
307                if raw_id < 0 || raw_id > i32::MAX as i64 {
308                    return Err(ldap_client_ber::BerError::InvalidInteger);
309                }
310                let message_id = MessageId(raw_id as i32);
311                let (tag, value) = msg.read_element()?;
312                let op_value = value.to_vec();
313
314                let mut controls = Vec::new();
315                if !msg.is_empty() {
316                    let peek = msg.peek_tag()?;
317                    if peek.class == Class::Context && peek.number == 0 && peek.constructed {
318                        controls = crate::controls::decode_controls(msg)?;
319                    }
320                }
321
322                Ok((message_id, tag, op_value, controls))
323            })?;
324
325        let operation = decode_operation(op_tag, &op_value)?;
326
327        Ok(LdapMessage {
328            message_id,
329            operation,
330            controls,
331        })
332    }
333}
334
335fn encode_operation(w: &mut BerWriter, op: &LdapOperation) {
336    match op {
337        LdapOperation::BindRequest(req) => {
338            w.write_sequence(Tag::application(APP_BIND_REQUEST), |inner| {
339                inner.write_integer(req.version);
340                inner.write_bytes(req.name.as_bytes());
341                match &req.authentication {
342                    BindAuthentication::Simple(pw) => {
343                        inner.write_octet_string(Tag::context(0), pw);
344                    }
345                    BindAuthentication::Sasl {
346                        mechanism,
347                        credentials,
348                    } => {
349                        inner.write_sequence(Tag::context_constructed(3), |sasl| {
350                            sasl.write_bytes(mechanism.as_bytes());
351                            if let Some(creds) = credentials {
352                                sasl.write_bytes(creds);
353                            }
354                        });
355                    }
356                }
357            });
358        }
359        LdapOperation::UnbindRequest => {
360            // UnbindRequest is [APPLICATION 2] NULL-like (no content).
361            w.write_octet_string(Tag::application_primitive(APP_UNBIND_REQUEST), &[]);
362        }
363        LdapOperation::SearchRequest(req) => {
364            w.write_sequence(Tag::application(APP_SEARCH_REQUEST), |inner| {
365                inner.write_bytes(req.base_dn.as_bytes());
366                inner.write_enumerated(req.scope as i64);
367                inner.write_enumerated(req.deref_aliases as i64);
368                inner.write_integer(req.size_limit as i64);
369                inner.write_integer(req.time_limit as i64);
370                inner.write_boolean(req.types_only);
371                req.filter.encode(inner);
372                inner.write_sequence(Tag::sequence(), |attrs| {
373                    for attr in &req.attributes {
374                        attrs.write_bytes(attr.as_bytes());
375                    }
376                });
377            });
378        }
379        LdapOperation::CompareRequest(req) => {
380            w.write_sequence(Tag::application(APP_COMPARE_REQUEST), |inner| {
381                inner.write_bytes(req.dn.as_bytes());
382                inner.write_sequence(Tag::sequence(), |ava| {
383                    ava.write_bytes(req.attr.as_bytes());
384                    ava.write_bytes(&req.value);
385                });
386            });
387        }
388        LdapOperation::AddRequest(req) => {
389            w.write_sequence(Tag::application(APP_ADD_REQUEST), |inner| {
390                inner.write_bytes(req.dn.as_bytes());
391                inner.write_sequence(Tag::sequence(), |attrs| {
392                    for attr in &req.attributes {
393                        encode_partial_attribute(attrs, attr);
394                    }
395                });
396            });
397        }
398        LdapOperation::DeleteRequest(dn) => {
399            // DelRequest is [APPLICATION 10] LDAPDN (primitive octet string).
400            w.write_octet_string(Tag::application_primitive(APP_DEL_REQUEST), dn.as_bytes());
401        }
402        LdapOperation::ModifyRequest(req) => {
403            w.write_sequence(Tag::application(APP_MODIFY_REQUEST), |inner| {
404                inner.write_bytes(req.dn.as_bytes());
405                inner.write_sequence(Tag::sequence(), |changes| {
406                    for modification in &req.changes {
407                        changes.write_sequence(Tag::sequence(), |change| {
408                            change.write_enumerated(modification.operation as i64);
409                            encode_partial_attribute(change, &modification.attribute);
410                        });
411                    }
412                });
413            });
414        }
415        LdapOperation::ModifyDnRequest(req) => {
416            w.write_sequence(Tag::application(APP_MODIFY_DN_REQUEST), |inner| {
417                inner.write_bytes(req.dn.as_bytes());
418                inner.write_bytes(req.new_rdn.as_bytes());
419                inner.write_boolean(req.delete_old_rdn);
420                if let Some(sup) = &req.new_superior {
421                    inner.write_octet_string(Tag::context(0), sup.as_bytes());
422                }
423            });
424        }
425        LdapOperation::AbandonRequest(id) => {
426            // AbandonRequest is [APPLICATION 16] MessageID (implicit integer).
427            let bytes = ldap_client_ber::writer::encode_i64_bytes(id.0 as i64);
428            w.write_octet_string(Tag::application_primitive(APP_ABANDON_REQUEST), &bytes);
429        }
430        LdapOperation::ExtendedRequest(req) => {
431            w.write_sequence(Tag::application(APP_EXTENDED_REQUEST), |inner| {
432                inner.write_octet_string(Tag::context(0), req.oid.as_bytes());
433                if let Some(val) = &req.value {
434                    inner.write_octet_string(Tag::context(1), val);
435                }
436            });
437        }
438        _ => unreachable!("attempted to encode a response operation"),
439    }
440}
441
442fn encode_partial_attribute(w: &mut BerWriter, attr: &PartialAttribute) {
443    w.write_sequence(Tag::sequence(), |inner| {
444        inner.write_bytes(attr.name.as_bytes());
445        inner.write_sequence(Tag::set(), |vals| {
446            for val in &attr.values {
447                vals.write_bytes(val);
448            }
449        });
450    });
451}
452
453// --- Decoding ---
454
455fn to_utf8(bytes: &[u8]) -> Result<String, ldap_client_ber::BerError> {
456    std::str::from_utf8(bytes)
457        .map(|s| s.to_owned())
458        .map_err(|_| ldap_client_ber::BerError::InvalidUtf8)
459}
460
461fn decode_operation(tag: Tag, value: &[u8]) -> Result<LdapOperation, ProtoError> {
462    if tag.class != Class::Application {
463        return Err(ProtoError::Protocol(format!(
464            "expected APPLICATION tag, got {tag:?}"
465        )));
466    }
467
468    let mut r = BerReader::new(value);
469
470    match tag.number {
471        APP_BIND_RESPONSE => {
472            let result = decode_ldap_result(&mut r)?;
473            let server_sasl_creds = if !r.is_empty() {
474                let tag = r.peek_tag()?;
475                if tag.class == Class::Context && tag.number == 7 {
476                    Some(r.read_tagged_implicit_octet_string(7)?.to_vec())
477                } else {
478                    None
479                }
480            } else {
481                None
482            };
483            Ok(LdapOperation::BindResponse(BindResponse {
484                result,
485                server_sasl_creds,
486            }))
487        }
488        APP_SEARCH_RESULT_ENTRY => {
489            let dn = to_utf8(r.read_octet_string()?)?;
490            let mut attributes = Vec::new();
491            r.read_sequence(Tag::sequence(), |attrs| {
492                while !attrs.is_empty() {
493                    attrs.read_sequence(Tag::sequence(), |attr| {
494                        let name = to_utf8(attr.read_octet_string()?)?;
495                        let mut values = Vec::new();
496                        attr.read_sequence(Tag::set(), |vals| {
497                            while !vals.is_empty() {
498                                values.push(vals.read_octet_string()?.to_vec());
499                            }
500                            Ok(())
501                        })?;
502                        attributes.push(PartialAttribute { name, values });
503                        Ok(())
504                    })?;
505                }
506                Ok(())
507            })?;
508            Ok(LdapOperation::SearchResultEntry(SearchResultEntry {
509                dn,
510                attributes,
511            }))
512        }
513        APP_SEARCH_RESULT_DONE => {
514            let result = decode_ldap_result(&mut r)?;
515            Ok(LdapOperation::SearchResultDone(result))
516        }
517        APP_SEARCH_RESULT_REFERENCE => {
518            let mut urls = Vec::new();
519            while !r.is_empty() {
520                urls.push(to_utf8(r.read_octet_string()?)?);
521            }
522            Ok(LdapOperation::SearchResultReference(urls))
523        }
524        APP_MODIFY_RESPONSE => {
525            let result = decode_ldap_result(&mut r)?;
526            Ok(LdapOperation::ModifyResponse(result))
527        }
528        APP_ADD_RESPONSE => {
529            let result = decode_ldap_result(&mut r)?;
530            Ok(LdapOperation::AddResponse(result))
531        }
532        APP_DEL_RESPONSE => {
533            let result = decode_ldap_result(&mut r)?;
534            Ok(LdapOperation::DeleteResponse(result))
535        }
536        APP_MODIFY_DN_RESPONSE => {
537            let result = decode_ldap_result(&mut r)?;
538            Ok(LdapOperation::ModifyDnResponse(result))
539        }
540        APP_COMPARE_RESPONSE => {
541            let result = decode_ldap_result(&mut r)?;
542            Ok(LdapOperation::CompareResponse(result))
543        }
544        APP_EXTENDED_RESPONSE => {
545            let result = decode_ldap_result(&mut r)?;
546            let mut oid = None;
547            let mut ext_value = None;
548            while !r.is_empty() {
549                let tag = r.peek_tag()?;
550                match (tag.class, tag.number) {
551                    (Class::Context, 10) => {
552                        oid = Some(to_utf8(r.read_tagged_implicit_octet_string(10)?)?);
553                    }
554                    (Class::Context, 11) => {
555                        ext_value = Some(r.read_tagged_implicit_octet_string(11)?.to_vec());
556                    }
557                    _ => {
558                        r.read_element()?;
559                    }
560                }
561            }
562            Ok(LdapOperation::ExtendedResponse(ExtendedResponse {
563                result,
564                oid,
565                value: ext_value,
566            }))
567        }
568        APP_INTERMEDIATE_RESPONSE => {
569            let mut oid = None;
570            let mut value = None;
571            while !r.is_empty() {
572                let tag = r.peek_tag()?;
573                match (tag.class, tag.number) {
574                    (Class::Context, 0) => {
575                        oid = Some(to_utf8(r.read_tagged_implicit_octet_string(0)?)?);
576                    }
577                    (Class::Context, 1) => {
578                        value = Some(r.read_tagged_implicit_octet_string(1)?.to_vec());
579                    }
580                    _ => {
581                        r.read_element()?;
582                    }
583                }
584            }
585            Ok(LdapOperation::IntermediateResponse(IntermediateResponse {
586                oid,
587                value,
588            }))
589        }
590        n => Err(ProtoError::Protocol(format!(
591            "unknown application tag: {n}"
592        ))),
593    }
594}
595
596fn decode_ldap_result(r: &mut BerReader<'_>) -> Result<LdapResult, ProtoError> {
597    let code = ResultCode::from_i64(r.read_enumerated()?);
598    let matched_dn = to_utf8(r.read_octet_string()?)?;
599    // Diagnostic messages use lossy conversion: some servers produce non-UTF-8 here.
600    let diagnostic_message = String::from_utf8_lossy(r.read_octet_string()?).into_owned();
601
602    let mut referral = Vec::new();
603    if !r.is_empty() {
604        let tag = r.peek_tag()?;
605        if tag.class == Class::Context && tag.number == 3 {
606            r.read_sequence(Tag::context_constructed(3), |inner| {
607                while !inner.is_empty() {
608                    referral.push(to_utf8(inner.read_octet_string()?)?);
609                }
610                Ok(())
611            })?;
612        }
613    }
614
615    Ok(LdapResult {
616        code,
617        matched_dn,
618        diagnostic_message,
619        referral,
620    })
621}