1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
use std::borrow::Cow;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};

use crate::filter::Unescaper;
use crate::result::{LdapError, Result};
use crate::search::Scope;

use percent_encoding::percent_decode_str;
use url::Url;

/// Escape a filter literal.
///
/// Literal values appearing in an LDAP filter can contain any character,
/// but some characters (parentheses, asterisk, backslash, NUL) must be
/// escaped in the filter's string representation. This function does the
/// escaping.
///
/// The argument, `lit`, can be owned or borrowed. The function doesn't
/// allocate the return value unless there's need to escape the input.
pub fn ldap_escape<'a, S: Into<Cow<'a, str>>>(lit: S) -> Cow<'a, str> {
    #[inline]
    fn needs_escape(c: u8) -> bool {
        c == b'\\' || c == b'*' || c == b'(' || c == b')' || c == 0
    }

    #[inline]
    fn xdigit(c: u8) -> u8 {
        c + if c < 10 { b'0' } else { b'a' - 10 }
    }

    let lit = lit.into();
    let mut output = None;
    for (i, &c) in lit.as_bytes().iter().enumerate() {
        if needs_escape(c) {
            if output.is_none() {
                output = Some(Vec::with_capacity(lit.len() + 12)); // guess: up to 4 escaped chars
                output.as_mut().unwrap().extend(lit[..i].as_bytes());
            }
            let output = output.as_mut().unwrap();
            output.push(b'\\');
            output.push(xdigit(c >> 4));
            output.push(xdigit(c & 0xF));
        } else if let Some(ref mut output) = output {
            output.push(c);
        }
    }
    if let Some(output) = output {
        Cow::Owned(String::from_utf8(output).expect("ldap escaped"))
    } else {
        lit
    }
}

/// Escape an attribute value in a relative distinguished name (RDN).
///
/// When a literal string is used to represent an attribute value in an RDN,
/// some of its characters might need to be escaped according to the rules
/// of [RFC 4514](https://tools.ietf.org/html/rfc4514).
///
/// The function is named `dn_escape()` instead of `rdn_escape()` because of
/// a long-standing association of its intended use with the handling of DNs.
///
/// The argument, `val`, can be owned or borrowed. The function doesn't
/// allocate the return value unless there's need to escape the input.
pub fn dn_escape<'a, S: Into<Cow<'a, str>>>(val: S) -> Cow<'a, str> {
    #[inline]
    fn always_escape(c: u8) -> bool {
        c == b'"'
            || c == b'+'
            || c == b','
            || c == b';'
            || c == b'<'
            || c == b'='
            || c == b'>'
            || c == b'\\'
            || c == 0
    }

    #[inline]
    fn escape_leading(c: u8) -> bool {
        c == b' ' || c == b'#'
    }

    #[inline]
    fn escape_trailing(c: u8) -> bool {
        c == b' '
    }

    #[inline]
    fn xdigit(c: u8) -> u8 {
        c + if c < 10 { b'0' } else { b'a' - 10 }
    }

    let val = val.into();
    let mut output = None;
    for (i, &c) in val.as_bytes().iter().enumerate() {
        if always_escape(c)
            || i == 0 && escape_leading(c)
            || i + 1 == val.len() && escape_trailing(c)
        {
            if output.is_none() {
                output = Some(Vec::with_capacity(val.len() + 12)); // guess: up to 4 escaped chars
                output.as_mut().unwrap().extend(val[..i].as_bytes());
            }
            let output = output.as_mut().unwrap();
            output.push(b'\\');
            output.push(xdigit(c >> 4));
            output.push(xdigit(c & 0xF));
        } else if let Some(ref mut output) = output {
            output.push(c);
        }
    }
    if let Some(output) = output {
        Cow::Owned(String::from_utf8(output).expect("dn escaped"))
    } else {
        val
    }
}

/// LDAP URL extensions.
///
/// Historically, very few extensions have been described in the LDAP standards,
/// and extension support is very library- and application-specific. This crate
/// recognizes two widely implemented extensions (__bindname__ and __x-bindpw__),
/// as well as several experimental ones.
#[derive(Clone, Debug)]
pub enum LdapUrlExt<'a> {
    /// __Bindname__, the DN for the Simple Bind operation. Originally specified in RFC 2256,
    /// but dropped from its successor, RFC 4516 ("lack of known implementations").
    Bindname(Cow<'a, str>),

    /// __X-bindpw__, the password for Simple Bind. Never standardized, and not recommended
    /// because of security implications.
    XBindpw(Cow<'a, str>), // draft-hedstrom-dhc-ldap-02

    /// __1.3.6.1.4.1.10094.1.5.1__, experimental.
    Credentials(Cow<'a, str>),

    /// __1.3.6.1.4.1.10094.1.5.2__, experimental.
    SaslMech(Cow<'a, str>),

    /// __1.3.6.1.4.1.1466.20037__, StartTLS extended operation. Has no value. Should signal
    /// to the application to use StartTLS when connecting.
    StartTLS,

    /// Unknown extension.
    Unknown(Cow<'a, str>),
}

impl<'a> PartialEq for LdapUrlExt<'a> {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (LdapUrlExt::Bindname(_), LdapUrlExt::Bindname(_)) => true,
            (LdapUrlExt::XBindpw(_), LdapUrlExt::XBindpw(_)) => true,
            (LdapUrlExt::Credentials(_), LdapUrlExt::Credentials(_)) => true,
            (LdapUrlExt::SaslMech(_), LdapUrlExt::SaslMech(_)) => true,
            (LdapUrlExt::StartTLS, LdapUrlExt::StartTLS) => true,
            (LdapUrlExt::Unknown(_), LdapUrlExt::Unknown(_)) => true,
            _ => false,
        }
    }
}

impl<'a> Eq for LdapUrlExt<'a> {}

impl<'a> Hash for LdapUrlExt<'a> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            LdapUrlExt::Bindname(_) => "Bindname".hash(state),
            LdapUrlExt::XBindpw(_) => "XBindpw".hash(state),
            LdapUrlExt::Credentials(_) => "Credentials".hash(state),
            LdapUrlExt::SaslMech(_) => "SaslMech".hash(state),
            LdapUrlExt::StartTLS => "StartTLS".hash(state),
            LdapUrlExt::Unknown(_) => "Unknown".hash(state),
        }
    }
}

/// Parameters of an LDAP URL.
///
/// The LDAP URL specification describes a number of optional URL parameters,
/// contained in the query part, which mostly provide Search operation settings.
/// Additionally, the URL can have a list of extensions, describing further options.
///
/// When populating the `extensions` set, extension value is ignored in hashing and
/// comparisons, meaning that only a single extension instance can be recognized.
/// Searching the set for a value-bearing variant should be done with an empty value:
///
/// ```rust
/// # use ldap3::{get_url_params, LdapUrlExt};
/// # use ldap3::result::Result;
/// # use url::Url;
/// # fn main() -> Result<()> {
/// let url = Url::parse("ldapi://%2fvar%2frun%2fldapi/????1.3.6.1.4.1.10094.1.5.2=EXTERNAL")?;
/// let params = get_url_params(&url)?;
/// let mech = match params.extensions.get(&LdapUrlExt::SaslMech("".into())) {
///     Some(&LdapUrlExt::SaslMech(ref val)) => val.as_ref(),
///     _ => "",
/// };
/// assert_eq!(mech, "EXTERNAL");
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug)]
pub struct LdapUrlParams<'a> {
    /// Search base, percent-decoded.
    pub base: Cow<'a, str>,

    /// Attribute list, returned as `*` (all attributes) if missing.
    pub attrs: Vec<&'a str>,

    /// Search scope, returned as `Scope::Subtree` if missing.
    pub scope: Scope,

    /// Filter string, percent-decoded.
    pub filter: Cow<'a, str>,

    /// Extensions, whose values are percent-decoded.
    pub extensions: HashSet<LdapUrlExt<'a>>,
}

#[inline]
fn ascii_lc_equal(s: &str, t: &str) -> bool {
    if s.len() != t.len() {
        return false;
    }
    s.as_bytes()
        .iter()
        .zip(t.as_bytes().iter().map(u8::to_ascii_lowercase))
        .all(|(&s, t)| s == t)
}

