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