Skip to main content

onvif_server/service/
device.rs

1use async_trait::async_trait;
2use bytes::Bytes;
3use chrono::{Datelike, Timelike};
4use quick_xml::events::Event;
5use quick_xml::NsReader;
6use soap_server::{escape_attr, escape_text, SoapFault, SoapHandler};
7use std::sync::Arc;
8
9use crate::error::OnvifError;
10use crate::traits::DeviceService;
11
12pub struct DeviceServiceHandler {
13    pub(crate) svc: Arc<dyn DeviceService>,
14    pub(crate) xaddr: String,
15    pub(crate) media_xaddr: String,
16    pub(crate) ptz_xaddr: String,
17    pub(crate) imaging_xaddr: String,
18    pub(crate) events_xaddr: String,
19}
20
21impl DeviceServiceHandler {
22    pub fn new(
23        svc: Arc<dyn DeviceService>,
24        xaddr: impl Into<String>,
25        media_xaddr: impl Into<String>,
26        ptz_xaddr: impl Into<String>,
27        imaging_xaddr: impl Into<String>,
28        events_xaddr: impl Into<String>,
29    ) -> Self {
30        Self {
31            svc,
32            xaddr: xaddr.into(),
33            media_xaddr: media_xaddr.into(),
34            ptz_xaddr: ptz_xaddr.into(),
35            imaging_xaddr: imaging_xaddr.into(),
36            events_xaddr: events_xaddr.into(),
37        }
38    }
39}
40
41#[async_trait]
42impl SoapHandler for DeviceServiceHandler {
43    async fn handle(&self, body: Bytes) -> Result<Bytes, SoapFault> {
44        let op = extract_local_name(&body)?;
45        match op.as_str() {
46            "GetSystemDateAndTime" => self.handle_get_system_date_and_time().await,
47            "GetCapabilities" => self.handle_get_capabilities().await,
48            "GetServices" => self.handle_get_services().await,
49            "GetDeviceInformation" => self.handle_get_device_information().await,
50            "GetScopes" => self.handle_get_scopes().await,
51            "GetHostname" => self.handle_get_hostname().await,
52            "GetNetworkInterfaces" => self.handle_get_network_interfaces().await,
53            _ => Err(OnvifError::ActionNotSupported.into_soap_fault()),
54        }
55    }
56}
57
58fn extract_local_name(body: &Bytes) -> Result<String, SoapFault> {
59    let mut reader = NsReader::from_reader(body.as_ref());
60    reader.config_mut().trim_text(true);
61    loop {
62        match reader
63            .read_resolved_event()
64            .map_err(|e| SoapFault::sender(format!("{e}")))?
65        {
66            (_, Event::Start(e)) | (_, Event::Empty(e)) => {
67                let local = std::str::from_utf8(e.local_name().as_ref())
68                    .map_err(|e| SoapFault::sender(format!("{e}")))?
69                    .to_string();
70                return Ok(local);
71            }
72            (_, Event::Eof) => return Err(SoapFault::sender("Empty body".to_string())),
73            _ => {}
74        }
75    }
76}
77
78impl DeviceServiceHandler {
79    async fn handle_get_system_date_and_time(&self) -> Result<Bytes, SoapFault> {
80        let dt = self
81            .svc
82            .get_system_date_and_time()
83            .await
84            .map_err(|e| e.into_soap_fault())?;
85        let xml = format!(
86            r#"<tds:GetSystemDateAndTimeResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
87  <tds:SystemDateAndTime>
88    <tt:DateTimeType>Manual</tt:DateTimeType>
89    <tt:DaylightSavings>false</tt:DaylightSavings>
90    <tt:TimeZone><tt:TZ>UTC</tt:TZ></tt:TimeZone>
91    <tt:UTCDateTime>
92      <tt:Time><tt:Hour>{}</tt:Hour><tt:Minute>{}</tt:Minute><tt:Second>{}</tt:Second></tt:Time>
93      <tt:Date><tt:Year>{}</tt:Year><tt:Month>{}</tt:Month><tt:Day>{}</tt:Day></tt:Date>
94    </tt:UTCDateTime>
95  </tds:SystemDateAndTime>
96</tds:GetSystemDateAndTimeResponse>"#,
97            dt.hour(),
98            dt.minute(),
99            dt.second(),
100            dt.year(),
101            dt.month(),
102            dt.day()
103        );
104        Ok(Bytes::from(xml))
105    }
106
107    async fn handle_get_capabilities(&self) -> Result<Bytes, SoapFault> {
108        // tt:Capabilities is an ordered xs:sequence: Analytics?, Device?, Events?, Imaging?, Media?, PTZ?, Extension?
109        let mut caps = String::new();
110        caps.push_str(&format!(
111            "    <tt:Device><tt:XAddr>{}</tt:XAddr></tt:Device>\n",
112            escape_text(&self.xaddr)
113        ));
114        if !self.events_xaddr.is_empty() {
115            caps.push_str(&format!(
116                "    <tt:Events>\n      <tt:XAddr>{}</tt:XAddr>\n      <tt:WSSubscriptionPolicySupport>false</tt:WSSubscriptionPolicySupport>\n      <tt:WSPullPointSupport>true</tt:WSPullPointSupport>\n      <tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>\n    </tt:Events>\n",
117                escape_text(&self.events_xaddr)
118            ));
119        }
120        if !self.imaging_xaddr.is_empty() {
121            caps.push_str(&format!(
122                "    <tt:Imaging><tt:XAddr>{}</tt:XAddr></tt:Imaging>\n",
123                escape_text(&self.imaging_xaddr)
124            ));
125        }
126        if !self.media_xaddr.is_empty() {
127            // tt:MediaCapabilities sequence: XAddr, StreamingCapabilities (required), Extension?
128            caps.push_str(&format!(
129                "    <tt:Media><tt:XAddr>{}</tt:XAddr><tt:StreamingCapabilities/></tt:Media>\n",
130                escape_text(&self.media_xaddr)
131            ));
132        }
133        if !self.ptz_xaddr.is_empty() {
134            caps.push_str(&format!(
135                "    <tt:PTZ><tt:XAddr>{}</tt:XAddr></tt:PTZ>\n",
136                escape_text(&self.ptz_xaddr)
137            ));
138        }
139        let xml = format!(
140            "<tds:GetCapabilitiesResponse xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:tt=\"http://www.onvif.org/ver10/schema\">\n  <tds:Capabilities>\n{caps}  </tds:Capabilities>\n</tds:GetCapabilitiesResponse>",
141        );
142        Ok(Bytes::from(xml))
143    }
144
145    async fn handle_get_services(&self) -> Result<Bytes, SoapFault> {
146        let mut services = String::new();
147        // Device service is always present.
148        services.push_str(&format!(
149            "  <tds:Service>\n    <tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>\n    <tds:XAddr>{}</tds:XAddr>\n    <tds:Version><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tds:Version>\n  </tds:Service>\n",
150            escape_text(&self.xaddr)
151        ));
152        if !self.media_xaddr.is_empty() {
153            services.push_str(&format!(
154                "  <tds:Service>\n    <tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>\n    <tds:XAddr>{}</tds:XAddr>\n    <tds:Version><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tds:Version>\n  </tds:Service>\n",
155                escape_text(&self.media_xaddr)
156            ));
157        }
158        if !self.ptz_xaddr.is_empty() {
159            services.push_str(&format!(
160                "  <tds:Service>\n    <tds:Namespace>http://www.onvif.org/ver10/ptz/wsdl</tds:Namespace>\n    <tds:XAddr>{}</tds:XAddr>\n    <tds:Version><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tds:Version>\n  </tds:Service>\n",
161                escape_text(&self.ptz_xaddr)
162            ));
163        }
164        if !self.imaging_xaddr.is_empty() {
165            services.push_str(&format!(
166                "  <tds:Service>\n    <tds:Namespace>http://www.onvif.org/ver20/imaging/wsdl</tds:Namespace>\n    <tds:XAddr>{}</tds:XAddr>\n    <tds:Version><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tds:Version>\n  </tds:Service>\n",
167                escape_text(&self.imaging_xaddr)
168            ));
169        }
170        if !self.events_xaddr.is_empty() {
171            services.push_str(&format!(
172                "  <tds:Service>\n    <tds:Namespace>http://www.onvif.org/ver10/events/wsdl</tds:Namespace>\n    <tds:XAddr>{}</tds:XAddr>\n    <tds:Version><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tds:Version>\n  </tds:Service>\n",
173                escape_text(&self.events_xaddr)
174            ));
175        }
176        let xml = format!(
177            "<tds:GetServicesResponse xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:tt=\"http://www.onvif.org/ver10/schema\">\n{services}</tds:GetServicesResponse>",
178        );
179        Ok(Bytes::from(xml))
180    }
181
182    async fn handle_get_device_information(&self) -> Result<Bytes, SoapFault> {
183        let info = self
184            .svc
185            .get_device_information()
186            .await
187            .map_err(|e| e.into_soap_fault())?;
188        let xml = format!(
189            r#"<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
190  <tds:Manufacturer>{}</tds:Manufacturer>
191  <tds:Model>{}</tds:Model>
192  <tds:FirmwareVersion>{}</tds:FirmwareVersion>
193  <tds:SerialNumber>{}</tds:SerialNumber>
194  <tds:HardwareId>{}</tds:HardwareId>
195</tds:GetDeviceInformationResponse>"#,
196            escape_text(&info.manufacturer),
197            escape_text(&info.model),
198            escape_text(&info.firmware_version),
199            escape_text(&info.serial_number),
200            escape_text(&info.hardware_id)
201        );
202        Ok(Bytes::from(xml))
203    }
204
205    async fn handle_get_scopes(&self) -> Result<Bytes, SoapFault> {
206        let scopes = self
207            .svc
208            .get_scopes()
209            .await
210            .map_err(|e| e.into_soap_fault())?;
211        let mut items = String::new();
212        for s in &scopes {
213            let def = match s.scope_def {
214                crate::generated::types::ScopeDefinition::Fixed => "Fixed",
215                crate::generated::types::ScopeDefinition::Configurable => "Configurable",
216            };
217            items.push_str(&format!(
218                "  <tds:Scopes>\n    <tt:ScopeDef>{}</tt:ScopeDef>\n    <tt:ScopeItem>{}</tt:ScopeItem>\n  </tds:Scopes>\n",
219                def, escape_text(&s.scope_item)
220            ));
221        }
222        let xml = format!(
223            "<tds:GetScopesResponse xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:tt=\"http://www.onvif.org/ver10/schema\">\n{}</tds:GetScopesResponse>",
224            items
225        );
226        Ok(Bytes::from(xml))
227    }
228
229    async fn handle_get_hostname(&self) -> Result<Bytes, SoapFault> {
230        let info = self
231            .svc
232            .get_hostname()
233            .await
234            .map_err(|e| e.into_soap_fault())?;
235        let name_el = match &info.name {
236            Some(n) => format!("    <tt:Name>{}</tt:Name>\n", escape_text(n)),
237            None => String::new(),
238        };
239        let xml = format!(
240            r#"<tds:GetHostnameResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
241  <tds:HostnameInformation>
242    <tt:FromDHCP>{}</tt:FromDHCP>
243{}  </tds:HostnameInformation>
244</tds:GetHostnameResponse>"#,
245            info.from_dhcp, name_el
246        );
247        Ok(Bytes::from(xml))
248    }
249
250    async fn handle_get_network_interfaces(&self) -> Result<Bytes, SoapFault> {
251        let ifaces = self
252            .svc
253            .get_network_interfaces()
254            .await
255            .map_err(|e| e.into_soap_fault())?;
256        let mut iface_els = String::new();
257        for iface in &ifaces {
258            iface_els.push_str(&format!(
259                r#"  <tds:NetworkInterfaces token="{token}">
260    <tt:Enabled>{enabled}</tt:Enabled>
261    <tt:Info>
262      <tt:Name>{name}</tt:Name>
263      <tt:HwAddress>{hw}</tt:HwAddress>
264      <tt:MTU>{mtu}</tt:MTU>
265    </tt:Info>
266  </tds:NetworkInterfaces>
267"#,
268                token = escape_attr(&iface.token),
269                enabled = iface.enabled,
270                name = escape_text(&iface.name),
271                hw = escape_text(&iface.hw_address),
272                mtu = iface.mtu,
273            ));
274        }
275        let xml = format!(
276            "<tds:GetNetworkInterfacesResponse xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:tt=\"http://www.onvif.org/ver10/schema\">\n{}</tds:GetNetworkInterfacesResponse>",
277            iface_els
278        );
279        Ok(Bytes::from(xml))
280    }
281}