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