diff --git a/Cargo.lock b/Cargo.lock index 5f08554593..45992e5812 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,7 +784,6 @@ dependencies = [ "assert_matches", "async-trait", "c8y_api", - "c8y_auth_proxy", "c8y_http_proxy", "camino", "clock", diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index 8a8dbb3b29..a9cb50ce29 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -1,3 +1,5 @@ +use crate::proxy_url::Protocol; +use crate::proxy_url::ProxyUrlGenerator; use crate::smartrest::error::SmartRestDeserializerError; use crate::smartrest::smartrest_deserializer::SmartRestJwtResponse; use camino::Utf8Path; @@ -16,7 +18,9 @@ use std::path::PathBuf; use std::time::Duration; use tedge_config::auth_method::AuthType; use tedge_config::mqtt_config::MqttConfigBuildError; +use tedge_config::ConfigNotSet; use tedge_config::MultiError; +use tedge_config::ReadError; use tedge_config::TEdgeConfig; use tedge_config::TopicPrefix; use tracing::debug; @@ -34,16 +38,61 @@ pub enum C8yEndPointError { pub struct C8yEndPoint { c8y_host: String, c8y_mqtt_host: String, + proxy: ProxyUrlGenerator, pub device_id: String, pub headers: HeaderMap, devices_internal_id: HashMap, } impl C8yEndPoint { - pub fn new(c8y_host: &str, c8y_mqtt_host: &str, device_id: &str) -> C8yEndPoint { + pub fn from_config( + tedge_config: &TEdgeConfig, + c8y_profile: Option<&str>, + ) -> Result { + let c8y_host = tedge_config + .c8y + .try_get(c8y_profile)? + .http + .or_config_not_set()? + .to_string(); + let c8y_mqtt_host = tedge_config + .c8y + .try_get(c8y_profile)? + .mqtt + .or_config_not_set()? + .to_string(); + let device_id = tedge_config.device.id.try_read(tedge_config)?.to_string(); + + let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; + let auth_proxy_addr = c8y_config.proxy.client.host.clone(); + let auth_proxy_port = c8y_config.proxy.client.port; + let auth_proxy_protocol = c8y_config + .proxy + .cert_path + .or_none() + .map_or(Protocol::Http, |_| Protocol::Https); + let proxy = ProxyUrlGenerator::new(auth_proxy_addr, auth_proxy_port, auth_proxy_protocol); + + Ok(C8yEndPoint { + c8y_host, + c8y_mqtt_host, + proxy, + device_id, + headers: HeaderMap::new(), + devices_internal_id: HashMap::new(), + }) + } + + pub fn new( + c8y_host: &str, + c8y_mqtt_host: &str, + device_id: &str, + proxy: ProxyUrlGenerator, + ) -> C8yEndPoint { C8yEndPoint { c8y_host: c8y_host.into(), c8y_mqtt_host: c8y_mqtt_host.into(), + proxy, device_id: device_id.into(), headers: HeaderMap::new(), devices_internal_id: HashMap::new(), @@ -106,6 +155,16 @@ impl C8yEndPoint { Url::parse(&url).unwrap() } + // Return the local url going through the local auth proxy to reach the given remote url + // + // Return the remote url unchanged if not related to the current tenant. + pub fn local_proxy_url(&self, remote_url: Url) -> Url { + self.maybe_tenant_url(remote_url.as_str()) + .filter(|tenant_url| tenant_url.scheme().starts_with("http")) + .map(|tenant_url| self.proxy.proxy_url(tenant_url)) + .unwrap_or(remote_url) + } + pub fn maybe_tenant_url(&self, url: &str) -> Option { // c8y URL may contain either `Tenant Name` or Tenant Id` so they can be one of following options: // * . eg: sample.c8y.io @@ -138,6 +197,22 @@ impl C8yEndPoint { } } +/// The errors that could occur while building `C8yEndPoint` struct. +#[derive(Debug, thiserror::Error)] +pub enum C8yEndPointConfigError { + #[error(transparent)] + FromReadError(#[from] ReadError), + + #[error(transparent)] + FromConfigNotSet(#[from] ConfigNotSet), + + #[error(transparent)] + FromMultiError(#[from] MultiError), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + pub enum C8yAuthRetriever { Basic { credentials_path: Utf8PathBuf, @@ -330,7 +405,12 @@ mod tests { #[test] fn get_url_for_get_id_returns_correct_address() { - let c8y = C8yEndPoint::new("test_host", "test_host", "test_device"); + let c8y = C8yEndPoint::new( + "test_host", + "test_host", + "test_device", + ProxyUrlGenerator::default(), + ); let res = c8y.get_url_for_internal_id("test_device"); assert_eq!( @@ -341,7 +421,12 @@ mod tests { #[test] fn get_url_for_sw_list_returns_correct_address() { - let mut c8y = C8yEndPoint::new("test_host", "test_host", "test_device"); + let mut c8y = C8yEndPoint::new( + "test_host", + "test_host", + "test_device", + ProxyUrlGenerator::default(), + ); c8y.devices_internal_id .insert("test_device".to_string(), "12345".to_string()); let internal_id = c8y.get_internal_id("test_device".to_string()).unwrap(); @@ -361,7 +446,12 @@ mod tests { #[test_case("https://t1124124.test.com/path/to/file")] #[test_case("https://t1124124.mqtt-url.com/path/to/file")] fn url_is_my_tenant_correct_urls(url: &str) { - let c8y = C8yEndPoint::new("test.test.com", "test.mqtt-url.com", "test_device"); + let c8y = C8yEndPoint::new( + "test.test.com", + "test.mqtt-url.com", + "test_device", + ProxyUrlGenerator::default(), + ); assert_eq!(c8y.maybe_tenant_url(url), Some(url.parse().unwrap())); } @@ -376,7 +466,12 @@ mod tests { #[test_case("https://t1124124.test.com/path/to/file")] #[test_case("https://t1124124.mqtt-url.com/path/to/file")] fn url_is_my_tenant_correct_urls_with_http_port(url: &str) { - let c8y = C8yEndPoint::new("test.test.com:443", "test.mqtt-url.com", "test_device"); + let c8y = C8yEndPoint::new( + "test.test.com:443", + "test.mqtt-url.com", + "test_device", + ProxyUrlGenerator::default(), + ); assert_eq!(c8y.maybe_tenant_url(url), Some(url.parse().unwrap())); } @@ -391,7 +486,12 @@ mod tests { #[test_case("https://t1124124.test.com/path/to/file")] #[test_case("https://t1124124.mqtt-url.com/path/to/file")] fn url_is_my_tenant_correct_urls_with_mqtt_port(url: &str) { - let c8y = C8yEndPoint::new("test.test.com", "test.mqtt-url.com:8883", "test_device"); + let c8y = C8yEndPoint::new( + "test.test.com", + "test.mqtt-url.com:8883", + "test_device", + ProxyUrlGenerator::default(), + ); assert_eq!(c8y.maybe_tenant_url(url), Some(url.parse().unwrap())); } @@ -403,20 +503,35 @@ mod tests { #[test_case("http://localhost")] #[test_case("http://abc.com")] fn url_is_my_tenant_incorrect_urls(url: &str) { - let c8y = C8yEndPoint::new("test.test.com", "test.mqtt-url.com", "test_device"); + let c8y = C8yEndPoint::new( + "test.test.com", + "test.mqtt-url.com", + "test_device", + ProxyUrlGenerator::default(), + ); assert!(c8y.maybe_tenant_url(url).is_none()); } #[test] fn url_is_my_tenant_with_hostname_without_commas() { - let c8y = C8yEndPoint::new("custom-domain", "non-custom-mqtt-domain", "test_device"); + let c8y = C8yEndPoint::new( + "custom-domain", + "non-custom-mqtt-domain", + "test_device", + ProxyUrlGenerator::default(), + ); let url = "http://custom-domain/path"; assert_eq!(c8y.maybe_tenant_url(url), Some(url.parse().unwrap())); } #[test] fn url_is_not_my_tenant_with_hostname_without_commas() { - let c8y = C8yEndPoint::new("custom-domain", "non-custom-mqtt-domain", "test_device"); + let c8y = C8yEndPoint::new( + "custom-domain", + "non-custom-mqtt-domain", + "test_device", + ProxyUrlGenerator::default(), + ); let url = "http://unrelated-domain/path"; assert!(c8y.maybe_tenant_url(url).is_none()); } @@ -424,14 +539,24 @@ mod tests { #[ignore = "Until #2804 is fixed"] #[test] fn url_is_my_tenant_check_not_too_broad() { - let c8y = C8yEndPoint::new("abc.com", "abc.com", "test_device"); + let c8y = C8yEndPoint::new( + "abc.com", + "abc.com", + "test_device", + ProxyUrlGenerator::default(), + ); dbg!(c8y.maybe_tenant_url("http://xyz.com")); assert!(c8y.maybe_tenant_url("http://xyz.com").is_none()); } #[test] fn check_non_cached_internal_id_for_a_device() { - let mut c8y = C8yEndPoint::new("test_host", "test_host", "test_device"); + let mut c8y = C8yEndPoint::new( + "test_host", + "test_host", + "test_device", + ProxyUrlGenerator::default(), + ); c8y.devices_internal_id .insert("test_device".to_string(), "12345".to_string()); let end_pt_err = c8y.get_internal_id("test_child".into()).unwrap_err(); @@ -441,4 +566,36 @@ mod tests { "Cumulocity internal id not found for the device: test_child".to_string() ); } + + #[test_case("http://aaa.test.com", "https://127.0.0.1:1234/c8y/")] + #[test_case("https://aaa.test.com", "https://127.0.0.1:1234/c8y/")] + #[test_case("http://aaa.unrelated.com", "http://aaa.unrelated.com")] // Unchanged: unrelated tenant + #[test_case("ftp://aaa.test.com", "ftp://aaa.test.com")] // Unchanged: unrelated protocol + #[test_case("https://t1124124.test.com", "https://127.0.0.1:1234/c8y/")] + #[test_case("https://t1124124.test.com:12345", "https://127.0.0.1:1234/c8y/")] + #[test_case("https://t1124124.test.com/path", "https://127.0.0.1:1234/c8y/path")] + #[test_case( + "https://t1124124.test.com/path/to/file.test", + "https://127.0.0.1:1234/c8y/path/to/file.test" + )] + #[test_case( + "https://t1124124.test.com/path/to/file", + "https://127.0.0.1:1234/c8y/path/to/file" + )] + #[test_case( + "https://t1124124.mqtt-url.com/path/to/file", + "https://127.0.0.1:1234/c8y/path/to/file" + )] + fn local_proxy_url(url: &str, proxy_url: &str) { + let c8y = C8yEndPoint::new( + "test.test.com", + "test.mqtt-url.com", + "test_device", + ProxyUrlGenerator::new("127.0.0.1".into(), 1234, Protocol::Https), + ); + assert_eq!( + c8y.local_proxy_url(url.parse().unwrap()), + proxy_url.parse().unwrap() + ); + } } diff --git a/crates/core/c8y_api/src/proxy_url.rs b/crates/core/c8y_api/src/proxy_url.rs index d69dd62d55..f0378c2072 100644 --- a/crates/core/c8y_api/src/proxy_url.rs +++ b/crates/core/c8y_api/src/proxy_url.rs @@ -7,6 +7,12 @@ pub struct ProxyUrlGenerator { protocol: Protocol, } +impl Default for ProxyUrlGenerator { + fn default() -> Self { + ProxyUrlGenerator::new("localhost".into(), 8000, Protocol::Http) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Protocol { Http, diff --git a/crates/extensions/c8y_firmware_manager/src/config.rs b/crates/extensions/c8y_firmware_manager/src/config.rs index c7d3d39bb5..081421dff1 100644 --- a/crates/extensions/c8y_firmware_manager/src/config.rs +++ b/crates/extensions/c8y_firmware_manager/src/config.rs @@ -37,9 +37,8 @@ impl FirmwareManagerConfig { tmp_dir: Utf8PathBuf, data_dir: DataDir, timeout_sec: Duration, - c8y_url: String, - c8y_mqtt: String, c8y_prefix: TopicPrefix, + c8y_end_point: C8yEndPoint, ) -> Self { let local_http_host = format!("{}:{}", local_http_host, local_http_port).into(); @@ -47,8 +46,6 @@ impl FirmwareManagerConfig { let firmware_update_response_topics = TopicFilter::new_unchecked(FIRMWARE_UPDATE_RESPONSE_TOPICS); - let c8y_end_point = C8yEndPoint::new(&c8y_url, &c8y_mqtt, &tedge_device_id); - Self { tedge_device_id, local_http_host, @@ -74,9 +71,8 @@ impl FirmwareManagerConfig { let timeout_sec = tedge_config.firmware.child.update.timeout.duration(); let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; - let c8y_url = c8y_config.http.or_config_not_set()?.to_string(); - let c8y_mqtt = c8y_config.mqtt.or_config_not_set()?.to_string(); let c8y_prefix = c8y_config.bridge.topic_prefix.clone(); + let c8y_end_point = C8yEndPoint::from_config(tedge_config, c8y_profile)?; Ok(Self::new( tedge_device_id, @@ -85,9 +81,8 @@ impl FirmwareManagerConfig { tmp_dir, data_dir, timeout_sec, - c8y_url, - c8y_mqtt, c8y_prefix, + c8y_end_point, )) } diff --git a/crates/extensions/c8y_firmware_manager/src/error.rs b/crates/extensions/c8y_firmware_manager/src/error.rs index 8deae96c3a..1c1fc0dac2 100644 --- a/crates/extensions/c8y_firmware_manager/src/error.rs +++ b/crates/extensions/c8y_firmware_manager/src/error.rs @@ -74,4 +74,7 @@ pub enum FirmwareManagementConfigBuildError { #[error(transparent)] MultiError(#[from] tedge_config::MultiError), + + #[error(transparent)] + C8yEndPointConfigError(#[from] c8y_api::http_proxy::C8yEndPointConfigError), } diff --git a/crates/extensions/c8y_firmware_manager/src/tests.rs b/crates/extensions/c8y_firmware_manager/src/tests.rs index 8ed58ccbf0..e2c10eeb11 100644 --- a/crates/extensions/c8y_firmware_manager/src/tests.rs +++ b/crates/extensions/c8y_firmware_manager/src/tests.rs @@ -1,5 +1,8 @@ use super::*; use assert_json_diff::assert_json_include; +use c8y_api::http_proxy::C8yEndPoint; +use c8y_api::proxy_url::Protocol; +use c8y_api::proxy_url::ProxyUrlGenerator; use c8y_api::smartrest::topic::C8yTopic; use c8y_http_proxy::credentials::HttpHeaderRequest; use c8y_http_proxy::HeaderMap; @@ -634,6 +637,12 @@ async fn spawn_firmware_manager( let device_id = "parent-device"; let tedge_host = TEDGE_HOST.into(); + let c8y_end_point = C8yEndPoint::new( + C8Y_HOST, + C8Y_HOST, + device_id, + ProxyUrlGenerator::new("localhost".into(), 8000, Protocol::Http), + ); let config = FirmwareManagerConfig::new( device_id.to_string(), tedge_host, @@ -641,9 +650,8 @@ async fn spawn_firmware_manager( tmp_dir.utf8_path_buf(), tmp_dir.utf8_path_buf().into(), timeout_sec, - C8Y_HOST.into(), - C8Y_HOST.into(), "c8y".try_into().unwrap(), + c8y_end_point, ); let mut mqtt_builder: SimpleMessageBoxBuilder = diff --git a/crates/extensions/c8y_http_proxy/src/actor.rs b/crates/extensions/c8y_http_proxy/src/actor.rs index 2a6574b36a..67b4382024 100644 --- a/crates/extensions/c8y_http_proxy/src/actor.rs +++ b/crates/extensions/c8y_http_proxy/src/actor.rs @@ -98,6 +98,7 @@ impl C8YHttpProxyActor { &config.c8y_http_host, &config.c8y_mqtt_host, &config.device_id, + config.proxy.clone(), ); C8YHttpProxyActor { config, diff --git a/crates/extensions/c8y_http_proxy/src/lib.rs b/crates/extensions/c8y_http_proxy/src/lib.rs index b4cdec36a8..68a6676bc7 100644 --- a/crates/extensions/c8y_http_proxy/src/lib.rs +++ b/crates/extensions/c8y_http_proxy/src/lib.rs @@ -28,6 +28,8 @@ pub mod credentials; pub mod handle; pub mod messages; +use c8y_api::proxy_url::Protocol; +use c8y_api::proxy_url::ProxyUrlGenerator; pub use http::HeaderMap; #[cfg(test)] @@ -41,6 +43,7 @@ pub struct C8YHttpConfig { pub device_id: String, pub tmp_dir: PathBuf, retry_interval: Duration, + proxy: ProxyUrlGenerator, } impl C8YHttpConfig { @@ -64,9 +67,21 @@ impl C8YHttpConfig { let tmp_dir = tedge_config.tmp.path.as_std_path().to_path_buf(); let retry_interval = Duration::from_secs(5); + // Temporary code: this will be deprecated along c8y_http_proxy + let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; + let auth_proxy_addr = c8y_config.proxy.client.host.clone(); + let auth_proxy_port = c8y_config.proxy.client.port; + let auth_proxy_protocol = c8y_config + .proxy + .cert_path + .or_none() + .map_or(Protocol::Http, |_| Protocol::Https); + let proxy = ProxyUrlGenerator::new(auth_proxy_addr, auth_proxy_port, auth_proxy_protocol); + Ok(Self { c8y_http_host, c8y_mqtt_host, + proxy, device_id, tmp_dir, retry_interval, diff --git a/crates/extensions/c8y_http_proxy/src/tests.rs b/crates/extensions/c8y_http_proxy/src/tests.rs index 29ddf34bad..97569111e1 100644 --- a/crates/extensions/c8y_http_proxy/src/tests.rs +++ b/crates/extensions/c8y_http_proxy/src/tests.rs @@ -9,6 +9,7 @@ use async_trait::async_trait; use c8y_api::json_c8y::C8yEventResponse; use c8y_api::json_c8y::C8yUpdateSoftwareListResponse; use c8y_api::json_c8y::InternalIdResponse; +use c8y_api::proxy_url::ProxyUrlGenerator; use http::header::AUTHORIZATION; use http::HeaderMap; use http::StatusCode; @@ -319,6 +320,7 @@ async fn retry_internal_id_on_expired_jwt_with_mock() { device_id: external_id.into(), tmp_dir: tmp_dir.into(), retry_interval: Duration::from_millis(100), + proxy: ProxyUrlGenerator::default(), }; let c8y_proxy_actor = C8YHttpProxyBuilder::new(config, &mut http_actor, &mut auth); let jwt_actor = ServerActor::new(DynamicJwtRetriever { count: 0 }, auth.build()); @@ -388,6 +390,7 @@ async fn retry_create_event_on_expired_jwt_with_mock() { device_id: external_id.into(), tmp_dir: tmp_dir.into(), retry_interval: Duration::from_millis(100), + proxy: ProxyUrlGenerator::default(), }; let c8y_proxy_actor = C8YHttpProxyBuilder::new(config, &mut http_actor, &mut jwt); let jwt_actor = ServerActor::new(DynamicJwtRetriever { count: 1 }, jwt.build()); @@ -537,6 +540,7 @@ async fn spawn_c8y_http_proxy( device_id, tmp_dir, retry_interval: Duration::from_millis(10), + proxy: ProxyUrlGenerator::default(), }; let mut c8y_proxy_actor = C8YHttpProxyBuilder::new(config, &mut http, &mut jwt); let proxy = C8YHttpProxy::new(&mut c8y_proxy_actor); diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index a5e0c14b8a..69268ebfd0 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -238,7 +238,7 @@ impl CumulocityConverter { let log_dir = config.logs_path.join(TEDGE_AGENT_LOG_DIR); let operation_logs = OperationLogs::try_new(log_dir)?; - let c8y_endpoint = C8yEndPoint::new(c8y_host, c8y_mqtt, &device_id); + let c8y_endpoint = C8yEndPoint::new(c8y_host, c8y_mqtt, &device_id, auth_proxy.clone()); let mqtt_schema = config.mqtt_schema.clone(); diff --git a/crates/extensions/c8y_mapper_ext/src/operations/handler.rs b/crates/extensions/c8y_mapper_ext/src/operations/handler.rs index 891d745282..a2557c98c8 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/handler.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/handler.rs @@ -79,6 +79,7 @@ impl OperationHandler { &c8y_mapper_config.c8y_host, &c8y_mapper_config.c8y_mqtt, &c8y_mapper_config.device_id, + auth_proxy.clone(), ), http_proxy: http_proxy.clone(), auth_proxy: auth_proxy.clone(),