risingwave_connector/connector_common/
mqtt_common.rs1use phf::{Set, phf_set};
16use risingwave_common::id::ActorId;
17use rumqttc::tokio_rustls::rustls;
18use rumqttc::v5::mqttbytes::QoS;
19use rumqttc::v5::mqttbytes::v5::ConnectProperties;
20use rumqttc::v5::{AsyncClient, EventLoop, MqttOptions};
21use serde::Deserialize;
22use serde_with::{DisplayFromStr, serde_as};
23use strum_macros::{Display, EnumString};
24use with_options::WithOptions;
25
26use super::common::{load_certs, load_private_key};
27use crate::deserialize_bool_from_string;
28use crate::enforce_secret::EnforceSecret;
29use crate::error::ConnectorResult;
30
31const DEFAULT_KEEP_ALIVE_SECS: u16 = 10;
32const MIN_KEEP_ALIVE_SECS: u16 = 5;
34
35fn deserialize_keep_alive_secs<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
38where
39 D: serde::Deserializer<'de>,
40{
41 let s: String = Deserialize::deserialize(deserializer)?;
42 let secs: u16 = s.trim().parse().map_err(|_| {
43 serde::de::Error::invalid_value(
44 serde::de::Unexpected::Str(&s),
45 &"an integer between 5 and 65535",
46 )
47 })?;
48 if secs < MIN_KEEP_ALIVE_SECS {
49 return Err(serde::de::Error::invalid_value(
50 serde::de::Unexpected::Unsigned(secs.into()),
51 &"an integer between 5 and 65535",
52 ));
53 }
54 Ok(Some(secs))
55}
56
57#[derive(Debug, Clone, PartialEq, Display, Deserialize, EnumString)]
58#[strum(serialize_all = "snake_case")]
59#[expect(clippy::enum_variant_names)]
60pub enum QualityOfService {
61 AtLeastOnce,
62 AtMostOnce,
63 ExactlyOnce,
64}
65
66#[serde_as]
67#[derive(Deserialize, Debug, Clone, WithOptions)]
68pub struct MqttCommon {
69 pub url: String,
74
75 #[serde_as(as = "Option<DisplayFromStr>")]
78 pub qos: Option<QualityOfService>,
79
80 #[serde(rename = "username")]
82 pub user: Option<String>,
83
84 pub password: Option<String>,
86
87 pub client_prefix: Option<String>,
90
91 #[serde(default, deserialize_with = "deserialize_bool_from_string")]
103 pub clean_start: bool,
104
105 #[serde(default, deserialize_with = "deserialize_keep_alive_secs")]
113 pub keep_alive_secs: Option<u16>,
114
115 #[serde_as(as = "Option<DisplayFromStr>")]
124 pub session_expiry_interval: Option<u32>,
125
126 #[serde_as(as = "Option<DisplayFromStr>")]
128 pub inflight_messages: Option<usize>,
129
130 #[serde_as(as = "Option<DisplayFromStr>")]
132 pub max_packet_size: Option<u32>,
133
134 #[serde(rename = "tls.ca")]
136 pub ca: Option<String>,
137 #[serde(rename = "tls.client_cert")]
140 pub client_cert: Option<String>,
141
142 #[serde(rename = "tls.client_key")]
145 pub client_key: Option<String>,
146}
147
148impl EnforceSecret for MqttCommon {
149 const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
150 "tls.client_cert",
151 "tls.client_key",
152 "password",
153 };
154}
155
156impl MqttCommon {
157 pub(crate) fn build_client(
158 &self,
159 actor_id: ActorId,
160 id: u32,
161 ) -> ConnectorResult<(AsyncClient, EventLoop)> {
162 let client_id = format!(
163 "{}_{}_{}",
164 self.client_prefix.as_deref().unwrap_or("risingwave"),
165 actor_id,
166 id
167 );
168
169 let mut url = url::Url::parse(&self.url)?;
170
171 let ssl = matches!(url.scheme(), "mqtts" | "ssl");
172
173 url.query_pairs_mut().append_pair("client_id", &client_id);
174
175 tracing::debug!("connecting mqtt using url: {}", url.as_str());
176
177 let mut options = MqttOptions::try_from(url)?;
178 options.set_keep_alive(std::time::Duration::from_secs(
179 self.keep_alive_secs
180 .unwrap_or(DEFAULT_KEEP_ALIVE_SECS)
181 .into(),
182 ));
183
184 options.set_clean_start(self.clean_start);
185
186 let mut connect_properties = ConnectProperties::new();
187 connect_properties.max_packet_size = self.max_packet_size;
188 connect_properties.session_expiry_interval = self.session_expiry_interval;
189 options.set_connect_properties(connect_properties);
190
191 if ssl {
192 let tls_config = self.get_tls_config()?;
193 options.set_transport(rumqttc::Transport::tls_with_config(
194 rumqttc::TlsConfiguration::Rustls(std::sync::Arc::new(tls_config)),
195 ));
196 }
197
198 if let Some(user) = &self.user {
199 options.set_credentials(user, self.password.as_deref().unwrap_or_default());
200 }
201
202 Ok(rumqttc::v5::AsyncClient::new(
203 options,
204 self.inflight_messages.unwrap_or(100),
205 ))
206 }
207
208 pub(crate) fn qos(&self) -> QoS {
209 self.qos
210 .as_ref()
211 .map(|qos| match qos {
212 QualityOfService::AtMostOnce => QoS::AtMostOnce,
213 QualityOfService::AtLeastOnce => QoS::AtLeastOnce,
214 QualityOfService::ExactlyOnce => QoS::ExactlyOnce,
215 })
216 .unwrap_or(QoS::AtMostOnce)
217 }
218
219 fn get_tls_config(&self) -> ConnectorResult<rustls::ClientConfig> {
220 let mut root_cert_store = rustls::RootCertStore::empty();
221 if let Some(ca) = &self.ca {
222 let certificates = load_certs(ca)?;
223 for cert in certificates {
224 root_cert_store.add(cert).unwrap();
225 }
226 } else {
227 for cert in
228 rustls_native_certs::load_native_certs().expect("could not load platform certs")
229 {
230 root_cert_store.add(cert).unwrap();
231 }
232 }
233
234 let builder = rustls::ClientConfig::builder().with_root_certificates(root_cert_store);
235
236 let tls_config = if let (Some(client_cert), Some(client_key)) =
237 (self.client_cert.as_ref(), self.client_key.as_ref())
238 {
239 let certs = load_certs(client_cert)?;
240 let key = load_private_key(client_key)?;
241
242 builder.with_client_auth_cert(certs, key)?
243 } else {
244 builder.with_no_client_auth()
245 };
246
247 Ok(tls_config)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use std::collections::BTreeMap;
254 use std::time::Duration;
255
256 use super::*;
257
258 fn parse(extra: &[(&str, &str)]) -> Result<MqttCommon, serde_json::Error> {
259 let mut props: BTreeMap<String, String> = BTreeMap::new();
260 props.insert("url".to_owned(), "tcp://localhost:1883".to_owned());
261 for (k, v) in extra {
262 props.insert((*k).to_owned(), (*v).to_owned());
263 }
264 serde_json::from_value(serde_json::to_value(props).unwrap())
265 }
266
267 #[test]
268 fn test_keep_alive_and_session_expiry_default() {
269 let common = parse(&[]).unwrap();
270 assert_eq!(common.keep_alive_secs, None);
271 assert_eq!(common.session_expiry_interval, None);
272
273 let (_client, eventloop) = common.build_client(1.into(), 2).unwrap();
274 assert_eq!(eventloop.options.keep_alive(), Duration::from_secs(10));
275 assert_eq!(eventloop.options.session_expiry_interval(), None);
276 }
277
278 #[test]
279 fn test_keep_alive_and_session_expiry_configured() {
280 let common = parse(&[
281 ("keep_alive_secs", "300"),
282 ("session_expiry_interval", "3600"),
283 ("clean_start", "false"),
284 ("max_packet_size", "1024"),
285 ])
286 .unwrap();
287 assert_eq!(common.keep_alive_secs, Some(300));
288 assert_eq!(common.session_expiry_interval, Some(3600));
289
290 let (_client, eventloop) = common.build_client(1.into(), 2).unwrap();
291 assert_eq!(eventloop.options.keep_alive(), Duration::from_secs(300));
292 assert_eq!(eventloop.options.session_expiry_interval(), Some(3600));
293 assert!(!eventloop.options.clean_start());
294 assert_eq!(
295 eventloop
296 .options
297 .connect_properties()
298 .and_then(|p| p.max_packet_size),
299 Some(1024)
300 );
301 }
302
303 #[test]
304 fn test_invalid_keep_alive_secs() {
305 for v in ["0", "4", "65536", "-1", "abc"] {
306 assert!(
307 parse(&[("keep_alive_secs", v)]).is_err(),
308 "keep_alive_secs = {v} should be rejected"
309 );
310 }
311 assert_eq!(
312 parse(&[("keep_alive_secs", "5")]).unwrap().keep_alive_secs,
313 Some(5)
314 );
315 assert_eq!(
316 parse(&[("keep_alive_secs", "65535")])
317 .unwrap()
318 .keep_alive_secs,
319 Some(65535)
320 );
321 }
322}