use std::time::Duration;
use crate::protocol::packet::MAX_PACKET_SIZE;
use crate::protocol::{MqttVersion, QoS};
use crate::support::secs_or;
pub(super) const DEFAULT_OPERATION_TIMEOUT_SECS: u32 = 15;
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct WillOptions {
pub topic: String,
pub payload: Vec<u8>,
pub qos: QoS,
pub retain: bool,
}
#[derive(Clone, PartialEq, Eq, uniffi::Record)]
pub struct TlsOptions {
pub ca_cert_pem: Option<Vec<u8>>,
pub client_cert_pem: Option<Vec<u8>>,
pub client_key_pem: Option<Vec<u8>>,
pub insecure_skip_certificate_verification: bool,
}
crate::redacted_debug!(TlsOptions {
ca_cert_pem, client_cert_pem, insecure_skip_certificate_verification
} secret { client_key_pem });
impl TlsOptions {
pub fn new() -> Self {
TlsOptions {
ca_cert_pem: None,
client_cert_pem: None,
client_key_pem: None,
insecure_skip_certificate_verification: false,
}
}
}
impl Default for TlsOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, PartialEq, Eq, uniffi::Record)]
pub struct ConnectOptions {
pub host: String,
pub port: u16,
pub client_id: String,
pub version: MqttVersion,
pub clean_start: bool,
pub keep_alive_secs: u16,
pub username: Option<String>,
pub password: Option<Vec<u8>>,
pub will: Option<WillOptions>,
pub connect_timeout_secs: u32,
pub operation_timeout_secs: u32,
pub auto_reconnect: bool,
pub reconnect_backoff_secs: u32,
pub reconnect_max_backoff_secs: u32,
pub tls: Option<TlsOptions>,
#[uniffi(default = 0)]
pub max_packet_size: u32,
#[uniffi(default = None)]
pub auth_method: Option<String>,
#[uniffi(default = None)]
pub auth_data: Option<Vec<u8>>,
}
crate::redacted_debug!(ConnectOptions {
host, port, client_id, version, clean_start, keep_alive_secs, username, will,
connect_timeout_secs, operation_timeout_secs, auto_reconnect,
reconnect_backoff_secs, reconnect_max_backoff_secs, tls, max_packet_size, auth_method
} secret { password, auth_data });
impl ConnectOptions {
pub fn new(
host: impl Into<String>,
port: u16,
client_id: impl Into<String>,
version: MqttVersion,
) -> Self {
ConnectOptions {
host: host.into(),
port,
client_id: client_id.into(),
version,
clean_start: true,
keep_alive_secs: 30,
username: None,
password: None,
will: None,
connect_timeout_secs: 10,
operation_timeout_secs: 0,
auto_reconnect: false,
reconnect_backoff_secs: 0,
reconnect_max_backoff_secs: 0,
tls: None,
max_packet_size: 0,
auth_method: None,
auth_data: None,
}
}
pub(super) fn operation_timeout(&self) -> Duration {
secs_or(self.operation_timeout_secs, DEFAULT_OPERATION_TIMEOUT_SECS)
}
pub(super) fn connect_timeout(&self) -> Duration {
secs_or(self.connect_timeout_secs, 1)
}
pub(super) fn initial_reconnect_backoff(&self) -> Duration {
secs_or(self.reconnect_backoff_secs, 1)
}
pub(super) fn max_reconnect_backoff(&self) -> Duration {
secs_or(self.reconnect_max_backoff_secs, 30)
}
pub(super) fn max_packet_size(&self) -> usize {
match self.max_packet_size {
0 => MAX_PACKET_SIZE,
n => n as usize,
}
}
pub(super) fn validate(&self) -> crate::error::MqttResult<()> {
use crate::error::MqttError::Protocol;
use crate::protocol::topic::is_valid_topic_name;
if self.host.is_empty() {
return Err(Protocol("ConnectOptions.host must not be empty".into()));
}
if self.client_id.len() > usize::from(u16::MAX) {
return Err(Protocol("ConnectOptions.client_id is too long".into()));
}
if self.password.is_some() && self.username.is_none() && !self.version.is_v5() {
return Err(Protocol(
"ConnectOptions.password requires a username in MQTT 3.1.1".into(),
));
}
if self.auth_method.is_some() && !self.version.is_v5() {
return Err(Protocol(
"enhanced authentication (auth_method) requires MQTT 5.0".into(),
));
}
if self.auth_data.is_some() && self.auth_method.is_none() {
return Err(Protocol("auth_data requires auth_method".into()));
}
if let Some(will) = &self.will {
if !is_valid_topic_name(&will.topic) {
return Err(Protocol(format!(
"invalid will topic {:?}: must be non-empty and wildcard-free",
will.topic
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Record)]
pub struct ConnectResult {
pub session_present: bool,
pub reason_code: u8,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct MqttMessage {
pub topic: String,
pub payload: Vec<u8>,
pub qos: QoS,
pub retain: bool,
}
impl From<&crate::protocol::publish::PublishPacket> for MqttMessage {
fn from(p: &crate::protocol::publish::PublishPacket) -> Self {
MqttMessage {
topic: p.topic.clone(),
payload: p.payload.to_vec(),
qos: p.qos,
retain: p.retain,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Record)]
pub struct SubscribeResult {
pub reason_code: u8,
}
#[uniffi::export(with_foreign)]
pub trait MqttAuthHandler: Send + Sync {
fn respond(&self, method: String, challenge: Vec<u8>) -> Option<Vec<u8>>;
}
#[uniffi::export(with_foreign)]
pub trait MqttMessageListener: Send + Sync {
fn on_message(&self, message: MqttMessage);
fn on_disconnected(&self, reason: String);
}
#[cfg(test)]
mod tests {
use super::*;
fn opts() -> ConnectOptions {
ConnectOptions::new("localhost", 1883, "cid", MqttVersion::V5)
}
#[test]
fn defaults_are_sane() {
let o = opts();
assert!(o.clean_start && !o.auto_reconnect);
assert_eq!(o.keep_alive_secs, 30);
assert_eq!(o.operation_timeout(), Duration::from_secs(15));
assert_eq!(o.initial_reconnect_backoff(), Duration::from_secs(1));
assert_eq!(o.max_reconnect_backoff(), Duration::from_secs(30));
assert_eq!(o.max_packet_size(), MAX_PACKET_SIZE);
assert!(o.validate().is_ok());
}
#[test]
fn explicit_values_override_the_zero_defaults() {
let mut o = opts();
o.operation_timeout_secs = 3;
o.reconnect_backoff_secs = 2;
o.reconnect_max_backoff_secs = 9;
o.max_packet_size = 4096;
assert_eq!(o.operation_timeout(), Duration::from_secs(3));
assert_eq!(o.initial_reconnect_backoff(), Duration::from_secs(2));
assert_eq!(o.max_reconnect_backoff(), Duration::from_secs(9));
assert_eq!(o.max_packet_size(), 4096);
}
#[test]
fn a_zero_connect_timeout_is_one_second_not_instant_failure() {
let mut o = opts();
o.connect_timeout_secs = 0;
assert_eq!(o.connect_timeout(), Duration::from_secs(1));
}
#[test]
fn validation_rejects_each_unusable_configuration() {
type Mutation = Box<dyn Fn(&mut ConnectOptions)>;
let cases: Vec<(&str, Mutation)> = vec![
("host", Box::new(|o| o.host.clear())),
("client_id", Box::new(|o| o.client_id = "x".repeat(70_000))),
(
"password requires a username",
Box::new(|o| {
o.version = MqttVersion::V311;
o.password = Some(b"pw".to_vec());
}),
),
(
"will topic",
Box::new(|o| {
o.will = Some(WillOptions {
topic: "a/+".into(),
payload: vec![],
qos: QoS::AtMostOnce,
retain: false,
})
}),
),
(
"requires MQTT 5.0",
Box::new(|o| {
o.version = MqttVersion::V311;
o.auth_method = Some("SCRAM".into());
}),
),
(
"auth_data requires auth_method",
Box::new(|o| o.auth_data = Some(vec![1])),
),
];
for (needle, mutate) in cases {
let mut o = opts();
mutate(&mut o);
let err = o.validate().expect_err(needle).to_string();
assert!(err.contains(needle), "{needle:?} not in {err:?}");
}
}
#[test]
fn password_without_username_is_fine_in_mqtt5() {
let mut o = opts();
o.password = Some(b"pw".to_vec());
assert!(o.validate().is_ok());
}
#[test]
fn debug_redacts_the_auth_data_too() {
let mut o = opts();
o.auth_method = Some("SCRAM".into());
o.auth_data = Some(b"client-first-SECRET".to_vec());
let shown = format!("{o:?}");
assert!(
shown.contains("SCRAM") && !shown.contains("SECRET"),
"{shown}"
);
}
}