1use async_trait::async_trait;
2use bytes::Bytes;
3use chrono::{DateTime, Duration, Utc};
4use quick_xml::events::Event;
5use quick_xml::NsReader;
6use soap_server::{escape_text, SoapFault, SoapHandler};
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use uuid::Uuid;
10
11use crate::error::OnvifError;
12use crate::service::xml_util::extract_text_ns;
13use crate::traits::EventService;
14
15const ONVIF_EVENTS_NS: &[u8] = b"http://www.onvif.org/ver10/events/wsdl";
17const ONVIF_SCHEMA_NS: &[u8] = b"http://www.onvif.org/ver10/schema";
18const WSNT_NS: &[u8] = b"http://docs.oasis-open.org/wsn/b-2";
20
21const DEFAULT_SUBSCRIPTION_SECS: i64 = 60;
24
25struct SubscriptionInfo {
26 termination_time: DateTime<Utc>,
27}
28
29#[allow(dead_code)]
30pub struct EventServiceHandler {
31 pub(crate) svc: Arc<dyn EventService>,
32 pub(crate) xaddr: String,
33 subscriptions: Arc<Mutex<HashMap<String, SubscriptionInfo>>>,
34}
35
36impl EventServiceHandler {
37 pub fn new(svc: Arc<dyn EventService>, xaddr: impl Into<String>) -> Self {
38 Self {
39 svc,
40 xaddr: xaddr.into(),
41 subscriptions: Arc::new(Mutex::new(HashMap::new())),
42 }
43 }
44}
45
46#[async_trait]
47impl SoapHandler for EventServiceHandler {
48 async fn handle(&self, body: Bytes) -> Result<Bytes, SoapFault> {
49 self.handle_with_headers(body, &[]).await
51 }
52
53 async fn handle_with_headers(
56 &self,
57 body: Bytes,
58 headers: &[Bytes],
59 ) -> Result<Bytes, SoapFault> {
60 let op = extract_local_name(&body)?;
61 match op.as_str() {
62 "GetEventProperties" => self.handle_get_event_properties().await,
63 "CreatePullPointSubscription" => {
64 self.handle_create_pull_point_subscription(&body).await
65 }
66 "PullMessages" => self.handle_pull_messages(&body, headers).await,
67 "Unsubscribe" => self.handle_unsubscribe(&body, headers).await,
68 _ => Err(OnvifError::ActionNotSupported.into_soap_fault()),
69 }
70 }
71}
72
73fn extract_local_name(body: &Bytes) -> Result<String, SoapFault> {
74 let mut reader = NsReader::from_reader(body.as_ref());
75 reader.config_mut().trim_text(true);
76 loop {
77 match reader
78 .read_resolved_event()
79 .map_err(|e| SoapFault::sender(format!("{e}")))?
80 {
81 (_, Event::Start(e)) | (_, Event::Empty(e)) => {
82 let local = std::str::from_utf8(e.local_name().as_ref())
83 .map_err(|e| SoapFault::sender(format!("{e}")))?
84 .to_string();
85 return Ok(local);
86 }
87 (_, Event::Eof) => return Err(SoapFault::sender("Empty body".to_string())),
88 _ => {}
89 }
90 }
91}
92
93fn extract_text_element(body: &Bytes, element_name: &str) -> Result<String, SoapFault> {
94 extract_text_ns(body, element_name, &[ONVIF_EVENTS_NS, ONVIF_SCHEMA_NS])
95}
96
97fn resolve_subscription_id(body: &Bytes, headers: &[Bytes]) -> Result<String, SoapFault> {
106 for header in headers {
108 if let Ok(id) = extract_text_ns(
109 header,
110 "SubscriptionId",
111 &[ONVIF_EVENTS_NS, ONVIF_SCHEMA_NS],
112 ) {
113 if !id.is_empty() {
114 return Ok(id);
115 }
116 }
117 }
118
119 extract_text_element(body, "SubscriptionId")
121}
122
123fn parse_termination_time(value: &str, now: DateTime<Utc>) -> Option<Duration> {
131 let trimmed = value.trim();
132
133 if trimmed.starts_with('P') {
135 return parse_iso8601_duration(trimmed);
136 }
137
138 if let Ok(absolute) = trimmed.parse::<DateTime<Utc>>() {
140 let delta = absolute.signed_duration_since(now);
141 if delta > Duration::zero() {
142 return Some(delta);
143 }
144 return None;
146 }
147
148 None
149}
150
151fn parse_iso8601_duration(s: &str) -> Option<Duration> {
155 let s = s.strip_prefix('P')?;
157
158 let mut total_secs: i64 = 0;
159 let (date_part, time_part) = if let Some(t) = s.split_once('T') {
160 (t.0, Some(t.1))
161 } else {
162 (s, None)
163 };
164
165 let mut remaining = date_part;
167 total_secs += consume_duration_field(&mut remaining, 'Y')? * 365 * 86400;
168 total_secs += consume_duration_field(&mut remaining, 'M')? * 30 * 86400;
169 total_secs += consume_duration_field(&mut remaining, 'D')? * 86400;
170
171 if let Some(time) = time_part {
173 let mut rem = time;
174 total_secs += consume_duration_field(&mut rem, 'H')? * 3600;
175 total_secs += consume_duration_field(&mut rem, 'M')? * 60;
176 total_secs += consume_duration_seconds(&mut rem)?;
178 if !rem.is_empty() {
179 return None;
180 }
181 } else if !remaining.is_empty() {
182 return None;
183 }
184
185 Some(Duration::seconds(total_secs))
186}
187
188fn consume_duration_field(s: &mut &str, designator: char) -> Option<i64> {
191 if let Some(pos) = s.find(designator) {
192 let num_str = &s[..pos];
194 if num_str.is_empty() {
195 return None;
196 }
197 let n: i64 = num_str.parse().ok()?;
198 *s = &s[pos + designator.len_utf8()..];
199 Some(n)
200 } else {
201 Some(0)
202 }
203}
204
205fn consume_duration_seconds(s: &mut &str) -> Option<i64> {
208 if let Some(pos) = s.find('S') {
209 let num_str = &s[..pos];
210 if num_str.is_empty() {
211 return None;
212 }
213 let n: i64 = if let Some(dot) = num_str.find('.') {
215 num_str[..dot].parse().ok()?
216 } else {
217 num_str.parse().ok()?
218 };
219 *s = &s[pos + 1..];
220 Some(n)
221 } else {
222 Some(0)
223 }
224}
225
226impl EventServiceHandler {
227 async fn handle_get_event_properties(&self) -> Result<Bytes, SoapFault> {
228 let xml = r#"<tev:GetEventPropertiesResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1"><tev:TopicNamespaceLocation>http://www.onvif.org/onvif/ver10/topics/topicns.xml</tev:TopicNamespaceLocation><wsnt:FixedTopicSet>true</wsnt:FixedTopicSet><wstop:TopicSet/><wsnt:TopicExpressionDialect>http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete</wsnt:TopicExpressionDialect><wsnt:TopicExpressionDialect>http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet</wsnt:TopicExpressionDialect><tev:MessageContentFilterDialect>http://www.onvif.org/ver10/tev/messageContentFilter/ItemFilter</tev:MessageContentFilterDialect><tev:MessageContentSchemaLocation>http://www.onvif.org/ver10/schema/onvif.xsd</tev:MessageContentSchemaLocation></tev:GetEventPropertiesResponse>"#;
229 Ok(Bytes::from(xml))
230 }
231
232 async fn handle_create_pull_point_subscription(
233 &self,
234 body: &Bytes,
235 ) -> Result<Bytes, SoapFault> {
236 let sub_id = Uuid::new_v4().to_string();
237 let now = Utc::now();
238
239 let lifetime = extract_text_ns(
245 body,
246 "InitialTerminationTime",
247 &[ONVIF_EVENTS_NS, ONVIF_SCHEMA_NS, WSNT_NS],
248 )
249 .ok()
250 .and_then(|v| parse_termination_time(&v, now))
251 .unwrap_or_else(|| Duration::seconds(DEFAULT_SUBSCRIPTION_SECS));
252
253 let termination = now + lifetime;
254
255 {
256 let mut subs = self
257 .subscriptions
258 .lock()
259 .map_err(|e| SoapFault::sender(format!("lock poisoned: {e}")))?;
260 subs.insert(
261 sub_id.clone(),
262 SubscriptionInfo {
263 termination_time: termination,
264 },
265 );
266 }
267
268 let xml = format!(
269 r#"<tev:CreatePullPointSubscriptionResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2"><tev:SubscriptionReference><wsa5:Address>{xaddr}</wsa5:Address><wsa5:ReferenceParameters><tev:SubscriptionId>{sub_id}</tev:SubscriptionId></wsa5:ReferenceParameters></tev:SubscriptionReference><wsnt:CurrentTime>{current}</wsnt:CurrentTime><wsnt:TerminationTime>{termination}</wsnt:TerminationTime></tev:CreatePullPointSubscriptionResponse>"#,
270 xaddr = escape_text(&self.xaddr),
271 sub_id = escape_text(&sub_id),
272 current = now.to_rfc3339(),
273 termination = termination.to_rfc3339(),
274 );
275 Ok(Bytes::from(xml))
276 }
277
278 async fn handle_pull_messages(
279 &self,
280 body: &Bytes,
281 headers: &[Bytes],
282 ) -> Result<Bytes, SoapFault> {
283 let sub_id = resolve_subscription_id(body, headers)?;
285
286 let now = Utc::now();
287 let termination_time = {
288 let subs = self
289 .subscriptions
290 .lock()
291 .map_err(|e| SoapFault::sender(format!("lock poisoned: {e}")))?;
292 match subs.get(&sub_id) {
293 Some(info) if info.termination_time > now => info.termination_time,
294 Some(_) => {
295 eprintln!("[onvif-events] PullMessages: subscription expired (id not echoed to client)");
298 return Err(SoapFault::sender("Subscription not found".to_string()));
299 }
300 None => {
301 eprintln!("[onvif-events] PullMessages: unknown subscription (id not echoed to client)");
303 return Err(SoapFault::sender("Subscription not found".to_string()));
304 }
305 }
306 };
307
308 let xml = format!(
309 r#"<tev:PullMessagesResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2"><tev:CurrentTime>{current}</tev:CurrentTime><tev:TerminationTime>{termination}</tev:TerminationTime></tev:PullMessagesResponse>"#,
310 current = now.to_rfc3339(),
311 termination = termination_time.to_rfc3339(),
312 );
313 Ok(Bytes::from(xml))
314 }
315
316 async fn handle_unsubscribe(
317 &self,
318 body: &Bytes,
319 headers: &[Bytes],
320 ) -> Result<Bytes, SoapFault> {
321 let sub_id = resolve_subscription_id(body, headers)?;
323
324 {
325 let mut subs = self
326 .subscriptions
327 .lock()
328 .map_err(|e| SoapFault::sender(format!("lock poisoned: {e}")))?;
329 if subs.remove(&sub_id).is_none() {
330 eprintln!("[onvif-events] Unsubscribe: unknown or expired subscription (id not echoed to client)");
332 return Err(SoapFault::sender("Subscription not found".to_string()));
333 }
334 }
335
336 let xml =
337 r#"<tev:UnsubscribeResponse xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>"#;
338 Ok(Bytes::from(xml))
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
349 fn parse_pt60s_returns_60_seconds() {
350 let d = parse_iso8601_duration("PT60S").unwrap();
351 assert_eq!(d.num_seconds(), 60);
352 }
353
354 #[test]
355 fn parse_pt300s_returns_300_seconds() {
356 let d = parse_iso8601_duration("PT300S").unwrap();
357 assert_eq!(d.num_seconds(), 300);
358 }
359
360 #[test]
361 fn parse_pt1h_returns_3600_seconds() {
362 let d = parse_iso8601_duration("PT1H").unwrap();
363 assert_eq!(d.num_seconds(), 3600);
364 }
365
366 #[test]
367 fn parse_pt1h30m_returns_5400_seconds() {
368 let d = parse_iso8601_duration("PT1H30M").unwrap();
369 assert_eq!(d.num_seconds(), 5400);
370 }
371
372 #[test]
373 fn parse_p1d_returns_86400_seconds() {
374 let d = parse_iso8601_duration("P1D").unwrap();
375 assert_eq!(d.num_seconds(), 86400);
376 }
377
378 #[test]
379 fn parse_empty_string_returns_none() {
380 assert!(parse_iso8601_duration("").is_none());
381 }
382
383 #[test]
384 fn parse_garbage_returns_none() {
385 assert!(parse_iso8601_duration("garbage").is_none());
386 assert!(parse_iso8601_duration("60S").is_none()); }
388
389 #[test]
392 fn parse_future_absolute_datetime_returns_positive_duration() {
393 let now = Utc::now();
394 let future = now + Duration::seconds(120);
395 let s = future.to_rfc3339();
396 let d = parse_termination_time(&s, now).unwrap();
397 assert!(d.num_seconds() >= 118 && d.num_seconds() <= 122);
399 }
400
401 #[test]
402 fn parse_past_absolute_datetime_returns_none() {
403 let now = Utc::now();
404 let past = now - Duration::seconds(10);
405 let s = past.to_rfc3339();
406 assert!(parse_termination_time(&s, now).is_none());
407 }
408
409 #[test]
412 fn resolve_sub_id_from_header_takes_priority_over_body() {
413 let body = Bytes::from(
414 r#"<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"><tev:SubscriptionId>body-id</tev:SubscriptionId></tev:PullMessages>"#,
415 );
416 let header = Bytes::from(
417 r#"<tev:SubscriptionId xmlns:tev="http://www.onvif.org/ver10/events/wsdl">header-id</tev:SubscriptionId>"#,
418 );
419 let id = resolve_subscription_id(&body, &[header]).unwrap();
420 assert_eq!(id, "header-id");
421 }
422
423 #[test]
424 fn resolve_sub_id_falls_back_to_body_when_no_header() {
425 let body = Bytes::from(
426 r#"<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"><tev:SubscriptionId>body-id</tev:SubscriptionId></tev:PullMessages>"#,
427 );
428 let id = resolve_subscription_id(&body, &[]).unwrap();
429 assert_eq!(id, "body-id");
430 }
431
432 #[test]
433 fn resolve_sub_id_unbound_namespace_in_header_accepted() {
434 let body = Bytes::from(
436 r#"<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"><tev:SubscriptionId>body-id</tev:SubscriptionId></tev:PullMessages>"#,
437 );
438 let header = Bytes::from(r#"<SubscriptionId>bare-id</SubscriptionId>"#);
439 let id = resolve_subscription_id(&body, &[header]).unwrap();
440 assert_eq!(id, "bare-id");
441 }
442}