Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Onvif auth #456

Merged
merged 21 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4f96f0e
src: lib: logger: Omit unused error variable
joaoantoniocardoso Nov 28, 2024
86388f3
src: lib: logger: Filter out unwanted logs from 3rd party crates
joaoantoniocardoso Nov 7, 2024
11d195d
cargo: Enable env clap create feature
joaoantoniocardoso Nov 28, 2024
3903086
src: lib: cli: Add onvif-auth CLI arg, with a fallback to MCM_ONVIF_A…
joaoantoniocardoso Nov 28, 2024
ea60de2
src: lib: controls: onvif: camera: Split OnvifCamera shareable proper…
joaoantoniocardoso Nov 28, 2024
e9f0222
src: lib: controls: onvif: camera: Add device_information to context
joaoantoniocardoso Nov 28, 2024
a4d0663
src: lib: controls: onvif: camera: Implement update_streams_information
joaoantoniocardoso Nov 28, 2024
9f70406
src: lib: controls: onvif: camera: Add OnvifDevice to OnvifCamera
joaoantoniocardoso Nov 28, 2024
fc480c2
src: lib: controls: onvif: camera: Instrument get_streams_information
joaoantoniocardoso Nov 28, 2024
933671d
src: lib: controls: onvif: camera: Make get_streams_information a cla…
joaoantoniocardoso Nov 28, 2024
74eb393
src: lib: controls: onvif: camera: Add separation spaces, prefet impl…
joaoantoniocardoso Nov 28, 2024
05706ce
src: lib: controls: onvif: camera: Add last_update to OnvifCamera
joaoantoniocardoso Nov 28, 2024
476eddb
src: lib: video: Add device_information to VideoSourceOnvif
joaoantoniocardoso Nov 28, 2024
31ccfaf
src: lib: controls: onvif: manager: Introduce OnvifDevice
joaoantoniocardoso Nov 28, 2024
cc5b5f2
src: lib: controls: onvif: manager: change instrument level to debug
joaoantoniocardoso Nov 28, 2024
9854824
src: lib: controls: onvif: manager: Add cdoc for cameras property fro…
joaoantoniocardoso Nov 28, 2024
6186c13
src: lib: controls: onvif: manager: Implement credentials
joaoantoniocardoso Nov 28, 2024
4146932
src: lib: controls: onvif: manager: Reimplement onvif discover
joaoantoniocardoso Nov 28, 2024
3b121a4
src: lib: controls: onvif: manager: Update code related to camera con…
joaoantoniocardoso Nov 28, 2024
c305428
src: lib: server: Add REST endpoints for onvif authentication
joaoantoniocardoso Nov 28, 2024
1139567
src: lib: controls: onvif: camera: Inject credentials into RTSP strea…
joaoantoniocardoso Dec 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ name = "mavlink-camera-manager"
path = "src/main.rs"

[dependencies]
clap = { version = "4.5", features = ["derive"] }
clap = { version = "4.5", features = ["derive", "env"] }
regex = "1.10.4"

#TODO: Investigate rweb to use openapi spec for free
Expand Down
44 changes: 43 additions & 1 deletion src/lib/cli/manager.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::Context;
use clap;
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use tracing::error;

