spin_factor_outbound_mqtt/
host.rs1use std::{sync::Arc, time::Duration};
2
3use anyhow::Result;
4use spin_core::{
5 async_trait,
6 wasmtime::component::{Accessor, Resource},
7};
8use spin_factor_otel::OtelFactorState;
9use spin_factor_outbound_networking::config::allowed_hosts::OutboundAllowedHosts;
10use spin_factor_outbound_networking::{ConnectionPermit, ConnectionSemaphore};
11use spin_world::spin::mqtt::mqtt as v3;
12use spin_world::v2::mqtt as v2;
13use tracing::{Level, instrument};
14
15use crate::{ClientCreator, allowed_hosts::AllowedHostChecker};
16
17pub struct InstanceState {
18 allowed_hosts: AllowedHostChecker,
19 connections: spin_resource_table::Table<(Arc<dyn MqttClient>, ConnectionPermit)>,
20 create_client: Arc<dyn ClientCreator>,
21 semaphore: ConnectionSemaphore,
22 otel: OtelFactorState,
23 max_payload_size_bytes: Option<usize>,
24}
25
26impl InstanceState {
27 pub fn new(
28 allowed_hosts: OutboundAllowedHosts,
29 create_client: Arc<dyn ClientCreator>,
30 semaphore: ConnectionSemaphore,
31 otel: OtelFactorState,
32 max_payload_size_bytes: Option<usize>,
33 ) -> Self {
34 Self {
35 allowed_hosts: AllowedHostChecker::new(allowed_hosts),
36 create_client,
37 connections: spin_resource_table::Table::new(1024),
38 semaphore,
39 otel,
40 max_payload_size_bytes,
41 }
42 }
43}
44
45#[async_trait]
46pub trait MqttClient: Send + Sync {
47 async fn publish_bytes(
48 &self,
49 topic: String,
50 qos: v3::Qos,
51 payload: Vec<u8>,
52 ) -> Result<(), v3::Error>;
53}
54
55impl InstanceState {
56 async fn is_address_allowed(&self, address: &str) -> Result<bool> {
57 self.allowed_hosts.is_address_allowed(address).await
58 }
59
60 async fn establish_connection(
61 &mut self,
62 address: String,
63 username: String,
64 password: String,
65 keep_alive_interval: Duration,
66 ) -> Result<Resource<v2::Connection>, v2::Error> {
67 let permit = self
68 .semaphore
69 .acquire()
70 .await
71 .map_err(|_| v2::Error::TooManyConnections)?;
72 let client =
73 (self.create_client).create(address, username, password, keep_alive_interval)?;
74 self.connections
75 .push((client, permit))
76 .map(Resource::new_own)
77 .map_err(|_| v2::Error::TooManyConnections)
78 }
79
80 fn get_conn(&self, connection: Resource<v2::Connection>) -> Result<&dyn MqttClient, v2::Error> {
81 self.connections
82 .get(connection.rep())
83 .ok_or(v2::Error::Other(
84 "could not find connection for resource".into(),
85 ))
86 .map(|(c, _permit)| c.as_ref())
87 }
88
89 fn get_conn_v3(
90 &self,
91 connection: Resource<v3::Connection>,
92 ) -> Result<Arc<dyn MqttClient>, v3::Error> {
93 self.connections
94 .get(connection.rep())
95 .map(|(c, _permit)| c.clone())
96 .ok_or(v3::Error::Other(
97 "could not find connection for resource".into(),
98 ))
99 }
100}
101
102impl v3::Host for InstanceState {
103 fn convert_error(&mut self, err: v3::Error) -> anyhow::Result<v3::Error> {
104 Ok(err)
105 }
106}
107
108impl v3::HostConnection for InstanceState {
109 async fn drop(&mut self, connection: Resource<v3::Connection>) -> anyhow::Result<()> {
110 self.connections.remove(connection.rep());
111 Ok(())
112 }
113}
114
115impl<T: Send> v3::HostConnectionWithStore<T> for crate::MqttFactorData {
116 #[instrument(name = "spin_outbound_mqtt.open_connection", skip(accessor, password), err(level = Level::INFO), fields(otel.kind = "client"))]
117 async fn open(
118 accessor: &Accessor<T, Self>,
119 address: String,
120 username: String,
121 password: String,
122 keep_alive_interval_in_secs: u64,
123 ) -> Result<Resource<v3::Connection>, v3::Error> {
124 let (allowed_host_checker, create_client, semaphore) = accessor.with(|mut access| {
125 let host = access.get();
126 host.otel.reparent_tracing_span();
127 (
128 host.allowed_hosts.clone(),
129 host.create_client.clone(),
130 host.semaphore.clone(),
131 )
132 });
133
134 if !allowed_host_checker
135 .is_address_allowed(&address)
136 .await
137 .map_err(|e| v3::Error::Other(e.to_string()))?
138 {
139 return Err(v3::Error::ConnectionFailed(format!(
140 "address {address} is not permitted"
141 )));
142 }
143
144 let permit = semaphore
145 .acquire()
146 .await
147 .map_err(|_| v3::Error::TooManyConnections)?;
148
149 let client = create_client.create(
150 address,
151 username,
152 password,
153 Duration::from_secs(keep_alive_interval_in_secs),
154 )?;
155
156 accessor.with(|mut access| {
157 let host = access.get();
158 host.connections
159 .push((client, permit))
160 .map(Resource::new_own)
161 .map_err(|_| v3::Error::TooManyConnections)
162 })
163 }
164
165 #[instrument(name = "spin_outbound_mqtt.publish", skip(accessor, connection, payload), err(level = Level::INFO),
166 fields(
167 otel.kind = "producer",
168 messaging.operation = "publish",
169 messaging.system = "mqtt",
170 messaging.destination.name = topic,
171 ))]
172 async fn publish(
173 accessor: &Accessor<T, Self>,
174 connection: Resource<v3::Connection>,
175 topic: String,
176 payload: v3::Payload,
177 qos: v3::Qos,
178 ) -> Result<(), v3::Error> {
179 let (conn, max_payload_size_bytes) = accessor.with(|mut access| {
180 let host = access.get();
181 host.otel.reparent_tracing_span();
182 host.get_conn_v3(connection)
183 .map(|c| (c, host.max_payload_size_bytes))
184 })?;
185
186 if let Some(limit) = max_payload_size_bytes
187 && payload.len() > limit
188 {
189 return Err(v3::Error::Other(format!(
190 "payload size {} exceeds the maximum allowed size of {} bytes",
191 payload.len(),
192 limit
193 )));
194 }
195
196 conn.publish_bytes(topic, qos, payload).await?;
197
198 Ok(())
199 }
200}
201
202impl v2::Host for InstanceState {
203 fn convert_error(&mut self, error: v2::Error) -> Result<v2::Error> {
204 Ok(error)
205 }
206}
207
208impl v2::HostConnection for InstanceState {
209 #[instrument(name = "spin_outbound_mqtt.open_connection", skip(self, password), err(level = Level::INFO), fields(otel.kind = "client"))]
210 async fn open(
211 &mut self,
212 address: String,
213 username: String,
214 password: String,
215 keep_alive_interval: u64,
216 ) -> Result<Resource<v2::Connection>, v2::Error> {
217 self.otel.reparent_tracing_span();
218
219 if !self
220 .is_address_allowed(&address)
221 .await
222 .map_err(|e| v2::Error::Other(e.to_string()))?
223 {
224 return Err(v2::Error::ConnectionFailed(format!(
225 "address {address} is not permitted"
226 )));
227 }
228 self.establish_connection(
229 address,
230 username,
231 password,
232 Duration::from_secs(keep_alive_interval),
233 )
234 .await
235 }
236
237 #[instrument(name = "spin_outbound_mqtt.publish", skip(self, connection, payload), err(level = Level::INFO),
243 fields(
244 otel.kind = "producer",
245 messaging.operation = "publish",
246 messaging.system = "mqtt",
247 messaging.destination.name = topic,
248 ))]
249 async fn publish(
250 &mut self,
251 connection: Resource<v2::Connection>,
252 topic: String,
253 payload: Vec<u8>,
254 qos: v2::Qos,
255 ) -> Result<(), v2::Error> {
256 self.otel.reparent_tracing_span();
257
258 if let Some(limit) = self.max_payload_size_bytes
259 && payload.len() > limit
260 {
261 return Err(v2::Error::Other(format!(
262 "payload size {} exceeds the maximum allowed size of {} bytes",
263 payload.len(),
264 limit
265 )));
266 }
267
268 let conn = self.get_conn(connection)?;
269
270 let qos = match qos {
271 v2::Qos::AtMostOnce => v3::Qos::AtMostOnce,
272 v2::Qos::AtLeastOnce => v3::Qos::AtLeastOnce,
273 v2::Qos::ExactlyOnce => v3::Qos::ExactlyOnce,
274 };
275
276 conn.publish_bytes(topic, qos, payload).await?;
277
278 Ok(())
279 }
280
281 async fn drop(&mut self, connection: Resource<v2::Connection>) -> anyhow::Result<()> {
282 self.connections.remove(connection.rep());
283 Ok(())
284 }
285}
286
287pub fn other_error_v3(e: impl std::fmt::Display) -> v3::Error {
288 v3::Error::Other(e.to_string())
289}