1pub mod actions;
20
21use anyhow::{Context, Result};
22use std::net::SocketAddr;
23use std::sync::Arc;
24use tokio::sync::mpsc;
25use tracing::{debug, error, trace, warn};
26
27#[cfg(not(target_arch = "wasm32"))]
28use crate::llm::action_helper::call_llm;
29#[cfg(not(target_arch = "wasm32"))]
30use crate::llm::ollama_client::OllamaClient;
31#[cfg(not(target_arch = "wasm32"))]
32use crate::llm::ActionResult;
33#[cfg(not(target_arch = "wasm32"))]
34use crate::logging::emit::Log;
35#[cfg(not(target_arch = "wasm32"))]
36use crate::protocol::Event;
37#[cfg(not(target_arch = "wasm32"))]
38use crate::server::LdapProtocol;
39#[cfg(not(target_arch = "wasm32"))]
40use crate::state::app_state::AppState;
41#[cfg(not(target_arch = "wasm32"))]
42use actions::{
43 LDAP_ADD_EVENT, LDAP_BIND_EVENT, LDAP_DELETE_EVENT, LDAP_MODIFY_EVENT, LDAP_SEARCH_EVENT,
44 LDAP_UNBIND_EVENT,
45};
46
47#[cfg(not(target_arch = "wasm32"))]
75const FIRST_MESSAGE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
76
77#[cfg(not(target_arch = "wasm32"))]
91const IDLE_BETWEEN_MESSAGES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
92
93#[cfg(not(target_arch = "wasm32"))]
99const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
100
101#[cfg(not(target_arch = "wasm32"))]
118const CONNECTION_CAP_REFUSAL: &[u8] = &[
119 0x30, 0x18, 0x02, 0x01, 0x00, 0x78, 0x13, 0x0A, 0x01, 0x34, 0x04, 0x00, 0x04, 0x00, 0x8A, 0x0A,
120 0x2B, 0x06, 0x01, 0x04, 0x01, 0x8B, 0x3A, 0x81, 0x9C, 0x44,
121];
122
123pub struct LdapServer;
125
126#[cfg(not(target_arch = "wasm32"))]
127impl LdapServer {
128 pub async fn spawn_with_llm_actions(
130 listen_addr: SocketAddr,
131 llm_client: OllamaClient,
132 app_state: Arc<AppState>,
133 status_tx: mpsc::UnboundedSender<String>,
134 server_id: crate::state::ServerId,
135 first_byte_timeout_secs: Option<u64>,
136 idle_timeout_secs: Option<u64>,
137 ) -> Result<SocketAddr> {
138 let first_message_timeout = first_byte_timeout_secs
142 .map(std::time::Duration::from_secs)
143 .unwrap_or(FIRST_MESSAGE_READ_TIMEOUT);
144 let idle_timeout = idle_timeout_secs
145 .map(std::time::Duration::from_secs)
146 .unwrap_or(IDLE_BETWEEN_MESSAGES_TIMEOUT);
147 let listener =
148 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
149 let local_addr = listener.local_addr()?;
150 Log::new(Some(&status_tx)).info(format!(
151 "LDAP server (action-based) listening on {}",
152 local_addr
153 ));
154
155 let protocol = Arc::new(LdapProtocol::new());
156
157 let task_registrar = app_state.clone();
158 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
159 let accept_handle = tokio::spawn(async move {
160 loop {
161 match crate::server::accept_bounded::accept_bounded(
162 &listener,
163 &limiter,
164 CONNECTION_CAP_REFUSAL,
165 "LDAP",
166 Some(&status_tx),
167 )
168 .await
169 {
170 Ok((stream, remote_addr, permit)) => {
171 let connection_id = crate::server::connection::ConnectionId::new(
172 app_state.get_next_unified_id().await,
173 );
174 Log::new(Some(&status_tx)).info(format!(
175 "LDAP connection {} from {}",
176 connection_id, remote_addr
177 ));
178
179 {
187 use crate::state::server::{
188 ConnectionState as ServerConnectionState, ConnectionStatus,
189 ProtocolConnectionInfo,
190 };
191 let now = crate::utils::clock::Instant::now();
192 let local_addr = stream.local_addr().unwrap_or(local_addr);
193 app_state
194 .add_connection_to_server(
195 server_id,
196 ServerConnectionState {
197 id: connection_id,
198 remote_addr,
199 local_addr,
200 bytes_sent: 0,
201 bytes_received: 0,
202 packets_sent: 0,
203 packets_received: 0,
204 last_activity: now,
205 status: ConnectionStatus::Active,
206 status_changed_at: now,
207 protocol_info: ProtocolConnectionInfo::empty(),
208 },
209 )
210 .await;
211 let _ = status_tx.send("__UPDATE_UI__".to_string());
212 }
213
214 let llm_clone = llm_client.clone();
215 let state_clone = app_state.clone();
216 let status_clone = status_tx.clone();
217 let protocol_clone = protocol.clone();
218
219 let task_owner = app_state.clone();
221 task_owner
222 .spawn_server_task(server_id, async move {
223 let _permit = permit;
227
228 let mut session = LdapSession {
229 stream,
230 connection_id,
231 server_id,
232 llm_client: llm_clone.clone(),
233 app_state: state_clone.clone(),
234 status_tx: status_clone.clone(),
235 protocol: protocol_clone.clone(),
236 authenticated: false,
237 bind_dn: None,
238 first_message_timeout,
239 idle_timeout,
240 };
241
242 if let Err(e) = session.handle().await {
244 Log::new(Some(&status_clone))
245 .error(format!("LDAP session error: {}", e));
246 }
247
248 state_clone
249 .close_connection_on_server(server_id, connection_id)
250 .await;
251 let _ = status_clone.send("__UPDATE_UI__".to_string());
252
253 Log::new(Some(&status_clone))
254 .info(format!("LDAP connection {} closed", connection_id));
255 })
256 .await;
257 }
258 Err(e) => {
259 Log::new(Some(&status_tx))
260 .error(format!("Failed to accept LDAP connection: {}", e));
261 break;
262 }
263 }
264 }
265 });
266
267 task_registrar
268 .register_server_task(server_id, accept_handle)
269 .await;
270
271 Ok(local_addr)
272 }
273}
274
275#[cfg(not(target_arch = "wasm32"))]
276struct LdapSession {
277 stream: tokio::net::TcpStream,
278 connection_id: crate::server::connection::ConnectionId,
279 server_id: crate::state::ServerId,
280 llm_client: OllamaClient,
281 app_state: Arc<AppState>,
282 status_tx: mpsc::UnboundedSender<String>,
283 protocol: Arc<LdapProtocol>,
284 authenticated: bool,
285 bind_dn: Option<String>,
286 first_message_timeout: std::time::Duration,
288 idle_timeout: std::time::Duration,
290}
291
292#[cfg(not(target_arch = "wasm32"))]
293impl LdapSession {
294 async fn handle(&mut self) -> Result<()> {
302 use tokio::io::{AsyncReadExt, AsyncWriteExt};
303
304 let mut buffer: Vec<u8> = Vec::new();
305 let mut chunk = vec![0u8; READ_CHUNK];
306 let mut answered_one = false;
310
311 loop {
312 loop {
314 let message_len = match ldap_message_len(&buffer) {
315 Ok(Some(len)) => len,
316 Ok(None) => break, Err(e) => {
318 Log::new(Some(&self.status_tx)).error(format!("LDAP framing error: {}", e));
319 return Ok(());
320 }
321 };
322
323 let message: Vec<u8> = buffer.drain(..message_len).collect();
324 trace!("LDAP message {} bytes: {:02x?}", message.len(), message);
325
326 let step = match self.handle_message(&message).await {
327 Ok(step) => step,
328 Err(e) => {
329 Log::new(Some(&self.status_tx)).error(format!("LDAP parse error: {}", e));
330 return Ok(());
331 }
332 };
333
334 let (response, close_after) = match step {
335 SessionStep::Respond(response) => (Some(response), false),
336 SessionStep::RespondAndClose(response) => (Some(response), true),
337 SessionStep::Close => (None, true),
338 };
339
340 if let Some(response) = response {
341 Log::new(Some(&self.status_tx)).trace(format!(
342 "LDAP sending {} bytes: {:02x?}",
343 response.len(),
344 response
345 ));
346 self.stream.write_all(&response).await?;
347 self.stream.flush().await?;
348 self.record_bytes(0, response.len() as u64).await;
349 }
350
351 answered_one = true;
352
353 if close_after {
354 return Ok(());
355 }
356 }
357
358 if buffer.len() >= MAX_LDAP_MESSAGE {
359 error!("LDAP receive buffer exceeded {} bytes", MAX_LDAP_MESSAGE);
362 return Ok(());
363 }
364
365 let read_deadline = if answered_one {
370 self.idle_timeout
371 } else {
372 self.first_message_timeout
373 };
374 let n = match tokio::time::timeout(read_deadline, self.stream.read(&mut chunk)).await {
375 Err(_) => {
376 Log::new(Some(&self.status_tx)).info(format!(
380 "LDAP connection {} sent nothing for {}s; closing \
381 decision=fail_closed_idle_timeout",
382 self.connection_id,
383 read_deadline.as_secs()
384 ));
385 break;
386 }
387 Ok(Ok(0)) => break, Ok(Ok(n)) => n,
389 Ok(Err(e)) => {
390 Log::new(Some(&self.status_tx)).error(format!("LDAP read error: {}", e));
391 break;
392 }
393 };
394 Log::new(Some(&self.status_tx)).trace(format!("LDAP received {} bytes", n));
395 self.record_bytes(n as u64, 0).await;
396 buffer.extend_from_slice(&chunk[..n]);
397 }
398
399 Ok(())
400 }
401
402 async fn record_bytes(&self, received: u64, sent: u64) {
409 self.app_state
410 .update_connection_stats(
411 self.server_id,
412 self.connection_id,
413 (received > 0).then_some(received),
414 (sent > 0).then_some(sent),
415 (received > 0).then_some(1),
416 (sent > 0).then_some(1),
417 )
418 .await;
419 }
420
421 async fn handle_message(&mut self, data: &[u8]) -> Result<SessionStep> {
423 let (msg_id, op_tag, op_value) = decode_ldap_message(data)?;
424 debug!(
425 "LDAP message id={} op=0x{:02x} ({} bytes)",
426 msg_id,
427 op_tag,
428 op_value.len()
429 );
430
431 match op_tag {
432 OP_BIND_REQUEST => self.handle_bind_request(msg_id, op_value).await,
433 OP_SEARCH_REQUEST => self.handle_search_request(msg_id, op_value).await,
434 OP_ADD_REQUEST => self.handle_add_request(msg_id, op_value).await,
435 OP_MODIFY_REQUEST => self.handle_modify_request(msg_id, op_value).await,
436 OP_DELETE_REQUEST => self.handle_delete_request(msg_id, op_value).await,
437 OP_UNBIND_REQUEST => self.handle_unbind_request().await,
438 other => {
439 Log::new(Some(&self.status_tx))
440 .debug(format!("LDAP unsupported operation: 0x{:02x}", other));
441 Ok(SessionStep::Respond(encode_ldap_result(
446 msg_id,
447 response_tag_for(other),
448 RESULT_PROTOCOL_ERROR,
449 "Operation not supported by this server",
450 )))
451 }
452 }
453 }
454
455 async fn respond_via_llm(
468 &mut self,
469 event: crate::protocol::Event,
470 default: Vec<u8>,
471 msg_id: i32,
472 response_tag: u8,
473 ) -> (SessionStep, Decided) {
474 let execution_result = match call_llm(
475 &self.llm_client,
476 &self.app_state,
477 self.server_id,
478 Some(self.connection_id),
479 &event,
480 self.protocol.as_ref(),
481 )
482 .await
483 {
484 Ok(result) => result,
485 Err(e) => {
486 Log::new(Some(&self.status_tx)).warn(format!(
487 "LDAP LLM call failed on connection {} (msg_id {}): {}",
488 self.connection_id, msg_id, e
489 ));
490
491 let (code, diagnostic) = if crate::llm::is_overload_error(&e) {
493 warn!(
494 "LDAP busy on connection {}: LLM capacity exhausted",
495 self.connection_id
496 );
497 (RESULT_BUSY, "Server busy, retry later")
498 } else {
499 (RESULT_UNAVAILABLE, "Server unavailable")
500 };
501 Log::new(Some(&self.status_tx)).error(format!(
508 "LDAP msg_id {} on connection {}: decision=fail_closed_llm_error \
509 result={} ({})",
510 msg_id, self.connection_id, code, diagnostic
511 ));
512 return (
513 SessionStep::Respond(encode_ldap_result(
514 msg_id,
515 response_tag,
516 code,
517 diagnostic,
518 )),
519 Decided::ByServer,
520 );
521 }
522 };
523
524 let mut output: Option<Vec<u8>> = None;
525 let mut close = false;
526 for protocol_result in execution_result.protocol_results {
527 match protocol_result {
528 ActionResult::Output(data) => {
529 if output.is_none() {
530 output = Some(data);
531 }
532 }
533 ActionResult::CloseConnection => close = true,
534 _ => {}
535 }
536 }
537
538 match (output, close) {
539 (Some(data), false) => (SessionStep::Respond(data), Decided::ByModel),
540 (Some(data), true) => (SessionStep::RespondAndClose(data), Decided::ByModel),
541 (None, true) => (SessionStep::Close, Decided::ByModel),
542 (None, false) => {
543 Log::new(Some(&self.status_tx)).error(format!(
549 "LDAP msg_id {} on connection {}: decision=fail_closed_no_action \
550 (handler returned no response action; sending the refusal default)",
551 msg_id, self.connection_id
552 ));
553 (SessionStep::Respond(default), Decided::ByServer)
554 }
555 }
556 }
557
558 async fn handle_bind_request(&mut self, msg_id: i32, data: &[u8]) -> Result<SessionStep> {
559 let version_element = read_ber_element(data).context("bind version")?;
562 let version = ber_integer(version_element.value).context("bind version value")?;
563
564 let after_version = &data[version_element.total_len..];
565 let name_element = read_ber_element(after_version).context("bind name")?;
566 let dn = ber_string(name_element.value);
567
568 let after_name = &after_version[name_element.total_len..];
569 let (auth_type, password) = if after_name.is_empty() {
570 ("none".to_string(), String::new())
571 } else {
572 let auth = read_ber_element(after_name).context("bind authentication")?;
573 match auth.tag {
574 AUTH_SIMPLE => ("simple".to_string(), ber_string(auth.value)),
575 AUTH_SASL => {
576 let mechanism = read_ber_element(auth.value)
578 .map(|m| ber_string(m.value))
579 .unwrap_or_default();
580 (format!("sasl:{}", mechanism), String::new())
581 }
582 other => (format!("unknown:0x{:02x}", other), String::new()),
583 }
584 };
585
586 Log::new(Some(&self.status_tx)).debug(format!(
587 "LDAP Bind request: version={}, dn={}, auth={}, password_len={}",
588 version,
589 dn,
590 auth_type,
591 password.len()
592 ));
593
594 let event = Event::new(
595 &LDAP_BIND_EVENT,
596 serde_json::json!({
597 "message_id": msg_id,
598 "version": version,
599 "dn": dn,
600 "password": password,
601 "auth_type": auth_type,
602 }),
603 );
604
605 let default =
608 encode_bind_response(msg_id, RESULT_INVALID_CREDENTIALS, "Invalid credentials");
609 let (step, decided) = self
610 .respond_via_llm(event, default, msg_id, RESPONSE_BIND)
611 .await;
612
613 if decided == Decided::ByModel {
619 if let SessionStep::Respond(ref response) | SessionStep::RespondAndClose(ref response) =
620 step
621 {
622 if bind_succeeded(response) {
623 self.authenticated = true;
624 self.bind_dn = Some(dn.clone());
625 Log::new(Some(&self.status_tx)).info(format!(
626 "LDAP bind for {:?} on connection {}: decision=model_accept",
627 dn, self.connection_id
628 ));
629 } else {
630 Log::new(Some(&self.status_tx)).info(format!(
631 "LDAP bind for {:?} on connection {}: decision=model_reject",
632 dn, self.connection_id
633 ));
634 }
635 }
636 }
637
638 Ok(step)
639 }
640
641 async fn handle_search_request(&mut self, msg_id: i32, data: &[u8]) -> Result<SessionStep> {
642 let SearchRequestFields {
643 base_dn,
644 scope,
645 filter,
646 attributes,
647 } = parse_search_request(data)?;
648
649 Log::new(Some(&self.status_tx)).debug(format!(
650 "LDAP Search request: base_dn={}, scope={}, filter={}",
651 base_dn, scope, filter
652 ));
653
654 let event = Event::new(
655 &LDAP_SEARCH_EVENT,
656 serde_json::json!({
657 "message_id": msg_id,
658 "base_dn": base_dn,
659 "authenticated": self.authenticated,
660 "bind_dn": self.bind_dn.as_deref().unwrap_or(""),
661 "scope": scope,
662 "filter": filter,
663 "attributes": attributes,
664 }),
665 );
666
667 let default = encode_search_done(msg_id, RESULT_SUCCESS, "");
671 Ok(self
672 .respond_via_llm(event, default, msg_id, RESPONSE_SEARCH_DONE)
673 .await
674 .0)
675 }
676
677 async fn handle_add_request(&mut self, msg_id: i32, data: &[u8]) -> Result<SessionStep> {
678 let entry_element = read_ber_element(data).context("add entry DN")?;
681 let dn = ber_string(entry_element.value);
682
683 let attributes = match read_ber_element(&data[entry_element.total_len..]) {
684 Ok(list) => parse_attribute_list(list.value),
685 Err(_) => serde_json::Map::new(),
686 };
687
688 Log::new(Some(&self.status_tx)).debug(format!(
689 "LDAP Add request: dn={}, {} attributes",
690 dn,
691 attributes.len()
692 ));
693
694 let event = Event::new(
695 &LDAP_ADD_EVENT,
696 serde_json::json!({
697 "message_id": msg_id,
698 "dn": dn,
699 "attributes": attributes,
700 "authenticated": self.authenticated,
701 "bind_dn": self.bind_dn.as_deref().unwrap_or(""),
702 }),
703 );
704
705 let default = encode_ldap_result(
706 msg_id,
707 RESPONSE_ADD,
708 RESULT_UNWILLING_TO_PERFORM,
709 "No response from server policy",
710 );
711 Ok(self
712 .respond_via_llm(event, default, msg_id, RESPONSE_ADD)
713 .await
714 .0)
715 }
716
717 async fn handle_modify_request(&mut self, msg_id: i32, data: &[u8]) -> Result<SessionStep> {
718 let object_element = read_ber_element(data).context("modify object DN")?;
722 let dn = ber_string(object_element.value);
723
724 let mut changes = Vec::new();
725 if let Ok(change_list) = read_ber_element(&data[object_element.total_len..]) {
726 let mut cursor = change_list.value;
727 while let Ok(change) = read_ber_element(cursor) {
728 cursor = &cursor[change.total_len..];
729
730 let Ok(operation_element) = read_ber_element(change.value) else {
731 continue;
732 };
733 let operation = match ber_integer(operation_element.value).unwrap_or(-1) {
734 0 => "add",
735 1 => "delete",
736 2 => "replace",
737 3 => "increment",
738 _ => "unknown",
739 };
740
741 let modification = &change.value[operation_element.total_len..];
742 let (attribute, values) = match read_ber_element(modification) {
743 Ok(partial) => parse_partial_attribute(partial.value),
744 Err(_) => (String::new(), Vec::new()),
745 };
746
747 changes.push(serde_json::json!({
748 "operation": operation,
749 "attribute": attribute,
750 "values": values,
751 }));
752 }
753 }
754
755 Log::new(Some(&self.status_tx)).debug(format!(
756 "LDAP Modify request: dn={}, {} changes",
757 dn,
758 changes.len()
759 ));
760
761 let event = Event::new(
762 &LDAP_MODIFY_EVENT,
763 serde_json::json!({
764 "message_id": msg_id,
765 "dn": dn,
766 "changes": changes,
767 "authenticated": self.authenticated,
768 "bind_dn": self.bind_dn.as_deref().unwrap_or(""),
769 }),
770 );
771
772 let default = encode_ldap_result(
773 msg_id,
774 RESPONSE_MODIFY,
775 RESULT_UNWILLING_TO_PERFORM,
776 "No response from server policy",
777 );
778 Ok(self
779 .respond_via_llm(event, default, msg_id, RESPONSE_MODIFY)
780 .await
781 .0)
782 }
783
784 async fn handle_delete_request(&mut self, msg_id: i32, data: &[u8]) -> Result<SessionStep> {
785 let dn = ber_string(data);
787
788 Log::new(Some(&self.status_tx)).debug(format!("LDAP Delete request: dn={}", dn));
789
790 let event = Event::new(
791 &LDAP_DELETE_EVENT,
792 serde_json::json!({
793 "message_id": msg_id,
794 "dn": dn,
795 "authenticated": self.authenticated,
796 "bind_dn": self.bind_dn.as_deref().unwrap_or(""),
797 }),
798 );
799
800 let default = encode_ldap_result(
801 msg_id,
802 RESPONSE_DELETE,
803 RESULT_UNWILLING_TO_PERFORM,
804 "No response from server policy",
805 );
806 Ok(self
807 .respond_via_llm(event, default, msg_id, RESPONSE_DELETE)
808 .await
809 .0)
810 }
811
812 async fn handle_unbind_request(&mut self) -> Result<SessionStep> {
813 Log::new(Some(&self.status_tx))
814 .debug(format!("LDAP Unbind request from {}", self.connection_id));
815
816 let event = Event::new(
825 &LDAP_UNBIND_EVENT,
826 serde_json::json!({
827 "bind_dn": self.bind_dn.as_deref().unwrap_or(""),
828 }),
829 );
830
831 let llm_client = self.llm_client.clone();
832 let app_state = self.app_state.clone();
833 let protocol = self.protocol.clone();
834 let server_id = self.server_id;
835 let connection_id = self.connection_id;
836 let task_owner = app_state.clone();
838 task_owner
839 .spawn_server_task(server_id, async move {
840 let _ = call_llm(
841 &llm_client,
842 &app_state,
843 server_id,
844 Some(connection_id),
845 &event,
846 protocol.as_ref(),
847 )
848 .await;
849 })
850 .await;
851
852 Ok(SessionStep::Close)
853 }
854}
855
856#[cfg(not(target_arch = "wasm32"))]
858#[derive(Debug, Clone, Copy, PartialEq, Eq)]
864enum Decided {
865 ByModel,
867 ByServer,
870}
871
872enum SessionStep {
873 Respond(Vec<u8>),
874 RespondAndClose(Vec<u8>),
875 Close,
876}
877
878#[cfg(not(target_arch = "wasm32"))]
890pub const MAX_LDAP_MESSAGE: usize = 1 << 20;
891
892#[cfg(not(target_arch = "wasm32"))]
894const READ_CHUNK: usize = 8192;
895
896#[cfg(not(target_arch = "wasm32"))]
902pub const MAX_FILTER_DEPTH: usize = 32;
903
904#[cfg(not(target_arch = "wasm32"))]
905const TAG_SEQUENCE: u8 = 0x30;
906#[cfg(not(target_arch = "wasm32"))]
907const TAG_INTEGER: u8 = 0x02;
908
909#[cfg(not(target_arch = "wasm32"))]
910const OP_BIND_REQUEST: u8 = 0x60;
911#[cfg(not(target_arch = "wasm32"))]
912const OP_SEARCH_REQUEST: u8 = 0x63;
913#[cfg(not(target_arch = "wasm32"))]
914const OP_MODIFY_REQUEST: u8 = 0x66;
915#[cfg(not(target_arch = "wasm32"))]
916const OP_ADD_REQUEST: u8 = 0x68;
917#[cfg(not(target_arch = "wasm32"))]
918const OP_DELETE_REQUEST: u8 = 0x4A;
919#[cfg(not(target_arch = "wasm32"))]
920const OP_UNBIND_REQUEST: u8 = 0x42;
921
922#[cfg(not(target_arch = "wasm32"))]
923const RESPONSE_BIND: u8 = 0x61;
924#[cfg(not(target_arch = "wasm32"))]
925const RESPONSE_SEARCH_DONE: u8 = 0x65;
926#[cfg(not(target_arch = "wasm32"))]
927const RESPONSE_MODIFY: u8 = 0x67;
928#[cfg(not(target_arch = "wasm32"))]
929const RESPONSE_ADD: u8 = 0x69;
930#[cfg(not(target_arch = "wasm32"))]
931const RESPONSE_DELETE: u8 = 0x6B;
932#[cfg(not(target_arch = "wasm32"))]
933const RESPONSE_EXTENDED: u8 = 0x78;
934
935#[cfg(not(target_arch = "wasm32"))]
936const AUTH_SIMPLE: u8 = 0x80;
937#[cfg(not(target_arch = "wasm32"))]
938const AUTH_SASL: u8 = 0xA3;
939
940#[cfg(not(target_arch = "wasm32"))]
941const RESULT_SUCCESS: u8 = 0;
942#[cfg(not(target_arch = "wasm32"))]
943const RESULT_PROTOCOL_ERROR: u8 = 2;
944#[cfg(not(target_arch = "wasm32"))]
945const RESULT_INVALID_CREDENTIALS: u8 = 49;
946#[cfg(not(target_arch = "wasm32"))]
947const RESULT_BUSY: u8 = 51;
948#[cfg(not(target_arch = "wasm32"))]
949const RESULT_UNAVAILABLE: u8 = 52;
950#[cfg(not(target_arch = "wasm32"))]
951const RESULT_UNWILLING_TO_PERFORM: u8 = 53;
952
953#[cfg(not(target_arch = "wasm32"))]
955fn response_tag_for(request_tag: u8) -> u8 {
956 match request_tag {
957 OP_BIND_REQUEST => RESPONSE_BIND,
958 OP_SEARCH_REQUEST => RESPONSE_SEARCH_DONE,
959 OP_MODIFY_REQUEST => RESPONSE_MODIFY,
960 OP_ADD_REQUEST => RESPONSE_ADD,
961 OP_DELETE_REQUEST => RESPONSE_DELETE,
962 _ => RESPONSE_EXTENDED,
963 }
964}
965
966#[cfg(not(target_arch = "wasm32"))]
968struct BerElement<'a> {
969 tag: u8,
970 value: &'a [u8],
972 total_len: usize,
974}
975
976#[cfg(not(target_arch = "wasm32"))]
978fn read_ber_element(data: &[u8]) -> Result<BerElement<'_>> {
979 if data.is_empty() {
980 anyhow::bail!("Truncated BER element: no tag");
981 }
982
983 let (content_len, len_bytes) = parse_ber_length(&data[1..])?;
984 let header_len = 1 + len_bytes;
985 let total_len = header_len
986 .checked_add(content_len)
987 .context("BER length overflow")?;
988
989 if total_len > data.len() {
990 anyhow::bail!(
991 "Truncated BER element: claims {} content bytes, {} available",
992 content_len,
993 data.len().saturating_sub(header_len)
994 );
995 }
996
997 Ok(BerElement {
998 tag: data[0],
999 value: &data[header_len..total_len],
1000 total_len,
1001 })
1002}
1003
1004#[cfg(not(target_arch = "wasm32"))]
1006fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
1007 if data.is_empty() {
1008 anyhow::bail!("Empty data for BER length");
1009 }
1010
1011 let first_byte = data[0];
1012
1013 if first_byte & 0x80 == 0 {
1014 Ok((first_byte as usize, 1))
1016 } else {
1017 let num_len_bytes = (first_byte & 0x7F) as usize;
1019 if num_len_bytes == 0 || num_len_bytes > 4 {
1020 anyhow::bail!(
1021 "Invalid BER length encoding ({} length bytes)",
1022 num_len_bytes
1023 );
1024 }
1025
1026 if data.len() < 1 + num_len_bytes {
1027 anyhow::bail!("Insufficient data for BER length");
1028 }
1029
1030 let mut length = 0usize;
1031 for byte in &data[1..1 + num_len_bytes] {
1032 length = (length << 8) | *byte as usize;
1033 }
1034
1035 Ok((length, 1 + num_len_bytes))
1036 }
1037}
1038
1039#[cfg(not(target_arch = "wasm32"))]
1041fn ber_integer(value: &[u8]) -> Result<i64> {
1042 if value.is_empty() {
1043 anyhow::bail!("Empty BER INTEGER");
1044 }
1045 if value.len() > 8 {
1046 anyhow::bail!("BER INTEGER too wide ({} bytes)", value.len());
1047 }
1048
1049 let mut result = if value[0] & 0x80 != 0 { -1i64 } else { 0i64 };
1051 for byte in value {
1052 result = (result << 8) | *byte as i64;
1053 }
1054 Ok(result)
1055}
1056
1057#[cfg(not(target_arch = "wasm32"))]
1059fn ber_string(value: &[u8]) -> String {
1060 String::from_utf8_lossy(value).to_string()
1061}
1062
1063#[cfg(not(target_arch = "wasm32"))]
1068pub fn ldap_message_len(buffer: &[u8]) -> Result<Option<usize>> {
1069 if buffer.is_empty() {
1070 return Ok(None);
1071 }
1072 if buffer[0] != TAG_SEQUENCE {
1073 anyhow::bail!(
1074 "Invalid LDAP message: expected SEQUENCE, got tag 0x{:02x}",
1075 buffer[0]
1076 );
1077 }
1078 if buffer.len() < 2 {
1079 return Ok(None);
1080 }
1081
1082 let first = buffer[1];
1083 let (content_len, len_bytes) = if first & 0x80 == 0 {
1084 (first as usize, 1)
1085 } else {
1086 let n = (first & 0x7F) as usize;
1087 if n == 0 || n > 4 {
1088 anyhow::bail!("Invalid BER length encoding ({} length bytes)", n);
1089 }
1090 if buffer.len() < 2 + n {
1091 return Ok(None);
1092 }
1093 let mut value = 0usize;
1094 for byte in &buffer[2..2 + n] {
1095 value = (value << 8) | *byte as usize;
1096 }
1097 (value, 1 + n)
1098 };
1099
1100 let total = 1 + len_bytes + content_len;
1101 if total > MAX_LDAP_MESSAGE {
1102 anyhow::bail!(
1103 "LDAP message of {} bytes exceeds the {} byte cap",
1104 total,
1105 MAX_LDAP_MESSAGE
1106 );
1107 }
1108 if buffer.len() < total {
1109 return Ok(None);
1110 }
1111 Ok(Some(total))
1112}
1113
1114#[cfg(not(target_arch = "wasm32"))]
1116fn parse_attribute_list(mut data: &[u8]) -> serde_json::Map<String, serde_json::Value> {
1117 let mut attributes = serde_json::Map::new();
1118 while let Ok(attribute) = read_ber_element(data) {
1119 data = &data[attribute.total_len..];
1120 let (name, values) = parse_partial_attribute(attribute.value);
1121 if !name.is_empty() {
1122 attributes.insert(name, serde_json::Value::Array(values));
1123 }
1124 }
1125 attributes
1126}
1127
1128#[cfg(not(target_arch = "wasm32"))]
1130fn parse_partial_attribute(data: &[u8]) -> (String, Vec<serde_json::Value>) {
1131 let Ok(type_element) = read_ber_element(data) else {
1132 return (String::new(), Vec::new());
1133 };
1134 let name = ber_string(type_element.value);
1135
1136 let mut values = Vec::new();
1137 if let Ok(set) = read_ber_element(&data[type_element.total_len..]) {
1138 let mut cursor = set.value;
1139 while let Ok(value_element) = read_ber_element(cursor) {
1140 cursor = &cursor[value_element.total_len..];
1141 values.push(serde_json::Value::String(ber_string(value_element.value)));
1142 }
1143 }
1144
1145 (name, values)
1146}
1147
1148#[cfg(not(target_arch = "wasm32"))]
1153pub fn decode_ldap_message(data: &[u8]) -> Result<(i32, u8, &[u8])> {
1154 let envelope = read_ber_element(data).context("LDAPMessage envelope")?;
1155 if envelope.tag != TAG_SEQUENCE {
1156 anyhow::bail!(
1157 "Invalid LDAP message: expected SEQUENCE, got tag 0x{:02x}",
1158 envelope.tag
1159 );
1160 }
1161 let body = envelope.value;
1162
1163 let id_element = read_ber_element(body).context("messageID")?;
1164 if id_element.tag != TAG_INTEGER {
1165 anyhow::bail!(
1166 "Invalid LDAP message: expected messageID INTEGER, got tag 0x{:02x}",
1167 id_element.tag
1168 );
1169 }
1170 let msg_id = ber_integer(id_element.value).context("messageID value")? as i32;
1171
1172 let op = read_ber_element(&body[id_element.total_len..]).context("protocolOp")?;
1173 Ok((msg_id, op.tag, op.value))
1174}
1175
1176#[cfg(not(target_arch = "wasm32"))]
1178#[derive(Debug, Clone, PartialEq)]
1179pub struct SearchRequestFields {
1180 pub base_dn: String,
1181 pub scope: String,
1183 pub filter: String,
1185 pub attributes: Vec<serde_json::Value>,
1186}
1187
1188#[cfg(not(target_arch = "wasm32"))]
1200pub fn parse_search_request(data: &[u8]) -> Result<SearchRequestFields> {
1201 let base_element = read_ber_element(data).context("search baseObject")?;
1202 let base_dn = ber_string(base_element.value);
1203
1204 let mut rest = &data[base_element.total_len..];
1205 let mut scope = "sub".to_string();
1206 if let Ok(scope_element) = read_ber_element(rest) {
1207 scope = match ber_integer(scope_element.value).unwrap_or(2) {
1208 0 => "base",
1209 1 => "one",
1210 _ => "sub",
1211 }
1212 .to_string();
1213 rest = &rest[scope_element.total_len..];
1214 }
1215
1216 for _ in 0..4 {
1218 match read_ber_element(rest) {
1219 Ok(element) => rest = &rest[element.total_len..],
1220 Err(_) => break,
1221 }
1222 }
1223
1224 let filter = match read_ber_element(rest) {
1225 Ok(filter_element) => {
1226 let text = render_filter(&filter_element, 0);
1227 rest = &rest[filter_element.total_len..];
1228 text
1229 }
1230 Err(_) => "(objectClass=*)".to_string(),
1231 };
1232
1233 let attributes = match read_ber_element(rest) {
1234 Ok(list) => {
1235 let mut names = Vec::new();
1236 let mut cursor = list.value;
1237 while let Ok(element) = read_ber_element(cursor) {
1238 names.push(serde_json::Value::String(ber_string(element.value)));
1239 cursor = &cursor[element.total_len..];
1240 }
1241 names
1242 }
1243 Err(_) => Vec::new(),
1244 };
1245
1246 Ok(SearchRequestFields {
1247 base_dn,
1248 scope,
1249 filter,
1250 attributes,
1251 })
1252}
1253
1254#[cfg(not(target_arch = "wasm32"))]
1259fn render_filter(element: &BerElement<'_>, depth: usize) -> String {
1260 if depth >= MAX_FILTER_DEPTH {
1261 return "(...)".to_string();
1262 }
1263
1264 let render_children = |data: &[u8], prefix: &str| -> String {
1265 let mut out = String::from("(");
1266 out.push_str(prefix);
1267 let mut cursor = data;
1268 while let Ok(child) = read_ber_element(cursor) {
1269 cursor = &cursor[child.total_len..];
1270 out.push_str(&render_filter(&child, depth + 1));
1271 }
1272 out.push(')');
1273 out
1274 };
1275
1276 let render_comparison = |data: &[u8], operator: &str| -> String {
1277 let Ok(attribute) = read_ber_element(data) else {
1278 return "(?)".to_string();
1279 };
1280 let value = read_ber_element(&data[attribute.total_len..])
1281 .map(|v| ber_string(v.value))
1282 .unwrap_or_default();
1283 format!("({}{}{})", ber_string(attribute.value), operator, value)
1284 };
1285
1286 match element.tag {
1287 0xA0 => render_children(element.value, "&"),
1288 0xA1 => render_children(element.value, "|"),
1289 0xA2 => render_children(element.value, "!"),
1290 0xA3 => render_comparison(element.value, "="),
1291 0xA5 => render_comparison(element.value, ">="),
1292 0xA6 => render_comparison(element.value, "<="),
1293 0xA8 => render_comparison(element.value, "~="),
1294 0xA4 => {
1295 let Ok(attribute) = read_ber_element(element.value) else {
1297 return "(?)".to_string();
1298 };
1299 let mut pattern = String::new();
1300 if let Ok(parts) = read_ber_element(&element.value[attribute.total_len..]) {
1301 let mut cursor = parts.value;
1302 let mut leading_star = true;
1303 while let Ok(part) = read_ber_element(cursor) {
1304 cursor = &cursor[part.total_len..];
1305 match part.tag {
1306 0x80 => {
1307 pattern.push_str(&ber_string(part.value));
1308 leading_star = false;
1309 }
1310 _ => {
1311 if leading_star {
1312 pattern.push('*');
1313 leading_star = false;
1314 }
1315 pattern.push('*');
1316 pattern.push_str(&ber_string(part.value));
1317 }
1318 }
1319 }
1320 }
1321 format!("({}={}*)", ber_string(attribute.value), pattern)
1322 }
1323 0x87 => format!("({}=*)", ber_string(element.value)),
1324 other => format!("(filter-0x{:02x}=*)", other),
1325 }
1326}
1327
1328#[cfg(not(target_arch = "wasm32"))]
1333fn bind_succeeded(response: &[u8]) -> bool {
1334 let Ok(envelope) = read_ber_element(response) else {
1335 return false;
1336 };
1337 let Ok(message_id) = read_ber_element(envelope.value) else {
1338 return false;
1339 };
1340 let Ok(op) = read_ber_element(&envelope.value[message_id.total_len..]) else {
1341 return false;
1342 };
1343 if op.tag != RESPONSE_BIND {
1344 return false;
1345 }
1346 read_ber_element(op.value)
1347 .ok()
1348 .and_then(|result_code| ber_integer(result_code.value).ok())
1349 .map(|code| code == RESULT_SUCCESS as i64)
1350 .unwrap_or(false)
1351}
1352
1353#[cfg(not(target_arch = "wasm32"))]
1354fn encode_ber_length(length: usize) -> Vec<u8> {
1355 if length < 128 {
1356 vec![length as u8]
1357 } else if length < 256 {
1358 vec![0x81, length as u8]
1359 } else if length < 65536 {
1360 vec![0x82, (length >> 8) as u8, length as u8]
1361 } else {
1362 vec![
1363 0x83,
1364 (length >> 16) as u8,
1365 (length >> 8) as u8,
1366 length as u8,
1367 ]
1368 }
1369}
1370
1371#[cfg(not(target_arch = "wasm32"))]
1372fn encode_ber_integer(value: i32) -> Vec<u8> {
1373 let mut result = vec![0x02]; if value >= 0 && value < 128 {
1376 result.push(0x01); result.push(value as u8);
1378 } else {
1379 result.push(0x04); result.extend_from_slice(&value.to_be_bytes());
1381 }
1382
1383 result
1384}
1385
1386#[cfg(not(target_arch = "wasm32"))]
1387fn encode_bind_response(msg_id: i32, result_code: u8, diagnostic_message: &str) -> Vec<u8> {
1388 let mut bind_resp = Vec::new();
1395
1396 bind_resp.push(0x0A); bind_resp.push(0x01); bind_resp.push(result_code);
1400
1401 bind_resp.push(0x04); bind_resp.push(0x00); bind_resp.push(0x04); let diag_bytes = diagnostic_message.as_bytes();
1408 bind_resp.extend_from_slice(&encode_ber_length(diag_bytes.len()));
1409 bind_resp.extend_from_slice(diag_bytes);
1410
1411 let mut bind_msg = vec![0x61]; bind_msg.extend_from_slice(&encode_ber_length(bind_resp.len()));
1414 bind_msg.extend_from_slice(&bind_resp);
1415
1416 encode_ldap_message(msg_id, bind_msg)
1418}
1419
1420#[cfg(not(target_arch = "wasm32"))]
1421fn encode_search_done(msg_id: i32, result_code: u8, diagnostic_message: &str) -> Vec<u8> {
1422 let mut result = Vec::new();
1425
1426 result.push(0x0A);
1428 result.push(0x01);
1429 result.push(result_code);
1430
1431 result.push(0x04);
1433 result.push(0x00);
1434
1435 result.push(0x04);
1437 let diag_bytes = diagnostic_message.as_bytes();
1438 result.extend_from_slice(&encode_ber_length(diag_bytes.len()));
1439 result.extend_from_slice(diag_bytes);
1440
1441 let mut search_msg = vec![0x65]; search_msg.extend_from_slice(&encode_ber_length(result.len()));
1444 search_msg.extend_from_slice(&result);
1445
1446 encode_ldap_message(msg_id, search_msg)
1447}
1448
1449#[cfg(not(target_arch = "wasm32"))]
1456fn encode_ldap_result(
1457 msg_id: i32,
1458 response_tag: u8,
1459 result_code: u8,
1460 diagnostic_message: &str,
1461) -> Vec<u8> {
1462 let mut result = Vec::new();
1463
1464 result.push(0x0A);
1466 result.push(0x01);
1467 result.push(result_code);
1468
1469 result.push(0x04);
1471 result.push(0x00);
1472
1473 result.push(0x04);
1475 let diag_bytes = diagnostic_message.as_bytes();
1476 result.extend_from_slice(&encode_ber_length(diag_bytes.len()));
1477 result.extend_from_slice(diag_bytes);
1478
1479 let mut message = vec![response_tag];
1480 message.extend_from_slice(&encode_ber_length(result.len()));
1481 message.extend_from_slice(&result);
1482
1483 encode_ldap_message(msg_id, message)
1484}
1485
1486#[cfg(not(target_arch = "wasm32"))]
1487fn encode_ldap_message(msg_id: i32, protocol_op: Vec<u8>) -> Vec<u8> {
1488 let mut content = Vec::new();
1494 content.extend_from_slice(&encode_ber_integer(msg_id));
1495 content.extend_from_slice(&protocol_op);
1496
1497 let mut message = vec![0x30]; message.extend_from_slice(&encode_ber_length(content.len()));
1499 message.extend_from_slice(&content);
1500
1501 message
1502}
1503
1504#[cfg(target_arch = "wasm32")]
1505impl LdapServer {
1506 pub async fn spawn_with_llm_actions(
1507 _listen_addr: SocketAddr,
1508 _llm_client: crate::llm::ollama_client::OllamaClient,
1509 _app_state: Arc<crate::state::app_state::AppState>,
1510 _status_tx: mpsc::UnboundedSender<String>,
1511 _server_id: crate::state::ServerId,
1512 _first_byte_timeout_secs: Option<u64>,
1513 _idle_timeout_secs: Option<u64>,
1514 ) -> Result<SocketAddr> {
1515 anyhow::bail!("LDAP feature not enabled")
1516 }
1517}