/// Extract parameters from an LDAP URL.
pub fn get_url_params(url: &Url) -> Result<LdapUrlParams<'_>> {
    let mut base = url.path();
    if base.chars().next().unwrap_or('\0') == '/' {
        base = &base[1..];
    }
    let base = percent_decode_str(base)
        .decode_utf8()
        .map_err(|_| LdapError::DecodingUTF8)?;
    let mut query = url.query().unwrap_or("").splitn(4, '?');
    let attrs = match query.next() {
        Some("") | None => vec!["*"],
        Some(alist) => alist.split(',').collect(),
    };
    let scope = match query.next() {
        Some("") | None => Scope::Subtree,
        Some(scope_str) => match scope_str {
            "base" => Scope::Base,
            "one" => Scope::OneLevel,
            "sub" => Scope::Subtree,
            any => return Err(LdapError::InvalidScopeString(any.into())),
        },
    };
    let filter = match query.next() {
        Some("") | None => "(objectClass=*)",
        Some(filter) => filter,
    };
    let filter = percent_decode_str(filter)
        .decode_utf8()
        .map_err(|_| LdapError::DecodingUTF8)?;
    let extensions = match query.next() {
        Some("") | None => HashSet::new(),
        Some(exts) => {
            let mut ext_set = HashSet::new();
            for ext in exts.split(',') {
                let (crit, id, val) = {
                    let mut crit = false;
                    let mut idv = ext.splitn(2, '=');
                    let mut id = idv.next().unwrap_or("");
                    if id != "" && &id[..1] == "!" {
                        id = &id[1..];
                        crit = true;
                    }
                    let val = idv.next();
                    (
                        crit,
                        id,
                        percent_decode_str(val.unwrap_or(""))
                            .decode_utf8()
                            .map_err(|_| LdapError::DecodingUTF8)?,
                    )
                };
                let ext = match id {
                    "1.3.6.1.4.1.10094.1.5.1" => LdapUrlExt::Credentials(val),
                    "1.3.6.1.4.1.10094.1.5.2" => LdapUrlExt::SaslMech(val),
                    "1.3.6.1.4.1.1466.20037" => LdapUrlExt::StartTLS,
                    ext => {
                        if ascii_lc_equal("bindname", ext) {
                            LdapUrlExt::Bindname(val)
                        } else if ascii_lc_equal("x-bindpw", ext) {
                            LdapUrlExt::XBindpw(val)
                        } else if crit {
                            return Err(LdapError::UnrecognizedCriticalExtension(format!(
                                "{:?}",
                                LdapUrlExt::Unknown(ext.into())
                            )));
                        } else {
                            LdapUrlExt::Unknown("".into())
                        }
                    }
                };
                if ext != LdapUrlExt::Unknown("".into()) {
                    ext_set.insert(ext);
                }
            }
            ext_set
        }
    };
    Ok(LdapUrlParams {
        base,
        attrs,
        scope,
        filter,
        extensions,
    })
}

/// Unescape a string using LDAP filter escapes.
///
/// If a string contains `\nn` hexadecimal escapes, return a string where those
/// escapes are turned back into characters they represent. The result must be
/// a valid UTF-8 string, otherwise an error is returned.
pub fn ldap_str_unescape<'a, S: Into<Cow<'a, str>>>(val: S) -> Result<Cow<'a, str>> {
    let val = val.into();
    let mut output = None;
    let mut esc = Unescaper::Value(0);
    for (i, &c) in val.as_bytes().iter().enumerate() {
        esc = esc.feed(c);
        match esc {
            Unescaper::WantFirst => {
                if output.is_none() {
                    output = Some(Vec::with_capacity(val.len() + 12)); // guess: up to 4 escaped chars
                    output.as_mut().unwrap().extend(val[..i].as_bytes());
                }
            }
            Unescaper::Value(c) => {
                if output.is_some() {
                    output.as_mut().unwrap().push(c);
                }
            }
            _ => (),
        }
    }
    if output.is_some() {
        if let Unescaper::Value(_) = esc {
            Ok(Cow::Owned(
                String::from_utf8(output.unwrap()).map_err(|_| LdapError::DecodingUTF8)?,
            ))
        } else {
            Err(LdapError::DecodingUTF8)
        }
    } else {
        Ok(val)
    }
}

#[cfg(test)]
mod test {
    use super::dn_escape;

    #[test]
    fn dn_esc_leading_space() {
        assert_eq!(dn_escape(" foo"), "\\20foo");
    }

    #[test]
    fn dn_esc_trailing_space() {
        assert_eq!(dn_escape("foo "), "foo\\20");
    }

    #[test]
    fn dn_esc_inner_space() {
        assert_eq!(dn_escape("f o o"), "f o o");
    }

    #[test]
    fn dn_esc_single_space() {
        assert_eq!(dn_escape(" "), "\\20");
    }

    #[test]
    fn dn_esc_two_spaces() {
        assert_eq!(dn_escape("  "), "\\20\\20");
    }

    #[test]
    fn dn_esc_three_spaces() {
        assert_eq!(dn_escape("   "), "\\20 \\20");
    }

    #[test]
    fn dn_esc_leading_hash() {
        assert_eq!(dn_escape("#rust"), "\\23rust");
    }
}