1use crate::client::bootstrap::bootstrap_error::BootstrapError;
13use crate::client::configuration::bootstrap_configuration::BootstrapConfiguration;
14use crate::client::configuration::configuration_error::ConfigurationError;
15#[cfg(feature = "geo_routing")]
16use crate::client::configuration::geo_configuration::GeoConfiguration;
17#[cfg(feature = "telemetry")]
18use crate::client::configuration::telemetry_configuration::TelemetryConfiguration;
19use crate::client::configuration::{get_optional_from_section, Configuration, MqttOptionWrapper};
20#[cfg(feature = "mobility")]
21use {
22 crate::client::configuration::{
23 mobility_configuration::MobilityConfiguration,
24 node_configuration::{NodeConfiguration, NODE_SECTION},
25 pick_mandatory_section,
26 },
27 std::sync::RwLock,
28};
29
30use crate::client::bootstrap::bootstrap_error::BootstrapError::{
31 InvalidResponse, MissingField, NotAString,
32};
33use crate::client::configuration::configuration_error::ConfigurationError::{
34 BootstrapFailure, MissingMandatoryField,
35};
36use ini::{Ini, Properties};
37use log::{debug, error, info, trace, warn};
38use reqwest::Url;
39use rumqttc::v5::MqttOptions;
40use serde_json::{json, Value};
41use std::collections::HashMap;
42use std::ops::Deref;
43
44mod bootstrap_error;
45
46#[derive(Debug)]
47struct Bootstrap {
48 id: String,
49 username: String,
50 password: String,
51 protocols: HashMap<String, String>,
52}
53
54impl TryFrom<Value> for Bootstrap {
55 type Error = BootstrapError;
56
57 fn try_from(value: Value) -> Result<Self, Self::Error> {
58 if let Some(protocols) = value.get("protocols") {
59 if let Some(protocols) = protocols.as_object() {
60 let protocols: Result<_, _> = protocols.iter().map(extract_protocol_pair).collect();
61 let protocols = protocols?;
62
63 Ok(Bootstrap {
64 id: extract_str("iot3_id", &value)?,
65 username: extract_str("psk_run_login", &value)?,
66 password: extract_str("psk_run_password", &value)?,
67 protocols,
68 })
69 } else {
70 warn!("Failed to convert {:?} as JSON object", protocols);
71 Err(InvalidResponse("'protocols' field is not a JSON object"))
72 }
73 } else {
74 Err(MissingField("protocols"))
75 }
76 }
77}
78
79pub async fn bootstrap(mut ini: Ini) -> Result<Configuration, ConfigurationError> {
98 info!("Beginning bootstrap...");
99
100 let bootstrap_configuration = BootstrapConfiguration::try_from(&mut ini)?;
101
102 match do_bootstrap(bootstrap_configuration).await {
103 Ok(b) => {
104 info!("Bootstrap call successful !");
105 debug!("{:?}", &b);
106
107 Ok(Configuration {
108 mqtt_options: mqtt_configuration_from_bootstrap(
109 &b,
110 ini.delete(Some("mqtt")).unwrap_or_default(),
111 )?,
112 #[cfg(feature = "geo_routing")]
113 geo: GeoConfiguration::try_from(&pick_mandatory_section(
114 crate::client::configuration::geo_configuration::GEO_SECTION,
115 &mut ini,
116 )?)?,
117 #[cfg(feature = "telemetry")]
118 telemetry: telemetry_configuration_from_bootstrap(
119 &b,
120 ini.delete(Some("telemetry")).unwrap_or_default(),
121 )?,
122 #[cfg(feature = "mobility")]
123 mobility: MobilityConfiguration::try_from(&pick_mandatory_section(
124 crate::client::configuration::mobility_configuration::STATION_SECTION,
125 &mut ini,
126 )?)?,
127 #[cfg(feature = "mobility")]
128 node: match ini.section(Some(NODE_SECTION)) {
129 Some(properties) => Some(RwLock::new(NodeConfiguration::try_from(properties)?)),
130 None => None,
131 },
132 custom_settings: Some(ini),
133 })
134 }
135 Err(e) => {
136 error!("Failed to proceed to bootstrap: {:?}", e);
137 Err(BootstrapFailure(format!("{}", e)))
138 }
139 }
140}
141
142fn mqtt_configuration_from_bootstrap(
143 bootstrap: &Bootstrap,
144 mut mqtt_section: Properties,
145) -> Result<MqttOptions, ConfigurationError> {
146 let tls = get_optional_from_section("use_tls", &mqtt_section)?.unwrap_or_default();
147 let ws = get_optional_from_section("use_websocket", &mqtt_section)?.unwrap_or_default();
148
149 let uri = match (tls, ws) {
150 (true, true) => bootstrap
151 .protocols
152 .get("mqtt-wss")
153 .ok_or(MissingMandatoryField("mqtt-wss", "protocols")),
154 (false, true) => bootstrap
155 .protocols
156 .get("mqtt-ws")
157 .ok_or(MissingMandatoryField("mqtt-ws", "protocols")),
158 (true, false) => bootstrap
159 .protocols
160 .get("mqtts")
161 .ok_or(MissingMandatoryField("mqtts", "protocols")),
162 (false, false) => bootstrap
163 .protocols
164 .get("mqtt")
165 .ok_or(MissingMandatoryField("mqtt", "protocols")),
166 }?;
167
168 let url: Url = {
169 if let Ok(url) = Url::parse(uri) {
170 Ok(url)
171 } else {
172 Err(BootstrapFailure(format!(
173 "Failed to convert '{}' as Url",
174 uri
175 )))
176 }
177 }?;
178
179 if ws {
180 mqtt_section.insert("host", url.authority());
181 } else {
182 mqtt_section.insert(
183 "host",
184 url.host_str()
185 .ok_or(BootstrapFailure("URL must have a host".to_string()))?,
186 );
187 }
188
189 mqtt_section.insert(
190 "port",
191 url.port()
192 .ok_or(BootstrapFailure("URL must have a port".to_string()))?
193 .to_string(),
194 );
195 mqtt_section.insert("client_id", &bootstrap.id);
196 mqtt_section.insert("username", &bootstrap.username);
197 mqtt_section.insert("password", &bootstrap.password);
198
199 match MqttOptionWrapper::try_from(&mqtt_section) {
200 Ok(wrapper) => Ok(wrapper.deref().clone()),
201 Err(e) => Err(e),
202 }
203}
204
205#[cfg(feature = "telemetry")]
206fn telemetry_configuration_from_bootstrap(
207 bootstrap: &Bootstrap,
208 mut telemetry_section: Properties,
209) -> Result<TelemetryConfiguration, ConfigurationError> {
210 let tls = get_optional_from_section("use_tls", &telemetry_section)?.unwrap_or_default();
211
212 let uri = if tls {
213 bootstrap
214 .protocols
215 .get("otlp-https")
216 .ok_or(MissingMandatoryField("otlp-https", "protocols"))
217 } else {
218 bootstrap
219 .protocols
220 .get("otlp-http")
221 .ok_or(MissingMandatoryField("otlp-http", "protocols"))
222 }?;
223
224 let url = Url::parse(uri).expect("Not an URL");
225
226 telemetry_section.insert(
228 "host",
229 url.host_str()
230 .ok_or(BootstrapFailure("URL must have a host".to_string()))?,
231 );
232 telemetry_section.insert(
233 "port",
234 url.port()
235 .ok_or(BootstrapFailure("URL must have a port".to_string()))?
236 .to_string(),
237 );
238 telemetry_section.insert("path", url.path());
239 telemetry_section.insert("username", &bootstrap.username);
240 telemetry_section.insert("password", &bootstrap.password);
241
242 TelemetryConfiguration::try_from(&telemetry_section)
243}
244
245async fn do_bootstrap(
246 bootstrap_configuration: BootstrapConfiguration,
247) -> Result<Bootstrap, BootstrapError> {
248 info!(
249 "Calling bootstrap on '{}'...",
250 bootstrap_configuration.endpoint
251 );
252
253 let client = reqwest::ClientBuilder::new()
254 .build()
255 .expect("Failed to create telemetry HTTP client");
256
257 let body = json!({
258 "ue_id": bootstrap_configuration.station_id,
259 "psk_login": bootstrap_configuration.username,
260 "psk_password": bootstrap_configuration.password,
261 "role": bootstrap_configuration.role
262 })
263 .to_string();
264
265 match client
266 .post(bootstrap_configuration.endpoint)
267 .basic_auth(
268 bootstrap_configuration.username,
269 Some(bootstrap_configuration.password),
270 )
271 .body(body)
272 .send()
273 .await
274 {
275 Ok(response) => match response.text().await {
276 Ok(body) => {
277 trace!("Bootstrap body = {:?}", body);
278 match serde_json::from_str::<Value>(body.as_str()) {
279 Ok(json_value) => Bootstrap::try_from(json_value),
280 Err(e) => {
281 warn!("Error: {:?}", e);
282 Err(InvalidResponse("Failed to parse response as JSON"))
283 }
284 }
285 }
286 Err(e) => {
287 debug!("Error: {:?}", e);
288 Err(BootstrapError::ContentError(e.to_string()))
289 }
290 },
291 Err(e) => {
292 debug!("Request error: {:?}", e);
293 Err(BootstrapError::NetworkError(e.to_string()))
294 }
295 }
296}
297
298fn extract_str(field: &'static str, json_value: &Value) -> Result<String, BootstrapError> {
299 if let Some(value) = json_value.get(field) {
300 if let Some(as_str) = value.as_str() {
301 Ok(as_str.to_string())
302 } else {
303 Err(NotAString(field.to_string()))
304 }
305 } else {
306 Err(MissingField(field))
307 }
308}
309
310fn extract_protocol_pair(entry: (&String, &Value)) -> Result<(String, String), BootstrapError> {
311 let key = entry.0.to_string();
312 if let Some(value) = entry.1.as_str() {
313 Ok((key, value.to_string()))
314 } else {
315 Err(NotAString(key))
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use crate::client::bootstrap::Bootstrap;
322 use serde_json::Value;
323
324 #[test]
325 fn try_from_valid_response() {
326 let response = serde_json::from_str::<Value>(
327 r#"
328 {
329 "iot3_id": "cool_id",
330 "psk_run_login": "notadmin",
331 "psk_run_password": "!s3CuR3",
332 "protocols": {
333 "mqtt": "mqtt://mqtt.domain.com:1884",
334 "mqtt-ws": "http://domain.com:8000/message",
335 "otlp-http": "http://domain.com:8000/collector",
336 "jaeger-http": "http://domain.com:8000/jaeger"
337 }
338 }"#,
339 )
340 .expect("Failed to create JSON from string");
341
342 let result = Bootstrap::try_from(response);
343
344 assert!(result.is_ok());
345 }
346
347 macro_rules! try_from_invalid_response_returns_error {
348 ($test_name:ident, $response:expr) => {
349 #[test]
350 fn $test_name() {
351 let response = serde_json::from_str::<Value>($response)
352 .expect("Failed to create JSON from string");
353
354 let result = Bootstrap::try_from(response);
355
356 assert!(result.is_err());
357 }
358 };
359 }
360 try_from_invalid_response_returns_error!(
361 iot3_id_is_not_a_string,
362 r#"
363 {
364 "iot3_id": ["cool_id"],
365 "psk_run_login": "notadmin",
366 "psk_run_password": "!s3CuR3",
367 "protocols": {
368 "mqtt": "mqtt://mqtt.domain.com:1884",
369 "mqtt-ws": "http://domain.com:8000/message",
370 "otlp-http": "http://domain.com:8000/collector",
371 "jaeger-http": "http://domain.com:8000/jaeger"
372 }
373 }"#
374 );
375 try_from_invalid_response_returns_error!(
376 psk_login_is_not_a_string,
377 r#"
378 {
379 "iot3_id": "cool_id",
380 "psk_run_login": {"value": "notadmin"},
381 "psk_run_password": "!s3CuR3",
382 "protocols": {
383 "mqtt": "mqtt://mqtt.domain.com:1884",
384 "mqtt-ws": "http://domain.com:8000/message",
385 "otlp-http": "http://domain.com:8000/collector",
386 "jaeger-http": "http://domain.com:8000/jaeger"
387 }
388 }"#
389 );
390 try_from_invalid_response_returns_error!(
391 psk_password_is_not_a_string,
392 r#"
393 {
394 "iot3_id": "cool_id",
395 "psk_run_login": "notadmin",
396 "psk_run_password": {"plain": "!s3CuR3"},
397 "protocols": {
398 "mqtt": "mqtt://mqtt.domain.com:1884",
399 "mqtt-ws": "http://domain.com:8000/message",
400 "otlp-http": "http://domain.com:8000/collector",
401 "jaeger-http": "http://domain.com:8000/jaeger"
402 }
403 }"#
404 );
405 try_from_invalid_response_returns_error!(
406 missing_protocols,
407 r#"
408 {
409 "iot3_id": "cool_id",
410 "psk_run_login": "notadmin",
411 "psk_run_password": "!s3CuR3",
412 "protocol": {
413 "mqtt": "mqtt://mqtt.domain.com:1884",
414 "mqtt-ws": "http://domain.com:8000/message",
415 "otlp-http": "http://domain.com:8000/collector",
416 "jaeger-http": "http://domain.com:8000/jaeger"
417 }
418 }"#
419 );
420 try_from_invalid_response_returns_error!(
421 protocols_is_not_an_object,
422 r#"
423 {
424 "iot3_id": "cool_id",
425 "psk_run_login": "notadmin",
426 "psk_run_password": "!s3CuR3",
427 "protocols": [
428 "mqtt://mqtt.domain.com:1884",
429 "http://domain.com:8000/message",
430 "http://domain.com:8000/collector",
431 "http://domain.com:8000/jaeger"
432 ]
433 }"#
434 );
435 try_from_invalid_response_returns_error!(
436 protocol_value_is_not_a_string,
437 r#"
438 {
439 "iot3_id": "cool_id",
440 "psk_run_login": "notadmin",
441 "psk_run_password": "!s3CuR3",
442 "protocols": {
443 "mqtt": ["mqtt://mqtt.domain.com:1884", "mqtts://mqtt.domain.com:8884"]
444 }
445 }"#
446 );
447}