mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0007 (20260522T043516Z-29b8)
This commit is contained in:
parent
ed63433c61
commit
e2940fc3cc
3 changed files with 636 additions and 57 deletions
|
|
@ -782,12 +782,20 @@ echo json_encode(["entity_expanded" => $expanded]) . "\n";
|
|||
/// Phase 06 — Track J.4 LDAP-injection harness for PHP (`ldap_search`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, splices it into a `(uid=<payload>)` filter,
|
||||
/// evaluates the filter against the in-sandbox LDAP directory (three
|
||||
/// users: `alice`, `bob`, `carol`) using the same RFC-4515 subset the
|
||||
/// [`crate::dynamic::stubs::ldap_server`] stub implements, and writes
|
||||
/// a `ProbeKind::Ldap { entries_returned }` probe whose `n` is the
|
||||
/// count the directory returned. Mirrors the synthetic-harness
|
||||
/// pattern used by Phase 03 / 04 / 05.
|
||||
/// and — when `NYX_LDAP_ENDPOINT` is set — routes the search through
|
||||
/// the in-sandbox LDAP stub over the real LDAPv3 BER wire (the stub's
|
||||
/// accept loop at [`crate::dynamic::stubs::ldap_server::accept_loop`]
|
||||
/// auto-detects the `0x30 SEQUENCE` lead byte and routes through the
|
||||
/// reader/writer at [`crate::dynamic::stubs::ldap_ber`]). Falls back
|
||||
/// to an in-process RFC 4515 subset matcher against three canonical
|
||||
/// users (`alice`, `bob`, `carol`) when the env var is unset, the
|
||||
/// filter does not parse as a supported RFC 4515 shape, or the socket
|
||||
/// exchange errors, so the harness still produces a verdict on hosts
|
||||
/// that exercise it outside the stub-backed corpus. Writes a
|
||||
/// `ProbeKind::Ldap { entries_returned }` probe whose `n` is the
|
||||
/// count the directory returned. The BER client is core-PHP only
|
||||
/// (`fsockopen` / `fwrite` / `fread`) so no `ext-ldap` extension is
|
||||
/// required.
|
||||
pub fn emit_ldap_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let shim = probe_shim();
|
||||
let body = format!(
|
||||
|
|
@ -866,7 +874,235 @@ function _nyx_match_one(string $filt, string $uid): bool {{
|
|||
return _nyx_attr_match($pattern, $uid);
|
||||
}}
|
||||
|
||||
function _nyx_ldap_count_via_stub(string $filt): ?int {{
|
||||
// --- LDAPv3 BER client (zero-dep, core PHP only) -------------------------
|
||||
// Tags this client emits / consumes. Mirrors `src/dynamic/stubs/ldap_ber.rs`.
|
||||
const _NYX_BER_BOOLEAN = 0x01;
|
||||
const _NYX_BER_INTEGER = 0x02;
|
||||
const _NYX_BER_OCTET_STRING = 0x04;
|
||||
const _NYX_BER_ENUMERATED = 0x0A;
|
||||
const _NYX_BER_SEQUENCE = 0x30;
|
||||
const _NYX_BER_BIND_REQUEST = 0x60;
|
||||
const _NYX_BER_BIND_RESPONSE = 0x61;
|
||||
const _NYX_BER_SEARCH_REQUEST = 0x63;
|
||||
const _NYX_BER_SEARCH_RESULT_ENTRY = 0x64;
|
||||
const _NYX_BER_SEARCH_RESULT_DONE = 0x65;
|
||||
const _NYX_BER_AUTH_SIMPLE = 0x80;
|
||||
const _NYX_BER_FILTER_AND = 0xA0;
|
||||
const _NYX_BER_FILTER_OR = 0xA1;
|
||||
const _NYX_BER_FILTER_EQUALITY = 0xA3;
|
||||
const _NYX_BER_FILTER_SUBSTRINGS = 0xA4;
|
||||
const _NYX_BER_FILTER_PRESENT = 0x87;
|
||||
const _NYX_BER_SUBSTR_INITIAL = 0x80;
|
||||
const _NYX_BER_SUBSTR_ANY = 0x81;
|
||||
const _NYX_BER_SUBSTR_FINAL = 0x82;
|
||||
|
||||
function _nyx_ber_length(int $n): string {{
|
||||
if ($n < 0x80) return chr($n);
|
||||
$tmp = '';
|
||||
while ($n > 0) {{
|
||||
$tmp = chr($n & 0xFF) . $tmp;
|
||||
$n >>= 8;
|
||||
}}
|
||||
return chr(0x80 | strlen($tmp)) . $tmp;
|
||||
}}
|
||||
|
||||
function _nyx_ber_tlv(int $tag, string $body): string {{
|
||||
return chr($tag) . _nyx_ber_length(strlen($body)) . $body;
|
||||
}}
|
||||
|
||||
function _nyx_ber_int(int $n): ?string {{
|
||||
if ($n < 0) return null;
|
||||
if ($n === 0) {{
|
||||
$body = "\x00";
|
||||
}} else {{
|
||||
$tmp = '';
|
||||
while ($n > 0) {{
|
||||
$tmp = chr($n & 0xFF) . $tmp;
|
||||
$n >>= 8;
|
||||
}}
|
||||
if (ord($tmp[0]) & 0x80) {{
|
||||
$tmp = "\x00" . $tmp;
|
||||
}}
|
||||
$body = $tmp;
|
||||
}}
|
||||
return _nyx_ber_tlv(_NYX_BER_INTEGER, $body);
|
||||
}}
|
||||
|
||||
function _nyx_ber_enum(int $n): string {{
|
||||
return _nyx_ber_tlv(_NYX_BER_ENUMERATED, chr($n & 0xFF));
|
||||
}}
|
||||
|
||||
function _nyx_ber_octstr(string $s): string {{
|
||||
return _nyx_ber_tlv(_NYX_BER_OCTET_STRING, $s);
|
||||
}}
|
||||
|
||||
function _nyx_ber_bool(bool $b): string {{
|
||||
return _nyx_ber_tlv(_NYX_BER_BOOLEAN, $b ? "\xFF" : "\x00");
|
||||
}}
|
||||
|
||||
function _nyx_ber_seq(string $body): string {{
|
||||
return _nyx_ber_tlv(_NYX_BER_SEQUENCE, $body);
|
||||
}}
|
||||
|
||||
function _nyx_valid_attr(string $a): bool {{
|
||||
if ($a === '') return false;
|
||||
$n = strlen($a);
|
||||
for ($i = 0; $i < $n; $i++) {{
|
||||
$c = $a[$i];
|
||||
if (!(ctype_alnum($c) || $c === '-' || $c === '_' || $c === '.')) return false;
|
||||
}}
|
||||
return true;
|
||||
}}
|
||||
|
||||
function _nyx_split_paren_children(string $s): ?array {{
|
||||
$out = [];
|
||||
$i = 0;
|
||||
$n = strlen($s);
|
||||
while ($i < $n) {{
|
||||
if ($s[$i] !== '(') return null;
|
||||
$depth = 0;
|
||||
$start = $i;
|
||||
while ($i < $n) {{
|
||||
$c = $s[$i];
|
||||
if ($c === '(') $depth++;
|
||||
elseif ($c === ')') {{
|
||||
$depth--;
|
||||
if ($depth === 0) {{ $i++; break; }}
|
||||
}}
|
||||
$i++;
|
||||
}}
|
||||
if ($depth !== 0) return null;
|
||||
$out[] = substr($s, $start, $i - $start);
|
||||
}}
|
||||
return $out;
|
||||
}}
|
||||
|
||||
function _nyx_encode_filter(string $filt): ?string {{
|
||||
$s = trim($filt);
|
||||
if (!str_starts_with($s, '(') || !str_ends_with($s, ')')) return null;
|
||||
$depth = 0;
|
||||
$n = strlen($s);
|
||||
for ($i = 0; $i < $n; $i++) {{
|
||||
$c = $s[$i];
|
||||
if ($c === '(') $depth++;
|
||||
elseif ($c === ')') {{
|
||||
$depth--;
|
||||
if ($depth < 0) return null;
|
||||
if ($depth === 0 && $i !== $n - 1) return null;
|
||||
}}
|
||||
}}
|
||||
if ($depth !== 0) return null;
|
||||
$inner = substr($s, 1, strlen($s) - 2);
|
||||
if ($inner === '') return null;
|
||||
$head = $inner[0];
|
||||
if ($head === '&' || $head === '|') {{
|
||||
$children = _nyx_split_paren_children(substr($inner, 1));
|
||||
if ($children === null || empty($children)) return null;
|
||||
$parts = '';
|
||||
foreach ($children as $c) {{
|
||||
$sub = _nyx_encode_filter($c);
|
||||
if ($sub === null) return null;
|
||||
$parts .= $sub;
|
||||
}}
|
||||
$tag = $head === '&' ? _NYX_BER_FILTER_AND : _NYX_BER_FILTER_OR;
|
||||
return _nyx_ber_tlv($tag, $parts);
|
||||
}}
|
||||
$eq = strpos($inner, '=');
|
||||
if ($eq === false) return null;
|
||||
$attr = substr($inner, 0, $eq);
|
||||
$val = substr($inner, $eq + 1);
|
||||
if (!_nyx_valid_attr($attr)) return null;
|
||||
if ($val === '*') {{
|
||||
return _nyx_ber_tlv(_NYX_BER_FILTER_PRESENT, $attr);
|
||||
}}
|
||||
if (strpos($val, '*') !== false) {{
|
||||
$parts = explode('*', $val);
|
||||
$last = count($parts) - 1;
|
||||
$seq = '';
|
||||
if ($parts[0] !== '') {{
|
||||
$seq .= _nyx_ber_tlv(_NYX_BER_SUBSTR_INITIAL, $parts[0]);
|
||||
}}
|
||||
for ($i = 1; $i < $last; $i++) {{
|
||||
if ($parts[$i] !== '') {{
|
||||
$seq .= _nyx_ber_tlv(_NYX_BER_SUBSTR_ANY, $parts[$i]);
|
||||
}}
|
||||
}}
|
||||
if ($parts[$last] !== '') {{
|
||||
$seq .= _nyx_ber_tlv(_NYX_BER_SUBSTR_FINAL, $parts[$last]);
|
||||
}}
|
||||
$body = _nyx_ber_octstr($attr) . _nyx_ber_seq($seq);
|
||||
return _nyx_ber_tlv(_NYX_BER_FILTER_SUBSTRINGS, $body);
|
||||
}}
|
||||
$body = _nyx_ber_octstr($attr) . _nyx_ber_octstr($val);
|
||||
return _nyx_ber_tlv(_NYX_BER_FILTER_EQUALITY, $body);
|
||||
}}
|
||||
|
||||
function _nyx_read_n($sock, int $n): ?string {{
|
||||
$out = '';
|
||||
while (strlen($out) < $n) {{
|
||||
$chunk = @fread($sock, $n - strlen($out));
|
||||
if ($chunk === false || $chunk === '') return null;
|
||||
$out .= $chunk;
|
||||
}}
|
||||
return $out;
|
||||
}}
|
||||
|
||||
function _nyx_read_ber_message($sock): ?string {{
|
||||
$head = _nyx_read_n($sock, 2);
|
||||
if ($head === null || ord($head[0]) !== _NYX_BER_SEQUENCE) return null;
|
||||
$first_len = ord($head[1]);
|
||||
if (($first_len & 0x80) === 0) {{
|
||||
$body_len = $first_len;
|
||||
$length_bytes = '';
|
||||
}} else {{
|
||||
$nl = $first_len & 0x7F;
|
||||
if ($nl === 0 || $nl > 4) return null;
|
||||
$length_bytes = _nyx_read_n($sock, $nl);
|
||||
if ($length_bytes === null) return null;
|
||||
$body_len = 0;
|
||||
for ($i = 0; $i < $nl; $i++) {{
|
||||
$body_len = ($body_len << 8) | ord($length_bytes[$i]);
|
||||
}}
|
||||
}}
|
||||
if ($body_len > 64 * 1024) return null;
|
||||
$body = _nyx_read_n($sock, $body_len);
|
||||
if ($body === null) return null;
|
||||
return $head . $length_bytes . $body;
|
||||
}}
|
||||
|
||||
function _nyx_decode_tlv(string $buf, int $offset): ?array {{
|
||||
if ($offset + 2 > strlen($buf)) return null;
|
||||
$tag = ord($buf[$offset]);
|
||||
$first_len = ord($buf[$offset + 1]);
|
||||
if (($first_len & 0x80) === 0) {{
|
||||
$body_len = $first_len;
|
||||
$body_start = $offset + 2;
|
||||
}} else {{
|
||||
$nl = $first_len & 0x7F;
|
||||
if ($nl === 0 || $nl > 4 || $offset + 2 + $nl > strlen($buf)) return null;
|
||||
$body_len = 0;
|
||||
for ($i = 0; $i < $nl; $i++) {{
|
||||
$body_len = ($body_len << 8) | ord($buf[$offset + 2 + $i]);
|
||||
}}
|
||||
$body_start = $offset + 2 + $nl;
|
||||
}}
|
||||
$body_end = $body_start + $body_len;
|
||||
if ($body_end > strlen($buf)) return null;
|
||||
return [$tag, substr($buf, $body_start, $body_len), $body_end];
|
||||
}}
|
||||
|
||||
function _nyx_decode_ldap_op(string $msg): ?array {{
|
||||
$outer = _nyx_decode_tlv($msg, 0);
|
||||
if ($outer === null || $outer[0] !== _NYX_BER_SEQUENCE) return null;
|
||||
$inner = $outer[1];
|
||||
$msg_id_tlv = _nyx_decode_tlv($inner, 0);
|
||||
if ($msg_id_tlv === null || $msg_id_tlv[0] !== _NYX_BER_INTEGER) return null;
|
||||
$op_tlv = _nyx_decode_tlv($inner, $msg_id_tlv[2]);
|
||||
if ($op_tlv === null) return null;
|
||||
return [$op_tlv[0], $op_tlv[1]];
|
||||
}}
|
||||
|
||||
function _nyx_ldap_count_via_ber(string $filt): ?int {{
|
||||
$ep = getenv('NYX_LDAP_ENDPOINT');
|
||||
if ($ep === false || $ep === '') return null;
|
||||
$sep = strrpos($ep, ':');
|
||||
|
|
@ -874,20 +1110,47 @@ function _nyx_ldap_count_via_stub(string $filt): ?int {{
|
|||
$host = substr($ep, 0, $sep);
|
||||
$port = (int) substr($ep, $sep + 1);
|
||||
if ($port <= 0) return null;
|
||||
$filter_bytes = _nyx_encode_filter($filt);
|
||||
if ($filter_bytes === null) return null;
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$sock = @fsockopen($host, $port, $errno, $errstr, 2.0);
|
||||
if ($sock === false) return null;
|
||||
stream_set_timeout($sock, 2);
|
||||
@fwrite($sock, 'SEARCH ' . $filt . "\n");
|
||||
$line = @fgets($sock);
|
||||
@fclose($sock);
|
||||
if ($line === false) return null;
|
||||
$line = rtrim($line, "\r\n");
|
||||
if (!str_starts_with($line, 'COUNT ')) return null;
|
||||
$tail = trim(substr($line, strlen('COUNT ')));
|
||||
if ($tail === '' || !ctype_digit($tail)) return null;
|
||||
return (int) $tail;
|
||||
$bind_body = _nyx_ber_int(3) . _nyx_ber_octstr('') . _nyx_ber_tlv(_NYX_BER_AUTH_SIMPLE, '');
|
||||
$bind_msg = _nyx_ber_seq(_nyx_ber_int(1) . _nyx_ber_tlv(_NYX_BER_BIND_REQUEST, $bind_body));
|
||||
if (@fwrite($sock, $bind_msg) === false) {{ @fclose($sock); return null; }}
|
||||
$resp = _nyx_read_ber_message($sock);
|
||||
if ($resp === null) {{ @fclose($sock); return null; }}
|
||||
$decoded = _nyx_decode_ldap_op($resp);
|
||||
if ($decoded === null || $decoded[0] !== _NYX_BER_BIND_RESPONSE) {{ @fclose($sock); return null; }}
|
||||
$search_body = _nyx_ber_octstr('')
|
||||
. _nyx_ber_enum(2)
|
||||
. _nyx_ber_enum(0)
|
||||
. _nyx_ber_int(0)
|
||||
. _nyx_ber_int(2)
|
||||
. _nyx_ber_bool(false)
|
||||
. $filter_bytes
|
||||
. _nyx_ber_seq('');
|
||||
$search_msg = _nyx_ber_seq(_nyx_ber_int(2) . _nyx_ber_tlv(_NYX_BER_SEARCH_REQUEST, $search_body));
|
||||
if (@fwrite($sock, $search_msg) === false) {{ @fclose($sock); return null; }}
|
||||
$count = 0;
|
||||
while (true) {{
|
||||
$resp = _nyx_read_ber_message($sock);
|
||||
if ($resp === null) {{ @fclose($sock); return null; }}
|
||||
$decoded = _nyx_decode_ldap_op($resp);
|
||||
if ($decoded === null) {{ @fclose($sock); return null; }}
|
||||
$op_tag = $decoded[0];
|
||||
if ($op_tag === _NYX_BER_SEARCH_RESULT_ENTRY) {{
|
||||
$count++;
|
||||
}} elseif ($op_tag === _NYX_BER_SEARCH_RESULT_DONE) {{
|
||||
@fclose($sock);
|
||||
return $count;
|
||||
}} else {{
|
||||
@fclose($sock);
|
||||
return $count;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
function _nyx_ldap_count_local(string $filt, array $users): int {{
|
||||
|
|
@ -904,8 +1167,8 @@ function _nyx_ldap_count_local(string $filt, array $users): int {{
|
|||
}}
|
||||
|
||||
function _nyx_ldap_count(string $filt, array $users): int {{
|
||||
$via_stub = _nyx_ldap_count_via_stub($filt);
|
||||
if ($via_stub !== null) return $via_stub;
|
||||
$via_ber = _nyx_ldap_count_via_ber($filt);
|
||||
if ($via_ber !== null) return $via_ber;
|
||||
return _nyx_ldap_count_local($filt, $users);
|
||||
}}
|
||||
|
||||
|
|
@ -2164,12 +2427,20 @@ mod tests {
|
|||
"PHP LDAP harness must open a TCP socket against the stub endpoint",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("'SEARCH '"),
|
||||
"PHP LDAP harness must write SEARCH <filter> over the wire",
|
||||
h.source.contains("_NYX_BER_BIND_REQUEST = 0x60"),
|
||||
"PHP LDAP harness must compose an LDAPv3 BindRequest (BER tag 0x60)",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("'COUNT '"),
|
||||
"PHP LDAP harness must parse the COUNT <n> reply line",
|
||||
h.source.contains("_NYX_BER_SEARCH_REQUEST = 0x63"),
|
||||
"PHP LDAP harness must compose an LDAPv3 SearchRequest (BER tag 0x63)",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_encode_filter"),
|
||||
"PHP LDAP harness must encode the RFC 4515 filter string into BER bytes",
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("'SEARCH '"),
|
||||
"PHP LDAP harness must no longer write the plaintext SEARCH <filter> tier-(a) framing",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2181,8 +2452,8 @@ mod tests {
|
|||
"PHP LDAP harness must keep the in-process matcher as a fallback for hosts without the stub",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_ldap_count_via_stub"),
|
||||
"PHP LDAP harness must dispatch through the stub-route helper",
|
||||
h.source.contains("_nyx_ldap_count_via_ber"),
|
||||
"PHP LDAP harness must dispatch through the BER stub-route helper",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1560,12 +1560,19 @@ if __name__ == "__main__":
|
|||
/// (`ldap.search_s`).
|
||||
///
|
||||
/// Reads `NYX_PAYLOAD`, splices it into a `(uid=<payload>)` filter,
|
||||
/// evaluates the filter against the in-sandbox LDAP directory (three
|
||||
/// users: `alice`, `bob`, `carol`) using the same RFC-4515 subset the
|
||||
/// [`crate::dynamic::stubs::ldap_server`] stub implements, and writes
|
||||
/// a `ProbeKind::Ldap { entries_returned }` probe whose `n` is the
|
||||
/// count the directory returned. Mirrors the synthetic-harness
|
||||
/// pattern used by Phase 03 / 04 / 05.
|
||||
/// and — when `NYX_LDAP_ENDPOINT` is set — routes the search through
|
||||
/// the in-sandbox LDAP stub over the real LDAPv3 BER wire (the stub's
|
||||
/// accept loop at [`crate::dynamic::stubs::ldap_server::accept_loop`]
|
||||
/// auto-detects the `0x30 SEQUENCE` lead byte and routes through the
|
||||
/// reader/writer at [`crate::dynamic::stubs::ldap_ber`]). Falls back
|
||||
/// to an in-process RFC 4515 subset matcher against three canonical
|
||||
/// users (`alice`, `bob`, `carol`) when the env var is unset, the
|
||||
/// filter does not parse as a supported RFC 4515 shape, or the socket
|
||||
/// exchange errors, so the harness still produces a verdict on hosts
|
||||
/// that exercise it outside the stub-backed corpus. Writes a
|
||||
/// `ProbeKind::Ldap { entries_returned }` probe whose `n` is the
|
||||
/// count the directory returned. The BER client is pure-stdlib (just
|
||||
/// `socket`) so no extra pip dep is required.
|
||||
pub fn emit_ldap_harness(_spec: &HarnessSpec) -> HarnessSource {
|
||||
let probe = probe_shim();
|
||||
let body = format!(
|
||||
|
|
@ -1644,12 +1651,250 @@ def _nyx_match_one(filt, uid):
|
|||
return _nyx_attr_match(pattern, uid)
|
||||
|
||||
|
||||
def _nyx_ldap_count_via_stub(filt):
|
||||
"""Route through the in-sandbox LDAP stub when NYX_LDAP_ENDPOINT is set.
|
||||
# --- LDAPv3 BER client (zero-dep, pure stdlib) ----------------------------
|
||||
# Tags this client emits / consumes. Mirrors `src/dynamic/stubs/ldap_ber.rs`.
|
||||
_NYX_BER_BOOLEAN = 0x01
|
||||
_NYX_BER_INTEGER = 0x02
|
||||
_NYX_BER_OCTET_STRING = 0x04
|
||||
_NYX_BER_ENUMERATED = 0x0A
|
||||
_NYX_BER_SEQUENCE = 0x30
|
||||
_NYX_BER_BIND_REQUEST = 0x60
|
||||
_NYX_BER_BIND_RESPONSE = 0x61
|
||||
_NYX_BER_SEARCH_REQUEST = 0x63
|
||||
_NYX_BER_SEARCH_RESULT_ENTRY = 0x64
|
||||
_NYX_BER_SEARCH_RESULT_DONE = 0x65
|
||||
_NYX_BER_AUTH_SIMPLE = 0x80
|
||||
_NYX_BER_FILTER_AND = 0xA0
|
||||
_NYX_BER_FILTER_OR = 0xA1
|
||||
_NYX_BER_FILTER_EQUALITY = 0xA3
|
||||
_NYX_BER_FILTER_SUBSTRINGS = 0xA4
|
||||
_NYX_BER_FILTER_PRESENT = 0x87
|
||||
_NYX_BER_SUBSTR_INITIAL = 0x80
|
||||
_NYX_BER_SUBSTR_ANY = 0x81
|
||||
_NYX_BER_SUBSTR_FINAL = 0x82
|
||||
|
||||
Returns the parsed `COUNT <n>` reply on success, or ``None`` when the
|
||||
env var is unset, the address fails to parse, or the socket exchange
|
||||
errors — caller falls back to the in-process matcher.
|
||||
|
||||
def _nyx_ber_length(n):
|
||||
if n < 0x80:
|
||||
return bytes([n])
|
||||
tmp = []
|
||||
while n:
|
||||
tmp.append(n & 0xFF)
|
||||
n >>= 8
|
||||
tmp.reverse()
|
||||
return bytes([0x80 | len(tmp)]) + bytes(tmp)
|
||||
|
||||
|
||||
def _nyx_ber_tlv(tag, body):
|
||||
return bytes([tag]) + _nyx_ber_length(len(body)) + body
|
||||
|
||||
|
||||
def _nyx_ber_int(n):
|
||||
if n < 0:
|
||||
return None
|
||||
if n == 0:
|
||||
body = b"\x00"
|
||||
else:
|
||||
tmp = []
|
||||
x = n
|
||||
while x > 0:
|
||||
tmp.append(x & 0xFF)
|
||||
x >>= 8
|
||||
tmp.reverse()
|
||||
body = bytes(tmp)
|
||||
if body[0] & 0x80:
|
||||
body = b"\x00" + body
|
||||
return _nyx_ber_tlv(_NYX_BER_INTEGER, body)
|
||||
|
||||
|
||||
def _nyx_ber_enum(n):
|
||||
return _nyx_ber_tlv(_NYX_BER_ENUMERATED, bytes([n & 0xFF]))
|
||||
|
||||
|
||||
def _nyx_ber_octstr(s):
|
||||
if isinstance(s, str):
|
||||
s = s.encode("utf-8")
|
||||
return _nyx_ber_tlv(_NYX_BER_OCTET_STRING, s)
|
||||
|
||||
|
||||
def _nyx_ber_bool(b):
|
||||
return _nyx_ber_tlv(_NYX_BER_BOOLEAN, b"\xFF" if b else b"\x00")
|
||||
|
||||
|
||||
def _nyx_ber_seq(body):
|
||||
return _nyx_ber_tlv(_NYX_BER_SEQUENCE, body)
|
||||
|
||||
|
||||
def _nyx_valid_attr(a):
|
||||
if not a:
|
||||
return False
|
||||
for ch in a:
|
||||
if not (ch.isalnum() or ch in "-_."):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _nyx_split_paren_children(s):
|
||||
"""Split a string of concatenated `(...)(...)` groups into a list."""
|
||||
out = []
|
||||
i = 0
|
||||
n = len(s)
|
||||
while i < n:
|
||||
if s[i] != "(":
|
||||
return None
|
||||
depth = 0
|
||||
start = i
|
||||
while i < n:
|
||||
c = s[i]
|
||||
if c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
i += 1
|
||||
break
|
||||
i += 1
|
||||
if depth != 0:
|
||||
return None
|
||||
out.append(s[start:i])
|
||||
return out
|
||||
|
||||
|
||||
def _nyx_encode_filter(filt):
|
||||
"""RFC 4515 (subset) -> BER bytes. Returns ``None`` for invalid /
|
||||
unsupported filter shapes; caller falls back to the local matcher."""
|
||||
s = filt.strip()
|
||||
if not s.startswith("(") or not s.endswith(")"):
|
||||
return None
|
||||
depth = 0
|
||||
for i, c in enumerate(s):
|
||||
if c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
return None
|
||||
if depth == 0 and i != len(s) - 1:
|
||||
return None
|
||||
if depth != 0:
|
||||
return None
|
||||
inner = s[1:-1]
|
||||
if not inner:
|
||||
return None
|
||||
head = inner[0]
|
||||
if head in ("&", "|"):
|
||||
children = _nyx_split_paren_children(inner[1:])
|
||||
if not children:
|
||||
return None
|
||||
parts = b""
|
||||
for c in children:
|
||||
sub = _nyx_encode_filter(c)
|
||||
if sub is None:
|
||||
return None
|
||||
parts += sub
|
||||
tag = _NYX_BER_FILTER_AND if head == "&" else _NYX_BER_FILTER_OR
|
||||
return _nyx_ber_tlv(tag, parts)
|
||||
if "=" not in inner:
|
||||
return None
|
||||
attr, _, val = inner.partition("=")
|
||||
if not _nyx_valid_attr(attr):
|
||||
return None
|
||||
if val == "*":
|
||||
return _nyx_ber_tlv(_NYX_BER_FILTER_PRESENT, attr.encode("utf-8"))
|
||||
if "*" in val:
|
||||
parts = val.split("*")
|
||||
seq = b""
|
||||
if parts[0]:
|
||||
seq += _nyx_ber_tlv(_NYX_BER_SUBSTR_INITIAL, parts[0].encode("utf-8"))
|
||||
for p in parts[1:-1]:
|
||||
if p:
|
||||
seq += _nyx_ber_tlv(_NYX_BER_SUBSTR_ANY, p.encode("utf-8"))
|
||||
if parts[-1]:
|
||||
seq += _nyx_ber_tlv(_NYX_BER_SUBSTR_FINAL, parts[-1].encode("utf-8"))
|
||||
body = _nyx_ber_octstr(attr) + _nyx_ber_seq(seq)
|
||||
return _nyx_ber_tlv(_NYX_BER_FILTER_SUBSTRINGS, body)
|
||||
body = _nyx_ber_octstr(attr) + _nyx_ber_octstr(val)
|
||||
return _nyx_ber_tlv(_NYX_BER_FILTER_EQUALITY, body)
|
||||
|
||||
|
||||
def _nyx_read_n(sock, n):
|
||||
out = b""
|
||||
while len(out) < n:
|
||||
chunk = sock.recv(n - len(out))
|
||||
if not chunk:
|
||||
return None
|
||||
out += chunk
|
||||
return out
|
||||
|
||||
|
||||
def _nyx_read_ber_message(sock):
|
||||
head = _nyx_read_n(sock, 2)
|
||||
if head is None or head[0] != _NYX_BER_SEQUENCE:
|
||||
return None
|
||||
if head[1] & 0x80 == 0:
|
||||
body_len = head[1]
|
||||
length_bytes = b""
|
||||
else:
|
||||
nl = head[1] & 0x7F
|
||||
if nl == 0 or nl > 4:
|
||||
return None
|
||||
length_bytes = _nyx_read_n(sock, nl)
|
||||
if length_bytes is None:
|
||||
return None
|
||||
body_len = 0
|
||||
for b in length_bytes:
|
||||
body_len = (body_len << 8) | b
|
||||
if body_len > 64 * 1024:
|
||||
return None
|
||||
body = _nyx_read_n(sock, body_len)
|
||||
if body is None:
|
||||
return None
|
||||
return head + length_bytes + body
|
||||
|
||||
|
||||
def _nyx_decode_tlv(buf, offset):
|
||||
if offset + 2 > len(buf):
|
||||
return None
|
||||
tag = buf[offset]
|
||||
first_len = buf[offset + 1]
|
||||
if first_len & 0x80 == 0:
|
||||
body_len = first_len
|
||||
body_start = offset + 2
|
||||
else:
|
||||
nl = first_len & 0x7F
|
||||
if nl == 0 or nl > 4 or offset + 2 + nl > len(buf):
|
||||
return None
|
||||
body_len = 0
|
||||
for b in buf[offset + 2:offset + 2 + nl]:
|
||||
body_len = (body_len << 8) | b
|
||||
body_start = offset + 2 + nl
|
||||
body_end = body_start + body_len
|
||||
if body_end > len(buf):
|
||||
return None
|
||||
return (tag, buf[body_start:body_end], body_end)
|
||||
|
||||
|
||||
def _nyx_decode_ldap_op(msg):
|
||||
"""Return ``(op_tag, op_body)`` for an LDAPMessage byte slice."""
|
||||
outer = _nyx_decode_tlv(msg, 0)
|
||||
if outer is None or outer[0] != _NYX_BER_SEQUENCE:
|
||||
return None
|
||||
inner = outer[1]
|
||||
msg_id_tlv = _nyx_decode_tlv(inner, 0)
|
||||
if msg_id_tlv is None or msg_id_tlv[0] != _NYX_BER_INTEGER:
|
||||
return None
|
||||
op_tlv = _nyx_decode_tlv(inner, msg_id_tlv[2])
|
||||
if op_tlv is None:
|
||||
return None
|
||||
return (op_tlv[0], op_tlv[1])
|
||||
|
||||
|
||||
def _nyx_ldap_count_via_ber(filt):
|
||||
"""Route through the in-sandbox LDAP stub via real LDAPv3 BER when
|
||||
`NYX_LDAP_ENDPOINT` is set. Returns the entry count on success, or
|
||||
``None`` when the env var is unset, the filter is not a supported
|
||||
RFC 4515 shape, the address fails to parse, the bind fails, or the
|
||||
socket exchange errors — caller falls back to the in-process matcher.
|
||||
"""
|
||||
ep = os.environ.get("NYX_LDAP_ENDPOINT", "")
|
||||
if not ep:
|
||||
|
|
@ -1662,23 +1907,56 @@ def _nyx_ldap_count_via_stub(filt):
|
|||
port = int(ep[sep + 1:])
|
||||
except ValueError:
|
||||
return None
|
||||
filter_bytes = _nyx_encode_filter(filt)
|
||||
if filter_bytes is None:
|
||||
return None
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=2.0) as sock:
|
||||
sock.sendall(("SEARCH " + filt + "\n").encode("utf-8"))
|
||||
buf = sock.makefile("rb")
|
||||
line = buf.readline()
|
||||
if not line:
|
||||
sock.settimeout(2.0)
|
||||
bind_body = (
|
||||
_nyx_ber_int(3)
|
||||
+ _nyx_ber_octstr(b"")
|
||||
+ _nyx_ber_tlv(_NYX_BER_AUTH_SIMPLE, b"")
|
||||
)
|
||||
bind_msg = _nyx_ber_seq(
|
||||
_nyx_ber_int(1) + _nyx_ber_tlv(_NYX_BER_BIND_REQUEST, bind_body)
|
||||
)
|
||||
sock.sendall(bind_msg)
|
||||
resp = _nyx_read_ber_message(sock)
|
||||
if resp is None:
|
||||
return None
|
||||
try:
|
||||
line_s = line.decode("utf-8", "replace").rstrip("\r\n")
|
||||
except Exception:
|
||||
return None
|
||||
if not line_s.startswith("COUNT "):
|
||||
return None
|
||||
try:
|
||||
return int(line_s[len("COUNT "):].strip())
|
||||
except ValueError:
|
||||
decoded = _nyx_decode_ldap_op(resp)
|
||||
if decoded is None or decoded[0] != _NYX_BER_BIND_RESPONSE:
|
||||
return None
|
||||
search_body = (
|
||||
_nyx_ber_octstr(b"")
|
||||
+ _nyx_ber_enum(2)
|
||||
+ _nyx_ber_enum(0)
|
||||
+ _nyx_ber_int(0)
|
||||
+ _nyx_ber_int(2)
|
||||
+ _nyx_ber_bool(False)
|
||||
+ filter_bytes
|
||||
+ _nyx_ber_seq(b"")
|
||||
)
|
||||
search_msg = _nyx_ber_seq(
|
||||
_nyx_ber_int(2) + _nyx_ber_tlv(_NYX_BER_SEARCH_REQUEST, search_body)
|
||||
)
|
||||
sock.sendall(search_msg)
|
||||
count = 0
|
||||
while True:
|
||||
resp = _nyx_read_ber_message(sock)
|
||||
if resp is None:
|
||||
return None
|
||||
decoded = _nyx_decode_ldap_op(resp)
|
||||
if decoded is None:
|
||||
return None
|
||||
op_tag = decoded[0]
|
||||
if op_tag == _NYX_BER_SEARCH_RESULT_ENTRY:
|
||||
count += 1
|
||||
elif op_tag == _NYX_BER_SEARCH_RESULT_DONE:
|
||||
return count
|
||||
else:
|
||||
return count
|
||||
except (OSError, socket.timeout):
|
||||
return None
|
||||
|
||||
|
|
@ -1695,9 +1973,9 @@ def _nyx_ldap_count_local(filt):
|
|||
|
||||
|
||||
def _nyx_ldap_count(filt):
|
||||
via_stub = _nyx_ldap_count_via_stub(filt)
|
||||
if via_stub is not None:
|
||||
return via_stub
|
||||
via_ber = _nyx_ldap_count_via_ber(filt)
|
||||
if via_ber is not None:
|
||||
return via_ber
|
||||
return _nyx_ldap_count_local(filt)
|
||||
|
||||
|
||||
|
|
@ -3150,12 +3428,20 @@ mod tests {
|
|||
"Python LDAP harness must open a TCP socket against the stub endpoint",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("SEARCH "),
|
||||
"Python LDAP harness must write SEARCH <filter> over the wire",
|
||||
h.source.contains("_NYX_BER_BIND_REQUEST = 0x60"),
|
||||
"Python LDAP harness must compose an LDAPv3 BindRequest (BER tag 0x60)",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("COUNT "),
|
||||
"Python LDAP harness must parse the COUNT <n> reply line",
|
||||
h.source.contains("_NYX_BER_SEARCH_REQUEST = 0x63"),
|
||||
"Python LDAP harness must compose an LDAPv3 SearchRequest (BER tag 0x63)",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_encode_filter"),
|
||||
"Python LDAP harness must encode the RFC 4515 filter string into BER bytes",
|
||||
);
|
||||
assert!(
|
||||
!h.source.contains("\"SEARCH \""),
|
||||
"Python LDAP harness must no longer write the plaintext SEARCH <filter> tier-(a) framing",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3167,8 +3453,8 @@ mod tests {
|
|||
"Python LDAP harness must keep the in-process matcher as a fallback for hosts without the stub",
|
||||
);
|
||||
assert!(
|
||||
h.source.contains("_nyx_ldap_count_via_stub"),
|
||||
"Python LDAP harness must dispatch through the stub-route helper",
|
||||
h.source.contains("_nyx_ldap_count_via_ber"),
|
||||
"Python LDAP harness must dispatch through the BER stub-route helper",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -562,6 +562,17 @@ mod e2e_phase_06 {
|
|||
events.iter().any(|e| e.summary.starts_with("SEARCH (uid=")),
|
||||
"Python harness stub events must carry a `(uid=…)` filter; got {events:?}",
|
||||
);
|
||||
// The Python emitter now dispatches via a pure-stdlib LDAPv3 BER
|
||||
// client, so the stub's BER handler must record `protocol=ldapv3`
|
||||
// on at least one event — pins the tier-(b) wire format and
|
||||
// prevents a regression that silently falls back to the plaintext
|
||||
// path.
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|e| e.detail.get("protocol").map(String::as_str) == Some("ldapv3")),
|
||||
"Python harness must exercise the LDAPv3 BER path; got {events:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -582,5 +593,16 @@ mod e2e_phase_06 {
|
|||
events.iter().any(|e| e.summary.starts_with("SEARCH (uid=")),
|
||||
"PHP harness stub events must carry a `(uid=…)` filter; got {events:?}",
|
||||
);
|
||||
// The PHP emitter now dispatches via a core-PHP LDAPv3 BER client
|
||||
// (no `ext-ldap` dep), so the stub's BER handler must record
|
||||
// `protocol=ldapv3` on at least one event — pins the tier-(b) wire
|
||||
// format and prevents a regression that silently falls back to the
|
||||
// plaintext path.
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|e| e.detail.get("protocol").map(String::as_str) == Some("ldapv3")),
|
||||
"PHP harness must exercise the LDAPv3 BER path; got {events:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue