Skip to main content

onvif_server/
server.rs

1// Server implementation — builder skeleton wired in plan 03; actual server start in phase 2
2use std::collections::HashSet;
3use std::sync::Arc;
4
5use crate::service::device::DeviceServiceHandler;
6use crate::service::events::EventServiceHandler;
7use crate::service::imaging::ImagingServiceHandler;
8use crate::service::media::MediaServiceHandler;
9use crate::service::ptz::PTZServiceHandler;
10use crate::traits::{DeviceService, EventService, ImagingService, MediaService, PTZService};
11use crate::wsdl_loader::EmbeddedWsdlLoader;
12
13/// Error returned by [`OnvifServerBuilder::build`] when required configuration is missing.
14#[derive(Debug, thiserror::Error)]
15pub enum BuildError {
16    #[error("Required service not registered: {0}")]
17    MissingRequiredService(String),
18}
19
20/// Error returned by [`OnvifServer::run`].
21///
22/// Distinguishes between startup/configuration failures and I/O failures at bind time.
23#[derive(Debug, thiserror::Error)]
24pub enum RunError {
25    /// The server failed to bind or serve on the configured port (I/O error).
26    #[error("I/O error: {0}")]
27    Io(#[from] std::io::Error),
28    /// A required service was not registered (validated again at run time for services
29    /// beyond `device_service`, which is checked at build time).
30    #[error("Startup error: {0}")]
31    Startup(String),
32}
33
34/// A built, configured ONVIF server handle.
35///
36/// Phase 1 stores all builder fields for Phase 2 to use when actually binding a port
37/// and starting the soap-server. No network activity occurs in Phase 1.
38///
39/// Fields are intentionally `pub(crate)` to prevent consumers from bypassing the
40/// builder API or accessing credentials directly. Use the provided accessor methods
41/// for fields that consumers legitimately need.
42pub struct OnvifServer {
43    pub(crate) port: u16,
44    pub(crate) username: Option<String>,
45    pub(crate) password: Option<String>,
46    pub(crate) device_service: Option<Arc<dyn DeviceService>>,
47    pub(crate) media_service: Option<Arc<dyn MediaService>>,
48    pub(crate) ptz_service: Option<Arc<dyn PTZService>>,
49    pub(crate) imaging_service: Option<Arc<dyn ImagingService>>,
50    pub(crate) event_service: Option<Arc<dyn EventService>>,
51    pub(crate) auth_bypass: HashSet<String>,
52    pub(crate) advertised_host: String,
53    /// Stable WS-Discovery EndpointReference UUID for this device (F-7).
54    ///
55    /// ONVIF WS-Discovery requires the EndpointReference Address to be a stable
56    /// per-device identity across all discovery cycles.  When explicitly set via
57    /// [`OnvifServerBuilder::discovery_uuid`], that UUID is used verbatim.  When
58    /// unset, the server derives a stable UUID-v5 from the advertised host so the
59    /// same device always produces the same endpoint identity.
60    pub(crate) discovery_uuid: uuid::Uuid,
61}
62
63impl OnvifServer {
64    /// Returns the port this server is configured to listen on.
65    pub fn port(&self) -> u16 {
66        self.port
67    }
68
69    /// Returns the advertised host used in XAddrs for WS-Discovery and capabilities.
70    pub fn advertised_host(&self) -> &str {
71        &self.advertised_host
72    }
73
74    /// Returns the stable WS-Discovery EndpointReference UUID for this device.
75    ///
76    /// ONVIF conformance (F-7) requires this to be identical across all discovery
77    /// cycles.  Set it explicitly via [`OnvifServerBuilder::discovery_uuid`], or let
78    /// the builder derive a stable UUID-v5 from the advertised host.
79    pub fn discovery_uuid(&self) -> uuid::Uuid {
80        self.discovery_uuid
81    }
82
83    /// Returns the configured username, if any.
84    pub fn username(&self) -> Option<&str> {
85        self.username.as_deref()
86    }
87
88    /// Create a new builder with default settings.
89    ///
90    /// Defaults: port 8080, `GetSystemDateAndTime` pre-registered as an auth bypass
91    /// operation (per ONVIF spec — clock sync must work without credentials).
92    pub fn builder() -> OnvifServerBuilder {
93        OnvifServerBuilder::new()
94    }
95
96    /// Build the merged axum `Router` for all registered services (device + any registered
97    /// media/ptz/imaging/events), WITHOUT binding a port or starting the WS-Discovery UDP task.
98    /// Used by `run()` and by in-process harnesses/tests (axum_test).
99    pub fn into_router(self) -> Result<axum::Router, RunError> {
100        let device_svc = self
101            .device_service
102            .ok_or_else(|| RunError::Startup("device_service is required".into()))?;
103
104        let xaddr = format!(
105            "http://{}:{}/onvif/device_service",
106            self.advertised_host, self.port
107        );
108
109        // Build optional XAddrs — only Some when the corresponding service is registered.
110        let media_xaddr = self.media_service.as_ref().map(|_| {
111            format!(
112                "http://{}:{}/onvif/media_service",
113                self.advertised_host, self.port
114            )
115        });
116        let ptz_xaddr = self.ptz_service.as_ref().map(|_| {
117            format!(
118                "http://{}:{}/onvif/ptz_service",
119                self.advertised_host, self.port
120            )
121        });
122        let imaging_xaddr = self.imaging_service.as_ref().map(|_| {
123            format!(
124                "http://{}:{}/onvif/imaging_service",
125                self.advertised_host, self.port
126            )
127        });
128        let events_xaddr = self.event_service.as_ref().map(|_| {
129            format!(
130                "http://{}:{}/onvif/events_service",
131                self.advertised_host, self.port
132            )
133        });
134
135        let handler = DeviceServiceHandler::new(
136            device_svc,
137            xaddr,
138            media_xaddr.clone().unwrap_or_default(),
139            ptz_xaddr.clone().unwrap_or_default(),
140            imaging_xaddr.clone().unwrap_or_default(),
141            events_xaddr.clone().unwrap_or_default(),
142        );
143
144        let auth_bypass = self.auth_bypass;
145        let credentials = self
146            .username
147            .as_ref()
148            .zip(self.password.as_ref())
149            .map(|(u, p)| (u.clone(), p.clone()));
150
151        // Helper macro: builds a soap_server::ServerBuilder for a given WSDL/path,
152        // attaches auth only when credentials are configured, then calls .build().
153        macro_rules! build_service {
154            ($wsdl_bytes:expr, $path:expr, $handler:expr, $bypass_iter:expr) => {{
155                let mut b = soap_server::ServerBuilder::from_wsdl_bytes_with_loader(
156                    $wsdl_bytes.to_vec(),
157                    EmbeddedWsdlLoader,
158                )
159                .path($path)
160                .default_handler($handler)
161                .auth_bypass($bypass_iter);
162
163                if let Some((ref u, ref p)) = credentials {
164                    let u = u.clone();
165                    let p = p.clone();
166                    b = b.auth(move |user: &str| -> Option<String> {
167                        if user == u {
168                            Some(p.clone())
169                        } else {
170                            None
171                        }
172                    });
173                }
174
175                b.build()
176                    .map_err(|e| RunError::Startup(format!("ServerBuilder::build failed: {e}")))?
177            }};
178        }
179
180        let soap_svc = build_service!(
181            include_bytes!("../wsdl/devicemgmt.wsdl"),
182            "/onvif/device_service",
183            handler,
184            auth_bypass.into_iter()
185        );
186
187        let mut router = soap_svc.into_router();
188
189        // Mount optional services — only when registered.
190        if let Some(media_svc) = self.media_service {
191            let media_handler =
192                MediaServiceHandler::new(media_svc, media_xaddr.as_deref().unwrap_or_default());
193            let media_soap_svc = build_service!(
194                include_bytes!("../wsdl/media.wsdl"),
195                "/onvif/media_service",
196                media_handler,
197                std::iter::empty::<String>()
198            );
199            router = router.merge(media_soap_svc.into_router());
200        }
201
202        if let Some(ptz_svc) = self.ptz_service {
203            let ptz_handler = PTZServiceHandler::new(ptz_svc);
204            let ptz_soap_svc = build_service!(
205                include_bytes!("../wsdl/ptz.wsdl"),
206                "/onvif/ptz_service",
207                ptz_handler,
208                std::iter::empty::<String>()
209            );
210            router = router.merge(ptz_soap_svc.into_router());
211        }
212
213        if let Some(imaging_svc) = self.imaging_service {
214            let imaging_handler = ImagingServiceHandler::new(imaging_svc);
215            let imaging_soap_svc = build_service!(
216                include_bytes!("../wsdl/imaging.wsdl"),
217                "/onvif/imaging_service",
218                imaging_handler,
219                std::iter::empty::<String>()
220            );
221            router = router.merge(imaging_soap_svc.into_router());
222        }
223
224        if let Some(event_svc) = self.event_service {
225            let events_xaddr_str = events_xaddr.as_deref().unwrap_or_default().to_string();
226            let events_handler = EventServiceHandler::new(event_svc, events_xaddr_str);
227            let events_soap_svc = build_service!(
228                include_bytes!("../wsdl/events.wsdl"),
229                "/onvif/events_service",
230                events_handler,
231                std::iter::empty::<String>()
232            );
233            router = router.merge(events_soap_svc.into_router());
234        }
235
236        Ok(router)
237    }
238
239    /// Bind the configured port and start serving SOAP requests.
240    ///
241    /// This method does not return until the server is shut down.
242    /// Requires a tokio async runtime (`#[tokio::main]` or `tokio::runtime::Runtime`).
243    ///
244    /// # Auth behaviour
245    ///
246    /// If the builder was configured with `.auth(username, password)`, WS-Security
247    /// UsernameToken authentication is enforced on all non-bypassed operations.
248    /// If `.auth()` was NOT called, the server runs **unauthenticated** — all
249    /// operations are accessible without credentials.
250    ///
251    /// # Service optionality
252    ///
253    /// Only `device_service` is required (checked at [`OnvifServerBuilder::build`] time).
254    /// Media, PTZ, Imaging, and Events services are optional; their routes are only
255    /// mounted and their capabilities only advertised when they are registered.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`RunError::Startup`] if `device_service` is somehow absent at run time.
260    /// Returns [`RunError::Io`] if the TCP listener fails to bind or serve.
261    pub async fn run(self) -> Result<(), RunError> {
262        // Capture port and xaddr BEFORE consuming self via into_router().
263        let port = self.port;
264        let disc_xaddr = format!(
265            "http://{}:{}/onvif/device_service",
266            self.advertised_host, self.port
267        );
268        // The device's WS-Discovery EndpointReference UUID is a STABLE identity for the
269        // device's lifetime (set via the builder, else a per-build default) — never
270        // regenerated per probe.
271        let disc_uuid = self.discovery_uuid;
272
273        #[cfg(feature = "discovery")]
274        {
275            let xaddr_for_disc = disc_xaddr.clone();
276            tokio::spawn(async move {
277                if let Err(e) =
278                    crate::discovery::run_discovery_with_uuid(xaddr_for_disc, disc_uuid).await
279                {
280                    eprintln!("[discovery] task exited: {e}");
281                }
282            });
283        }
284        let _ = disc_uuid;
285        // Suppress unused-variable warning when discovery feature is off.
286        let _ = disc_xaddr;
287
288        let router = self.into_router()?;
289
290        let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;
291        axum::serve(listener, router).await?;
292        Ok(())
293    }
294}
295
296/// Builder for configuring and constructing an [`OnvifServer`].
297///
298/// Service registration, auth credentials, port, and auth bypass operations are
299/// all set here. Fields are `pub(crate)` — use the builder methods to configure
300/// the server.
301pub struct OnvifServerBuilder {
302    pub(crate) port: u16,
303    pub(crate) username: Option<String>,
304    pub(crate) password: Option<String>,
305    pub(crate) device_service: Option<Arc<dyn DeviceService>>,
306    pub(crate) media_service: Option<Arc<dyn MediaService>>,
307    pub(crate) ptz_service: Option<Arc<dyn PTZService>>,
308    pub(crate) imaging_service: Option<Arc<dyn ImagingService>>,
309    pub(crate) event_service: Option<Arc<dyn EventService>>,
310    pub(crate) auth_bypass: HashSet<String>,
311    pub(crate) advertised_host: String,
312    pub(crate) discovery_uuid: uuid::Uuid,
313}
314
315impl OnvifServerBuilder {
316    fn new() -> Self {
317        let mut auth_bypass = HashSet::new();
318        // ONVIF spec requires GetSystemDateAndTime to be accessible without auth
319        // so clients can synchronise their clocks before authenticating.
320        auth_bypass.insert("GetSystemDateAndTime".to_string());
321
322        Self {
323            port: 8080,
324            username: None,
325            password: None,
326            device_service: None,
327            media_service: None,
328            ptz_service: None,
329            imaging_service: None,
330            event_service: None,
331            auth_bypass,
332            advertised_host: "0.0.0.0".to_string(),
333            discovery_uuid: uuid::Uuid::new_v4(),
334        }
335    }
336
337    /// Set the port the server will listen on. Defaults to 8080.
338    pub fn port(mut self, port: u16) -> Self {
339        self.port = port;
340        self
341    }
342
343    /// Set the host advertised in XAddrs for GetCapabilities, GetServices, and WS-Discovery.
344    /// Real ONVIF clients need a routable address (e.g. "192.168.1.10"), not "0.0.0.0".
345    /// Defaults to "0.0.0.0" for backward compatibility.
346    pub fn advertised_host(mut self, host: &str) -> Self {
347        self.advertised_host = host.to_string();
348        self
349    }
350
351    /// Set the credentials used for WS-Security digest auth validation.
352    ///
353    /// When called, WS-Security UsernameToken authentication is enforced on all
354    /// non-bypassed operations during [`OnvifServer::run`]. When NOT called, the
355    /// server runs **unauthenticated** — all operations are accessible without
356    /// credentials.
357    pub fn auth(mut self, username: &str, password: &str) -> Self {
358        self.username = Some(username.to_string());
359        self.password = Some(password.to_string());
360        self
361    }
362
363    /// Register a Device Management Service implementation.
364    pub fn device_service(mut self, svc: impl DeviceService + 'static) -> Self {
365        self.device_service = Some(Arc::new(svc));
366        self
367    }
368
369    /// Register a Media Service implementation.
370    ///
371    /// Optional: if not registered, the media route is not mounted and media
372    /// capabilities are not advertised.
373    pub fn media_service(mut self, svc: impl MediaService + 'static) -> Self {
374        self.media_service = Some(Arc::new(svc));
375        self
376    }
377
378    /// Register a PTZ Service implementation.
379    ///
380    /// Optional: if not registered, the PTZ route is not mounted and PTZ
381    /// capabilities are not advertised.
382    pub fn ptz_service(mut self, svc: impl PTZService + 'static) -> Self {
383        self.ptz_service = Some(Arc::new(svc));
384        self
385    }
386
387    /// Register an Imaging Service implementation.
388    ///
389    /// Optional: if not registered, the imaging route is not mounted and imaging
390    /// capabilities are not advertised.
391    pub fn imaging_service(mut self, svc: impl ImagingService + 'static) -> Self {
392        self.imaging_service = Some(Arc::new(svc));
393        self
394    }
395
396    /// Register an Event Service implementation.
397    ///
398    /// Optional: if not registered, the events route is not mounted and events
399    /// capabilities are not advertised.
400    pub fn event_service(mut self, svc: impl EventService + 'static) -> Self {
401        self.event_service = Some(Arc::new(svc));
402        self
403    }
404
405    /// Override the stable WS-Discovery EndpointReference UUID for this device.
406    ///
407    /// When not called, the builder defaults to a random UUID-v4.  Callers that
408    /// need a deterministic identity across restarts should supply a stable UUID
409    /// here (e.g. derived from hardware ID or stored configuration).
410    pub fn discovery_uuid(mut self, uuid: uuid::Uuid) -> Self {
411        self.discovery_uuid = uuid;
412        self
413    }
414
415    /// Accessor for the auth bypass operation set. Used in tests and Phase 2 wiring.
416    pub fn auth_bypass_set(&self) -> &HashSet<String> {
417        &self.auth_bypass
418    }
419
420    /// Build the configured [`OnvifServer`].
421    ///
422    /// Returns `Err(BuildError::MissingRequiredService("device_service"))` if no
423    /// device service has been registered. `device_service` is required by the ONVIF
424    /// spec — it provides `GetSystemDateAndTime` and core device management operations.
425    ///
426    /// All other services (media, PTZ, imaging, events) are optional. When omitted,
427    /// their routes are not mounted at run time and their capabilities are not
428    /// advertised in `GetCapabilities` / `GetServices`.
429    pub fn build(self) -> Result<OnvifServer, BuildError> {
430        if self.device_service.is_none() {
431            return Err(BuildError::MissingRequiredService(
432                "device_service".to_string(),
433            ));
434        }
435        Ok(OnvifServer {
436            port: self.port,
437            username: self.username,
438            password: self.password,
439            device_service: self.device_service,
440            media_service: self.media_service,
441            ptz_service: self.ptz_service,
442            imaging_service: self.imaging_service,
443            event_service: self.event_service,
444            auth_bypass: self.auth_bypass,
445            advertised_host: self.advertised_host,
446            discovery_uuid: self.discovery_uuid,
447        })
448    }
449}