From a60d8ff4834f35aff5a3b49842a84769e8e9c3de Mon Sep 17 00:00:00 2001 From: "Kamal S. Fuseini" Date: Tue, 17 Oct 2023 10:42:37 -0700 Subject: [PATCH 01/10] wsdl_rs/media2: Configuration macro Use macro to generate configs for correct serialization --- wsdl_rs/media2/src/lib.rs | 67 +++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/wsdl_rs/media2/src/lib.rs b/wsdl_rs/media2/src/lib.rs index 922ca2c..06ca662 100644 --- a/wsdl_rs/media2/src/lib.rs +++ b/wsdl_rs/media2/src/lib.rs @@ -406,25 +406,32 @@ pub struct DeleteProfileResponse {} impl Validate for DeleteProfileResponse {} -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -#[yaserde( - prefix = "tr2", - namespace = "tr2: http://www.onvif.org/ver20/media/wsdl" -)] -pub struct GetConfiguration { - // Token of the requested configuration. - #[yaserde(prefix = "tr2", rename = "ConfigurationToken")] - pub configuration_token: Option, - - // Contains the token of an existing media profile the configurations shall - // be compatible with. - #[yaserde(prefix = "tr2", rename = "ProfileToken")] - pub profile_token: Option, -} +macro_rules! config { + ($name:ident) => { + #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] + #[yaserde( + prefix = "tr2", + namespace = "tr2: http://www.onvif.org/ver20/media/wsdl" + )] + pub struct $name { + // fields + // Token of the requested configuration. + #[yaserde(prefix = "tr2", rename = "ConfigurationToken")] + pub configuration_token: Option, + + // Contains the token of an existing media profile the configurations shall + // be compatible with. + #[yaserde(prefix = "tr2", rename = "ProfileToken")] + pub profile_token: Option, + } + }; +} + +config!(GetConfiguration); impl Validate for GetConfiguration {} -pub type GetVideoEncoderConfigurations = GetConfiguration; +config!(GetVideoEncoderConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -439,7 +446,7 @@ pub struct GetVideoEncoderConfigurationsResponse { impl Validate for GetVideoEncoderConfigurationsResponse {} -pub type GetVideoSourceConfigurations = GetConfiguration; +config!(GetVideoSourceConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -454,7 +461,7 @@ pub struct GetVideoSourceConfigurationsResponse { impl Validate for GetVideoSourceConfigurationsResponse {} -pub type GetAudioEncoderConfigurations = GetConfiguration; +config!(GetAudioEncoderConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -469,7 +476,7 @@ pub struct GetAudioEncoderConfigurationsResponse { impl Validate for GetAudioEncoderConfigurationsResponse {} -pub type GetAudioSourceConfigurations = GetConfiguration; +config!(GetAudioSourceConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -484,7 +491,7 @@ pub struct GetAudioSourceConfigurationsResponse { impl Validate for GetAudioSourceConfigurationsResponse {} -pub type GetAnalyticsConfigurations = GetConfiguration; +config!(GetAnalyticsConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -499,7 +506,7 @@ pub struct GetAnalyticsConfigurationsResponse { impl Validate for GetAnalyticsConfigurationsResponse {} -pub type GetMetadataConfigurations = GetConfiguration; +config!(GetMetadataConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -514,7 +521,7 @@ pub struct GetMetadataConfigurationsResponse { impl Validate for GetMetadataConfigurationsResponse {} -pub type GetAudioOutputConfigurations = GetConfiguration; +config!(GetAudioOutputConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -529,7 +536,7 @@ pub struct GetAudioOutputConfigurationsResponse { impl Validate for GetAudioOutputConfigurationsResponse {} -pub type GetAudioDecoderConfigurations = GetConfiguration; +config!(GetAudioDecoderConfigurations); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -664,7 +671,7 @@ pub struct SetAudioDecoderConfiguration { impl Validate for SetAudioDecoderConfiguration {} pub type SetAudioDecoderConfigurationResponse = SetConfigurationResponse; -pub type GetVideoSourceConfigurationOptions = GetConfiguration; +config!(GetVideoSourceConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -683,7 +690,7 @@ pub struct GetVideoSourceConfigurationOptionsResponse { impl Validate for GetVideoSourceConfigurationOptionsResponse {} -pub type GetVideoEncoderConfigurationOptions = GetConfiguration; +config!(GetVideoEncoderConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -697,7 +704,7 @@ pub struct GetVideoEncoderConfigurationOptionsResponse { impl Validate for GetVideoEncoderConfigurationOptionsResponse {} -pub type GetAudioSourceConfigurationOptions = GetConfiguration; +config!(GetAudioSourceConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -716,7 +723,7 @@ pub struct GetAudioSourceConfigurationOptionsResponse { impl Validate for GetAudioSourceConfigurationOptionsResponse {} -pub type GetAudioEncoderConfigurationOptions = GetConfiguration; +config!(GetAudioEncoderConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -735,7 +742,7 @@ pub struct GetAudioEncoderConfigurationOptionsResponse { impl Validate for GetAudioEncoderConfigurationOptionsResponse {} -pub type GetMetadataConfigurationOptions = GetConfiguration; +config!(GetMetadataConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -754,7 +761,7 @@ pub struct GetMetadataConfigurationOptionsResponse { impl Validate for GetMetadataConfigurationOptionsResponse {} -pub type GetAudioOutputConfigurationOptions = GetConfiguration; +config!(GetAudioOutputConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( @@ -773,7 +780,7 @@ pub struct GetAudioOutputConfigurationOptionsResponse { impl Validate for GetAudioOutputConfigurationOptionsResponse {} -pub type GetAudioDecoderConfigurationOptions = GetConfiguration; +config!(GetAudioDecoderConfigurationOptions); #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( From ebb3cc345ca3db45139071a42e925dda768fc47e Mon Sep 17 00:00:00 2001 From: "Kamal S. Fuseini" Date: Wed, 18 Oct 2023 12:16:45 -0700 Subject: [PATCH 02/10] schema/test: Add test for media2 serialization --- schema/src/tests.rs | 69 +++++++++++++++++++++++++++++++++++++++ schema/src/tests/utils.rs | 4 +++ 2 files changed, 73 insertions(+) diff --git a/schema/src/tests.rs b/schema/src/tests.rs index 40da2da..bb0a9d7 100644 --- a/schema/src/tests.rs +++ b/schema/src/tests.rs @@ -620,3 +620,72 @@ fn extension_inside_extension() { let _ = yaserde::de::from_str::(ser).unwrap(); } + +#[test] +#[cfg(feature = "media2")] +fn media2_configs_name_serialization() { + assert_eq!( + "media2::GetConfiguration", + utils::type_of(&media2::GetConfiguration::default()) + ); + assert_eq!( + "media2::GetVideoEncoderConfigurations", + utils::type_of(&media2::GetVideoEncoderConfigurations::default()) + ); + assert_eq!( + "media2::GetVideoSourceConfigurations", + utils::type_of(&media2::GetVideoSourceConfigurations::default()) + ); + assert_eq!( + "media2::GetAudioEncoderConfigurations", + utils::type_of(&media2::GetAudioEncoderConfigurations::default()) + ); + assert_eq!( + "media2::GetAudioSourceConfigurations", + utils::type_of(&media2::GetAudioSourceConfigurations::default()) + ); + assert_eq!( + "media2::GetAnalyticsConfigurations", + utils::type_of(&media2::GetAnalyticsConfigurations::default()) + ); + assert_eq!( + "media2::GetMetadataConfigurations", + utils::type_of(&media2::GetMetadataConfigurations::default()) + ); + assert_eq!( + "media2::GetAudioOutputConfigurations", + utils::type_of(&media2::GetAudioOutputConfigurations::default()) + ); + assert_eq!( + "media2::GetAudioDecoderConfigurations", + utils::type_of(&media2::GetAudioDecoderConfigurations::default()) + ); + assert_eq!( + "media2::GetVideoSourceConfigurationOptions", + utils::type_of(&media2::GetVideoSourceConfigurationOptions::default()) + ); + assert_eq!( + "media2::GetVideoEncoderConfigurationOptions", + utils::type_of(&media2::GetVideoEncoderConfigurationOptions::default()) + ); + assert_eq!( + "media2::GetAudioSourceConfigurationOptions", + utils::type_of(&media2::GetAudioSourceConfigurationOptions::default()) + ); + assert_eq!( + "media2::GetAudioEncoderConfigurationOptions", + utils::type_of(&media2::GetAudioEncoderConfigurationOptions::default()) + ); + assert_eq!( + "media2::GetMetadataConfigurationOptions", + utils::type_of(&media2::GetMetadataConfigurationOptions::default()) + ); + assert_eq!( + "media2::GetAudioOutputConfigurationOptions", + utils::type_of(&media2::GetAudioOutputConfigurationOptions::default()) + ); + assert_eq!( + "media2::GetAudioDecoderConfigurationOptions", + utils::type_of(&media2::GetAudioDecoderConfigurationOptions::default()) + ); +} diff --git a/schema/src/tests/utils.rs b/schema/src/tests/utils.rs index 31cf80f..c0abace 100644 --- a/schema/src/tests/utils.rs +++ b/schema/src/tests/utils.rs @@ -11,3 +11,7 @@ fn without_whitespaces( .into_iter() .filter(|e| !matches!(e, Ok(xml::reader::XmlEvent::Whitespace(_)))) } + +pub fn type_of(_: &T) -> &str { + std::any::type_name::() +} From b92af4eddf5ed1559803f8f9dddf9076a2412727 Mon Sep 17 00:00:00 2001 From: "Kamal S. Fuseini" Date: Fri, 20 Oct 2023 09:20:22 -0700 Subject: [PATCH 03/10] schema/test: Move `type_of` fn to where its used --- schema/src/tests.rs | 36 ++++++++++++++++++++---------------- schema/src/tests/utils.rs | 4 ---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/schema/src/tests.rs b/schema/src/tests.rs index bb0a9d7..1552da2 100644 --- a/schema/src/tests.rs +++ b/schema/src/tests.rs @@ -624,68 +624,72 @@ fn extension_inside_extension() { #[test] #[cfg(feature = "media2")] fn media2_configs_name_serialization() { + pub fn type_of(_: &T) -> &str { + std::any::type_name::() + } + assert_eq!( "media2::GetConfiguration", - utils::type_of(&media2::GetConfiguration::default()) + type_of(&media2::GetConfiguration::default()) ); assert_eq!( "media2::GetVideoEncoderConfigurations", - utils::type_of(&media2::GetVideoEncoderConfigurations::default()) + type_of(&media2::GetVideoEncoderConfigurations::default()) ); assert_eq!( "media2::GetVideoSourceConfigurations", - utils::type_of(&media2::GetVideoSourceConfigurations::default()) + type_of(&media2::GetVideoSourceConfigurations::default()) ); assert_eq!( "media2::GetAudioEncoderConfigurations", - utils::type_of(&media2::GetAudioEncoderConfigurations::default()) + type_of(&media2::GetAudioEncoderConfigurations::default()) ); assert_eq!( "media2::GetAudioSourceConfigurations", - utils::type_of(&media2::GetAudioSourceConfigurations::default()) + type_of(&media2::GetAudioSourceConfigurations::default()) ); assert_eq!( "media2::GetAnalyticsConfigurations", - utils::type_of(&media2::GetAnalyticsConfigurations::default()) + type_of(&media2::GetAnalyticsConfigurations::default()) ); assert_eq!( "media2::GetMetadataConfigurations", - utils::type_of(&media2::GetMetadataConfigurations::default()) + type_of(&media2::GetMetadataConfigurations::default()) ); assert_eq!( "media2::GetAudioOutputConfigurations", - utils::type_of(&media2::GetAudioOutputConfigurations::default()) + type_of(&media2::GetAudioOutputConfigurations::default()) ); assert_eq!( "media2::GetAudioDecoderConfigurations", - utils::type_of(&media2::GetAudioDecoderConfigurations::default()) + type_of(&media2::GetAudioDecoderConfigurations::default()) ); assert_eq!( "media2::GetVideoSourceConfigurationOptions", - utils::type_of(&media2::GetVideoSourceConfigurationOptions::default()) + type_of(&media2::GetVideoSourceConfigurationOptions::default()) ); assert_eq!( "media2::GetVideoEncoderConfigurationOptions", - utils::type_of(&media2::GetVideoEncoderConfigurationOptions::default()) + type_of(&media2::GetVideoEncoderConfigurationOptions::default()) ); assert_eq!( "media2::GetAudioSourceConfigurationOptions", - utils::type_of(&media2::GetAudioSourceConfigurationOptions::default()) + type_of(&media2::GetAudioSourceConfigurationOptions::default()) ); assert_eq!( "media2::GetAudioEncoderConfigurationOptions", - utils::type_of(&media2::GetAudioEncoderConfigurationOptions::default()) + type_of(&media2::GetAudioEncoderConfigurationOptions::default()) ); assert_eq!( "media2::GetMetadataConfigurationOptions", - utils::type_of(&media2::GetMetadataConfigurationOptions::default()) + type_of(&media2::GetMetadataConfigurationOptions::default()) ); assert_eq!( "media2::GetAudioOutputConfigurationOptions", - utils::type_of(&media2::GetAudioOutputConfigurationOptions::default()) + type_of(&media2::GetAudioOutputConfigurationOptions::default()) ); assert_eq!( "media2::GetAudioDecoderConfigurationOptions", - utils::type_of(&media2::GetAudioDecoderConfigurationOptions::default()) + type_of(&media2::GetAudioDecoderConfigurationOptions::default()) ); } diff --git a/schema/src/tests/utils.rs b/schema/src/tests/utils.rs index c0abace..31cf80f 100644 --- a/schema/src/tests/utils.rs +++ b/schema/src/tests/utils.rs @@ -11,7 +11,3 @@ fn without_whitespaces( .into_iter() .filter(|e| !matches!(e, Ok(xml::reader::XmlEvent::Whitespace(_)))) } - -pub fn type_of(_: &T) -> &str { - std::any::type_name::() -} From 9e945d4d2ee0957f91fabcb2d36d1448d804ac59 Mon Sep 17 00:00:00 2001 From: asuper0 <41113804+asuper0@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:24:19 +0800 Subject: [PATCH 04/10] Shorter Nonce and Created in UsernameToken (#115) Shorter Nonce and Created in UsernameToken, to let it working with HIKVISION camera --- onvif/src/soap/auth/username_token.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/onvif/src/soap/auth/username_token.rs b/onvif/src/soap/auth/username_token.rs index f206486..a2622bf 100644 --- a/onvif/src/soap/auth/username_token.rs +++ b/onvif/src/soap/auth/username_token.rs @@ -8,19 +8,25 @@ pub struct UsernameToken { impl UsernameToken { pub fn new(username: &str, password: &str) -> UsernameToken { - let nonce = uuid::Uuid::new_v4().to_string(); - let created = chrono::Utc::now().to_rfc3339(); - let concat = format!("{}{}{}", nonce, created, password); + let uuid = uuid::Uuid::new_v4(); + let nonce = uuid.as_bytes(); + let created = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); + + let mut concat = Vec::with_capacity(nonce.len() + created.len() + password.len()); + + concat.extend_from_slice(nonce); + concat.extend_from_slice(created.as_bytes()); + concat.extend_from_slice(password.as_bytes()); let digest = { let mut hasher = sha1::Sha1::new(); - hasher.update(concat.as_bytes()); + hasher.update(&concat); hasher.digest().bytes() }; UsernameToken { username: username.to_string(), - nonce: base64::encode(&nonce), + nonce: base64::encode(nonce), digest: base64::encode(digest), created, } From 5e8159907cfc3d5c2d14aed9168f7a476022698e Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Tue, 6 Feb 2024 14:50:04 -0500 Subject: [PATCH 05/10] Add support for the endpoint reference --- onvif/src/discovery/mod.rs | 27 +++++++++++++++++++++------ wsdl_rs/ws_discovery/src/lib.rs | 29 +++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/onvif/src/discovery/mod.rs b/onvif/src/discovery/mod.rs index 43a6c2e..acbc43e 100644 --- a/onvif/src/discovery/mod.rs +++ b/onvif/src/discovery/mod.rs @@ -33,6 +33,8 @@ pub enum Error { #[derive(Clone, Eq, Hash, PartialEq)] pub struct Device { + /// The WS-Discovery UUID / address reference + pub address: String, pub name: Option, pub urls: Vec, } @@ -42,6 +44,7 @@ impl Debug for Device { f.debug_struct("Device") .field("name", &self.name) .field("url", &DisplayList(&self.urls)) + .field("address", &self.address) .finish() } } @@ -226,8 +229,13 @@ fn device_from_envelope(envelope: probe_matches::Envelope) -> Option { let name = onvif_probe_match.name(); let urls = onvif_probe_match.x_addrs(); + let address = onvif_probe_match.endpoint_reference_address(); - Some(Device { name, urls }) + Some(Device { + name, + urls, + address, + }) } fn build_probe() -> probe::Envelope { @@ -245,7 +253,9 @@ fn build_probe() -> probe::Envelope { #[test] fn test_xaddrs_extraction() { - fn make_xml(relates_to: &str, xaddrs: &str) -> String { + const DEVICE_ADDRESS: &str = "an address"; + + let make_xml = |relates_to: &str, xaddrs: &str| -> String { format!( r#" http://something.else + + {device_address} + onvif://www.onvif.org/name/MyCamera2000 {xaddrs} @@ -270,9 +283,10 @@ fn test_xaddrs_extraction() { "#, relates_to = relates_to, - xaddrs = xaddrs + xaddrs = xaddrs, + device_address = DEVICE_ADDRESS ) - } + }; let our_uuid = "uuid:84ede3de-7dec-11d0-c360-F01234567890"; let bad_uuid = "uuid:84ede3de-7dec-11d0-c360-F00000000000"; @@ -298,9 +312,10 @@ fn test_xaddrs_extraction() { urls: vec![ Url::parse("http://addr_20").unwrap(), Url::parse("http://addr_21").unwrap(), - Url::parse("http://addr_22").unwrap() + Url::parse("http://addr_22").unwrap(), ], - name: Some("MyCamera2000".to_string()) + name: Some("MyCamera2000".to_string()), + address: DEVICE_ADDRESS.to_string(), }] ); } diff --git a/wsdl_rs/ws_discovery/src/lib.rs b/wsdl_rs/ws_discovery/src/lib.rs index 67ac2e4..08c700e 100644 --- a/wsdl_rs/ws_discovery/src/lib.rs +++ b/wsdl_rs/ws_discovery/src/lib.rs @@ -1,5 +1,4 @@ pub mod probe { - use yaserde_derive::YaSerialize; #[derive(Default, Eq, PartialEq, Debug, YaSerialize)] @@ -56,8 +55,22 @@ pub mod probe { } } -pub mod probe_matches { +pub mod endpoint_reference { + use yaserde_derive::YaDeserialize; + #[derive(Default, Eq, PartialEq, Debug, YaDeserialize)] + #[yaserde( + prefix = "wsa", + namespace = "wsa: http://schemas.xmlsoap.org/ws/2004/08/addressing" + )] + pub struct EndpointReference { + #[yaserde(prefix = "wsa", rename = "Address")] + pub address: String, + } +} + +pub mod probe_matches { + use crate::endpoint_reference::EndpointReference; use percent_encoding::percent_decode_str; use url::Url; use yaserde_derive::YaDeserialize; @@ -65,9 +78,13 @@ pub mod probe_matches { #[derive(Default, Eq, PartialEq, Debug, YaDeserialize)] #[yaserde( prefix = "d", - namespace = "d: http://schemas.xmlsoap.org/ws/2005/04/discovery" + namespace = "d: http://schemas.xmlsoap.org/ws/2005/04/discovery", + namespace = "wsa: http://schemas.xmlsoap.org/ws/2004/08/addressing" )] pub struct ProbeMatch { + #[yaserde(prefix = "wsa", rename = "EndpointReference")] + pub endpoint_reference: EndpointReference, + #[yaserde(prefix = "d", rename = "Types")] pub types: String, @@ -141,6 +158,10 @@ pub mod probe_matches { self.find_in_scopes("onvif://www.onvif.org/hardware/") } + pub fn endpoint_reference_address(&self) -> String { + self.endpoint_reference.address.to_string() + } + pub fn find_in_scopes(&self, prefix: &str) -> Option { self.scopes().iter().find_map(|url| { url.as_str() @@ -197,7 +218,7 @@ pub mod probe_matches { de.x_addrs(), vec![ Url::parse("http://192.168.0.100:80/onvif/device_service").unwrap(), - Url::parse("http://10.0.0.200:80/onvif/device_service").unwrap() + Url::parse("http://10.0.0.200:80/onvif/device_service").unwrap(), ] ); } From c9fba093f9055c8298e6e79be16d86dea1e9c0ab Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Thu, 8 Feb 2024 13:25:37 -0500 Subject: [PATCH 06/10] Allow a pre-initalized HTTP client to be passed in the client builder This PR allows a user to re-use an HTTP client to back a soap client. This can be meaningful when trying limit the amount of outstanding connections taken by each client's connection pool. I also modified the accessor of the auth module: ```diff -pub(crate) mod digest; -pub(crate) mod username_token; +pub mod digest; +pub mod username_token; ``` You cleverly allowed the transport to be a generic, so users are free to re-implement their client as they want, but not providing the auth crates mean they have to re-implement everything. This aims to make it a bit more flexible. --- onvif/src/soap/auth.rs | 4 ++-- onvif/src/soap/client.rs | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/onvif/src/soap/auth.rs b/onvif/src/soap/auth.rs index bde8c74..decd756 100644 --- a/onvif/src/soap/auth.rs +++ b/onvif/src/soap/auth.rs @@ -1,2 +1,2 @@ -pub(crate) mod digest; -pub(crate) mod username_token; +pub mod digest; +pub mod username_token; diff --git a/onvif/src/soap/client.rs b/onvif/src/soap/client.rs index e23b2d7..a87626e 100644 --- a/onvif/src/soap/client.rs +++ b/onvif/src/soap/client.rs @@ -34,22 +34,31 @@ pub struct Client { #[derive(Clone)] pub struct ClientBuilder { + client: Option, config: Config, } impl ClientBuilder { + const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); + pub fn new(uri: &Url) -> Self { Self { + client: None, config: Config { uri: uri.clone(), credentials: None, response_patcher: None, auth_type: AuthType::Any, - timeout: Duration::from_secs(5), + timeout: ClientBuilder::DEFAULT_TIMEOUT, }, } } + pub fn http_client(mut self, client: reqwest::Client) -> Self { + self.client = Some(client); + self + } + pub fn credentials(mut self, credentials: Option) -> Self { self.config.credentials = credentials; self @@ -71,10 +80,25 @@ impl ClientBuilder { } pub fn build(self) -> Client { + let client = if let Some(client) = self.client { + client + } else { + ClientBuilder::default_http_client_builder() + .timeout(self.config.timeout) + .build() + .unwrap() + }; + + Client { + client, + config: self.config, + } + } + + pub fn default_http_client_builder() -> reqwest::ClientBuilder { #[allow(unused_mut)] - let mut client_builder = reqwest::Client::builder() - .redirect(reqwest::redirect::Policy::none()) - .timeout(self.config.timeout); + let mut client_builder = + reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()); #[cfg(feature = "tls")] { @@ -86,10 +110,7 @@ impl ClientBuilder { .danger_accept_invalid_certs(true); } - Client { - client: client_builder.build().unwrap(), - config: self.config, - } + client_builder } } From 7e0eb5a5581e23dd1778dee82b32485e18b918e4 Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Thu, 8 Feb 2024 14:05:49 -0500 Subject: [PATCH 07/10] Let the be pub --- onvif/src/soap/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onvif/src/soap/client.rs b/onvif/src/soap/client.rs index a87626e..cc9b179 100644 --- a/onvif/src/soap/client.rs +++ b/onvif/src/soap/client.rs @@ -39,7 +39,7 @@ pub struct ClientBuilder { } impl ClientBuilder { - const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); + pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); pub fn new(uri: &Url) -> Self { Self { From 9f3508c42d7e2bb856597c6371460efa6a5e8c7e Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Mon, 19 Feb 2024 09:04:58 -0500 Subject: [PATCH 08/10] Just link the existing methods --- onvif/src/discovery/mod.rs | 12 ++++++++++++ wsdl_rs/ws_discovery/src/lib.rs | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/onvif/src/discovery/mod.rs b/onvif/src/discovery/mod.rs index acbc43e..fe871c0 100644 --- a/onvif/src/discovery/mod.rs +++ b/onvif/src/discovery/mod.rs @@ -35,7 +35,9 @@ pub enum Error { pub struct Device { /// The WS-Discovery UUID / address reference pub address: String, + pub hardware: Option, pub name: Option, + pub types: Vec, pub urls: Vec, } @@ -229,12 +231,20 @@ fn device_from_envelope(envelope: probe_matches::Envelope) -> Option { let name = onvif_probe_match.name(); let urls = onvif_probe_match.x_addrs(); + let hardware = onvif_probe_match.hardware(); let address = onvif_probe_match.endpoint_reference_address(); + let types = onvif_probe_match + .types() + .into_iter() + .map(Into::into) + .collect(); Some(Device { name, urls, address, + hardware, + types, }) } @@ -315,7 +325,9 @@ fn test_xaddrs_extraction() { Url::parse("http://addr_22").unwrap(), ], name: Some("MyCamera2000".to_string()), + hardware: None, address: DEVICE_ADDRESS.to_string(), + types: vec![] }] ); } diff --git a/wsdl_rs/ws_discovery/src/lib.rs b/wsdl_rs/ws_discovery/src/lib.rs index 08c700e..4da10a5 100644 --- a/wsdl_rs/ws_discovery/src/lib.rs +++ b/wsdl_rs/ws_discovery/src/lib.rs @@ -139,7 +139,16 @@ pub mod probe_matches { impl ProbeMatch { pub fn types(&self) -> Vec<&str> { - self.types.split_whitespace().collect() + self.types + .split_whitespace() + .map(|t: &str| { + // Remove WSDL prefixes + match t.find(':') { + Some(idx) => t.split_at(idx + 1).1, + None => t, + } + }) + .collect() } pub fn scopes(&self) -> Vec { @@ -221,5 +230,7 @@ pub mod probe_matches { Url::parse("http://10.0.0.200:80/onvif/device_service").unwrap(), ] ); + + assert_eq!(de.types(), vec!["NetworkVideoTransmitter", "Device"]); } } From ddcbed8047e52c181ec3b0053b070ebe25822233 Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Thu, 7 Mar 2024 11:25:32 -0500 Subject: [PATCH 09/10] Add unicast search mode (#120) --- onvif/Cargo.toml | 2 + onvif/src/discovery/mod.rs | 392 +++++++++++++++------ onvif/src/discovery/network_enumeration.rs | 54 +++ 3 files changed, 343 insertions(+), 105 deletions(-) create mode 100644 onvif/src/discovery/network_enumeration.rs diff --git a/onvif/Cargo.toml b/onvif/Cargo.toml index f03f615..a613b13 100644 --- a/onvif/Cargo.toml +++ b/onvif/Cargo.toml @@ -14,7 +14,9 @@ base64 = "0.13.0" bigdecimal = "0.3.0" chrono = "0.4.19" digest_auth = "0.3.0" +futures = "0.3.30" futures-core = "0.3.8" +futures-util = "0.3.30" num-bigint = "0.4.2" reqwest = { version = "0.11.20", default-features = false } schema = { version = "0.1.0", path = "../schema", default-features = false, features = ["analytics", "devicemgmt", "event", "media", "ptz"] } diff --git a/onvif/src/discovery/mod.rs b/onvif/src/discovery/mod.rs index fe871c0..2440edb 100644 --- a/onvif/src/discovery/mod.rs +++ b/onvif/src/discovery/mod.rs @@ -1,5 +1,10 @@ +mod network_enumeration; + +use crate::discovery::network_enumeration::enumerate_network_v4; +use futures::stream::{self, StreamExt}; use futures_core::stream::Stream; use schema::ws_discovery::{probe, probe_matches}; +use std::iter::Iterator; use std::{ collections::HashSet, fmt::{Debug, Formatter}, @@ -31,6 +36,24 @@ pub enum Error { Unsupported(String), } +/// How to discover the devices on the network. Officially, only [DiscoveryMode::Multicast] (the +/// default) is supported by all onvif devices. However, it is said that sending unicast packets +/// can work. +#[derive(Debug, Clone)] +pub enum DiscoveryMode { + /// The normal WS-Discovery Mode + Multicast, + /// The unicast approach + Unicast { + /// The network IP address. Must be a valid network address, otherwise the behavior + /// will be undefined + network: Ipv4Addr, + /// The network mask, written out in "dotted notation". Must be a valid network mask, + /// otherwise the behavior will be undefined. + network_mask: Ipv4Addr, + }, +} + #[derive(Clone, Eq, Hash, PartialEq)] pub struct Device { /// The WS-Discovery UUID / address reference @@ -55,6 +78,7 @@ impl Debug for Device { pub struct DiscoveryBuilder { duration: Duration, listen_address: IpAddr, + discovery_mode: DiscoveryMode, } impl Default for DiscoveryBuilder { @@ -62,11 +86,17 @@ impl Default for DiscoveryBuilder { Self { duration: Duration::from_secs(5), listen_address: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + discovery_mode: DiscoveryMode::Multicast, } } } impl DiscoveryBuilder { + const LOCAL_PORT: u16 = 0; + const MULTI_PORT: u16 = 3702; + const WS_DISCOVERY_BROADCAST_ADDR: Ipv4Addr = Ipv4Addr::new(239, 255, 255, 250); + const MAX_CONCURRENT_SOCK: usize = 32; + /// How long to listen for the responses from the network. pub fn duration(&mut self, duration: Duration) -> &mut Self { self.duration = duration; @@ -83,79 +113,136 @@ impl DiscoveryBuilder { self } - /// Discovers devices on a local network asynchronously using WS-discovery. - /// - /// Internally it sends a multicast probe and waits for responses for a specified amount of time. - /// The result is a stream of discovered devices. - /// The stream is terminated after provided amount of time. - /// - /// There are many different ways to iterate over and process the values in a `Stream` - /// https://rust-lang.github.io/async-book/05_streams/02_iteration_and_concurrency.html - /// - /// # Examples - /// - /// You can access each element on the stream concurrently as soon as devices respond: - /// - /// ``` - /// use onvif::discovery; - /// use futures_util::stream::StreamExt; // to use for_each_concurrent - /// - /// const MAX_CONCURRENT_JUMPERS: usize = 100; - /// - /// async { - /// discovery::DiscoveryBuilder::default().run() - /// .await - /// .unwrap() - /// .for_each_concurrent(MAX_CONCURRENT_JUMPERS, |addr| { - /// async move { - /// println!("Device found: {:?}", addr); - /// } - /// }) - /// .await; - /// }; - /// ``` - /// - /// Or you can await on a collection of unique devices found in one second: - /// - /// ``` - /// use onvif::discovery; - /// use futures_util::stream::StreamExt; // to use collect - /// use std::collections::HashSet; - /// - /// async { - /// let devices = discovery::DiscoveryBuilder::default().run() - /// .await - /// .unwrap() - /// .collect::>() - /// .await; - /// - /// println!("Devices found: {:?}", devices); - /// }; - /// ``` - pub async fn run(&self) -> Result, Error> { - let Self { - duration, - listen_address, - } = self; + /// Set the discovery mode. See [DiscoveryMode] for a description of how this works. + /// By default, the multicast mode is chosen. + pub fn discovery_mode(&mut self, discovery_mode: DiscoveryMode) -> &mut Self { + self.discovery_mode = discovery_mode; + self + } + async fn run_unicast( + &self, + duration: &Duration, + listen_address: &IpAddr, + network: &Ipv4Addr, + network_mask: &Ipv4Addr, + ) -> Result, Error> { let probe = Arc::new(build_probe()); let probe_xml = yaserde::ser::to_string(probe.as_ref()).map_err(Error::Serde)?; - debug!("Probe XML: {}", probe_xml); + debug!("Unicast Probe XML: {}. Since you are using unicast, some devices might not be detected", probe_xml); - let socket = { - const LOCAL_PORT: u16 = 0; + let message_id = Arc::new(probe.header.message_id.clone()); + let payload = Arc::new(probe_xml.as_bytes().to_vec()); + + let (device_sender, device_receiver) = channel(32); + let device_receiver = ReceiverStream::new(device_receiver); + + let mut unicast_requests = vec![]; + + // Prepare the list of UDP queries to execute. + for target_address in enumerate_network_v4(*network, *network_mask) { + let local_sock_addr = SocketAddr::new(*listen_address, Self::LOCAL_PORT); + let target_sock_addr = SocketAddr::new(IpAddr::V4(target_address), Self::MULTI_PORT); + + unicast_requests.push(( + local_sock_addr, + target_sock_addr, + payload.clone(), + message_id.clone(), + )); + } - const MULTI_IPV4_ADDR: Ipv4Addr = Ipv4Addr::new(239, 255, 255, 250); - const MULTI_PORT: u16 = 3702; + let total_socks = unicast_requests.len(); + let batches = total_socks / Self::MAX_CONCURRENT_SOCK; + + let max_time_per_sock = + Duration::from_secs(((duration.as_secs() as usize) / batches) as u64); + + let produce_devices = async move { + let futures = unicast_requests + .iter() + .map( + |(local_sock_addr, target_sock_addr, payload, message_id)| async move { + let socket = UdpSocket::bind(local_sock_addr).await.ok()?; + + socket.send_to(payload, target_sock_addr).await.ok()?; + let (xml, _) = timeout(max_time_per_sock, recv_string(&socket)) + .await + .ok()? + .ok()?; + + debug!("Probe match XML: {}", xml); + + let envelope = match yaserde::de::from_str::(&xml) + { + Ok(envelope) => envelope, + Err(e) => { + debug!("Deserialization failed: {e}"); + return None; + } + }; + + if envelope.header.relates_to != **message_id { + debug!("Unrelated message"); + return None; + } + + if let Some(device) = device_from_envelope(envelope) { + debug!("Found device {device:?}"); + Some(device) + } else { + None + } + }, + ) + .collect::>(); + + let mut stream = stream::iter(futures).buffer_unordered(Self::MAX_CONCURRENT_SOCK); + + // Gets stopped by the timeout below, executing in a background task, but we can + // stop early as well + while let Some(device_or_empty) = stream.next().await { + if let Some(device) = device_or_empty { + // It's ok to ignore the sending error as user can drop the receiver soon + // (for example, after the first device discovered). + if device_sender.send(device).await.is_err() { + debug!("Failure to send to the device sender; Ignoring on purpose.") + } + } + } + }; - let local_socket_addr = SocketAddr::new(*listen_address, LOCAL_PORT); - let multi_socket_addr = SocketAddr::new(IpAddr::V4(MULTI_IPV4_ADDR), MULTI_PORT); + // Give a grace of 100ms since we divided the time equally but some sockets will need a little more. + let global_timeout_duration = *duration + Duration::from_millis(100); + tokio::spawn(timeout(global_timeout_duration, produce_devices)); + + Ok(device_receiver) + } + + async fn run_multicast( + &self, + duration: &Duration, + listen_address: &IpAddr, + ) -> Result, Error> { + let probe = Arc::new(build_probe()); + let probe_xml = yaserde::ser::to_string(probe.as_ref()).map_err(Error::Serde)?; + + debug!("Probe XML: {}", probe_xml); + + let socket = { + let local_socket_addr = SocketAddr::new(*listen_address, Self::LOCAL_PORT); + let multi_socket_addr = SocketAddr::new( + IpAddr::V4(Self::WS_DISCOVERY_BROADCAST_ADDR), + Self::MULTI_PORT, + ); let socket = UdpSocket::bind(local_socket_addr).await?; match listen_address { - IpAddr::V4(addr) => socket.join_multicast_v4(MULTI_IPV4_ADDR, *addr)?, + IpAddr::V4(addr) => { + socket.join_multicast_v4(Self::WS_DISCOVERY_BROADCAST_ADDR, *addr)? + } IpAddr::V6(_) => return Err(Error::Unsupported("Discovery with IPv6".to_owned())), } @@ -208,6 +295,78 @@ impl DiscoveryBuilder { Ok(device_receiver) } + + /// Discovers devices on a local network asynchronously using WS-discovery. + /// + /// Internally it sends a multicast probe and waits for responses for a specified amount of time. + /// You alternatively have the choice to send multiple unicast probes. See [DiscoveryMode]. This + /// is to allow the discovery process to operate within a Docker container or an environment where + /// the hosts network might be different than the target network. + /// + /// The result is a stream of discovered devices. + /// The stream is terminated after provided amount of time. + /// + /// There are many different ways to iterate over and process the values in a `Stream` + /// https://rust-lang.github.io/async-book/05_streams/02_iteration_and_concurrency.html + /// + /// # Examples + /// + /// You can access each element on the stream concurrently as soon as devices respond: + /// + /// ``` + /// use onvif::discovery; + /// use futures_util::stream::StreamExt; // to use for_each_concurrent + /// + /// const MAX_CONCURRENT_JUMPERS: usize = 100; + /// + /// async { + /// discovery::DiscoveryBuilder::default().run() + /// .await + /// .unwrap() + /// .for_each_concurrent(MAX_CONCURRENT_JUMPERS, |addr| { + /// async move { + /// println!("Device found: {:?}", addr); + /// } + /// }) + /// .await; + /// }; + /// ``` + /// + /// Or you can await on a collection of unique devices found in one second: + /// + /// ``` + /// use onvif::discovery; + /// use futures_util::stream::StreamExt; // to use collect + /// use std::collections::HashSet; + /// + /// async { + /// let devices = discovery::DiscoveryBuilder::default().run() + /// .await + /// .unwrap() + /// .collect::>() + /// .await; + /// + /// println!("Devices found: {:?}", devices); + /// }; + /// ``` + pub async fn run(&self) -> Result, Error> { + let Self { + duration, + listen_address, + discovery_mode, + } = self; + + match discovery_mode { + DiscoveryMode::Multicast => self.run_multicast(duration, listen_address).await, + DiscoveryMode::Unicast { + network, + network_mask, + } => { + self.run_unicast(duration, listen_address, network, network_mask) + .await + } + } + } } async fn recv_string(s: &UdpSocket) -> io::Result<(String, SocketAddr)> { @@ -261,13 +420,35 @@ fn build_probe() -> probe::Envelope { } } -#[test] -fn test_xaddrs_extraction() { - const DEVICE_ADDRESS: &str = "an address"; +#[cfg(test)] +mod tests { + use super::*; + use tokio_stream::StreamExt; + + /// This test serves more as an example of how the unicast discovery works. + #[tokio::test] + async fn test_unicast() { + let devices = DiscoveryBuilder::default() + .discovery_mode(DiscoveryMode::Unicast { + network: Ipv4Addr::new(192, 168, 1, 0), + network_mask: Ipv4Addr::new(255, 255, 255, 0), + }) + .run() + .await + .unwrap() + .collect::>() + .await; + + println!("Devices found: {:?}", devices); + } - let make_xml = |relates_to: &str, xaddrs: &str| -> String { - format!( - r#" + #[test] + fn test_xaddrs_extraction() { + const DEVICE_ADDRESS: &str = "an address"; + + let make_xml = |relates_to: &str, xaddrs: &str| -> String { + format!( + r#" "#, - relates_to = relates_to, - xaddrs = xaddrs, - device_address = DEVICE_ADDRESS - ) - }; - - let our_uuid = "uuid:84ede3de-7dec-11d0-c360-F01234567890"; - let bad_uuid = "uuid:84ede3de-7dec-11d0-c360-F00000000000"; - - let input = vec![ - make_xml(our_uuid, "http://addr_20 http://addr_21 http://addr_22"), - make_xml(bad_uuid, "http://addr_30 http://addr_31"), - ]; + relates_to = relates_to, + xaddrs = xaddrs, + device_address = DEVICE_ADDRESS + ) + }; - let actual = input - .iter() - .filter_map(|xml| yaserde::de::from_str::(xml).ok()) - .filter(|envelope| envelope.header.relates_to == our_uuid) - .filter_map(device_from_envelope) - .collect::>(); - - assert_eq!(actual.len(), 1); - - // OK: message UUID matches and addr responds - assert_eq!( - actual, - &[Device { - urls: vec![ - Url::parse("http://addr_20").unwrap(), - Url::parse("http://addr_21").unwrap(), - Url::parse("http://addr_22").unwrap(), - ], - name: Some("MyCamera2000".to_string()), - hardware: None, - address: DEVICE_ADDRESS.to_string(), - types: vec![] - }] - ); + let our_uuid = "uuid:84ede3de-7dec-11d0-c360-F01234567890"; + let bad_uuid = "uuid:84ede3de-7dec-11d0-c360-F00000000000"; + + let input = vec![ + make_xml(our_uuid, "http://addr_20 http://addr_21 http://addr_22"), + make_xml(bad_uuid, "http://addr_30 http://addr_31"), + ]; + + let actual = input + .iter() + .filter_map(|xml| yaserde::de::from_str::(xml).ok()) + .filter(|envelope| envelope.header.relates_to == our_uuid) + .filter_map(device_from_envelope) + .collect::>(); + + assert_eq!(actual.len(), 1); + + // OK: message UUID matches and addr responds + assert_eq!( + actual, + &[Device { + urls: vec![ + Url::parse("http://addr_20").unwrap(), + Url::parse("http://addr_21").unwrap(), + Url::parse("http://addr_22").unwrap(), + ], + name: Some("MyCamera2000".to_string()), + hardware: None, + address: DEVICE_ADDRESS.to_string(), + types: vec![], + }] + ); + } } diff --git a/onvif/src/discovery/network_enumeration.rs b/onvif/src/discovery/network_enumeration.rs new file mode 100644 index 0000000..a97f52e --- /dev/null +++ b/onvif/src/discovery/network_enumeration.rs @@ -0,0 +1,54 @@ +use std::net::Ipv4Addr; + +#[inline] +fn octets_to_u32(octets: [u8; 4]) -> u32 { + (octets[0] as u32) << (3 * 8) + | (octets[1] as u32) << (2 * 8) + | (octets[2] as u32) << 8 + | (octets[3] as u32) +} + +/// Enumerate the list of IPs on the network given the network address and the mask. +pub fn enumerate_network_v4(network: Ipv4Addr, mask: Ipv4Addr) -> Vec { + let network = octets_to_u32(network.octets()); + let mask = octets_to_u32(mask.octets()); + + let mask = !mask; + + let mut ips = Vec::with_capacity(mask as usize); + + for value in 1..mask { + let addr = network | value; + ips.push(Ipv4Addr::from(addr)) + } + + ips +} + +/// Tests the enumeration method. See http://jodies.de/ipcalc for examples. +#[cfg(test)] +mod test_enumerate_v4 { + use super::*; + + #[test] + pub fn test_basic_home_network() { + let home_net = Ipv4Addr::new(192, 168, 0, 0); + let net_mask = Ipv4Addr::new(255, 255, 255, 0); + + let ips = enumerate_network_v4(home_net, net_mask); + + assert_eq!(254, ips.len()) + } + + #[test] + pub fn test_more_complex_net() { + let home_net = Ipv4Addr::new(192, 168, 0, 0); + let net_mask = Ipv4Addr::new(255, 255, 254, 0); + + let ips = enumerate_network_v4(home_net, net_mask); + + dbg!(&ips); + + assert_eq!(510, ips.len()) + } +} From e1b5cca3adbaf98743837be1349d361b010835b9 Mon Sep 17 00:00:00 2001 From: Samuel Yvon Date: Fri, 8 Mar 2024 08:56:09 -0500 Subject: [PATCH 10/10] Fix issue with time slicing --- onvif/src/discovery/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/onvif/src/discovery/mod.rs b/onvif/src/discovery/mod.rs index 2440edb..c299adb 100644 --- a/onvif/src/discovery/mod.rs +++ b/onvif/src/discovery/mod.rs @@ -154,10 +154,9 @@ impl DiscoveryBuilder { } let total_socks = unicast_requests.len(); - let batches = total_socks / Self::MAX_CONCURRENT_SOCK; + let batches = (total_socks / Self::MAX_CONCURRENT_SOCK) as f64; - let max_time_per_sock = - Duration::from_secs(((duration.as_secs() as usize) / batches) as u64); + let max_time_per_sock = Duration::from_secs_f64(duration.as_secs_f64() / batches); let produce_devices = async move { let futures = unicast_requests