use crate::{custom, stream::gst::utils::PluginRankConfig};
Expand Down Expand Up @@ -101,6 +101,10 @@ struct Args {
/// Sets the MAVLink System ID.
#[arg(long, value_name = "SYSTEM_ID", default_value = "1")]
mavlink_system_id: u8,

/// Sets Onvif authentications. Alternatively, this can be passed as `MCM_ONVIF_AUTH` environment variable.
#[clap(long, value_name = "onvif://<USERNAME>:<PASSWORD>@<HOST>", value_delimiter = ',', value_parser = onvif_auth_validator, env = "MCM_ONVIF_AUTH")]
onvif_auth: Vec<String>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -258,6 +262,34 @@ pub fn gst_feature_rank() -> Vec<PluginRankConfig> {
.collect()
}

pub fn onvif_auth() -> HashMap<std::net::Ipv4Addr, onvif::soap::client::Credentials> {
MANAGER
.clap_matches
.onvif_auth
.iter()
.filter_map(|val| {
let url = match url::Url::parse(val) {
Ok(url) => url,
Err(error) => {
error!("Failed parsing onvif auth url: {error:?}");
return None;
}
};

let (host, credentials) =
match crate::controls::onvif::manager::Manager::credentials_from_url(&url) {
Ok((host, credentials)) => (host, credentials),
Err(error) => {
error!("Failed to get credentials from url {url}: {error:?}");
return None;
}
};

Some((host, credentials))
})
.collect()
}

fn gst_feature_rank_validator(val: &str) -> Result<String, String> {
if let Some((_key, value_str)) = val.split_once('=') {
if value_str.parse::<i32>().is_err() {
Expand All @@ -279,6 +311,16 @@ fn turn_servers_validator(val: &str) -> Result<String, String> {
Ok(val.to_owned())
}

fn onvif_auth_validator(val: &str) -> Result<String, String> {
let url = url::Url::parse(val).map_err(|e| format!("Failed parsing onvif auth url: {e:?}"))?;

if !matches!(url.scheme().to_lowercase().as_str(), "onvif") {
return Err("Onvif authentication scheme should be \"onvif\"".to_owned());
}

Ok(val.to_owned())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
159 changes: 125 additions & 34 deletions src/lib/controls/onvif/camera.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
use std::sync::Arc;

use onvif::soap;
use onvif_schema::{devicemgmt::GetDeviceInformationResponse, transport};
use onvif_schema::transport;

use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::*;

use crate::{stream::gst::utils::get_encode_from_rtspsrc, video::types::Format};

use super::manager::OnvifDevice;

#[derive(Clone)]
pub struct OnvifCamera {
pub context: Arc<RwLock<OnvifCameraContext>>,
pub last_update: std::time::Instant,
}

pub struct OnvifCameraContext {
pub device: OnvifDevice,
pub device_information: OnvifDeviceInformation,
pub streams_information: Option<Vec<OnvifStreamInformation>>,
pub credentials: Option<soap::client::Credentials>,
devicemgmt: soap::client::Client,
event: Option<soap::client::Client>,
deviceio: Option<soap::client::Client>,
Expand All @@ -16,7 +31,6 @@ pub struct OnvifCamera {
imaging: Option<soap::client::Client>,
ptz: Option<soap::client::Client>,
analytics: Option<soap::client::Client>,
pub streams_information: Option<Vec<OnvifStreamInformation>>,
}

pub struct Auth {
Expand All @@ -30,29 +44,56 @@ pub struct OnvifStreamInformation {
pub format: Format,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OnvifDeviceInformation {
pub manufacturer: String,
pub model: String,
pub firmware_version: String,
pub serial_number: String,
pub hardware_id: String,
}

impl OnvifCamera {
#[instrument(level = "trace", skip(auth))]
pub async fn try_new(auth: &Auth) -> Result<Self> {
pub async fn try_new(device: &OnvifDevice, auth: &Auth) -> Result<Self> {
let creds = &auth.credentials;
let devicemgmt_uri = &auth.url;
let base_uri = &devicemgmt_uri.origin().ascii_serialization();

let mut this = Self {
devicemgmt: soap::client::ClientBuilder::new(devicemgmt_uri)
.credentials(creds.clone())
.build(),
let devicemgmt = soap::client::ClientBuilder::new(devicemgmt_uri)
.credentials(creds.clone())
.build();

let device_information =
onvif_schema::devicemgmt::get_device_information(&devicemgmt, &Default::default())
.await
.map(|i| OnvifDeviceInformation {
manufacturer: i.manufacturer,
model: i.model,
firmware_version: i.firmware_version,
serial_number: i.serial_number,
hardware_id: i.hardware_id,
})?;

let mut context = OnvifCameraContext {
device: device.clone(),
device_information,
streams_information: None,
credentials: creds.clone(),
devicemgmt,
imaging: None,
ptz: None,
event: None,
deviceio: None,
media: None,
media2: None,
analytics: None,
streams_information: None,
};

let services =
onvif_schema::devicemgmt::get_services(&this.devicemgmt, &Default::default()).await?;
onvif_schema::devicemgmt::get_services(&context.devicemgmt, &Default::default())
.await
.context("Failed to get services")?;

for service in &services.service {
let service_url = url::Url::parse(&service.x_addr).map_err(anyhow::Error::msg)?;
Expand All @@ -74,44 +115,75 @@ impl OnvifCamera {
);
}
}
"http://www.onvif.org/ver10/events/wsdl" => this.event = svc,
"http://www.onvif.org/ver10/deviceIO/wsdl" => this.deviceio = svc,
"http://www.onvif.org/ver10/media/wsdl" => this.media = svc,
"http://www.onvif.org/ver20/media/wsdl" => this.media2 = svc,
"http://www.onvif.org/ver20/imaging/wsdl" => this.imaging = svc,
"http://www.onvif.org/ver20/ptz/wsdl" => this.ptz = svc,
"http://www.onvif.org/ver20/analytics/wsdl" => this.analytics = svc,
_ => trace!("unknown service: {:?}", service),
"http://www.onvif.org/ver10/events/wsdl" => context.event = svc,
"http://www.onvif.org/ver10/deviceIO/wsdl" => context.deviceio = svc,
"http://www.onvif.org/ver10/media/wsdl" => context.media = svc,
"http://www.onvif.org/ver20/media/wsdl" => context.media2 = svc,
"http://www.onvif.org/ver20/imaging/wsdl" => context.imaging = svc,
"http://www.onvif.org/ver20/ptz/wsdl" => context.ptz = svc,
"http://www.onvif.org/ver20/analytics/wsdl" => context.analytics = svc,
_ => trace!("Unknwon service: {service:?}"),
}
}

this.streams_information
.replace(this.get_streams_information().await?);
let context = Arc::new(RwLock::new(context));

if let Err(error) = Self::update_streams_information(&context).await {
warn!("Failed to update streams information: {error:?}");
}

Ok(this)
Ok(Self {
context,
last_update: std::time::Instant::now(),
})
}

#[instrument(level = "trace", skip(self))]
pub async fn get_device_information(
&self,
) -> Result<GetDeviceInformationResponse, transport::Error> {
onvif_schema::devicemgmt::get_device_information(&self.devicemgmt, &Default::default())
.await
#[instrument(level = "trace", skip_all)]
async fn update_streams_information(context: &Arc<RwLock<OnvifCameraContext>>) -> Result<()> {
let mut context = context.write().await;

let media_client = context
.media
.clone()
.ok_or_else(|| transport::Error::Other("Client media is not available".into()))?;

// Sometimes a camera responds empty, so we try a couple of times to improve our reliability
let mut tries = 10;
let new_streams_information = loop {
let new_streams_information =
OnvifCamera::get_streams_information(&media_client, &context.credentials)
.await
.context("Failed to get streams information")?;

if new_streams_information.is_empty() {
if tries == 0 {
return Err(anyhow!("No streams information found"));
}

tries -= 1;
continue;
}

break new_streams_information;
};

context.streams_information.replace(new_streams_information);

Ok(())
}

#[instrument(level = "trace", skip_all)]
async fn get_streams_information(
&self,
media_client: &soap::client::Client,
credentials: &Option<soap::client::Credentials>,
) -> Result<Vec<OnvifStreamInformation>, transport::Error> {
let mut streams_information = vec![];

let media_client = self
.media
.as_ref()
.ok_or_else(|| transport::Error::Other("Client media is not available".into()))?;
let profiles = onvif_schema::media::get_profiles(media_client, &Default::default())
.await?
.profiles;
trace!("get_profiles response: {:#?}", &profiles);
trace!("get_profiles response: {profiles:#?}");

let requests: Vec<_> = profiles
.iter()
.map(
Expand All @@ -138,7 +210,7 @@ impl OnvifCamera {
trace!("token={} name={}", &profile.token.0, &profile.name.0);
trace!("\t{}", &stream_uri_response.media_uri.uri);

let stream_uri = match url::Url::parse(&stream_uri_response.media_uri.uri) {
let mut stream_uri = match url::Url::parse(&stream_uri_response.media_uri.uri) {
Ok(stream_url) => stream_url,
Err(error) => {
error!(
Expand All @@ -154,7 +226,25 @@ impl OnvifCamera {
continue;
};

if let Some(credentials) = &credentials {
if stream_uri.set_username(&credentials.username).is_err() {
warn!("Failed setting username");
continue;
}

if stream_uri
.set_password(Some(&credentials.password))
.is_err()
{
warn!("Failed setting password");
continue;
}

trace!("Using credentials {credentials:?}");
}

let Some(encode) = get_encode_from_rtspsrc(&stream_uri).await else {
warn!("Failed getting encoding from RTSP stream at {stream_uri}");
continue;
};

Expand Down Expand Up @@ -182,6 +272,7 @@ impl OnvifCamera {

streams_information.push(OnvifStreamInformation { stream_uri, format });
}

Ok(streams_information)
}
}
Loading
Loading