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(Debug, 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}
37
38pub enum NegotiatedClient {
42 #[cfg(feature = "ocpp_1_6")]
43 V1_6(crate::ocpp_1_6::OCPP1_6Client),
44 #[cfg(feature = "ocpp_2_0_1")]
45 V2_0_1(crate::ocpp_2_0_1::OCPP2_0_1Client),
46 #[cfg(feature = "ocpp_2_1")]
47 V2_1(crate::ocpp_2_1::OCPP2_1Client),
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum OcppVersion {
54 #[cfg(feature = "ocpp_1_6")]
55 V1_6,
56 #[cfg(feature = "ocpp_2_0_1")]
57 V2_0_1,
58 #[cfg(feature = "ocpp_2_1")]
59 V2_1,
60}
61
62impl OcppVersion {
63 fn protocol(self) -> &'static str {
64 match self {
65 #[cfg(feature = "ocpp_1_6")]
66 OcppVersion::V1_6 => "ocpp1.6",
67 #[cfg(feature = "ocpp_2_0_1")]
68 OcppVersion::V2_0_1 => "ocpp2.0.1",
69 #[cfg(feature = "ocpp_2_1")]
70 OcppVersion::V2_1 => "ocpp2.1",
71 }
72 }
73
74 #[allow(clippy::vec_init_then_push)]
77 fn all_compiled_in() -> Vec<OcppVersion> {
78 let mut versions = Vec::new();
79 #[cfg(feature = "ocpp_2_1")]
80 versions.push(OcppVersion::V2_1);
81 #[cfg(feature = "ocpp_2_0_1")]
82 versions.push(OcppVersion::V2_0_1);
83 #[cfg(feature = "ocpp_1_6")]
84 versions.push(OcppVersion::V1_6);
85 versions
86 }
87}
88
89pub async fn connect(
97 address: &str,
98 versions: Option<&[OcppVersion]>,
99 options: Option<ConnectOptions<'_>>,
100) -> Result<NegotiatedClient, Box<dyn std::error::Error + Send + Sync>> {
101 let all_compiled_in;
102 let versions = match versions {
103 Some(versions) => versions,
104 None => {
105 all_compiled_in = OcppVersion::all_compiled_in();
106 &all_compiled_in
107 }
108 };
109 let offered = versions
110 .iter()
111 .map(|v| v.protocol())
112 .collect::<Vec<_>>()
113 .join(", ");
114 let (stream, negotiated) = setup_socket(address, &offered, options.clone()).await?;
115 let protocol = versions
116 .iter()
117 .find(|v| v.protocol() == negotiated)
118 .map(|v| v.protocol())
119 .ok_or_else(|| format!("Server negotiated unsupported protocol: {negotiated}"))?;
120 let (timeout, reconnector, policy) = prepare(address, protocol, options);
121 let (sink, source) = crate::transport::websocket::split(stream);
122
123 Ok(match protocol {
124 #[cfg(feature = "ocpp_1_6")]
125 "ocpp1.6" => NegotiatedClient::V1_6(crate::Client::from_transport_with_reconnect(
126 sink,
127 source,
128 timeout,
129 Box::new(crate::runtime::tokio::TokioExecutor),
130 Box::new(crate::runtime::tokio::TokioTimer),
131 reconnector,
132 policy,
133 )),
134 #[cfg(feature = "ocpp_2_0_1")]
135 "ocpp2.0.1" => NegotiatedClient::V2_0_1(crate::Client::from_transport_with_reconnect(
136 sink,
137 source,
138 timeout,
139 Box::new(crate::runtime::tokio::TokioExecutor),
140 Box::new(crate::runtime::tokio::TokioTimer),
141 reconnector,
142 policy,
143 )),
144 #[cfg(feature = "ocpp_2_1")]
145 "ocpp2.1" => NegotiatedClient::V2_1(crate::Client::from_transport_with_reconnect(
146 sink,
147 source,
148 timeout,
149 Box::new(crate::runtime::tokio::TokioExecutor),
150 Box::new(crate::runtime::tokio::TokioTimer),
151 reconnector,
152 policy,
153 )),
154 _ => unreachable!("protocol only ever holds a value returned by OcppVersion::protocol"),
155 })
156}
157
158#[cfg(feature = "ocpp_1_6")]
160pub async fn connect_1_6(
161 address: &str,
162 options: Option<ConnectOptions<'_>>,
163) -> Result<crate::ocpp_1_6::OCPP1_6Client, Box<dyn std::error::Error + Send + Sync>> {
164 let (timeout, reconnector, policy) = prepare(address, "ocpp1.6", options.clone());
165 let (stream, _protocol) = setup_socket(address, "ocpp1.6", options).await?;
166 let (sink, source) = crate::transport::websocket::split(stream);
167 Ok(crate::Client::from_transport_with_reconnect(
168 sink,
169 source,
170 timeout,
171 Box::new(crate::runtime::tokio::TokioExecutor),
172 Box::new(crate::runtime::tokio::TokioTimer),
173 reconnector,
174 policy,
175 ))
176}
177
178#[cfg(feature = "ocpp_2_0_1")]
180pub async fn connect_2_0_1(
181 address: &str,
182 options: Option<ConnectOptions<'_>>,
183) -> Result<crate::ocpp_2_0_1::OCPP2_0_1Client, Box<dyn std::error::Error + Send + Sync>> {
184 let (timeout, reconnector, policy) = prepare(address, "ocpp2.0.1", options.clone());
185 let (stream, _protocol) = setup_socket(address, "ocpp2.0.1", options).await?;
186 let (sink, source) = crate::transport::websocket::split(stream);
187 Ok(crate::Client::from_transport_with_reconnect(
188 sink,
189 source,
190 timeout,
191 Box::new(crate::runtime::tokio::TokioExecutor),
192 Box::new(crate::runtime::tokio::TokioTimer),
193 reconnector,
194 policy,
195 ))
196}
197
198#[cfg(feature = "ocpp_2_1")]
200pub async fn connect_2_1(
201 address: &str,
202 options: Option<ConnectOptions<'_>>,
203) -> Result<crate::ocpp_2_1::OCPP2_1Client, Box<dyn std::error::Error + Send + Sync>> {
204 let (timeout, reconnector, policy) = prepare(address, "ocpp2.1", options.clone());
205 let (stream, _protocol) = setup_socket(address, "ocpp2.1", options).await?;
206 let (sink, source) = crate::transport::websocket::split(stream);
207 Ok(crate::Client::from_transport_with_reconnect(
208 sink,
209 source,
210 timeout,
211 Box::new(crate::runtime::tokio::TokioExecutor),
212 Box::new(crate::runtime::tokio::TokioTimer),
213 reconnector,
214 policy,
215 ))
216}
217
218fn prepare(
222 address: &str,
223 protocol: &'static str,
224 options: Option<ConnectOptions<'_>>,
225) -> (Duration, Option<Box<dyn Reconnector>>, ReconnectPolicy) {
226 let timeout = options
227 .as_ref()
228 .and_then(|o| o.timeout)
229 .unwrap_or(DEFAULT_TIMEOUT);
230 let reconnect = options.as_ref().map(|o| o.reconnect).unwrap_or_default();
231 let username = options
232 .as_ref()
233 .and_then(|o| o.username)
234 .map(str::to_string);
235 let password = options
236 .as_ref()
237 .and_then(|o| o.password)
238 .map(str::to_string);
239 let tls_config = options.as_ref().and_then(|o| o.tls_config.clone());
240
241 match reconnect {
242 ReconnectBehavior::Disabled => (timeout, None, ReconnectPolicy::default()),
243 ReconnectBehavior::Enabled(policy) => {
244 let reconnector: Box<dyn Reconnector> = Box::new(WebSocketReconnector {
245 address: address.to_string(),
246 protocol,
247 username,
248 password,
249 tls_config,
250 });
251 (timeout, Some(reconnector), policy)
252 }
253 }
254}
255
256struct WebSocketReconnector {
259 address: String,
260 protocol: &'static str,
261 username: Option<String>,
262 password: Option<String>,
263 tls_config: Option<Arc<rustls::ClientConfig>>,
264}
265
266impl Reconnector for WebSocketReconnector {
267 fn connect<'a>(
268 &'a self,
269 ) -> Pin<
270 Box<
271 dyn Future<
272 Output = Result<
273 (Box<dyn TransportSink>, Box<dyn TransportStream>),
274 TransportError,
275 >,
276 > + Send
277 + 'a,
278 >,
279 > {
280 Box::pin(async move {
281 let options = ConnectOptions {
282 username: self.username.as_deref(),
283 password: self.password.as_deref(),
284 timeout: None,
285 reconnect: ReconnectBehavior::Disabled,
286 tls_config: self.tls_config.clone(),
287 };
288 let (stream, _protocol) =
289 setup_socket(&self.address, self.protocol, Some(options)).await?;
290 Ok(crate::transport::websocket::split(stream))
291 })
292 }
293}
294
295async fn setup_socket(
296 address: &str,
297 protocols: &str,
298 options: Option<ConnectOptions<'_>>,
299) -> Result<
300 (WebSocketStream<MaybeTlsStream<TcpStream>>, String),
301 Box<dyn std::error::Error + Send + Sync>,
302> {
303 let address = Url::parse(address)?;
304
305 let socket_addrs = address.socket_addrs(|| None)?;
306 let stream = TcpStream::connect(&*socket_addrs).await?;
307
308 let mut request: Request<()> = address.to_string().into_client_request()?;
309 request
310 .headers_mut()
311 .insert(SEC_WEBSOCKET_PROTOCOL, protocols.parse()?);
312 let mut tls_config = None;
313 if let Some(options) = options {
314 if let Some(username) = options.username {
315 let data = format!("{}:{}", username, options.password.unwrap_or(""));
316 let encoded = BASE64_STANDARD.encode(data);
317 request
318 .headers_mut()
319 .insert(AUTHORIZATION, format!("Basic {encoded}").parse()?);
320 }
321 tls_config = options.tls_config;
322 }
323
324 let connector = tls_config.map(Connector::Rustls);
325 let (stream, response) = client_async_tls_with_config(request, stream, None, connector).await?;
326
327 let protocol = response
328 .headers()
329 .get(SEC_WEBSOCKET_PROTOCOL)
330 .ok_or("No OCPP protocol negotiated")?;
331
332 Ok((stream, protocol.to_str()?.to_string()))
333}