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