1use crate::reconnect::{ReconnectBehavior, ReconnectPolicy, Reconnector};
2use crate::transport::{TransportError, TransportSink, TransportStream};
3use base64::Engine;
4use base64::prelude::BASE64_STANDARD;
5use core::future::Future;
6use core::pin::Pin;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::net::TcpStream;
10use tokio_tungstenite::tungstenite::client::IntoClientRequest;
11use tokio_tungstenite::tungstenite::http::Request;
12use tokio_tungstenite::tungstenite::http::header::{AUTHORIZATION, SEC_WEBSOCKET_PROTOCOL};
13use tokio_tungstenite::{Connector, MaybeTlsStream, WebSocketStream, client_async_tls_with_config};
14use url::Url;
15
16const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
17
18#[derive(Clone, Default)]
19pub struct ConnectOptions<'a> {
20 pub username: Option<&'a str>,
21 pub password: Option<&'a str>,
22 pub timeout: Option<Duration>,
23 pub reconnect: ReconnectBehavior,
27 pub tls_config: Option<Arc<rustls::ClientConfig>>,
36 pub reconnector: Option<Arc<dyn Reconnector>>,
55}
56
57impl core::fmt::Debug for ConnectOptions<'_> {
58 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61 f.debug_struct("ConnectOptions")
62 .field("username", &self.username)
63 .field(
64 "password",
65 &self.password.map(|_| "<redacted>").unwrap_or("None"),
66 )
67 .field("timeout", &self.timeout)
68 .field("reconnect", &self.reconnect)
69 .field("tls_config", &self.tls_config.as_ref().map(|_| "<set>"))
70 .field(
71 "reconnector",
72 &self.reconnector.as_ref().map(|_| "<custom>"),
73 )
74 .finish()
75 }
76}
77
78pub async fn websocket_transport(
104 address: &str,
105 version: OcppVersion,
106 options: Option<ConnectOptions<'_>>,
107) -> Result<(Box<dyn TransportSink>, Box<dyn TransportStream>), TransportError> {
108 let (stream, _protocol) = setup_socket(address, version.protocol(), options).await?;
109 Ok(crate::transport::websocket::split(stream))
110}
111
112pub enum NegotiatedClient {
116 #[cfg(feature = "ocpp_1_6")]
117 V1_6(crate::ocpp_1_6::OCPP1_6Client),
118 #[cfg(feature = "ocpp_2_0_1")]
119 V2_0_1(crate::ocpp_2_0_1::OCPP2_0_1Client),
120 #[cfg(feature = "ocpp_2_1")]
121 V2_1(crate::ocpp_2_1::OCPP2_1Client),
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum OcppVersion {
128 #[cfg(feature = "ocpp_1_6")]
129 V1_6,
130 #[cfg(feature = "ocpp_2_0_1")]
131 V2_0_1,
132 #[cfg(feature = "ocpp_2_1")]
133 V2_1,
134}
135
136impl OcppVersion {
137 fn protocol(self) -> &'static str {
138 match self {
139 #[cfg(feature = "ocpp_1_6")]
140 OcppVersion::V1_6 => "ocpp1.6",
141 #[cfg(feature = "ocpp_2_0_1")]
142 OcppVersion::V2_0_1 => "ocpp2.0.1",
143 #[cfg(feature = "ocpp_2_1")]
144 OcppVersion::V2_1 => "ocpp2.1",
145 }
146 }
147
148 #[allow(clippy::vec_init_then_push)]
151 fn all_compiled_in() -> Vec<OcppVersion> {
152 let mut versions = Vec::new();
153 #[cfg(feature = "ocpp_2_1")]
154 versions.push(OcppVersion::V2_1);
155 #[cfg(feature = "ocpp_2_0_1")]
156 versions.push(OcppVersion::V2_0_1);
157 #[cfg(feature = "ocpp_1_6")]
158 versions.push(OcppVersion::V1_6);
159 versions
160 }
161}
162
163pub async fn connect(
171 address: &str,
172 versions: Option<&[OcppVersion]>,
173 options: Option<ConnectOptions<'_>>,
174) -> Result<NegotiatedClient, Box<dyn std::error::Error + Send + Sync>> {
175 let all_compiled_in;
176 let versions = match versions {
177 Some(versions) => versions,
178 None => {
179 all_compiled_in = OcppVersion::all_compiled_in();
180 &all_compiled_in
181 }
182 };
183 let offered = versions
184 .iter()
185 .map(|v| v.protocol())
186 .collect::<Vec<_>>()
187 .join(", ");
188 let (stream, negotiated) = setup_socket(address, &offered, options.clone()).await?;
189 let protocol = versions
190 .iter()
191 .find(|v| v.protocol() == negotiated)
192 .map(|v| v.protocol())
193 .ok_or_else(|| format!("Server negotiated unsupported protocol: {negotiated}"))?;
194 let (timeout, reconnector, policy) = prepare(address, protocol, options);
195 let (sink, source) = crate::transport::websocket::split(stream);
196
197 Ok(match protocol {
198 #[cfg(feature = "ocpp_1_6")]
199 "ocpp1.6" => NegotiatedClient::V1_6(crate::Client::from_transport_with_reconnect(
200 sink,
201 source,
202 timeout,
203 Box::new(crate::runtime::tokio::TokioExecutor),
204 Box::new(crate::runtime::tokio::TokioTimer),
205 reconnector,
206 policy,
207 )),
208 #[cfg(feature = "ocpp_2_0_1")]
209 "ocpp2.0.1" => NegotiatedClient::V2_0_1(crate::Client::from_transport_with_reconnect(
210 sink,
211 source,
212 timeout,
213 Box::new(crate::runtime::tokio::TokioExecutor),
214 Box::new(crate::runtime::tokio::TokioTimer),
215 reconnector,
216 policy,
217 )),
218 #[cfg(feature = "ocpp_2_1")]
219 "ocpp2.1" => NegotiatedClient::V2_1(crate::Client::from_transport_with_reconnect(
220 sink,
221 source,
222 timeout,
223 Box::new(crate::runtime::tokio::TokioExecutor),
224 Box::new(crate::runtime::tokio::TokioTimer),
225 reconnector,
226 policy,
227 )),
228 _ => unreachable!("protocol only ever holds a value returned by OcppVersion::protocol"),
229 })
230}
231
232#[cfg(feature = "ocpp_1_6")]
234pub async fn connect_1_6(
235 address: &str,
236 options: Option<ConnectOptions<'_>>,
237) -> Result<crate::ocpp_1_6::OCPP1_6Client, Box<dyn std::error::Error + Send + Sync>> {
238 let (timeout, reconnector, policy) = prepare(address, "ocpp1.6", options.clone());
239 let (stream, _protocol) = setup_socket(address, "ocpp1.6", options).await?;
240 let (sink, source) = crate::transport::websocket::split(stream);
241 Ok(crate::Client::from_transport_with_reconnect(
242 sink,
243 source,
244 timeout,
245 Box::new(crate::runtime::tokio::TokioExecutor),
246 Box::new(crate::runtime::tokio::TokioTimer),
247 reconnector,
248 policy,
249 ))
250}
251
252#[cfg(feature = "ocpp_2_0_1")]
254pub async fn connect_2_0_1(
255 address: &str,
256 options: Option<ConnectOptions<'_>>,
257) -> Result<crate::ocpp_2_0_1::OCPP2_0_1Client, Box<dyn std::error::Error + Send + Sync>> {
258 let (timeout, reconnector, policy) = prepare(address, "ocpp2.0.1", options.clone());
259 let (stream, _protocol) = setup_socket(address, "ocpp2.0.1", options).await?;
260 let (sink, source) = crate::transport::websocket::split(stream);
261 Ok(crate::Client::from_transport_with_reconnect(
262 sink,
263 source,
264 timeout,
265 Box::new(crate::runtime::tokio::TokioExecutor),
266 Box::new(crate::runtime::tokio::TokioTimer),
267 reconnector,
268 policy,
269 ))
270}
271
272#[cfg(feature = "ocpp_2_1")]
274pub async fn connect_2_1(
275 address: &str,
276 options: Option<ConnectOptions<'_>>,
277) -> Result<crate::ocpp_2_1::OCPP2_1Client, Box<dyn std::error::Error + Send + Sync>> {
278 let (timeout, reconnector, policy) = prepare(address, "ocpp2.1", options.clone());
279 let (stream, _protocol) = setup_socket(address, "ocpp2.1", options).await?;
280 let (sink, source) = crate::transport::websocket::split(stream);
281 Ok(crate::Client::from_transport_with_reconnect(
282 sink,
283 source,
284 timeout,
285 Box::new(crate::runtime::tokio::TokioExecutor),
286 Box::new(crate::runtime::tokio::TokioTimer),
287 reconnector,
288 policy,
289 ))
290}
291
292fn prepare(
296 address: &str,
297 protocol: &'static str,
298 options: Option<ConnectOptions<'_>>,
299) -> (Duration, Option<Box<dyn Reconnector>>, ReconnectPolicy) {
300 let timeout = options
301 .as_ref()
302 .and_then(|o| o.timeout)
303 .unwrap_or(DEFAULT_TIMEOUT);
304 let reconnect = options.as_ref().map(|o| o.reconnect).unwrap_or_default();
305 let username = options
306 .as_ref()
307 .and_then(|o| o.username)
308 .map(str::to_string);
309 let password = options
310 .as_ref()
311 .and_then(|o| o.password)
312 .map(str::to_string);
313 let tls_config = options.as_ref().and_then(|o| o.tls_config.clone());
314
315 let custom = options.as_ref().and_then(|o| o.reconnector.clone());
316
317 match reconnect {
318 ReconnectBehavior::Disabled => (timeout, None, ReconnectPolicy::default()),
319 ReconnectBehavior::Enabled(policy) if custom.is_some() => {
320 let custom = custom.expect("guarded by the match arm");
321 (timeout, Some(Box::new(SharedReconnector(custom))), policy)
322 }
323 ReconnectBehavior::Enabled(policy) => {
324 let reconnector: Box<dyn Reconnector> = Box::new(WebSocketReconnector {
325 address: address.to_string(),
326 protocol,
327 username,
328 password,
329 tls_config,
330 });
331 (timeout, Some(reconnector), policy)
332 }
333 }
334}
335
336struct SharedReconnector(Arc<dyn Reconnector>);
340
341impl Reconnector for SharedReconnector {
342 fn connect<'a>(
343 &'a self,
344 ) -> Pin<
345 Box<
346 dyn Future<
347 Output = Result<
348 (Box<dyn TransportSink>, Box<dyn TransportStream>),
349 TransportError,
350 >,
351 > + Send
352 + 'a,
353 >,
354 > {
355 self.0.connect()
356 }
357}
358
359struct WebSocketReconnector {
362 address: String,
363 protocol: &'static str,
364 username: Option<String>,
365 password: Option<String>,
366 tls_config: Option<Arc<rustls::ClientConfig>>,
367}
368
369impl Reconnector for WebSocketReconnector {
370 fn connect<'a>(
371 &'a self,
372 ) -> Pin<
373 Box<
374 dyn Future<
375 Output = Result<
376 (Box<dyn TransportSink>, Box<dyn TransportStream>),
377 TransportError,
378 >,
379 > + Send
380 + 'a,
381 >,
382 > {
383 Box::pin(async move {
384 let options = ConnectOptions {
385 username: self.username.as_deref(),
386 password: self.password.as_deref(),
387 timeout: None,
388 reconnect: ReconnectBehavior::Disabled,
389 tls_config: self.tls_config.clone(),
390 reconnector: None,
391 };
392 let (stream, _protocol) =
393 setup_socket(&self.address, self.protocol, Some(options)).await?;
394 Ok(crate::transport::websocket::split(stream))
395 })
396 }
397}
398
399async fn setup_socket(
400 address: &str,
401 protocols: &str,
402 options: Option<ConnectOptions<'_>>,
403) -> Result<
404 (WebSocketStream<MaybeTlsStream<TcpStream>>, String),
405 Box<dyn std::error::Error + Send + Sync>,
406> {
407 let address = Url::parse(address)?;
408
409 let socket_addrs = address.socket_addrs(|| None)?;
410 let stream = TcpStream::connect(&*socket_addrs).await?;
411
412 let mut request: Request<()> = address.to_string().into_client_request()?;
413 request
414 .headers_mut()
415 .insert(SEC_WEBSOCKET_PROTOCOL, protocols.parse()?);
416 let mut tls_config = None;
417 if let Some(options) = options {
418 if let Some(username) = options.username {
419 let data = format!("{}:{}", username, options.password.unwrap_or(""));
420 let encoded = BASE64_STANDARD.encode(data);
421 request
422 .headers_mut()
423 .insert(AUTHORIZATION, format!("Basic {encoded}").parse()?);
424 }
425 tls_config = options.tls_config;
426 }
427
428 let connector = tls_config.map(Connector::Rustls);
429 let (stream, response) = client_async_tls_with_config(request, stream, None, connector).await?;
430
431 let protocol = response
432 .headers()
433 .get(SEC_WEBSOCKET_PROTOCOL)
434 .ok_or("No OCPP protocol negotiated")?;
435
436 Ok((stream, protocol.to_str()?.to_string()))
437}