use std::collections::HashSet;
use std::sync::Arc;
use crate::service::device::DeviceServiceHandler;
use crate::service::events::EventServiceHandler;
use crate::service::imaging::ImagingServiceHandler;
use crate::service::media::MediaServiceHandler;
use crate::service::ptz::PTZServiceHandler;
use crate::traits::{DeviceService, EventService, ImagingService, MediaService, PTZService};
use crate::wsdl_loader::EmbeddedWsdlLoader;
#[derive(Debug, thiserror::Error)]
pub enum BuildError {
#[error("Required service not registered: {0}")]
MissingRequiredService(String),
}
#[derive(Debug, thiserror::Error)]
pub enum RunError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Startup error: {0}")]
Startup(String),
}
pub struct OnvifServer {
pub(crate) port: u16,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) device_service: Option<Arc<dyn DeviceService>>,
pub(crate) media_service: Option<Arc<dyn MediaService>>,
pub(crate) ptz_service: Option<Arc<dyn PTZService>>,
pub(crate) imaging_service: Option<Arc<dyn ImagingService>>,
pub(crate) event_service: Option<Arc<dyn EventService>>,
pub(crate) auth_bypass: HashSet<String>,
pub(crate) advertised_host: String,
pub(crate) discovery_uuid: uuid::Uuid,
}
impl OnvifServer {
pub fn port(&self) -> u16 {
self.port
}
pub fn advertised_host(&self) -> &str {
&self.advertised_host
}
pub fn discovery_uuid(&self) -> uuid::Uuid {
self.discovery_uuid
}
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
pub fn builder() -> OnvifServerBuilder {
OnvifServerBuilder::new()
}
pub fn into_router(self) -> Result<axum::Router, RunError> {
let device_svc = self
.device_service
.ok_or_else(|| RunError::Startup("device_service is required".into()))?;
let xaddr = format!(
"http://{}:{}/onvif/device_service",
self.advertised_host, self.port
);
let media_xaddr = self.media_service.as_ref().map(|_| {
format!(
"http://{}:{}/onvif/media_service",
self.advertised_host, self.port
)
});
let ptz_xaddr = self.ptz_service.as_ref().map(|_| {
format!(
"http://{}:{}/onvif/ptz_service",
self.advertised_host, self.port
)
});
let imaging_xaddr = self.imaging_service.as_ref().map(|_| {
format!(
"http://{}:{}/onvif/imaging_service",
self.advertised_host, self.port
)
});
let events_xaddr = self.event_service.as_ref().map(|_| {
format!(
"http://{}:{}/onvif/events_service",
self.advertised_host, self.port
)
});
let handler = DeviceServiceHandler::new(
device_svc,
xaddr,
media_xaddr.clone().unwrap_or_default(),
ptz_xaddr.clone().unwrap_or_default(),
imaging_xaddr.clone().unwrap_or_default(),
events_xaddr.clone().unwrap_or_default(),
);
let auth_bypass = self.auth_bypass;
let credentials = self
.username
.as_ref()
.zip(self.password.as_ref())
.map(|(u, p)| (u.clone(), p.clone()));
macro_rules! build_service {
($wsdl_bytes:expr, $path:expr, $handler:expr, $bypass_iter:expr) => {{
let mut b = soap_server::ServerBuilder::from_wsdl_bytes_with_loader(
$wsdl_bytes.to_vec(),
EmbeddedWsdlLoader,
)
.path($path)
.default_handler($handler)
.auth_bypass($bypass_iter);
if let Some((ref u, ref p)) = credentials {
let u = u.clone();
let p = p.clone();
b = b.auth(move |user: &str| -> Option<String> {
if user == u {
Some(p.clone())
} else {
None
}
});
}
b.build()
.map_err(|e| RunError::Startup(format!("ServerBuilder::build failed: {e}")))?
}};
}
let soap_svc = build_service!(
include_bytes!("../wsdl/devicemgmt.wsdl"),
"/onvif/device_service",
handler,
auth_bypass.into_iter()
);
let mut router = soap_svc.into_router();
if let Some(media_svc) = self.media_service {
let media_handler =
MediaServiceHandler::new(media_svc, media_xaddr.as_deref().unwrap_or_default());
let media_soap_svc = build_service!(
include_bytes!("../wsdl/media.wsdl"),
"/onvif/media_service",
media_handler,
std::iter::empty::<String>()
);
router = router.merge(media_soap_svc.into_router());
}
if let Some(ptz_svc) = self.ptz_service {
let ptz_handler = PTZServiceHandler::new(ptz_svc);
let ptz_soap_svc = build_service!(
include_bytes!("../wsdl/ptz.wsdl"),
"/onvif/ptz_service",
ptz_handler,
std::iter::empty::<String>()
);
router = router.merge(ptz_soap_svc.into_router());
}
if let Some(imaging_svc) = self.imaging_service {
let imaging_handler = ImagingServiceHandler::new(imaging_svc);
let imaging_soap_svc = build_service!(
include_bytes!("../wsdl/imaging.wsdl"),
"/onvif/imaging_service",
imaging_handler,
std::iter::empty::<String>()
);
router = router.merge(imaging_soap_svc.into_router());
}
if let Some(event_svc) = self.event_service {
let events_xaddr_str = events_xaddr.as_deref().unwrap_or_default().to_string();
let events_handler = EventServiceHandler::new(event_svc, events_xaddr_str);
let events_soap_svc = build_service!(
include_bytes!("../wsdl/events.wsdl"),
"/onvif/events_service",
events_handler,
std::iter::empty::<String>()
);
router = router.merge(events_soap_svc.into_router());
}
Ok(router)
}
pub async fn run(self) -> Result<(), RunError> {
let port = self.port;
let disc_xaddr = format!(
"http://{}:{}/onvif/device_service",
self.advertised_host, self.port
);
let disc_uuid = self.discovery_uuid;
#[cfg(feature = "discovery")]
{
let xaddr_for_disc = disc_xaddr.clone();
tokio::spawn(async move {
if let Err(e) =
crate::discovery::run_discovery_with_uuid(xaddr_for_disc, disc_uuid).await
{
eprintln!("[discovery] task exited: {e}");
}
});
}
let _ = disc_uuid;
let _ = disc_xaddr;
let router = self.into_router()?;
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
axum::serve(listener, router).await?;
Ok(())
}
}
pub struct OnvifServerBuilder {
pub(crate) port: u16,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) device_service: Option<Arc<dyn DeviceService>>,
pub(crate) media_service: Option<Arc<dyn MediaService>>,
pub(crate) ptz_service: Option<Arc<dyn PTZService>>,
pub(crate) imaging_service: Option<Arc<dyn ImagingService>>,
pub(crate) event_service: Option<Arc<dyn EventService>>,
pub(crate) auth_bypass: HashSet<String>,
pub(crate) advertised_host: String,
pub(crate) discovery_uuid: uuid::Uuid,
}
impl OnvifServerBuilder {
fn new() -> Self {
let mut auth_bypass = HashSet::new();
auth_bypass.insert("GetSystemDateAndTime".to_string());
Self {
port: 8080,
username: None,
password: None,
device_service: None,
media_service: None,
ptz_service: None,
imaging_service: None,
event_service: None,
auth_bypass,
advertised_host: "0.0.0.0".to_string(),
discovery_uuid: uuid::Uuid::new_v4(),
}
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn advertised_host(mut self, host: &str) -> Self {
self.advertised_host = host.to_string();
self
}
pub fn auth(mut self, username: &str, password: &str) -> Self {
self.username = Some(username.to_string());
self.password = Some(password.to_string());
self
}
pub fn device_service(mut self, svc: impl DeviceService + 'static) -> Self {
self.device_service = Some(Arc::new(svc));
self
}
pub fn media_service(mut self, svc: impl MediaService + 'static) -> Self {
self.media_service = Some(Arc::new(svc));
self
}
pub fn ptz_service(mut self, svc: impl PTZService + 'static) -> Self {
self.ptz_service = Some(Arc::new(svc));
self
}
pub fn imaging_service(mut self, svc: impl ImagingService + 'static) -> Self {
self.imaging_service = Some(Arc::new(svc));
self
}
pub fn event_service(mut self, svc: impl EventService + 'static) -> Self {
self.event_service = Some(Arc::new(svc));
self
}
pub fn discovery_uuid(mut self, uuid: uuid::Uuid) -> Self {
self.discovery_uuid = uuid;
self
}
pub fn auth_bypass_set(&self) -> &HashSet<String> {
&self.auth_bypass
}
pub fn build(self) -> Result<OnvifServer, BuildError> {
if self.device_service.is_none() {
return Err(BuildError::MissingRequiredService(
"device_service".to_string(),
));
}
Ok(OnvifServer {
port: self.port,
username: self.username,
password: self.password,
device_service: self.device_service,
media_service: self.media_service,
ptz_service: self.ptz_service,
imaging_service: self.imaging_service,
event_service: self.event_service,
auth_bypass: self.auth_bypass,
advertised_host: self.advertised_host,
discovery_uuid: self.discovery_uuid,
})
}
}