mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0005 (20260522T043516Z-29b8)
This commit is contained in:
parent
d6e570ec51
commit
eacec70a13
3 changed files with 992 additions and 4 deletions
706
src/dynamic/stubs/ldap_ber.rs
Normal file
706
src/dynamic/stubs/ldap_ber.rs
Normal file
|
|
@ -0,0 +1,706 @@
|
|||
//! Minimal BER (ASN.1) reader/writer for LDAPv3 bind + search messages.
|
||||
//!
|
||||
//! The Phase 06 LDAP stub at [`super::ldap_server`] speaks a custom
|
||||
//! plaintext `SEARCH <filter>\n` / `COUNT <n>\n` framed-line protocol so
|
||||
//! per-language harnesses can drive it without linking a real LDAP
|
||||
//! client. The deferred work for that phase tracks "tier (b)" — a
|
||||
//! real LDAPv3 ASN.1 BER wire round-trip so a harness using
|
||||
//! `javax.naming.directory.InitialDirContext` (or any other stock LDAP
|
||||
//! client) can talk to the stub directly.
|
||||
//!
|
||||
//! This module is the unblocking primitive: a zero-dependency BER
|
||||
//! reader+writer that covers exactly the tags LDAPv3 bind +
|
||||
//! search-request + search-result-entry + search-result-done messages
|
||||
//! need. It deliberately rejects everything else so a malformed
|
||||
//! payload cannot exfiltrate state through the parser; rejection falls
|
||||
//! through to `None` and the caller short-circuits to the plaintext
|
||||
//! fallback path.
|
||||
//!
|
||||
//! # Scope
|
||||
//!
|
||||
//! Universal tags: `INTEGER` (0x02), `OCTET STRING` (0x04),
|
||||
//! `ENUMERATED` (0x0A), `SEQUENCE` (0x30).
|
||||
//!
|
||||
//! Application tags (LDAP RFC 4511 §4.1):
|
||||
//! `BindRequest` (0x60), `BindResponse` (0x61), `SearchRequest` (0x63),
|
||||
//! `SearchResultEntry` (0x64), `SearchResultDone` (0x65).
|
||||
//!
|
||||
//! Context-specific tags inside `Filter` (RFC 4511 §4.5.1):
|
||||
//! and [0], or [1], not [2], equalityMatch [3], substrings [4],
|
||||
//! greaterOrEqual [5], lessOrEqual [6], present [7], approxMatch [8].
|
||||
//! Plus simple-auth [0] inside `AuthenticationChoice`.
|
||||
//!
|
||||
//! Length encoding: short-form (single byte 0x00-0x7F) and long-form
|
||||
//! (0x81-0x84 length-of-length, value up to 32 bits). Indefinite
|
||||
//! length (0x80) is rejected — LDAP DER never uses it.
|
||||
//!
|
||||
//! Integer encoding: two's-complement, big-endian, minimum-byte form
|
||||
//! (LDAP integers are non-negative `MessageID` / version / result-code
|
||||
//! values, but the decoder accepts the full two's-complement range so
|
||||
//! a hand-rolled client that emits leading zero bytes still parses).
|
||||
//!
|
||||
//! # Filter rendering
|
||||
//!
|
||||
//! The decoded `SearchRequest` filter is re-rendered into the
|
||||
//! RFC 4515 string syntax (`(uid=alice)`, `(|(uid=alice)(uid=*))`) so
|
||||
//! the existing [`super::ldap_server::LdapStub::evaluate`] subset
|
||||
//! matcher consumes it without a parallel evaluator. Only the four
|
||||
//! filter shapes the matcher already covers are rendered; anything
|
||||
//! richer (`>=`, `<=`, `~=`, `not`) collapses to `*` so an exotic
|
||||
//! adversarial payload over-matches rather than zero-matches.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
/// LDAPv3 BER tag bytes the stub recognises.
|
||||
pub mod tags {
|
||||
/// Universal primitive integer (RFC 4511 §5).
|
||||
pub const INTEGER: u8 = 0x02;
|
||||
/// Universal primitive octet string.
|
||||
pub const OCTET_STRING: u8 = 0x04;
|
||||
/// Universal primitive enumerated.
|
||||
pub const ENUMERATED: u8 = 0x0A;
|
||||
/// Universal constructed sequence.
|
||||
pub const SEQUENCE: u8 = 0x30;
|
||||
|
||||
/// `BindRequest` `[APPLICATION 0]` constructed (RFC 4511 §4.2).
|
||||
pub const BIND_REQUEST: u8 = 0x60;
|
||||
/// `BindResponse` `[APPLICATION 1]` constructed.
|
||||
pub const BIND_RESPONSE: u8 = 0x61;
|
||||
/// `SearchRequest` `[APPLICATION 3]` constructed.
|
||||
pub const SEARCH_REQUEST: u8 = 0x63;
|
||||
/// `SearchResultEntry` `[APPLICATION 4]` constructed.
|
||||
pub const SEARCH_RESULT_ENTRY: u8 = 0x64;
|
||||
/// `SearchResultDone` `[APPLICATION 5]` constructed.
|
||||
pub const SEARCH_RESULT_DONE: u8 = 0x65;
|
||||
|
||||
/// `simple` `[0]` primitive OCTET STRING inside
|
||||
/// `AuthenticationChoice`.
|
||||
pub const AUTH_SIMPLE: u8 = 0x80;
|
||||
|
||||
/// Filter `and` `[0]` constructed SET.
|
||||
pub const FILTER_AND: u8 = 0xA0;
|
||||
/// Filter `or` `[1]` constructed SET.
|
||||
pub const FILTER_OR: u8 = 0xA1;
|
||||
/// Filter `not` `[2]` constructed wrapper.
|
||||
pub const FILTER_NOT: u8 = 0xA2;
|
||||
/// Filter `equalityMatch` `[3]` constructed
|
||||
/// `AttributeValueAssertion`.
|
||||
pub const FILTER_EQUALITY: u8 = 0xA3;
|
||||
/// Filter `substrings` `[4]` constructed.
|
||||
pub const FILTER_SUBSTRINGS: u8 = 0xA4;
|
||||
/// Filter `present` `[7]` primitive `AttributeDescription`.
|
||||
pub const FILTER_PRESENT: u8 = 0x87;
|
||||
|
||||
/// Substring `initial` `[0]` primitive.
|
||||
pub const SUBSTR_INITIAL: u8 = 0x80;
|
||||
/// Substring `any` `[1]` primitive.
|
||||
pub const SUBSTR_ANY: u8 = 0x81;
|
||||
/// Substring `final` `[2]` primitive.
|
||||
pub const SUBSTR_FINAL: u8 = 0x82;
|
||||
}
|
||||
|
||||
/// Decoded TLV view. `body` is borrowed from the source buffer; the
|
||||
/// caller never has to allocate during parsing.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Tlv<'a> {
|
||||
/// Raw tag byte. Match against [`tags`] constants.
|
||||
pub tag: u8,
|
||||
/// The value-octets slice (length-prefix already stripped).
|
||||
pub body: &'a [u8],
|
||||
/// Offset into the source buffer immediately after this TLV.
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
/// Read a single TLV starting at `offset` in `buf`. Returns `None`
|
||||
/// when the buffer is too short, the length is indefinite (0x80), or
|
||||
/// the long-form length-of-length exceeds 4 bytes (>4 GiB messages are
|
||||
/// out of scope for the in-process stub).
|
||||
pub fn read_tlv(buf: &[u8], offset: usize) -> Option<Tlv<'_>> {
|
||||
if offset >= buf.len() {
|
||||
return None;
|
||||
}
|
||||
let tag = buf[offset];
|
||||
let first_len = *buf.get(offset + 1)?;
|
||||
let (length, length_consumed) = if first_len & 0x80 == 0 {
|
||||
(first_len as usize, 1usize)
|
||||
} else {
|
||||
let length_of_length = (first_len & 0x7F) as usize;
|
||||
if length_of_length == 0 || length_of_length > 4 {
|
||||
// 0x80 is indefinite length; >4 bytes is too long for the
|
||||
// in-process stub.
|
||||
return None;
|
||||
}
|
||||
let len_start = offset + 2;
|
||||
let len_end = len_start + length_of_length;
|
||||
if len_end > buf.len() {
|
||||
return None;
|
||||
}
|
||||
let mut acc: usize = 0;
|
||||
for &b in &buf[len_start..len_end] {
|
||||
acc = (acc << 8) | (b as usize);
|
||||
}
|
||||
(acc, 1 + length_of_length)
|
||||
};
|
||||
let body_start = offset + 1 + length_consumed;
|
||||
let body_end = body_start.checked_add(length)?;
|
||||
if body_end > buf.len() {
|
||||
return None;
|
||||
}
|
||||
Some(Tlv {
|
||||
tag,
|
||||
body: &buf[body_start..body_end],
|
||||
end: body_end,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode an `INTEGER` value-octets slice into an `i64`. Rejects
|
||||
/// inputs longer than 8 bytes — LDAP versions, message IDs, and result
|
||||
/// codes all fit in 32 bits.
|
||||
pub fn decode_integer(body: &[u8]) -> Option<i64> {
|
||||
if body.is_empty() || body.len() > 8 {
|
||||
return None;
|
||||
}
|
||||
let sign_extend: i64 = if body[0] & 0x80 != 0 { -1 } else { 0 };
|
||||
let mut acc: i64 = sign_extend;
|
||||
for &b in body {
|
||||
acc = (acc << 8) | (b as i64 & 0xFF);
|
||||
}
|
||||
Some(acc)
|
||||
}
|
||||
|
||||
/// Append an `INTEGER` TLV to `out`. Minimum-byte two's-complement
|
||||
/// encoding.
|
||||
pub fn write_integer(out: &mut Vec<u8>, n: i64) {
|
||||
let mut bytes = n.to_be_bytes().to_vec();
|
||||
while bytes.len() > 1
|
||||
&& ((bytes[0] == 0x00 && bytes[1] & 0x80 == 0)
|
||||
|| (bytes[0] == 0xFF && bytes[1] & 0x80 != 0))
|
||||
{
|
||||
bytes.remove(0);
|
||||
}
|
||||
write_tlv(out, tags::INTEGER, &bytes);
|
||||
}
|
||||
|
||||
/// Append an `ENUMERATED` TLV to `out`. Single-byte encoding (LDAP
|
||||
/// scope / result-code values all fit in one byte).
|
||||
pub fn write_enumerated(out: &mut Vec<u8>, n: u8) {
|
||||
write_tlv(out, tags::ENUMERATED, &[n]);
|
||||
}
|
||||
|
||||
/// Append an `OCTET STRING` TLV to `out`.
|
||||
pub fn write_octet_string(out: &mut Vec<u8>, s: &[u8]) {
|
||||
write_tlv(out, tags::OCTET_STRING, s);
|
||||
}
|
||||
|
||||
/// Append a TLV with arbitrary tag + body to `out`. Encodes length in
|
||||
/// short-form when `body.len() < 128`; long-form otherwise.
|
||||
pub fn write_tlv(out: &mut Vec<u8>, tag: u8, body: &[u8]) {
|
||||
out.push(tag);
|
||||
write_length(out, body.len());
|
||||
out.extend_from_slice(body);
|
||||
}
|
||||
|
||||
fn write_length(out: &mut Vec<u8>, len: usize) {
|
||||
if len < 0x80 {
|
||||
out.push(len as u8);
|
||||
return;
|
||||
}
|
||||
let mut bytes: Vec<u8> = Vec::with_capacity(5);
|
||||
let mut n = len;
|
||||
while n != 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
out.push(0x80 | bytes.len() as u8);
|
||||
out.extend_from_slice(&bytes);
|
||||
}
|
||||
|
||||
/// Wrap `body` as a `SEQUENCE` TLV. Convenience helper for assembling
|
||||
/// LDAP messages.
|
||||
pub fn wrap_sequence(body: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(body.len() + 4);
|
||||
write_tlv(&mut out, tags::SEQUENCE, body);
|
||||
out
|
||||
}
|
||||
|
||||
/// Decoded LDAPMessage header — protocol operation TLV's tag plus
|
||||
/// body, ready to dispatch on.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LdapMessageHeader<'a> {
|
||||
/// The LDAP message ID the client picked. Echoed verbatim on the
|
||||
/// matching response.
|
||||
pub message_id: i64,
|
||||
/// The protocol op application tag (e.g. [`tags::BIND_REQUEST`]).
|
||||
pub op_tag: u8,
|
||||
/// The protocol op value-octets. Pass into [`decode_bind_request`]
|
||||
/// / [`decode_search_request`] depending on `op_tag`.
|
||||
pub op_body: &'a [u8],
|
||||
}
|
||||
|
||||
/// Decode an LDAP message header. The outer `SEQUENCE` must already
|
||||
/// be the top-level TLV in `buf`. Returns `None` for malformed input,
|
||||
/// missing fields, or unrecognised protocol-op classes.
|
||||
pub fn decode_ldap_message(buf: &[u8]) -> Option<LdapMessageHeader<'_>> {
|
||||
let outer = read_tlv(buf, 0)?;
|
||||
if outer.tag != tags::SEQUENCE {
|
||||
return None;
|
||||
}
|
||||
let msg_id_tlv = read_tlv(outer.body, 0)?;
|
||||
if msg_id_tlv.tag != tags::INTEGER {
|
||||
return None;
|
||||
}
|
||||
let message_id = decode_integer(msg_id_tlv.body)?;
|
||||
let op_tlv = read_tlv(outer.body, msg_id_tlv.end)?;
|
||||
Some(LdapMessageHeader {
|
||||
message_id,
|
||||
op_tag: op_tlv.tag,
|
||||
op_body: op_tlv.body,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decoded `BindRequest` (RFC 4511 §4.2).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BindRequest<'a> {
|
||||
/// Protocol version (always 3 for LDAPv3).
|
||||
pub version: i64,
|
||||
/// The bind DN ("" for anonymous bind).
|
||||
pub name: &'a [u8],
|
||||
/// `simple` authentication credential bytes, if present. Other
|
||||
/// `AuthenticationChoice` variants (SASL) collapse to `None`.
|
||||
pub simple_password: Option<&'a [u8]>,
|
||||
}
|
||||
|
||||
/// Decode the value-octets of a `BindRequest`.
|
||||
pub fn decode_bind_request(body: &[u8]) -> Option<BindRequest<'_>> {
|
||||
let version_tlv = read_tlv(body, 0)?;
|
||||
if version_tlv.tag != tags::INTEGER {
|
||||
return None;
|
||||
}
|
||||
let version = decode_integer(version_tlv.body)?;
|
||||
let name_tlv = read_tlv(body, version_tlv.end)?;
|
||||
if name_tlv.tag != tags::OCTET_STRING {
|
||||
return None;
|
||||
}
|
||||
let auth_tlv = read_tlv(body, name_tlv.end)?;
|
||||
let simple_password = if auth_tlv.tag == tags::AUTH_SIMPLE {
|
||||
Some(auth_tlv.body)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(BindRequest {
|
||||
version,
|
||||
name: name_tlv.body,
|
||||
simple_password,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decoded `SearchRequest` (RFC 4511 §4.5.1).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SearchRequest<'a> {
|
||||
/// Base object DN the search is anchored at.
|
||||
pub base_object: &'a [u8],
|
||||
/// Scope enum value (0=baseObject, 1=singleLevel, 2=wholeSubtree).
|
||||
pub scope: u8,
|
||||
/// Filter rendered into the RFC 4515 string subset the existing
|
||||
/// [`super::ldap_server::LdapStub::evaluate`] matcher consumes.
|
||||
pub filter: String,
|
||||
}
|
||||
|
||||
/// Decode the value-octets of a `SearchRequest`.
|
||||
pub fn decode_search_request(body: &[u8]) -> Option<SearchRequest<'_>> {
|
||||
let base_tlv = read_tlv(body, 0)?;
|
||||
if base_tlv.tag != tags::OCTET_STRING {
|
||||
return None;
|
||||
}
|
||||
let scope_tlv = read_tlv(body, base_tlv.end)?;
|
||||
if scope_tlv.tag != tags::ENUMERATED || scope_tlv.body.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let scope = scope_tlv.body[0];
|
||||
let deref_tlv = read_tlv(body, scope_tlv.end)?;
|
||||
let size_tlv = read_tlv(body, deref_tlv.end)?;
|
||||
let time_tlv = read_tlv(body, size_tlv.end)?;
|
||||
let typesonly_tlv = read_tlv(body, time_tlv.end)?;
|
||||
let filter_tlv = read_tlv(body, typesonly_tlv.end)?;
|
||||
let filter = render_filter(filter_tlv.tag, filter_tlv.body);
|
||||
Some(SearchRequest {
|
||||
base_object: base_tlv.body,
|
||||
scope,
|
||||
filter,
|
||||
})
|
||||
}
|
||||
|
||||
/// Render a decoded filter TLV into the RFC 4515 subset
|
||||
/// [`super::ldap_server::LdapStub::evaluate`] accepts. Unrecognised
|
||||
/// shapes collapse to bare `*` so adversarial payloads over-match.
|
||||
pub fn render_filter(tag: u8, body: &[u8]) -> String {
|
||||
match tag {
|
||||
tags::FILTER_AND => render_set("&", body),
|
||||
tags::FILTER_OR => render_set("|", body),
|
||||
tags::FILTER_EQUALITY => render_equality(body),
|
||||
tags::FILTER_PRESENT => {
|
||||
let attr = String::from_utf8_lossy(body);
|
||||
format!("({attr}=*)")
|
||||
}
|
||||
tags::FILTER_SUBSTRINGS => render_substrings(body),
|
||||
_ => "*".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_set(operator: &str, body: &[u8]) -> String {
|
||||
let mut out = String::from("(");
|
||||
out.push_str(operator);
|
||||
let mut cur = 0usize;
|
||||
while cur < body.len() {
|
||||
let Some(child) = read_tlv(body, cur) else {
|
||||
// Truncated SET — break out and let the outer caller fall
|
||||
// through to over-match.
|
||||
out.push('*');
|
||||
break;
|
||||
};
|
||||
out.push_str(&render_filter(child.tag, child.body));
|
||||
cur = child.end;
|
||||
}
|
||||
out.push(')');
|
||||
out
|
||||
}
|
||||
|
||||
fn render_equality(body: &[u8]) -> String {
|
||||
let Some(attr_tlv) = read_tlv(body, 0) else {
|
||||
return "*".to_string();
|
||||
};
|
||||
if attr_tlv.tag != tags::OCTET_STRING {
|
||||
return "*".to_string();
|
||||
}
|
||||
let Some(value_tlv) = read_tlv(body, attr_tlv.end) else {
|
||||
return "*".to_string();
|
||||
};
|
||||
if value_tlv.tag != tags::OCTET_STRING {
|
||||
return "*".to_string();
|
||||
}
|
||||
let attr = String::from_utf8_lossy(attr_tlv.body);
|
||||
let value = String::from_utf8_lossy(value_tlv.body);
|
||||
format!("({attr}={value})")
|
||||
}
|
||||
|
||||
fn render_substrings(body: &[u8]) -> String {
|
||||
let Some(attr_tlv) = read_tlv(body, 0) else {
|
||||
return "*".to_string();
|
||||
};
|
||||
if attr_tlv.tag != tags::OCTET_STRING {
|
||||
return "*".to_string();
|
||||
}
|
||||
let Some(seq_tlv) = read_tlv(body, attr_tlv.end) else {
|
||||
return "*".to_string();
|
||||
};
|
||||
if seq_tlv.tag != tags::SEQUENCE {
|
||||
return "*".to_string();
|
||||
}
|
||||
let attr = String::from_utf8_lossy(attr_tlv.body);
|
||||
let mut initial = String::new();
|
||||
let mut any_parts: Vec<String> = Vec::new();
|
||||
let mut tail = String::new();
|
||||
let mut cur = 0usize;
|
||||
while cur < seq_tlv.body.len() {
|
||||
let Some(piece) = read_tlv(seq_tlv.body, cur) else {
|
||||
break;
|
||||
};
|
||||
let text = String::from_utf8_lossy(piece.body).into_owned();
|
||||
match piece.tag {
|
||||
tags::SUBSTR_INITIAL => initial = text,
|
||||
tags::SUBSTR_ANY => any_parts.push(text),
|
||||
tags::SUBSTR_FINAL => tail = text,
|
||||
_ => {}
|
||||
}
|
||||
cur = piece.end;
|
||||
}
|
||||
let mut joined = initial;
|
||||
if !any_parts.is_empty() {
|
||||
joined.push('*');
|
||||
joined.push_str(&any_parts.join("*"));
|
||||
}
|
||||
joined.push('*');
|
||||
joined.push_str(&tail);
|
||||
format!("({attr}={joined})")
|
||||
}
|
||||
|
||||
/// Encode a complete `LDAPMessage` carrying `op_tag` + `op_body` as
|
||||
/// the protocol op. Wraps everything in the outer `SEQUENCE`.
|
||||
pub fn encode_ldap_message(message_id: i64, op_tag: u8, op_body: &[u8]) -> Vec<u8> {
|
||||
let mut inner = Vec::with_capacity(op_body.len() + 8);
|
||||
write_integer(&mut inner, message_id);
|
||||
write_tlv(&mut inner, op_tag, op_body);
|
||||
wrap_sequence(&inner)
|
||||
}
|
||||
|
||||
/// LDAP result codes the stub uses (RFC 4511 §4.1.9).
|
||||
pub mod result_codes {
|
||||
/// Operation completed successfully.
|
||||
pub const SUCCESS: u8 = 0;
|
||||
/// Operation rejected — used here for unrecognised request shapes.
|
||||
pub const UNWILLING_TO_PERFORM: u8 = 53;
|
||||
}
|
||||
|
||||
/// Encode a minimal `BindResponse` (success, empty matchedDN, empty
|
||||
/// diagnosticMessage).
|
||||
pub fn encode_bind_response(message_id: i64, result_code: u8) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8);
|
||||
write_enumerated(&mut body, result_code);
|
||||
write_octet_string(&mut body, b"");
|
||||
write_octet_string(&mut body, b"");
|
||||
encode_ldap_message(message_id, tags::BIND_RESPONSE, &body)
|
||||
}
|
||||
|
||||
/// Encode a `SearchResultEntry` carrying `dn` with no attributes. The
|
||||
/// Phase 06 LDAP stub's directory model only ever publishes the DN —
|
||||
/// callers that need attributes can extend this once a fixture surfaces
|
||||
/// the need.
|
||||
pub fn encode_search_result_entry(message_id: i64, dn: &[u8]) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(dn.len() + 8);
|
||||
write_octet_string(&mut body, dn);
|
||||
// PartialAttributeList ::= SEQUENCE OF partial Attribute — empty.
|
||||
write_tlv(&mut body, tags::SEQUENCE, &[]);
|
||||
encode_ldap_message(message_id, tags::SEARCH_RESULT_ENTRY, &body)
|
||||
}
|
||||
|
||||
/// Encode a `SearchResultDone` (RFC 4511 §4.5.2).
|
||||
pub fn encode_search_result_done(message_id: i64, result_code: u8) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(8);
|
||||
write_enumerated(&mut body, result_code);
|
||||
write_octet_string(&mut body, b"");
|
||||
write_octet_string(&mut body, b"");
|
||||
encode_ldap_message(message_id, tags::SEARCH_RESULT_DONE, &body)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn read_tlv_short_form_length() {
|
||||
// tag=0x04, len=0x03, body="abc"
|
||||
let buf = b"\x04\x03abc";
|
||||
let tlv = read_tlv(buf, 0).expect("tlv");
|
||||
assert_eq!(tlv.tag, 0x04);
|
||||
assert_eq!(tlv.body, b"abc");
|
||||
assert_eq!(tlv.end, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_tlv_long_form_length() {
|
||||
// 200-byte body → length 0x81 0xC8
|
||||
let mut buf = vec![0x04, 0x81, 200];
|
||||
buf.extend(std::iter::repeat_n(b'a', 200));
|
||||
let tlv = read_tlv(&buf, 0).expect("tlv");
|
||||
assert_eq!(tlv.body.len(), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_tlv_rejects_indefinite_length() {
|
||||
let buf = [0x30u8, 0x80, 0x00, 0x00];
|
||||
assert!(read_tlv(&buf, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_tlv_rejects_truncated_body() {
|
||||
let buf = [0x04u8, 0x05, b'a', b'b'];
|
||||
assert!(read_tlv(&buf, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_integer_handles_single_byte() {
|
||||
assert_eq!(decode_integer(&[3]), Some(3));
|
||||
assert_eq!(decode_integer(&[0]), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_integer_handles_negative_via_sign_extension() {
|
||||
// 0xFF is -1 in two's complement
|
||||
assert_eq!(decode_integer(&[0xFF]), Some(-1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_integer_rejects_empty_and_oversized() {
|
||||
assert!(decode_integer(&[]).is_none());
|
||||
assert!(decode_integer(&[0u8; 9]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_integer_minimum_byte_form() {
|
||||
let mut out = Vec::new();
|
||||
write_integer(&mut out, 0);
|
||||
assert_eq!(out, vec![0x02, 0x01, 0x00]);
|
||||
|
||||
let mut out = Vec::new();
|
||||
write_integer(&mut out, 127);
|
||||
assert_eq!(out, vec![0x02, 0x01, 0x7F]);
|
||||
|
||||
let mut out = Vec::new();
|
||||
write_integer(&mut out, 128);
|
||||
// Need leading zero byte because high bit of 0x80 would make
|
||||
// the value negative under two's-complement.
|
||||
assert_eq!(out, vec![0x02, 0x02, 0x00, 0x80]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_round_trips() {
|
||||
for n in [0i64, 1, 3, 127, 128, 255, 256, 65535, 65536, -1, -128, -129] {
|
||||
let mut buf = Vec::new();
|
||||
write_integer(&mut buf, n);
|
||||
let tlv = read_tlv(&buf, 0).expect("tlv");
|
||||
assert_eq!(tlv.tag, tags::INTEGER);
|
||||
assert_eq!(decode_integer(tlv.body), Some(n));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_form_length_round_trip() {
|
||||
let mut buf = Vec::new();
|
||||
let body = vec![0xABu8; 1024];
|
||||
write_tlv(&mut buf, tags::OCTET_STRING, &body);
|
||||
let tlv = read_tlv(&buf, 0).expect("tlv");
|
||||
assert_eq!(tlv.body, &body[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_request_round_trip() {
|
||||
// version=3, name="cn=admin", simple_password="secret"
|
||||
let mut body = Vec::new();
|
||||
write_integer(&mut body, 3);
|
||||
write_octet_string(&mut body, b"cn=admin");
|
||||
write_tlv(&mut body, tags::AUTH_SIMPLE, b"secret");
|
||||
let msg = encode_ldap_message(/*id=*/ 7, tags::BIND_REQUEST, &body);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
assert_eq!(hdr.message_id, 7);
|
||||
assert_eq!(hdr.op_tag, tags::BIND_REQUEST);
|
||||
let req = decode_bind_request(hdr.op_body).expect("bind body");
|
||||
assert_eq!(req.version, 3);
|
||||
assert_eq!(req.name, b"cn=admin");
|
||||
assert_eq!(req.simple_password, Some(b"secret".as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_response_round_trip_decodes_via_header() {
|
||||
let msg = encode_bind_response(/*id=*/ 7, result_codes::SUCCESS);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
assert_eq!(hdr.message_id, 7);
|
||||
assert_eq!(hdr.op_tag, tags::BIND_RESPONSE);
|
||||
// BindResponse body: ENUMERATED + 2x OCTET STRING
|
||||
let tlv = read_tlv(hdr.op_body, 0).expect("rc");
|
||||
assert_eq!(tlv.tag, tags::ENUMERATED);
|
||||
assert_eq!(tlv.body, &[0]);
|
||||
}
|
||||
|
||||
fn build_search_msg(message_id: i64, filter_tag: u8, filter_body: &[u8]) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
write_octet_string(&mut body, b"ou=people,dc=nyx,dc=test");
|
||||
write_enumerated(&mut body, 2); // wholeSubtree
|
||||
write_enumerated(&mut body, 0); // derefAliases neverDerefAliases
|
||||
write_integer(&mut body, 0); // sizeLimit
|
||||
write_integer(&mut body, 0); // timeLimit
|
||||
// typesOnly BOOLEAN false; encoded as 0x01 0x01 0x00
|
||||
write_tlv(&mut body, 0x01, &[0x00]);
|
||||
write_tlv(&mut body, filter_tag, filter_body);
|
||||
// attributes: empty SEQUENCE
|
||||
write_tlv(&mut body, tags::SEQUENCE, &[]);
|
||||
encode_ldap_message(message_id, tags::SEARCH_REQUEST, &body)
|
||||
}
|
||||
|
||||
fn equality_body(attr: &[u8], value: &[u8]) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
write_octet_string(&mut body, attr);
|
||||
write_octet_string(&mut body, value);
|
||||
body
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_request_equality_filter_renders_to_rfc4515() {
|
||||
let eq = equality_body(b"uid", b"alice");
|
||||
let msg = build_search_msg(11, tags::FILTER_EQUALITY, &eq);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
let req = decode_search_request(hdr.op_body).expect("search");
|
||||
assert_eq!(req.base_object, b"ou=people,dc=nyx,dc=test");
|
||||
assert_eq!(req.scope, 2);
|
||||
assert_eq!(req.filter, "(uid=alice)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_request_present_filter_renders_with_wildcard() {
|
||||
let msg = build_search_msg(11, tags::FILTER_PRESENT, b"uid");
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
let req = decode_search_request(hdr.op_body).expect("search");
|
||||
assert_eq!(req.filter, "(uid=*)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_request_or_filter_nests_equalities() {
|
||||
let mut set_body = Vec::new();
|
||||
let eq_a = equality_body(b"uid", b"alice");
|
||||
let eq_b = equality_body(b"uid", b"bob");
|
||||
write_tlv(&mut set_body, tags::FILTER_EQUALITY, &eq_a);
|
||||
write_tlv(&mut set_body, tags::FILTER_EQUALITY, &eq_b);
|
||||
let msg = build_search_msg(11, tags::FILTER_OR, &set_body);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
let req = decode_search_request(hdr.op_body).expect("search");
|
||||
assert_eq!(req.filter, "(|(uid=alice)(uid=bob))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_request_and_filter_nests_equalities() {
|
||||
let mut set_body = Vec::new();
|
||||
let eq_a = equality_body(b"uid", b"alice");
|
||||
let eq_b = equality_body(b"cn", b"admin");
|
||||
write_tlv(&mut set_body, tags::FILTER_EQUALITY, &eq_a);
|
||||
write_tlv(&mut set_body, tags::FILTER_EQUALITY, &eq_b);
|
||||
let msg = build_search_msg(11, tags::FILTER_AND, &set_body);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
let req = decode_search_request(hdr.op_body).expect("search");
|
||||
assert_eq!(req.filter, "(&(uid=alice)(cn=admin))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_request_substrings_filter_renders_prefix_star_suffix() {
|
||||
let mut sub_body = Vec::new();
|
||||
write_octet_string(&mut sub_body, b"uid");
|
||||
let mut inner = Vec::new();
|
||||
write_tlv(&mut inner, tags::SUBSTR_INITIAL, b"al");
|
||||
write_tlv(&mut inner, tags::SUBSTR_FINAL, b"ce");
|
||||
write_tlv(&mut sub_body, tags::SEQUENCE, &inner);
|
||||
let msg = build_search_msg(11, tags::FILTER_SUBSTRINGS, &sub_body);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
let req = decode_search_request(hdr.op_body).expect("search");
|
||||
assert_eq!(req.filter, "(uid=al*ce)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_request_unknown_filter_collapses_to_wildcard() {
|
||||
// 0xA5 = greaterOrEqual — not rendered, falls through to "*".
|
||||
let body = equality_body(b"uid", b"alice");
|
||||
let msg = build_search_msg(11, 0xA5, &body);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
let req = decode_search_request(hdr.op_body).expect("search");
|
||||
assert_eq!(req.filter, "*");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_search_result_entry_round_trip() {
|
||||
let msg = encode_search_result_entry(/*id=*/ 11, b"uid=alice,ou=people");
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
assert_eq!(hdr.message_id, 11);
|
||||
assert_eq!(hdr.op_tag, tags::SEARCH_RESULT_ENTRY);
|
||||
let dn_tlv = read_tlv(hdr.op_body, 0).expect("dn");
|
||||
assert_eq!(dn_tlv.tag, tags::OCTET_STRING);
|
||||
assert_eq!(dn_tlv.body, b"uid=alice,ou=people");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_search_result_done_round_trip() {
|
||||
let msg = encode_search_result_done(/*id=*/ 11, result_codes::SUCCESS);
|
||||
let hdr = decode_ldap_message(&msg).expect("header");
|
||||
assert_eq!(hdr.op_tag, tags::SEARCH_RESULT_DONE);
|
||||
let rc = read_tlv(hdr.op_body, 0).expect("rc");
|
||||
assert_eq!(rc.tag, tags::ENUMERATED);
|
||||
assert_eq!(rc.body, &[0]);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,24 @@
|
|||
//! Endpoint: `127.0.0.1:{port}` (no scheme; the harness composes
|
||||
//! `ldap://` itself if it wants).
|
||||
//!
|
||||
//! # BER (LDAPv3) dispatch
|
||||
//!
|
||||
//! The accept loop peeks the first byte on each connection. When it
|
||||
//! sees the universal `SEQUENCE` tag (`0x30`) — the leading byte of
|
||||
//! every well-formed LDAPv3 [`LDAPMessage`] — it routes the
|
||||
//! conversation through [`super::ldap_ber`] so a harness using a stock
|
||||
//! LDAP client (`javax.naming.directory.InitialDirContext`,
|
||||
//! `python-ldap`, `ldap3`, …) can talk to the stub on the LDAPv3 wire
|
||||
//! protocol. The plaintext `SEARCH <filter>\n` framing remains for
|
||||
//! every other first-byte value, so the existing tier-(a) harnesses
|
||||
//! keep round-tripping unchanged.
|
||||
//!
|
||||
//! No env var gates this — the dispatch is byte-shape driven so a
|
||||
//! tier-(a) shim that accidentally emits a leading `0x30` will skip
|
||||
//! the BER path's failure-mode fallback (the BER decoder bails to
|
||||
//! `None` on a non-LDAPv3 payload, which closes the connection without
|
||||
//! corrupting state).
|
||||
//!
|
||||
//! # Directory state
|
||||
//!
|
||||
//! Three users are provisioned at startup: `alice`, `bob`, `carol`. An
|
||||
|
|
@ -41,9 +59,10 @@
|
|||
//! Signals the accept thread to shut down and connects to itself to
|
||||
//! wake the blocking `accept()`.
|
||||
|
||||
use super::ldap_ber;
|
||||
use super::{StubEvent, StubKind, StubProvider, monotonic_ns};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -167,11 +186,32 @@ fn accept_loop(
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_connection(mut stream: TcpStream, max_bytes: usize, events: &Arc<Mutex<Vec<StubEvent>>>) {
|
||||
let mut reader = match stream.try_clone() {
|
||||
Ok(s) => BufReader::new(s),
|
||||
fn handle_connection(stream: TcpStream, max_bytes: usize, events: &Arc<Mutex<Vec<StubEvent>>>) {
|
||||
let reader_stream = match stream.try_clone() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut reader = BufReader::new(reader_stream);
|
||||
// Peek the first byte to decide between the plaintext and BER
|
||||
// protocol paths. `fill_buf` does not consume — the chosen
|
||||
// handler reads from `reader` again.
|
||||
let first_byte = match reader.fill_buf() {
|
||||
Ok(buf) if !buf.is_empty() => buf[0],
|
||||
_ => return,
|
||||
};
|
||||
if first_byte == ldap_ber::tags::SEQUENCE {
|
||||
handle_ber_connection(reader, stream, max_bytes, events);
|
||||
} else {
|
||||
handle_plaintext_connection(reader, stream, max_bytes, events);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_plaintext_connection(
|
||||
mut reader: BufReader<TcpStream>,
|
||||
mut stream: TcpStream,
|
||||
max_bytes: usize,
|
||||
events: &Arc<Mutex<Vec<StubEvent>>>,
|
||||
) {
|
||||
let mut line = String::new();
|
||||
match reader.read_line(&mut line) {
|
||||
Ok(0) => return,
|
||||
|
|
@ -212,6 +252,118 @@ fn handle_connection(mut stream: TcpStream, max_bytes: usize, events: &Arc<Mutex
|
|||
}
|
||||
}
|
||||
|
||||
/// LDAPv3 BER dispatch: bind then search loop. Returns silently on
|
||||
/// any decode error so a malformed payload never corrupts state.
|
||||
fn handle_ber_connection(
|
||||
mut reader: BufReader<TcpStream>,
|
||||
mut stream: TcpStream,
|
||||
max_bytes: usize,
|
||||
events: &Arc<Mutex<Vec<StubEvent>>>,
|
||||
) {
|
||||
loop {
|
||||
let Some(msg) = read_ber_message(&mut reader, max_bytes) else {
|
||||
return;
|
||||
};
|
||||
let Some(hdr) = ldap_ber::decode_ldap_message(&msg) else {
|
||||
return;
|
||||
};
|
||||
match hdr.op_tag {
|
||||
ldap_ber::tags::BIND_REQUEST => {
|
||||
// Anonymous + simple binds both succeed — the stub
|
||||
// does not enforce credentials.
|
||||
let reply =
|
||||
ldap_ber::encode_bind_response(hdr.message_id, ldap_ber::result_codes::SUCCESS);
|
||||
if stream.write_all(&reply).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ldap_ber::tags::SEARCH_REQUEST => {
|
||||
let Some(req) = ldap_ber::decode_search_request(hdr.op_body) else {
|
||||
let done = ldap_ber::encode_search_result_done(
|
||||
hdr.message_id,
|
||||
ldap_ber::result_codes::UNWILLING_TO_PERFORM,
|
||||
);
|
||||
let _ = stream.write_all(&done);
|
||||
return;
|
||||
};
|
||||
let matches = match_filter(&req.filter);
|
||||
let count = matches.len();
|
||||
for uid in &matches {
|
||||
let dn = format!("uid={uid},ou=people,dc=nyx,dc=test");
|
||||
let entry =
|
||||
ldap_ber::encode_search_result_entry(hdr.message_id, dn.as_bytes());
|
||||
if stream.write_all(&entry).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let done = ldap_ber::encode_search_result_done(
|
||||
hdr.message_id,
|
||||
ldap_ber::result_codes::SUCCESS,
|
||||
);
|
||||
if stream.write_all(&done).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = stream.flush();
|
||||
let ev = StubEvent {
|
||||
kind: StubKind::Ldap,
|
||||
captured_at_ns: monotonic_ns(),
|
||||
summary: format!("SEARCH {filter}", filter = req.filter),
|
||||
detail: {
|
||||
let mut d = BTreeMap::new();
|
||||
d.insert("filter".to_owned(), req.filter);
|
||||
d.insert("protocol".to_owned(), "ldapv3".to_owned());
|
||||
d.insert("entries_returned".to_owned(), count.to_string());
|
||||
d
|
||||
},
|
||||
};
|
||||
if let Ok(mut g) = events.lock() {
|
||||
g.push(ev);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unbind / abandon / extended / etc. — bail. The
|
||||
// verifier oracle only cares about search results.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a single LDAPv3 BER `LDAPMessage` off the wire. Parses just
|
||||
/// enough of the outer TLV to compute the message length, then reads
|
||||
/// exactly that many body bytes. Returns `None` for malformed
|
||||
/// framing or when the message size exceeds `max_bytes`.
|
||||
fn read_ber_message(reader: &mut BufReader<TcpStream>, max_bytes: usize) -> Option<Vec<u8>> {
|
||||
let mut header = vec![0u8; 2];
|
||||
reader.read_exact(&mut header).ok()?;
|
||||
if header[0] != ldap_ber::tags::SEQUENCE {
|
||||
return None;
|
||||
}
|
||||
let body_len = if header[1] & 0x80 == 0 {
|
||||
header[1] as usize
|
||||
} else {
|
||||
let length_of_length = (header[1] & 0x7F) as usize;
|
||||
if length_of_length == 0 || length_of_length > 4 {
|
||||
return None;
|
||||
}
|
||||
let mut len_bytes = vec![0u8; length_of_length];
|
||||
reader.read_exact(&mut len_bytes).ok()?;
|
||||
let mut acc: usize = 0;
|
||||
for &b in &len_bytes {
|
||||
acc = (acc << 8) | (b as usize);
|
||||
}
|
||||
header.extend_from_slice(&len_bytes);
|
||||
acc
|
||||
};
|
||||
if header.len() + body_len > max_bytes {
|
||||
return None;
|
||||
}
|
||||
let mut body = vec![0u8; body_len];
|
||||
reader.read_exact(&mut body).ok()?;
|
||||
header.extend_from_slice(&body);
|
||||
Some(header)
|
||||
}
|
||||
|
||||
/// RFC-4515-subset matcher. See module docs for the grammar.
|
||||
fn match_filter(filter: &str) -> Vec<&'static str> {
|
||||
let trimmed = filter.trim();
|
||||
|
|
@ -453,4 +605,133 @@ mod tests {
|
|||
std::thread::sleep(Duration::from_millis(50));
|
||||
let _ = TcpListener::bind(format!("127.0.0.1:{port}"));
|
||||
}
|
||||
|
||||
fn build_ber_bind(message_id: i64) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
ldap_ber::write_integer(&mut body, 3);
|
||||
ldap_ber::write_octet_string(&mut body, b"");
|
||||
ldap_ber::write_tlv(&mut body, ldap_ber::tags::AUTH_SIMPLE, b"");
|
||||
ldap_ber::encode_ldap_message(message_id, ldap_ber::tags::BIND_REQUEST, &body)
|
||||
}
|
||||
|
||||
fn build_ber_search(message_id: i64, filter_tag: u8, filter_body: &[u8]) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
ldap_ber::write_octet_string(&mut body, b"ou=people,dc=nyx,dc=test");
|
||||
ldap_ber::write_enumerated(&mut body, 2);
|
||||
ldap_ber::write_enumerated(&mut body, 0);
|
||||
ldap_ber::write_integer(&mut body, 0);
|
||||
ldap_ber::write_integer(&mut body, 0);
|
||||
ldap_ber::write_tlv(&mut body, 0x01, &[0x00]);
|
||||
ldap_ber::write_tlv(&mut body, filter_tag, filter_body);
|
||||
ldap_ber::write_tlv(&mut body, ldap_ber::tags::SEQUENCE, &[]);
|
||||
ldap_ber::encode_ldap_message(message_id, ldap_ber::tags::SEARCH_REQUEST, &body)
|
||||
}
|
||||
|
||||
fn read_ber_reply(stream: &mut TcpStream) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
// Read until the peer closes (the BER handler stays open
|
||||
// until the client disconnects). A short read timeout was
|
||||
// configured at accept time, so a stuck reader would unblock
|
||||
// there anyway.
|
||||
let _ = stream.read_to_end(&mut buf);
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ber_bind_then_search_wildcard_returns_three_entries() {
|
||||
let stub = LdapStub::start().unwrap();
|
||||
let mut s = TcpStream::connect(format!("127.0.0.1:{}", stub.port())).unwrap();
|
||||
let bind = build_ber_bind(1);
|
||||
s.write_all(&bind).unwrap();
|
||||
let search = build_ber_search(2, ldap_ber::tags::FILTER_PRESENT, b"uid");
|
||||
s.write_all(&search).unwrap();
|
||||
s.shutdown(std::net::Shutdown::Write).unwrap();
|
||||
let reply = read_ber_reply(&mut s);
|
||||
// Walk the reply: BindResponse (msg id 1, tag 0x61), then
|
||||
// 3x SearchResultEntry (tag 0x64), then SearchResultDone
|
||||
// (tag 0x65).
|
||||
let bind_resp = ldap_ber::read_tlv(&reply, 0).expect("bind tlv");
|
||||
assert_eq!(bind_resp.tag, ldap_ber::tags::SEQUENCE);
|
||||
let bind_hdr = ldap_ber::decode_ldap_message(&reply[..bind_resp.end]).expect("bind hdr");
|
||||
assert_eq!(bind_hdr.op_tag, ldap_ber::tags::BIND_RESPONSE);
|
||||
assert_eq!(bind_hdr.message_id, 1);
|
||||
|
||||
let mut cur = bind_resp.end;
|
||||
let mut entries: usize = 0;
|
||||
let mut saw_done = false;
|
||||
while cur < reply.len() {
|
||||
let tlv = ldap_ber::read_tlv(&reply, cur).expect("tlv");
|
||||
assert_eq!(tlv.tag, ldap_ber::tags::SEQUENCE);
|
||||
let hdr = ldap_ber::decode_ldap_message(&reply[cur..tlv.end]).expect("hdr");
|
||||
match hdr.op_tag {
|
||||
ldap_ber::tags::SEARCH_RESULT_ENTRY => entries += 1,
|
||||
ldap_ber::tags::SEARCH_RESULT_DONE => {
|
||||
saw_done = true;
|
||||
break;
|
||||
}
|
||||
_ => panic!("unexpected op tag {:#x}", hdr.op_tag),
|
||||
}
|
||||
cur = tlv.end;
|
||||
}
|
||||
assert_eq!(entries, 3);
|
||||
assert!(saw_done);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
let events = stub.drain_events();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(
|
||||
events[0].detail.get("entries_returned").map(String::as_str),
|
||||
Some("3"),
|
||||
);
|
||||
assert_eq!(
|
||||
events[0].detail.get("protocol").map(String::as_str),
|
||||
Some("ldapv3"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ber_search_concrete_uid_returns_one_entry() {
|
||||
let stub = LdapStub::start().unwrap();
|
||||
let mut s = TcpStream::connect(format!("127.0.0.1:{}", stub.port())).unwrap();
|
||||
s.write_all(&build_ber_bind(1)).unwrap();
|
||||
let mut eq_body = Vec::new();
|
||||
ldap_ber::write_octet_string(&mut eq_body, b"uid");
|
||||
ldap_ber::write_octet_string(&mut eq_body, b"alice");
|
||||
s.write_all(&build_ber_search(2, ldap_ber::tags::FILTER_EQUALITY, &eq_body))
|
||||
.unwrap();
|
||||
s.shutdown(std::net::Shutdown::Write).unwrap();
|
||||
let reply = read_ber_reply(&mut s);
|
||||
// Skip past the BindResponse.
|
||||
let bind_resp = ldap_ber::read_tlv(&reply, 0).expect("bind tlv");
|
||||
let mut cur = bind_resp.end;
|
||||
let mut entry_dns: Vec<String> = Vec::new();
|
||||
let mut saw_done = false;
|
||||
while cur < reply.len() {
|
||||
let tlv = ldap_ber::read_tlv(&reply, cur).expect("tlv");
|
||||
let hdr = ldap_ber::decode_ldap_message(&reply[cur..tlv.end]).expect("hdr");
|
||||
if hdr.op_tag == ldap_ber::tags::SEARCH_RESULT_ENTRY {
|
||||
let dn_tlv = ldap_ber::read_tlv(hdr.op_body, 0).expect("dn");
|
||||
entry_dns.push(String::from_utf8_lossy(dn_tlv.body).into_owned());
|
||||
} else if hdr.op_tag == ldap_ber::tags::SEARCH_RESULT_DONE {
|
||||
saw_done = true;
|
||||
break;
|
||||
}
|
||||
cur = tlv.end;
|
||||
}
|
||||
assert_eq!(entry_dns, vec!["uid=alice,ou=people,dc=nyx,dc=test"]);
|
||||
assert!(saw_done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plaintext_path_still_works_after_ber_branch_added() {
|
||||
// Same shape as `search_request_returns_three_for_wildcard_via_socket`
|
||||
// but the leading byte is `S` (0x53), not `0x30`, so the
|
||||
// accept-loop dispatches plaintext.
|
||||
let stub = LdapStub::start().unwrap();
|
||||
let mut s = TcpStream::connect(format!("127.0.0.1:{}", stub.port())).unwrap();
|
||||
s.write_all(b"SEARCH (uid=*)\n").unwrap();
|
||||
s.flush().unwrap();
|
||||
let mut out = String::new();
|
||||
s.read_to_string(&mut out).unwrap();
|
||||
assert!(out.starts_with("COUNT 3\n"), "got {out:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ pub mod broker_rabbit;
|
|||
pub mod broker_sqs;
|
||||
pub mod filesystem;
|
||||
pub mod http;
|
||||
pub mod ldap_ber;
|
||||
pub mod ldap_server;
|
||||
pub mod mocks;
|
||||
pub mod redis;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue