1pub mod actions;
31pub mod v5;
32
33use crate::llm::action_helper::call_llm;
34use crate::llm::actions::protocol_trait::ActionResult;
35use crate::llm::ollama_client::OllamaClient;
36use crate::logging::emit::Log;
37use crate::protocol::Event;
38use crate::server::connection::ConnectionId;
39use crate::state::app_state::AppState;
40use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
41use actions::{
42 MqttProtocol, MQTT_CONNECT_EVENT, MQTT_PUBLISH_EVENT, MQTT_SUBSCRIBE_EVENT,
43 MQTT_UNSUBSCRIBE_EVENT,
44};
45use anyhow::Result;
46use std::net::SocketAddr;
47use std::sync::Arc;
48use std::time::Duration;
49use tokio::io::{AsyncReadExt, AsyncWriteExt};
50use tokio::net::{TcpListener, TcpStream};
51use tokio::sync::mpsc;
52use tracing::{debug, error, info, trace, warn};
53
54const DEFAULT_MAX_PACKET_SIZE: usize = 256 * 1024;
60
61pub const MAX_PACKET_SIZE_LIMIT: usize = 16 * 1024 * 1024;
63
64pub const PKT_CONNECT: u8 = 1;
69pub const PKT_CONNACK: u8 = 2;
70pub const PKT_PUBLISH: u8 = 3;
71pub const PKT_PUBACK: u8 = 4;
72pub const PKT_PUBREC: u8 = 5;
73pub const PKT_PUBREL: u8 = 6;
74pub const PKT_PUBCOMP: u8 = 7;
75pub const PKT_SUBSCRIBE: u8 = 8;
76pub const PKT_SUBACK: u8 = 9;
77pub const PKT_UNSUBSCRIBE: u8 = 10;
78pub const PKT_UNSUBACK: u8 = 11;
79pub const PKT_PINGREQ: u8 = 12;
80pub const PKT_PINGRESP: u8 = 13;
81pub const PKT_DISCONNECT: u8 = 14;
82pub const PKT_AUTH: u8 = 15;
84
85pub const TOPIC_ALIAS_MAX: u16 = 16;
89
90pub const CONNACK_SERVER_UNAVAILABLE: u8 = 3;
95
96pub const SUBACK_FAILURE: u8 = 0x80;
98
99const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
108
109const CONNECTION_CAP_REFUSAL: &[u8] = &[PKT_CONNACK << 4, 0x02, 0x00, CONNACK_SERVER_UNAVAILABLE];
125
126pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
134
135pub const IDLE_WITHOUT_KEEP_ALIVE: Duration = Duration::from_secs(900);
145
146#[derive(Clone, Copy, Debug)]
148pub struct MqttBounds {
149 pub connect: Duration,
151 pub idle_without_keep_alive: Duration,
153}
154
155pub fn session_idle_bound(keep_alive_secs: u16, fallback: Duration) -> Duration {
167 if keep_alive_secs == 0 {
168 fallback
169 } else {
170 Duration::from_millis(u64::from(keep_alive_secs) * 1500)
171 }
172}
173
174pub struct MqttServer;
176
177impl MqttServer {
178 pub async fn spawn_with_llm_actions(
183 listen_addr: SocketAddr,
184 llm_client: OllamaClient,
185 app_state: Arc<AppState>,
186 status_tx: mpsc::UnboundedSender<String>,
187 server_id: crate::state::ServerId,
188 startup_params: Option<crate::protocol::StartupParams>,
189 ) -> Result<SocketAddr> {
190 let max_packet_size = match startup_params.as_ref() {
191 Some(params) => match params.get_optional_u64("max_packet_size") {
192 Ok(Some(v)) => (v as usize).clamp(64, MAX_PACKET_SIZE_LIMIT),
193 Ok(None) => DEFAULT_MAX_PACKET_SIZE,
194 Err(e) => return Err(anyhow::anyhow!("MQTT startup parameter error: {}", e)),
195 },
196 None => DEFAULT_MAX_PACKET_SIZE,
197 };
198
199 let secs = |name: &str| -> Result<Option<Duration>> {
203 match startup_params.as_ref() {
204 Some(params) => params
205 .get_optional_u64(name)
206 .map(|v| v.map(Duration::from_secs))
207 .map_err(|e| anyhow::anyhow!("MQTT startup parameter error: {}", e)),
208 None => Ok(None),
209 }
210 };
211 let bounds = MqttBounds {
212 connect: secs("first_byte_timeout_secs")?.unwrap_or(CONNECT_TIMEOUT),
213 idle_without_keep_alive: secs("idle_timeout_secs")?.unwrap_or(IDLE_WITHOUT_KEEP_ALIVE),
214 };
215
216 let listener = TcpListener::bind(listen_addr).await?;
217 let local_addr = listener.local_addr()?;
218
219 Log::new(Some(&status_tx)).info(format!("MQTT broker listening on {}", local_addr));
220
221 let accept_state = app_state.clone();
222 let accept_status_tx = status_tx.clone();
223
224 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
225 let accept_handle = tokio::spawn(async move {
226 loop {
227 match crate::server::accept_bounded::accept_bounded(
228 &listener,
229 &limiter,
230 CONNECTION_CAP_REFUSAL,
231 "MQTT",
232 Some(&accept_status_tx),
233 )
234 .await
235 {
236 Ok((socket, peer_addr, permit)) => {
237 Log::new(Some(&accept_status_tx))
238 .debug(format!("MQTT connection from {}", peer_addr));
239
240 let llm_client = llm_client.clone();
241 let app_state = accept_state.clone();
242 let status_tx = accept_status_tx.clone();
243
244 let task_owner = app_state.clone();
246 task_owner
247 .spawn_server_task(server_id, async move {
248 let permit = std::sync::Arc::new(permit);
254 if let Err(e) = handle_mqtt_connection(
255 socket,
256 peer_addr,
257 local_addr,
258 llm_client,
259 app_state,
260 status_tx,
261 server_id,
262 max_packet_size,
263 permit,
264 bounds,
265 )
266 .await
267 {
268 error!("MQTT connection error ({}): {}", peer_addr, e);
269 }
270 })
271 .await;
272 }
273 Err(e) => {
274 Log::new(Some(&accept_status_tx))
275 .error(format!("MQTT accept error: {}", e));
276 break;
277 }
278 }
279 }
280 });
281
282 app_state
285 .register_server_task(server_id, accept_handle)
286 .await;
287
288 let _ = status_tx.send("__UPDATE_UI__".to_string());
289 Ok(local_addr)
290 }
291}
292
293struct RawPacket {
296 packet_type: u8,
297 flags: u8,
298 body: Vec<u8>,
299}
300
301#[derive(Default)]
303struct Session {
304 client_id: Option<String>,
306 keep_alive: Option<u16>,
308 v5: bool,
310 aliases: std::collections::HashMap<u16, String>,
313}
314
315impl Session {
316 fn refuse(&self, out_tx: &mpsc::UnboundedSender<Vec<u8>>, reason: u8) {
320 if self.v5 {
321 let _ = out_tx.send(build_disconnect_v5(reason, &v5::PropertyWriter::new()));
322 }
323 }
324}
325
326#[allow(clippy::too_many_arguments)]
327async fn handle_mqtt_connection(
328 socket: TcpStream,
329 peer_addr: SocketAddr,
330 local_addr: SocketAddr,
331 llm_client: OllamaClient,
332 app_state: Arc<AppState>,
333 status_tx: mpsc::UnboundedSender<String>,
334 server_id: crate::state::ServerId,
335 max_packet_size: usize,
336 permit: Arc<crate::server::accept_bounded::ConnectionPermit>,
339 bounds: MqttBounds,
340) -> Result<()> {
341 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
342
343 let (mut read_half, write_half) = tokio::io::split(socket);
344
345 let write_half = Arc::new(tokio::sync::Mutex::new(write_half));
350
351 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
355 let writer_status_tx = status_tx.clone();
356 let writer_write_half = write_half.clone();
357 let writer_state = app_state.clone();
358 let writer_permit = Arc::clone(&permit);
359 let client_max_packet = Arc::new(std::sync::atomic::AtomicU32::new(0));
364 let writer_client_max = client_max_packet.clone();
365 let writer_handle = tokio::spawn(async move {
366 let _permit = writer_permit;
370 while let Some(bytes) = out_rx.recv().await {
371 let n = bytes.len();
372 let limit = writer_client_max.load(std::sync::atomic::Ordering::SeqCst) as usize;
373 if limit > 0 && n > limit {
374 Log::new(Some(&writer_status_tx)).warn(format!(
375 "MQTT dropped a {n}-byte packet (type {}) for connection {connection_id}: \
376 the client's Maximum Packet Size is {limit} \
377 decision=dropped_over_client_max_packet_size",
378 bytes.first().map(|b| b >> 4).unwrap_or(0)
379 ));
380 continue;
381 }
382 let mut guard = writer_write_half.lock().await;
383 if let Err(e) = guard.write_all(&bytes).await {
384 Log::new(Some(&writer_status_tx)).debug(format!("MQTT write failed: {}", e));
385 break;
386 }
387 let _ = guard.flush().await;
388 drop(guard);
389 writer_state
392 .update_connection_stats(
393 server_id,
394 connection_id,
395 None,
396 Some(n as u64),
397 None,
398 Some(1),
399 )
400 .await;
401 }
402 });
403
404 let now = crate::utils::clock::Instant::now();
405 let conn_state = ConnectionState {
406 id: connection_id,
407 remote_addr: peer_addr,
408 local_addr,
409 bytes_sent: 0,
410 bytes_received: 0,
411 packets_sent: 0,
412 packets_received: 0,
413 last_activity: now,
414 status: ConnectionStatus::Active,
415 status_changed_at: now,
416 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
417 "client_id": serde_json::Value::Null,
418 })),
419 };
420 app_state
421 .add_connection_to_server(server_id, conn_state)
422 .await;
423 let _ = status_tx.send("__UPDATE_UI__".to_string());
424
425 let protocol = Arc::new(MqttProtocol::for_connection(
426 server_id,
427 connection_id,
428 out_tx.clone(),
429 status_tx.clone(),
430 ));
431
432 let peer_rx = crate::server::peer_support::register_peer_channel(
439 &app_state,
440 server_id,
441 connection_id.as_u32(),
442 )
443 .await;
444 crate::server::peer_support::spawn_peer_command_task(
445 peer_rx,
446 protocol.clone(),
447 app_state.clone(),
448 server_id,
449 connection_id.as_u32(),
450 write_half.clone(),
451 status_tx.clone(),
452 );
453
454 let mut session = Session::default();
455 let mut buffer: Vec<u8> = Vec::with_capacity(1024);
456 let mut chunk = vec![0u8; 4096];
457
458 let close_reason = loop {
459 let bound = match session.keep_alive {
463 None => bounds.connect,
464 Some(ka) => session_idle_bound(ka, bounds.idle_without_keep_alive),
465 };
466 let read = match tokio::time::timeout(bound, read_half.read(&mut chunk)).await {
467 Ok(read) => read,
468 Err(_) => {
469 Log::new(Some(&status_tx)).info(format!(
470 "MQTT connection {} from {} sent nothing for {}ms decision=idle_timeout ({}); closing",
471 connection_id,
472 peer_addr,
473 bound.as_millis(),
474 match session.keep_alive {
475 None => "no CONNECT".to_string(),
476 Some(0) => "keep-alive 0".to_string(),
477 Some(ka) => format!("1.5x keep-alive {}s", ka),
478 }
479 ));
480 break "idle deadline expired";
481 }
482 };
483 let n = match read {
484 Ok(0) => break "peer closed the connection",
485 Ok(n) => {
486 app_state
487 .update_connection_stats(
488 server_id,
489 connection_id,
490 Some(n as u64),
491 None,
492 Some(1),
493 None,
494 )
495 .await;
496 n
497 }
498 Err(e) => {
499 debug!("MQTT read error from {}: {}", peer_addr, e);
500 break "read error";
501 }
502 };
503 buffer.extend_from_slice(chunk.get(..n).unwrap_or(&[]));
504
505 loop {
507 let packet = match take_packet(&mut buffer, max_packet_size) {
508 Ok(Some(p)) => p,
509 Ok(None) => break, Err(e) => {
511 Log::new(Some(&status_tx)).warn(format!(
512 "MQTT protocol error from {} decision=protocol_error: {}",
513 peer_addr, e
514 ));
515 session.refuse(&out_tx, e.reason_code());
517 finish_connection(
518 protocol,
519 out_tx,
520 writer_handle,
521 &app_state,
522 server_id,
523 connection_id,
524 &session.client_id,
525 &status_tx,
526 )
527 .await;
528 return Ok(());
529 }
530 };
531
532 trace!(
533 "MQTT packet type={} flags={:#x} len={} from {}",
534 packet.packet_type,
535 packet.flags,
536 packet.body.len(),
537 peer_addr
538 );
539
540 let keep_open = dispatch_packet(
541 packet,
542 &mut session,
543 &client_max_packet,
544 max_packet_size,
545 &llm_client,
546 &app_state,
547 &status_tx,
548 server_id,
549 connection_id,
550 protocol.clone(),
551 &out_tx,
552 )
553 .await;
554
555 if !keep_open {
556 finish_connection(
557 protocol,
558 out_tx,
559 writer_handle,
560 &app_state,
561 server_id,
562 connection_id,
563 &session.client_id,
564 &status_tx,
565 )
566 .await;
567 return Ok(());
568 }
569 }
570 };
571
572 debug!(
573 "MQTT connection {} closing: {}",
574 connection_id, close_reason
575 );
576 Log::new(Some(&status_tx)).info(format!(
577 "MQTT client {} disconnected ({})",
578 session.client_id.as_deref().unwrap_or("<unidentified>"),
579 close_reason
580 ));
581
582 finish_connection(
583 protocol,
584 out_tx,
585 writer_handle,
586 &app_state,
587 server_id,
588 connection_id,
589 &session.client_id,
590 &status_tx,
591 )
592 .await;
593 Ok(())
594}
595
596#[allow(clippy::too_many_arguments)]
610async fn finish_connection(
611 protocol: Arc<MqttProtocol>,
612 out_tx: mpsc::UnboundedSender<Vec<u8>>,
613 writer_handle: tokio::task::JoinHandle<()>,
614 app_state: &Arc<AppState>,
615 server_id: crate::state::ServerId,
616 connection_id: ConnectionId,
617 client_id: &Option<String>,
618 status_tx: &mpsc::UnboundedSender<String>,
619) {
620 app_state
624 .remove_peer_handle(server_id, connection_id.as_u32())
625 .await;
626 cleanup(app_state, server_id, connection_id, client_id, status_tx).await;
627 drop(protocol);
628 drop(out_tx);
629 if tokio::time::timeout(std::time::Duration::from_secs(5), writer_handle)
630 .await
631 .is_err()
632 {
633 warn!(
634 "MQTT connection {}: writer did not finish within 5s, abandoning it",
635 connection_id
636 );
637 }
638}
639
640async fn cleanup(
641 app_state: &AppState,
642 server_id: crate::state::ServerId,
643 connection_id: ConnectionId,
644 client_id: &Option<String>,
645 status_tx: &mpsc::UnboundedSender<String>,
646) {
647 if let Some(id) = client_id {
648 actions::unregister_client(server_id, id);
649 }
650 app_state
651 .update_connection_status(server_id, connection_id, ConnectionStatus::Closed)
652 .await;
653 let _ = status_tx.send("__UPDATE_UI__".to_string());
654}
655
656#[allow(clippy::too_many_arguments)]
658async fn dispatch_packet(
659 packet: RawPacket,
660 session: &mut Session,
661 client_max_packet: &std::sync::atomic::AtomicU32,
662 max_packet_size: usize,
663 llm_client: &OllamaClient,
664 app_state: &Arc<AppState>,
665 status_tx: &mpsc::UnboundedSender<String>,
666 server_id: crate::state::ServerId,
667 connection_id: ConnectionId,
668 protocol: Arc<MqttProtocol>,
669 out_tx: &mpsc::UnboundedSender<Vec<u8>>,
670) -> bool {
671 match packet.packet_type {
672 PKT_CONNECT => {
673 let connect = match parse_connect(&packet.body) {
674 Ok(c) => c,
675 Err(e) => {
676 Log::new(Some(&status_tx)).warn(format!(
677 "MQTT malformed CONNECT from client decision=protocol_error, closing: {e}"
678 ));
679 if let Some(code) = e.v5_connack_code() {
682 let _ =
683 out_tx.send(build_connack_v5(code, false, &v5::PropertyWriter::new()));
684 }
685 return false;
686 }
687 };
688
689 if session.client_id.is_some() {
690 Log::new(Some(&status_tx))
692 .warn("MQTT duplicate CONNECT decision=protocol_error, closing connection");
693 session.refuse(out_tx, 0x82);
694 return false;
695 }
696
697 session.v5 = connect.protocol_level == 5;
698 protocol.set_v5(session.v5);
699 let wants_enhanced_auth =
702 session.v5 && connect.properties.text(v5::AUTHENTICATION_METHOD).is_some();
703 let assigned = connect.client_id.is_empty();
704 let effective_id = if assigned {
705 format!("anon-{}", connection_id.as_u32())
706 } else {
707 connect.client_id.clone()
708 };
709 session.client_id = Some(effective_id.clone());
710 session.keep_alive = Some(connect.keep_alive);
711 if session.v5 {
712 if let Some(limit) = connect.properties.int(v5::MAXIMUM_PACKET_SIZE) {
713 client_max_packet.store(limit, std::sync::atomic::Ordering::SeqCst);
714 }
715 let mut auto = v5::PropertyWriter::new();
719 if assigned {
720 auto.text(v5::ASSIGNED_CLIENT_IDENTIFIER, &effective_id);
721 }
722 auto.u32(v5::MAXIMUM_PACKET_SIZE, max_packet_size as u32)
723 .u16(v5::TOPIC_ALIAS_MAXIMUM, TOPIC_ALIAS_MAX);
724 protocol.set_connack_properties(auto);
725 }
726 actions::register_client(server_id, &effective_id, out_tx.clone(), session.v5);
727 protocol.set_client_id(&effective_id);
728
729 app_state
730 .with_server_mut(server_id, |server| {
731 if let Some(conn) = server.connections.get_mut(&connection_id) {
732 conn.protocol_info = ProtocolConnectionInfo::new(serde_json::json!({
733 "client_id": effective_id,
734 "protocol_version": version_name(connect.protocol_level),
735 }));
736 }
737 })
738 .await;
739
740 info!(
741 "MQTT client '{}' connected (v{}, clean_session={}, keep_alive={}s)",
742 effective_id,
743 version_name(connect.protocol_level),
744 connect.clean_session,
745 connect.keep_alive
746 );
747 Log::new(Some(&status_tx)).info(format!(
748 "MQTT CONNECT from client '{}' (MQTT {}, user={}, clean_session={})",
749 effective_id,
750 version_name(connect.protocol_level),
751 connect.username.as_deref().unwrap_or("-"),
752 connect.clean_session
753 ));
754
755 let mut data = serde_json::json!({
756 "client_id": effective_id,
757 "username": connect.username,
758 "has_password": connect.has_password,
759 "clean_session": connect.clean_session,
760 "keep_alive": connect.keep_alive,
761 "protocol_name": connect.protocol_name,
762 "protocol_level": connect.protocol_level,
763 "protocol_version": version_name(connect.protocol_level),
764 "will_topic": connect.will_topic,
765 "will_message": connect.will_message,
766 });
767 if session.v5 {
768 data["properties"] = connect.properties.to_json();
769 data["will_properties"] = connect.will_properties.to_json();
770 data["assigned_client_id"] = serde_json::json!(assigned);
771 }
772 let event = Event::new(&MQTT_CONNECT_EVENT, data);
773
774 let outcome = run_llm(
775 llm_client,
776 app_state,
777 server_id,
778 connection_id,
779 &event,
780 protocol.as_ref(),
781 status_tx,
782 Some(PKT_CONNACK),
783 )
784 .await;
785
786 match outcome {
787 LlmOutcome::Closed => false,
788 LlmOutcome::Responded => !protocol.connack_refused(),
791 LlmOutcome::Silent if wants_enhanced_auth => {
792 Log::new(Some(&status_tx)).warn(format!(
797 "MQTT CONNECT from '{}' names an authentication method and the handler \
798 sent no mqtt_connack decision=fail_closed_auth_method, refusing with \
799 CONNACK 0x8C",
800 effective_id
801 ));
802 let _ = out_tx.send(build_connack_v5(0x8C, false, &v5::PropertyWriter::new()));
803 false
804 }
805 LlmOutcome::Silent => {
806 Log::new(Some(&status_tx)).warn(format!(
819 "MQTT no mqtt_connack from handler for '{}'; accepting by default \
820 (CONNACK 0 — an accepted session nothing decided)",
821 effective_id
822 ));
823 let _ = out_tx.send(if session.v5 {
824 build_connack_v5(0, false, &protocol.connack_properties())
825 } else {
826 build_connack(0, false)
827 });
828 true
829 }
830 LlmOutcome::Failed(failure) => {
831 Log::new(Some(&status_tx)).warn(format!(
842 "MQTT refusing CONNECT from '{}' with CONNACK {} (server {}): \
843 backend failed",
844 effective_id,
845 if session.v5 { "0x88/0x89" } else { "3" },
846 if failure.is_overloaded() {
847 "busy"
848 } else {
849 "unavailable"
850 }
851 ));
852 let _ = out_tx.send(if session.v5 {
853 let code = if failure.is_overloaded() { 0x89 } else { 0x88 };
854 let mut props = v5::PropertyWriter::new();
855 props.text(v5::REASON_STRING, failure.text());
856 build_connack_v5(code, false, &props)
857 } else {
858 build_connack(CONNACK_SERVER_UNAVAILABLE, false)
859 });
860 false
861 }
862 }
863 }
864
865 PKT_PUBLISH => {
866 let qos = (packet.flags >> 1) & 0x03;
867 if qos > 2 {
868 Log::new(Some(&status_tx))
869 .warn("MQTT PUBLISH with invalid QoS 3 decision=protocol_error, closing");
870 session.refuse(out_tx, 0x81);
871 return false;
872 }
873 let mut publish = match parse_publish(&packet.body, qos, session.v5) {
874 Ok(p) => p,
875 Err(e) => {
876 Log::new(Some(&status_tx)).warn(format!(
877 "MQTT malformed PUBLISH decision=protocol_error, closing connection: {e}"
878 ));
879 session.refuse(out_tx, e.reason_code());
880 return false;
881 }
882 };
883
884 if let Some(alias) = publish.properties.int(v5::TOPIC_ALIAS) {
887 let alias = alias as u16;
888 if alias == 0 || alias > TOPIC_ALIAS_MAX {
889 Log::new(Some(&status_tx)).warn(format!(
890 "MQTT PUBLISH with topic alias {alias} outside 1..={TOPIC_ALIAS_MAX} \
891 decision=protocol_error, closing"
892 ));
893 session.refuse(out_tx, 0x94);
894 return false;
895 }
896 if publish.topic.is_empty() {
897 match session.aliases.get(&alias) {
898 Some(topic) => publish.topic = topic.clone(),
899 None => {
900 Log::new(Some(&status_tx)).warn(format!(
901 "MQTT PUBLISH uses topic alias {alias}, which this connection \
902 never defined decision=protocol_error, closing"
903 ));
904 session.refuse(out_tx, 0x82);
905 return false;
906 }
907 }
908 } else {
909 session.aliases.insert(alias, publish.topic.clone());
910 }
911 } else if session.v5 && publish.topic.is_empty() {
912 Log::new(Some(&status_tx)).warn(
913 "MQTT PUBLISH with an empty topic and no topic alias \
914 decision=protocol_error, closing",
915 );
916 session.refuse(out_tx, 0x82);
917 return false;
918 }
919
920 let (payload, payload_is_text) = decode_payload(&publish.payload);
921 let name = session
922 .client_id
923 .clone()
924 .unwrap_or_else(|| "<no CONNECT>".into());
925
926 Log::new(Some(&status_tx)).debug(format!(
927 "MQTT PUBLISH from '{}' topic='{}' qos={} retain={} {} bytes",
928 name,
929 publish.topic,
930 qos,
931 (packet.flags & 0x01) == 1,
932 publish.payload.len()
933 ));
934
935 let mut data = serde_json::json!({
936 "client_id": name,
937 "topic": publish.topic,
938 "payload": payload,
939 "payload_is_text": payload_is_text,
940 "payload_size": publish.payload.len(),
941 "qos": qos,
942 "retain": (packet.flags & 0x01) == 1,
943 "duplicate": (packet.flags & 0x08) != 0,
944 "packet_id": publish.packet_id,
945 "connected_clients": actions::list_clients(server_id),
946 });
947 if session.v5 {
948 data["properties"] = publish.properties.to_json();
949 }
950 let event = Event::new(&MQTT_PUBLISH_EVENT, data);
951
952 let outcome = run_llm(
953 llm_client,
954 app_state,
955 server_id,
956 connection_id,
957 &event,
958 protocol.as_ref(),
959 status_tx,
960 match qos {
961 1 => Some(PKT_PUBACK),
962 2 => Some(PKT_PUBREC),
963 _ => None,
964 },
965 )
966 .await;
967
968 match outcome {
969 LlmOutcome::Closed => false,
970 LlmOutcome::Responded => true,
971 LlmOutcome::Silent => {
972 match qos {
977 1 => {
978 let _ = out_tx.send(build_ack(PKT_PUBACK, publish.packet_id));
979 }
980 2 => {
981 let _ = out_tx.send(build_ack(PKT_PUBREC, publish.packet_id));
982 }
983 _ => {}
984 }
985 true
986 }
987 LlmOutcome::Failed(failure) => {
988 Log::new(Some(&status_tx)).warn(format!(
1002 "MQTT closing connection: backend failed on PUBLISH (qos={}, id={}), \
1003 acknowledging it would claim delivery",
1004 qos, publish.packet_id
1005 ));
1006 if session.v5 {
1007 let mut props = v5::PropertyWriter::new();
1008 props.text(v5::REASON_STRING, failure.text());
1009 let code = if failure.is_overloaded() { 0x89 } else { 0x80 };
1010 let _ = out_tx.send(build_disconnect_v5(code, &props));
1011 }
1012 false
1013 }
1014 }
1015 }
1016
1017 PKT_PUBREL => {
1018 let packet_id = read_u16_at(&packet.body, 0).unwrap_or(0);
1021 trace!("MQTT PUBREL id={} -> PUBCOMP", packet_id);
1022 let _ = out_tx.send(build_ack(PKT_PUBCOMP, packet_id));
1023 true
1024 }
1025
1026 PKT_SUBSCRIBE => {
1027 let sub = match parse_subscribe(&packet.body, session.v5) {
1028 Ok(s) => s,
1029 Err(e) => {
1030 Log::new(Some(&status_tx)).warn(format!(
1031 "MQTT malformed SUBSCRIBE decision=protocol_error, closing connection: {e}"
1032 ));
1033 session.refuse(out_tx, e.reason_code());
1034 return false;
1035 }
1036 };
1037 if sub.topics.is_empty() {
1038 Log::new(Some(&status_tx))
1039 .warn("MQTT SUBSCRIBE with no topic filter decision=protocol_error, closing");
1040 session.refuse(out_tx, 0x82);
1041 return false;
1042 }
1043
1044 let name = session
1045 .client_id
1046 .clone()
1047 .unwrap_or_else(|| "<no CONNECT>".into());
1048 Log::new(Some(&status_tx)).info(format!(
1049 "MQTT SUBSCRIBE from '{}': {}",
1050 name,
1051 sub.topics
1052 .iter()
1053 .map(|(f, q)| format!("{} (qos {})", f, q))
1054 .collect::<Vec<_>>()
1055 .join(", ")
1056 ));
1057
1058 let topics: Vec<serde_json::Value> = sub
1059 .topics
1060 .iter()
1061 .zip(sub.options.iter())
1062 .map(|((filter, qos), opts)| {
1063 let mut t = serde_json::json!({ "filter": filter, "qos": qos });
1064 if session.v5 {
1065 t["no_local"] = serde_json::json!(opts.no_local);
1066 t["retain_as_published"] = serde_json::json!(opts.retain_as_published);
1067 t["retain_handling"] = serde_json::json!(opts.retain_handling);
1068 }
1069 t
1070 })
1071 .collect();
1072
1073 let mut data = serde_json::json!({
1074 "client_id": name,
1075 "packet_id": sub.packet_id,
1076 "topics": topics,
1077 "answer_with": subscribe_answer_with(sub.packet_id, &sub.topics),
1078 });
1079 if session.v5 {
1080 data["properties"] = sub.properties.to_json();
1081 }
1082 let event = Event::new(&MQTT_SUBSCRIBE_EVENT, data);
1083
1084 let outcome = run_llm(
1085 llm_client,
1086 app_state,
1087 server_id,
1088 connection_id,
1089 &event,
1090 protocol.as_ref(),
1091 status_tx,
1092 Some(PKT_SUBACK),
1093 )
1094 .await;
1095
1096 match outcome {
1097 LlmOutcome::Closed => false,
1098 LlmOutcome::Responded => true,
1099 LlmOutcome::Silent => {
1100 let granted: Vec<u8> = sub.topics.iter().map(|(_, q)| *q).collect();
1102 Log::new(Some(&status_tx)).warn(format!(
1103 "MQTT no mqtt_suback from handler; granting requested QoS to '{}'",
1104 name
1105 ));
1106 let _ = out_tx.send(if session.v5 {
1107 build_suback_v5(sub.packet_id, &granted, &v5::PropertyWriter::new())
1108 } else {
1109 build_suback(sub.packet_id, &granted)
1110 });
1111 true
1112 }
1113 LlmOutcome::Failed(failure) => {
1114 let refused = vec![SUBACK_FAILURE; sub.topics.len()];
1120 Log::new(Some(&status_tx)).warn(format!(
1121 "MQTT refusing all {} subscription(s) from '{}' with SUBACK 0x80: \
1122 backend failed",
1123 sub.topics.len(),
1124 name
1125 ));
1126 let _ = out_tx.send(if session.v5 {
1127 let mut props = v5::PropertyWriter::new();
1128 props.text(v5::REASON_STRING, failure.text());
1129 build_suback_v5(sub.packet_id, &refused, &props)
1130 } else {
1131 build_suback(sub.packet_id, &refused)
1132 });
1133 true
1134 }
1135 }
1136 }
1137
1138 PKT_UNSUBSCRIBE => {
1139 let unsub = match parse_unsubscribe(&packet.body, session.v5) {
1140 Ok(u) => u,
1141 Err(e) => {
1142 Log::new(Some(&status_tx)).warn(format!(
1143 "MQTT malformed UNSUBSCRIBE decision=protocol_error, closing connection: \
1144 {e}"
1145 ));
1146 session.refuse(out_tx, e.reason_code());
1147 return false;
1148 }
1149 };
1150
1151 let name = session
1152 .client_id
1153 .clone()
1154 .unwrap_or_else(|| "<no CONNECT>".into());
1155 Log::new(Some(&status_tx)).info(format!(
1156 "MQTT UNSUBSCRIBE from '{}': {}",
1157 name,
1158 unsub.topics.join(", ")
1159 ));
1160 protocol.note_unsubscribe(unsub.packet_id, unsub.topics.len());
1163
1164 let mut data = serde_json::json!({
1165 "client_id": name,
1166 "packet_id": unsub.packet_id,
1167 "topics": unsub.topics,
1168 });
1169 if session.v5 {
1170 data["properties"] = unsub.properties.to_json();
1171 }
1172 let event = Event::new(&MQTT_UNSUBSCRIBE_EVENT, data);
1173
1174 let outcome = run_llm(
1175 llm_client,
1176 app_state,
1177 server_id,
1178 connection_id,
1179 &event,
1180 protocol.as_ref(),
1181 status_tx,
1182 Some(PKT_UNSUBACK),
1183 )
1184 .await;
1185
1186 match outcome {
1187 LlmOutcome::Closed => false,
1188 LlmOutcome::Responded => true,
1189 LlmOutcome::Silent => {
1190 let _ = out_tx.send(if session.v5 {
1191 build_unsuback_v5(
1192 unsub.packet_id,
1193 &vec![0x00; unsub.topics.len()],
1194 &v5::PropertyWriter::new(),
1195 )
1196 } else {
1197 build_ack(PKT_UNSUBACK, unsub.packet_id)
1198 });
1199 true
1200 }
1201 LlmOutcome::Failed(failure) if session.v5 => {
1202 Log::new(Some(&status_tx)).warn(format!(
1206 "MQTT refusing UNSUBSCRIBE (id={}) from '{}' with UNSUBACK 0x80: \
1207 backend failed",
1208 unsub.packet_id, name
1209 ));
1210 let mut props = v5::PropertyWriter::new();
1211 props.text(v5::REASON_STRING, failure.text());
1212 let _ = out_tx.send(build_unsuback_v5(
1213 unsub.packet_id,
1214 &vec![0x80; unsub.topics.len()],
1215 &props,
1216 ));
1217 true
1218 }
1219 LlmOutcome::Failed(_) => {
1220 Log::new(Some(&status_tx)).warn(format!(
1225 "MQTT closing connection: backend failed on UNSUBSCRIBE (id={}), \
1226 acknowledging it would claim the subscriptions were removed",
1227 unsub.packet_id
1228 ));
1229 false
1230 }
1231 }
1232 }
1233
1234 PKT_PINGREQ => {
1235 trace!("MQTT PINGREQ -> PINGRESP");
1237 let _ = out_tx.send(vec![PKT_PINGRESP << 4, 0x00]);
1238 true
1239 }
1240
1241 PKT_DISCONNECT => {
1242 let (reason, detail) = if session.v5 {
1245 let reason = packet.body.first().copied().unwrap_or(0);
1246 let mut pos = 1usize;
1247 let text = if packet.body.len() > 1 {
1248 v5::read_properties(&packet.body, &mut pos)
1249 .ok()
1250 .and_then(|p| p.text(v5::REASON_STRING).map(str::to_string))
1251 } else {
1252 None
1253 };
1254 (Some(reason), text)
1255 } else {
1256 (None, None)
1257 };
1258 Log::new(Some(&status_tx)).info(format!(
1259 "MQTT client '{}' sent DISCONNECT{}{}",
1260 session.client_id.as_deref().unwrap_or("<unidentified>"),
1261 reason
1262 .map(|r| format!(" reason=0x{r:02x}"))
1263 .unwrap_or_default(),
1264 detail.map(|d| format!(" ({d})")).unwrap_or_default()
1265 ));
1266 false
1267 }
1268
1269 PKT_AUTH if session.v5 => {
1270 Log::new(Some(&status_tx)).warn(
1274 "MQTT AUTH packet: enhanced authentication is not supported \
1275 decision=protocol_error, closing",
1276 );
1277 session.refuse(out_tx, 0x82);
1278 false
1279 }
1280
1281 other => {
1282 trace!("MQTT ignoring inbound packet type {}", other);
1285 true
1286 }
1287 }
1288}
1289
1290pub fn version_name(level: u8) -> &'static str {
1292 match level {
1293 3 => "3.1",
1294 4 => "3.1.1",
1295 5 => "5.0",
1296 _ => "unknown",
1297 }
1298}
1299
1300pub fn subscribe_answer_with(packet_id: u16, topics: &[(String, u8)]) -> String {
1309 let filters: Vec<&str> = topics.iter().map(|(f, _)| f.as_str()).collect();
1310 let granted: Vec<String> = topics.iter().map(|(_, q)| q.to_string()).collect();
1311 format!(
1312 "mqtt_suback with packet_id {packet_id} and granted_qos [{}] (one entry per filter; \
1313 128 refuses one). Then, for each topic matching {:?} that your instruction gives a \
1314 retained message or current value, mqtt_publish with that topic, the value as \
1315 payload and retain true - the subscriber is waiting for it. Publish nothing if the \
1316 instruction gives none.",
1317 granted.join(", "),
1318 filters
1319 )
1320}
1321
1322enum LlmOutcome {
1324 Responded,
1326 Silent,
1328 Closed,
1330 Failed(crate::utils::wire_failure::WireFailure),
1348}
1349
1350#[allow(clippy::too_many_arguments)]
1359async fn run_llm(
1360 llm_client: &OllamaClient,
1361 app_state: &Arc<AppState>,
1362 server_id: crate::state::ServerId,
1363 connection_id: ConnectionId,
1364 event: &Event,
1365 protocol: &MqttProtocol,
1366 status_tx: &mpsc::UnboundedSender<String>,
1367 required_reply: Option<u8>,
1368) -> LlmOutcome {
1369 protocol.clear_written_types();
1370
1371 match call_llm(
1374 llm_client,
1375 app_state,
1376 server_id,
1377 Some(connection_id),
1378 event,
1379 protocol,
1380 )
1381 .await
1382 {
1383 Ok(result) => {
1384 let client = event
1389 .data
1390 .get("client_id")
1391 .and_then(|v| v.as_str())
1392 .unwrap_or("<unidentified>");
1393 let log = Log::new(Some(&status_tx));
1394
1395 if result
1396 .protocol_results
1397 .iter()
1398 .any(|r| matches!(r, ActionResult::CloseConnection))
1399 {
1400 log.info(format!(
1401 "MQTT {} from '{}' ({}) decision=model_reject (close_connection)",
1402 event.event_type.id, client, connection_id
1403 ));
1404 return LlmOutcome::Closed;
1405 }
1406
1407 let answered = match required_reply {
1408 None => !result.raw_actions.is_empty(),
1411 Some(packet_type) => protocol.wrote_packet_type(packet_type),
1412 };
1413
1414 if answered {
1415 log.info(format!(
1416 "MQTT {} from '{}' ({}) decision=model_answer",
1417 event.event_type.id, client, connection_id
1418 ));
1419 LlmOutcome::Responded
1420 } else {
1421 log.warn(format!(
1424 "MQTT {} from '{}' ({}) decision=model_silent actions={} — the protocol \
1425 default applies",
1426 event.event_type.id,
1427 client,
1428 connection_id,
1429 result.raw_actions.len()
1430 ));
1431 match required_reply {
1432 None => LlmOutcome::Responded,
1433 Some(_) => LlmOutcome::Silent,
1434 }
1435 }
1436 }
1437 Err(e) => {
1438 let decision = if crate::llm::is_overload_error(&e) {
1443 "fail_closed_llm_overloaded"
1444 } else {
1445 "fail_closed_llm_error"
1446 };
1447 let client = event
1448 .data
1449 .get("client_id")
1450 .and_then(|v| v.as_str())
1451 .unwrap_or("<unidentified>");
1452 Log::new(Some(&status_tx)).error(format!(
1453 "MQTT {} from '{}' ({}) decision={}: {}",
1454 event.event_type.id, client, connection_id, decision, e
1455 ));
1456 LlmOutcome::Failed(crate::utils::wire_failure::WireFailure::classify(&e))
1457 }
1458 }
1459}
1460
1461#[derive(Debug, Clone)]
1467pub struct PacketError {
1468 pub reason: u8,
1470 pub message: String,
1471 pub v5_connect: bool,
1474}
1475
1476impl PacketError {
1477 fn malformed(message: impl Into<String>) -> Self {
1478 Self {
1479 reason: 0x81,
1480 message: message.into(),
1481 v5_connect: false,
1482 }
1483 }
1484
1485 fn protocol(message: impl Into<String>) -> Self {
1486 Self {
1487 reason: 0x82,
1488 message: message.into(),
1489 v5_connect: false,
1490 }
1491 }
1492
1493 pub fn reason_code(&self) -> u8 {
1494 self.reason
1495 }
1496
1497 pub fn v5_connack_code(&self) -> Option<u8> {
1499 self.v5_connect.then_some(self.reason)
1500 }
1501}
1502
1503impl std::fmt::Display for PacketError {
1504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1505 f.write_str(&self.message)
1506 }
1507}
1508
1509impl From<v5::PropertyError> for PacketError {
1510 fn from(e: v5::PropertyError) -> Self {
1511 match e {
1512 v5::PropertyError::Malformed(_) => PacketError::malformed(e.to_string()),
1513 v5::PropertyError::Duplicate(_) | v5::PropertyError::Protocol(_) => {
1514 PacketError::protocol(e.to_string())
1515 }
1516 }
1517 }
1518}
1519
1520fn take_packet(
1526 buffer: &mut Vec<u8>,
1527 max_packet_size: usize,
1528) -> std::result::Result<Option<RawPacket>, PacketError> {
1529 let Some(&first) = buffer.first() else {
1530 return Ok(None);
1531 };
1532 let packet_type = first >> 4;
1533 if packet_type == 0 {
1534 return Err(PacketError::malformed(format!(
1535 "invalid packet type {}",
1536 packet_type
1537 )));
1538 }
1539
1540 let (remaining, len_bytes) = match decode_remaining_length(&buffer[1..]) {
1541 Some(v) => v,
1542 None => {
1543 if buffer.len() > 5 {
1545 return Err(PacketError::malformed("malformed remaining length"));
1546 }
1547 return Ok(None);
1548 }
1549 };
1550
1551 if remaining > max_packet_size {
1552 return Err(PacketError {
1553 reason: 0x95,
1554 message: format!(
1555 "packet of {} bytes exceeds max_packet_size {}",
1556 remaining, max_packet_size
1557 ),
1558 v5_connect: false,
1559 });
1560 }
1561
1562 let total = 1 + len_bytes + remaining;
1563 if buffer.len() < total {
1564 return Ok(None);
1565 }
1566
1567 let body = buffer[1 + len_bytes..total].to_vec();
1568 buffer.drain(..total);
1569
1570 Ok(Some(RawPacket {
1571 packet_type,
1572 flags: first & 0x0F,
1573 body,
1574 }))
1575}
1576
1577fn decode_remaining_length(buf: &[u8]) -> Option<(usize, usize)> {
1580 let mut value: usize = 0;
1581 let mut multiplier: usize = 1;
1582 for i in 0..4 {
1583 let byte = *buf.get(i)?;
1584 value += (byte & 0x7F) as usize * multiplier;
1585 if byte & 0x80 == 0 {
1586 return Some((value, i + 1));
1587 }
1588 multiplier *= 128;
1589 }
1590 None
1591}
1592
1593fn read_u16_at(buf: &[u8], pos: usize) -> Option<u16> {
1594 let hi = *buf.get(pos)?;
1595 let lo = *buf.get(pos + 1)?;
1596 Some(u16::from_be_bytes([hi, lo]))
1597}
1598
1599fn read_string(buf: &[u8], pos: &mut usize) -> Option<String> {
1601 let len = read_u16_at(buf, *pos)? as usize;
1602 let start = pos.checked_add(2)?;
1603 let end = start.checked_add(len)?;
1604 let bytes = buf.get(start..end)?;
1605 *pos = end;
1606 Some(String::from_utf8_lossy(bytes).into_owned())
1607}
1608
1609fn read_bytes(buf: &[u8], pos: &mut usize) -> Option<Vec<u8>> {
1611 let len = read_u16_at(buf, *pos)? as usize;
1612 let start = pos.checked_add(2)?;
1613 let end = start.checked_add(len)?;
1614 let bytes = buf.get(start..end)?.to_vec();
1615 *pos = end;
1616 Some(bytes)
1617}
1618
1619fn decode_payload(bytes: &[u8]) -> (String, bool) {
1622 match std::str::from_utf8(bytes) {
1623 Ok(s) => (s.to_string(), true),
1624 Err(_) => (String::from_utf8_lossy(bytes).into_owned(), false),
1625 }
1626}
1627
1628pub struct ConnectPacket {
1630 pub protocol_name: String,
1631 pub protocol_level: u8,
1632 pub clean_session: bool,
1633 pub keep_alive: u16,
1634 pub client_id: String,
1635 pub username: Option<String>,
1636 pub has_password: bool,
1637 pub will_topic: Option<String>,
1638 pub will_message: Option<String>,
1639 pub properties: v5::Properties,
1641 pub will_properties: v5::Properties,
1643}
1644
1645pub fn parse_connect(body: &[u8]) -> std::result::Result<ConnectPacket, PacketError> {
1646 let short = || PacketError::malformed("CONNECT is truncated");
1647 let mut pos = 0usize;
1648 let protocol_name = read_string(body, &mut pos).ok_or_else(short)?;
1649 let protocol_level = *body.get(pos).ok_or_else(short)?;
1650 pos += 1;
1651 let v5 = protocol_level == 5;
1652 let tag = |mut e: PacketError| {
1653 e.v5_connect = v5;
1654 e
1655 };
1656 let flags = *body.get(pos).ok_or_else(short).map_err(tag)?;
1657 pos += 1;
1658 let keep_alive = read_u16_at(body, pos).ok_or_else(short).map_err(tag)?;
1659 pos += 2;
1660
1661 let properties = if v5 {
1662 let props = v5::read_properties(body, &mut pos)
1663 .map_err(PacketError::from)
1664 .map_err(tag)?;
1665 v5::forbid(&props, &[v5::SUBSCRIPTION_IDENTIFIER], "CONNECT")
1666 .map_err(PacketError::from)
1667 .map_err(tag)?;
1668 if props.int(v5::MAXIMUM_PACKET_SIZE) == Some(0) {
1671 return Err(tag(PacketError::protocol("maximum packet size 0")));
1672 }
1673 props
1674 } else {
1675 v5::Properties::default()
1676 };
1677
1678 let client_id = read_string(body, &mut pos).ok_or_else(short).map_err(tag)?;
1679
1680 let mut will_properties = v5::Properties::default();
1681 let (will_topic, will_message) = if flags & 0x04 != 0 {
1682 if v5 {
1683 will_properties = v5::read_properties(body, &mut pos)
1684 .map_err(PacketError::from)
1685 .map_err(tag)?;
1686 v5::forbid(
1687 &will_properties,
1688 &[v5::SUBSCRIPTION_IDENTIFIER],
1689 "Will Properties",
1690 )
1691 .map_err(PacketError::from)
1692 .map_err(tag)?;
1693 }
1694 let topic = read_string(body, &mut pos).ok_or_else(short).map_err(tag)?;
1695 let message = read_bytes(body, &mut pos).ok_or_else(short).map_err(tag)?;
1696 (Some(topic), Some(decode_payload(&message).0))
1697 } else {
1698 (None, None)
1699 };
1700
1701 let username = if flags & 0x80 != 0 {
1702 Some(read_string(body, &mut pos).ok_or_else(short).map_err(tag)?)
1703 } else {
1704 None
1705 };
1706 let has_password = flags & 0x40 != 0;
1707
1708 Ok(ConnectPacket {
1709 protocol_name,
1710 protocol_level,
1711 clean_session: flags & 0x02 != 0,
1712 keep_alive,
1713 client_id,
1714 username,
1715 has_password,
1716 will_topic,
1717 will_message,
1718 properties,
1719 will_properties,
1720 })
1721}
1722
1723pub struct PublishPacket {
1725 pub topic: String,
1726 pub packet_id: u16,
1727 pub payload: Vec<u8>,
1728 pub properties: v5::Properties,
1729}
1730
1731pub fn parse_publish(
1732 body: &[u8],
1733 qos: u8,
1734 v5: bool,
1735) -> std::result::Result<PublishPacket, PacketError> {
1736 let short = || PacketError::malformed("PUBLISH is truncated");
1737 let mut pos = 0usize;
1738 let topic = read_string(body, &mut pos).ok_or_else(short)?;
1739 let packet_id = if qos > 0 {
1740 let id = read_u16_at(body, pos).ok_or_else(short)?;
1741 pos += 2;
1742 id
1743 } else {
1744 0
1745 };
1746 let properties = if v5 {
1747 let props = v5::read_properties(body, &mut pos)?;
1748 v5::forbid(&props, &[v5::SUBSCRIPTION_IDENTIFIER], "a client's PUBLISH")?;
1750 props
1751 } else {
1752 v5::Properties::default()
1753 };
1754 let payload = body.get(pos..).ok_or_else(short)?.to_vec();
1755 Ok(PublishPacket {
1756 topic,
1757 packet_id,
1758 payload,
1759 properties,
1760 })
1761}
1762
1763#[derive(Clone, Copy, Debug, Default)]
1765pub struct SubscriptionOptions {
1766 pub no_local: bool,
1767 pub retain_as_published: bool,
1768 pub retain_handling: u8,
1769}
1770
1771pub struct SubscribePacket {
1772 pub packet_id: u16,
1773 pub topics: Vec<(String, u8)>,
1774 pub options: Vec<SubscriptionOptions>,
1776 pub properties: v5::Properties,
1777}
1778
1779pub fn parse_subscribe(body: &[u8], v5: bool) -> std::result::Result<SubscribePacket, PacketError> {
1780 let short = || PacketError::malformed("SUBSCRIBE is truncated");
1781 let mut pos = 0usize;
1782 let packet_id = read_u16_at(body, pos).ok_or_else(short)?;
1783 pos += 2;
1784 let properties = if v5 {
1785 v5::read_properties(body, &mut pos)?
1788 } else {
1789 v5::Properties::default()
1790 };
1791
1792 let mut topics = Vec::new();
1793 let mut options = Vec::new();
1794 while pos < body.len() {
1795 let filter = read_string(body, &mut pos).ok_or_else(short)?;
1796 let byte = *body.get(pos).ok_or_else(short)?;
1797 pos += 1;
1798 let qos = byte & 0x03;
1799 let mut opts = SubscriptionOptions::default();
1800 if v5 {
1801 if qos == 3 || (byte >> 4) & 0x03 == 3 || byte & 0xC0 != 0 {
1804 return Err(PacketError::malformed(format!(
1805 "invalid subscription options byte 0x{byte:02x} for '{filter}'"
1806 )));
1807 }
1808 opts = SubscriptionOptions {
1809 no_local: byte & 0x04 != 0,
1810 retain_as_published: byte & 0x08 != 0,
1811 retain_handling: (byte >> 4) & 0x03,
1812 };
1813 }
1814 topics.push((filter, qos));
1815 options.push(opts);
1816 }
1817 Ok(SubscribePacket {
1818 packet_id,
1819 topics,
1820 options,
1821 properties,
1822 })
1823}
1824
1825pub struct UnsubscribePacket {
1826 pub packet_id: u16,
1827 pub topics: Vec<String>,
1828 pub properties: v5::Properties,
1829}
1830
1831pub fn parse_unsubscribe(
1832 body: &[u8],
1833 v5: bool,
1834) -> std::result::Result<UnsubscribePacket, PacketError> {
1835 let short = || PacketError::malformed("UNSUBSCRIBE is truncated");
1836 let mut pos = 0usize;
1837 let packet_id = read_u16_at(body, pos).ok_or_else(short)?;
1838 pos += 2;
1839 let properties = if v5 {
1840 let props = v5::read_properties(body, &mut pos)?;
1841 v5::forbid(&props, &[v5::SUBSCRIPTION_IDENTIFIER], "UNSUBSCRIBE")?;
1842 props
1843 } else {
1844 v5::Properties::default()
1845 };
1846
1847 let mut topics = Vec::new();
1848 while pos < body.len() {
1849 topics.push(read_string(body, &mut pos).ok_or_else(short)?);
1850 }
1851 Ok(UnsubscribePacket {
1852 packet_id,
1853 topics,
1854 properties,
1855 })
1856}
1857
1858pub fn encode_remaining_length(mut len: usize) -> Vec<u8> {
1864 let mut out = Vec::with_capacity(4);
1865 loop {
1866 let mut byte = (len % 128) as u8;
1867 len /= 128;
1868 if len > 0 {
1869 byte |= 0x80;
1870 }
1871 out.push(byte);
1872 if len == 0 || out.len() == 4 {
1873 break;
1874 }
1875 }
1876 out
1877}
1878
1879pub fn build_connack(return_code: u8, session_present: bool) -> Vec<u8> {
1881 vec![
1882 PKT_CONNACK << 4,
1883 0x02,
1884 if session_present { 0x01 } else { 0x00 },
1885 return_code,
1886 ]
1887}
1888
1889pub fn build_ack(packet_type: u8, packet_id: u16) -> Vec<u8> {
1891 let id = packet_id.to_be_bytes();
1892 let header = if packet_type == PKT_PUBREL {
1893 (packet_type << 4) | 0x02
1894 } else {
1895 packet_type << 4
1896 };
1897 vec![header, 0x02, id[0], id[1]]
1898}
1899
1900pub fn build_suback(packet_id: u16, granted_qos: &[u8]) -> Vec<u8> {
1903 let mut body = packet_id.to_be_bytes().to_vec();
1904 body.extend_from_slice(granted_qos);
1905 let mut out = vec![PKT_SUBACK << 4];
1906 out.extend_from_slice(&encode_remaining_length(body.len()));
1907 out.extend_from_slice(&body);
1908 out
1909}
1910
1911pub fn build_publish(topic: &str, payload: &str, qos: u8, retain: bool, packet_id: u16) -> Vec<u8> {
1913 let topic_bytes = topic.as_bytes();
1914 let payload_bytes = payload.as_bytes();
1915
1916 let mut body = Vec::with_capacity(2 + topic_bytes.len() + 2 + payload_bytes.len());
1917 body.extend_from_slice(&(topic_bytes.len() as u16).to_be_bytes());
1918 body.extend_from_slice(topic_bytes);
1919 if qos > 0 {
1920 body.extend_from_slice(&packet_id.to_be_bytes());
1921 }
1922 body.extend_from_slice(payload_bytes);
1923
1924 let mut flags = (qos & 0x03) << 1;
1925 if retain {
1926 flags |= 0x01;
1927 }
1928
1929 let mut out = vec![(PKT_PUBLISH << 4) | flags];
1930 out.extend_from_slice(&encode_remaining_length(body.len()));
1931 out.extend_from_slice(&body);
1932 out
1933}
1934
1935fn frame(first_byte: u8, body: &[u8]) -> Vec<u8> {
1937 let mut out = vec![first_byte];
1938 out.extend_from_slice(&encode_remaining_length(body.len()));
1939 out.extend_from_slice(body);
1940 out
1941}
1942
1943pub fn build_connack_v5(reason: u8, session_present: bool, props: &v5::PropertyWriter) -> Vec<u8> {
1945 let mut body = vec![u8::from(session_present), reason];
1946 body.extend_from_slice(&props.finish());
1947 frame(PKT_CONNACK << 4, &body)
1948}
1949
1950pub fn build_ack_v5(
1953 packet_type: u8,
1954 packet_id: u16,
1955 reason: u8,
1956 props: &v5::PropertyWriter,
1957) -> Vec<u8> {
1958 if reason == 0 && props.is_empty() {
1959 return build_ack(packet_type, packet_id);
1960 }
1961 let mut body = packet_id.to_be_bytes().to_vec();
1962 body.push(reason);
1963 body.extend_from_slice(&props.finish());
1964 let first = if packet_type == PKT_PUBREL {
1965 (packet_type << 4) | 0x02
1966 } else {
1967 packet_type << 4
1968 };
1969 frame(first, &body)
1970}
1971
1972pub fn build_suback_v5(packet_id: u16, codes: &[u8], props: &v5::PropertyWriter) -> Vec<u8> {
1974 let mut body = packet_id.to_be_bytes().to_vec();
1975 body.extend_from_slice(&props.finish());
1976 body.extend_from_slice(codes);
1977 frame(PKT_SUBACK << 4, &body)
1978}
1979
1980pub fn build_unsuback_v5(packet_id: u16, codes: &[u8], props: &v5::PropertyWriter) -> Vec<u8> {
1983 let mut body = packet_id.to_be_bytes().to_vec();
1984 body.extend_from_slice(&props.finish());
1985 body.extend_from_slice(codes);
1986 frame(PKT_UNSUBACK << 4, &body)
1987}
1988
1989pub fn build_disconnect_v5(reason: u8, props: &v5::PropertyWriter) -> Vec<u8> {
1991 let mut body = vec![reason];
1992 body.extend_from_slice(&props.finish());
1993 frame(PKT_DISCONNECT << 4, &body)
1994}
1995
1996pub fn build_publish_v5(
1998 topic: &str,
1999 payload: &[u8],
2000 qos: u8,
2001 retain: bool,
2002 packet_id: u16,
2003 props: &v5::PropertyWriter,
2004) -> Vec<u8> {
2005 let mut body = Vec::with_capacity(2 + topic.len() + 2 + 8 + payload.len());
2006 body.extend_from_slice(&(topic.len() as u16).to_be_bytes());
2007 body.extend_from_slice(topic.as_bytes());
2008 if qos > 0 {
2009 body.extend_from_slice(&packet_id.to_be_bytes());
2010 }
2011 body.extend_from_slice(&props.finish());
2012 body.extend_from_slice(payload);
2013 let mut flags = (qos & 0x03) << 1;
2014 if retain {
2015 flags |= 0x01;
2016 }
2017 frame((PKT_PUBLISH << 4) | flags, &body)
2018}
2019
2020#[derive(Clone, Debug)]
2023pub struct OutPublish {
2024 pub topic: String,
2025 pub payload: String,
2026 pub qos: u8,
2027 pub retain: bool,
2028 pub packet_id: u16,
2029 pub properties: v5::PropertyWriter,
2031}
2032
2033impl OutPublish {
2034 pub fn encode(&self, v5: bool) -> Vec<u8> {
2035 if v5 {
2036 build_publish_v5(
2037 &self.topic,
2038 self.payload.as_bytes(),
2039 self.qos,
2040 self.retain,
2041 self.packet_id,
2042 &self.properties,
2043 )
2044 } else {
2045 build_publish(
2046 &self.topic,
2047 &self.payload,
2048 self.qos,
2049 self.retain,
2050 self.packet_id,
2051 )
2052 }
2053 }
2054}