use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use rumqttc::Transport;
use rumqttc::v5::mqttbytes::v5::LastWill;
use rumqttc::v5::{AsyncClient, MqttOptions};
use ruststream::{Broker, ConnectedBroker, DefaultPublish, DescribeServer, ServerSpec, Subscribe};
use tokio::sync::{OnceCell, mpsc, oneshot};
use crate::conn::{Conn, Shared, run};
use crate::error::MqttError;
use crate::filter::{MqttTopic, Qos};
use crate::publisher::{MqttPublish, MqttPublisher};
use crate::subscriber::MqttSubscriber;
pub(crate) struct Core {
pub(crate) client: AsyncClient,
pub(crate) shared: Arc<Shared>,
}
impl std::fmt::Debug for Core {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Core").finish_non_exhaustive()
}
}
pub(crate) type CoreCell = Arc<OnceCell<Core>>;
#[derive(Debug, Clone)]
#[must_use]
pub struct MqttBroker {
url: String,
client_id: String,
credentials: Option<(String, String)>,
keep_alive: Option<Duration>,
clean_start: Option<bool>,
session_expiry: Option<u32>,
max_packet_size: u32,
receive_maximum: u16,
last_will: Option<(String, Vec<u8>, Qos, bool)>,
tls_ca: Option<Vec<u8>>,
tls_client_auth: Option<(Vec<u8>, Vec<u8>)>,
cell: CoreCell,
}
impl MqttBroker {
pub fn new(url: impl Into<String>, client_id: impl Into<String>) -> Self {
Self {
url: url.into(),
client_id: client_id.into(),
credentials: None,
keep_alive: None,
clean_start: None,
session_expiry: None,
max_packet_size: 1024 * 1024,
receive_maximum: 1000,
last_will: None,
tls_ca: None,
tls_client_auth: None,
cell: Arc::new(OnceCell::new()),
}
}
pub fn credentials(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
self.credentials = Some((username.into(), password.into()));
self
}
pub fn keep_alive(mut self, keep_alive: Duration) -> Self {
self.keep_alive = Some(keep_alive);
self
}
pub fn clean_start(mut self, clean_start: bool) -> Self {
self.clean_start = Some(clean_start);
self
}
pub fn session_expiry(mut self, expiry: Duration) -> Self {
self.session_expiry = Some(u32::try_from(expiry.as_secs()).unwrap_or(u32::MAX));
self
}
pub fn max_packet_size(mut self, bytes: u32) -> Self {
self.max_packet_size = bytes;
self
}
pub fn receive_maximum(mut self, maximum: u16) -> Self {
self.receive_maximum = maximum;
self
}
pub fn last_will(
mut self,
topic: impl Into<String>,
payload: impl Into<Vec<u8>>,
qos: Qos,
retain: bool,
) -> Self {
self.last_will = Some((topic.into(), payload.into(), qos, retain));
self
}
pub fn tls_ca(mut self, ca: impl Into<Vec<u8>>) -> Self {
self.tls_ca = Some(ca.into());
self
}
pub fn tls_client_auth(mut self, cert: impl Into<Vec<u8>>, key: impl Into<Vec<u8>>) -> Self {
self.tls_client_auth = Some((cert.into(), key.into()));
self
}
#[must_use]
pub fn publisher(&self) -> MqttPublisher {
MqttPublisher::new(Arc::clone(&self.cell), Qos::default(), false)
}
fn options(&self) -> Result<MqttOptions, MqttError> {
let (tls_from_scheme, rest) = self.url.strip_prefix("mqtts://").map_or_else(
|| {
(
false,
self.url
.strip_prefix("mqtt://")
.unwrap_or(self.url.as_str()),
)
},
|rest| (true, rest),
);
let (host, port) = match rest.rsplit_once(':') {
Some((host, port)) => (
host.to_owned(),
port.parse::<u16>()
.map_err(|_| MqttError::Invalid(format!("'{port}' is not a valid port")))?,
),
None => (rest.to_owned(), if tls_from_scheme { 8883 } else { 1883 }),
};
if host.is_empty() {
return Err(MqttError::Invalid("host must be non-empty".into()));
}
if let Some(keep_alive) = self.keep_alive {
if keep_alive < Duration::from_secs(5) {
return Err(MqttError::Invalid(
"keep_alive must be at least 5 seconds".into(),
));
}
}
let mut options = MqttOptions::new(self.client_id.clone(), host, port);
if let Some(keep_alive) = self.keep_alive {
options.set_keep_alive(keep_alive);
}
if let Some(clean_start) = self.clean_start {
options.set_clean_start(clean_start);
}
if let Some((username, password)) = &self.credentials {
options.set_credentials(username.clone(), password.clone());
}
if let Some(expiry) = self.session_expiry {
options.set_session_expiry_interval(Some(expiry));
}
options.set_max_packet_size(Some(self.max_packet_size));
options.set_receive_maximum(Some(self.receive_maximum));
options.set_manual_acks(true);
if let Some((topic, payload, qos, retain)) = &self.last_will {
options.set_last_will(LastWill::new(
topic.clone(),
payload.clone(),
qos.to_client(),
*retain,
None,
));
}
if tls_from_scheme || self.tls_ca.is_some() {
let ca = self.tls_ca.clone().unwrap_or_default();
options.set_transport(Transport::tls(ca, self.tls_client_auth.clone(), None));
}
Ok(options)
}
}
impl Broker for MqttBroker {
type Error = MqttError;
type Connected = ConnectedMqttBroker;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
let core = self
.cell
.get_or_try_init(async || {
let options = self.options()?;
let (client, eventloop) = AsyncClient::new(options, 64);
let shared = Arc::new(Shared::new());
let (connack_tx, connack_rx) = oneshot::channel();
tokio::spawn(run(Conn {
client: client.clone(),
eventloop,
shared: Arc::clone(&shared),
first_connack: Some(connack_tx),
}));
match tokio::time::timeout(Duration::from_secs(30), connack_rx).await {
Ok(Ok(Ok(()))) => {}
Ok(Ok(Err(err))) => return Err(err),
Ok(Err(_)) => {
return Err(MqttError::Connect(Box::from(
"the connection task exited before the first CONNACK",
)));
}
Err(_) => {
shared.closed.store(true, Ordering::Release);
return Err(MqttError::Connect(Box::from(
"timed out waiting for the broker's CONNACK",
)));
}
}
Ok::<_, MqttError>(Core { client, shared })
})
.await?;
Ok(ConnectedMqttBroker {
client: core.client.clone(),
shared: Arc::clone(&core.shared),
cell: self.cell,
})
}
}
impl DescribeServer for MqttBroker {
fn describe_server(&self) -> ServerSpec {
ServerSpec::new(
self.url
.trim_start_matches("mqtts://")
.trim_start_matches("mqtt://"),
"mqtt",
)
}
}
pub struct ConnectedMqttBroker {
client: AsyncClient,
shared: Arc<Shared>,
cell: CoreCell,
}
impl std::fmt::Debug for ConnectedMqttBroker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectedMqttBroker")
.finish_non_exhaustive()
}
}
impl ConnectedMqttBroker {
#[must_use]
pub fn publisher(&self) -> MqttPublisher {
MqttPublisher::new(Arc::clone(&self.cell), Qos::default(), false)
}
pub(crate) fn publisher_with(&self, policy: MqttPublish) -> MqttPublisher {
policy.into_publisher(Arc::clone(&self.cell))
}
pub async fn subscribe_topic(&self, topic: MqttTopic) -> Result<MqttSubscriber, MqttError> {
topic.validate()?;
self.shared.ensure_open()?;
let wire_filter = topic.wire_filter();
let (tx, rx) = mpsc::unbounded_channel();
let (done, wait) = oneshot::channel();
let id = self.shared.register(
wire_filter.clone(),
topic.filter().to_owned(),
topic.qos_value().to_client(),
tx,
done,
);
if self
.client
.subscribe(wire_filter, topic.qos_value().to_client())
.await
.is_err()
{
self.shared.remove(id);
return Err(MqttError::Subscribe {
filter: topic.filter().to_owned(),
reason: "the mqtt connection task has shut down".to_owned(),
});
}
wait.await.map_err(|_| MqttError::Subscribe {
filter: topic.filter().to_owned(),
reason: "the mqtt connection task has shut down".to_owned(),
})??;
Ok(MqttSubscriber::new(
topic.filter().to_owned(),
id,
Arc::clone(&self.shared),
self.client.clone(),
rx,
))
}
}
impl ConnectedBroker for ConnectedMqttBroker {
type Error = MqttError;
type Closed = ();
async fn shutdown(self) -> Result<(), Self::Error> {
self.shared.closed.store(true, Ordering::Release);
let _ = self.client.disconnect().await;
Ok(())
}
}
impl Subscribe for ConnectedMqttBroker {
type Subscriber = MqttSubscriber;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
self.subscribe_topic(MqttTopic::new(name)).await
}
}
impl DefaultPublish for ConnectedMqttBroker {
type Policy = MqttPublish;
}