1use std::time::Duration;
15
16use sipx_sip::event::{
17 BAD_EVENT, Packages, Reason, State, Subscription, granted_expiry, is_unsubscribe,
18};
19use sipx_sip::headers::{CSeq, Expires, From as FromHeader};
20use sipx_sip::{HeaderName, Method, Request};
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct Id {
29 pub call_id: String,
31 pub from_tag: String,
33 pub event: String,
35}
36
37impl Id {
38 #[must_use]
40 pub fn from_request(request: &Request) -> Option<Self> {
41 if request.headers.count(&HeaderName::CallId) != 1
42 || request.headers.count(&HeaderName::From) != 1
43 || request.headers.count(&HeaderName::Event) != 1
44 {
45 return None;
46 }
47 let call_id = request.headers.value(&HeaderName::CallId)?;
48 let call_id = std::str::from_utf8(&call_id).ok()?;
49 if call_id.is_empty() {
50 return None;
51 }
52 let from = request.headers.typed::<FromHeader>()?.ok()?;
53 let mut tags = from.params.iter().filter(|parameter| parameter.is("tag"));
54 let tag = tags.next()?.value.as_deref()?;
55 if tags.next().is_some() || tag.is_empty() || !tag.iter().copied().all(is_token_char) {
56 return None;
57 }
58 Some(Self {
59 call_id: call_id.to_owned(),
60 from_tag: std::str::from_utf8(tag).ok()?.to_owned(),
61 event: event_identity(&request.headers.value(&HeaderName::Event)?)?,
62 })
63 }
64}
65
66fn event_identity(value: &[u8]) -> Option<String> {
67 let segments = event_segments(value)?;
68 let event = trim_ows(segments.first()?);
69 if !valid_event_type(event) {
70 return None;
71 }
72
73 let mut id = None;
74 for segment in segments.iter().skip(1) {
75 let parameter = trim_ows(segment);
76 if parameter.is_empty() {
77 return None;
78 }
79 let (name, value) =
80 parameter
81 .iter()
82 .position(|byte| *byte == b'=')
83 .map_or((parameter, None), |equals| {
84 (
85 trim_ows(parameter.get(..equals).unwrap_or_default()),
86 Some(trim_ows(
87 parameter
88 .get(equals.saturating_add(1)..)
89 .unwrap_or_default(),
90 )),
91 )
92 });
93 if name.is_empty() || !name.iter().copied().all(is_token_char) {
94 return None;
95 }
96 if name.eq_ignore_ascii_case(b"id") {
97 let value = value?;
98 if id.is_some() || value.is_empty() || !value.iter().copied().all(is_token_char) {
99 return None;
100 }
101 id = Some(value);
102 } else if !value.is_none_or(valid_generic_value) {
103 return None;
104 }
105 }
106
107 let event = std::str::from_utf8(event).ok()?;
108 match id {
109 Some(id) => Some(format!("{event};id={}", std::str::from_utf8(id).ok()?)),
110 None => Some(event.to_owned()),
111 }
112}
113
114fn event_segments(value: &[u8]) -> Option<Vec<&[u8]>> {
115 let mut segments = Vec::new();
116 let mut start = 0;
117 let mut quoted = false;
118 let mut escaped = false;
119 for (index, byte) in value.iter().copied().enumerate() {
120 if quoted && escaped {
121 escaped = false;
122 continue;
123 }
124 match byte {
125 b'\\' if quoted => escaped = true,
126 b'"' => quoted = !quoted,
127 b';' if !quoted => {
128 segments.push(value.get(start..index)?);
129 start = index.saturating_add(1);
130 }
131 _ => {}
132 }
133 }
134 if quoted || escaped {
135 return None;
136 }
137 segments.push(value.get(start..)?);
138 Some(segments)
139}
140
141fn valid_event_type(value: &[u8]) -> bool {
142 !value.is_empty()
143 && value
144 .split(|byte| *byte == b'.')
145 .all(|token| !token.is_empty() && token.iter().copied().all(is_token_nodot_char))
146}
147
148fn valid_generic_value(value: &[u8]) -> bool {
149 if value.len() >= 2 && value.first() == Some(&b'"') && value.last() == Some(&b'"') {
150 return true;
151 }
152 !value.is_empty()
153 && value.iter().all(|byte| {
154 byte.is_ascii_graphic() && !matches!(byte, b';' | b',' | b'"' | b'<' | b'>')
155 })
156}
157
158fn trim_ows(mut value: &[u8]) -> &[u8] {
159 while matches!(value.first(), Some(b' ' | b'\t')) {
160 value = value.get(1..).unwrap_or_default();
161 }
162 while matches!(value.last(), Some(b' ' | b'\t')) {
163 value = value
164 .get(..value.len().saturating_sub(1))
165 .unwrap_or_default();
166 }
167 value
168}
169
170fn is_token_char(byte: u8) -> bool {
171 is_token_nodot_char(byte) || byte == b'.'
172}
173
174fn is_token_nodot_char(byte: u8) -> bool {
175 byte.is_ascii_alphanumeric()
176 || matches!(
177 byte,
178 b'-' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
179 )
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct Served {
185 pub id: Id,
187 pub state: State,
189 pub expires_at: u64,
191 pub remote_cseq: u32,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum Answer {
198 Established {
200 id: Id,
202 expires: Duration,
204 },
205 Refreshed {
207 id: Id,
209 expires: Duration,
211 },
212 Unsubscribed {
217 id: Id,
219 },
220 Unserved {
222 status: u16,
224 },
225 AtCapacity,
227 Malformed,
229 OutOfOrder {
231 id: Id,
233 },
234}
235
236#[derive(Debug)]
238pub struct Subscriptions {
239 packages: Packages,
240 policy_maximum: Duration,
241 capacity: usize,
242 held: Vec<Served>,
243}
244
245impl Subscriptions {
246 #[must_use]
248 pub fn new(packages: Packages, policy_maximum: Duration) -> Self {
249 Self {
250 packages,
251 policy_maximum,
252 capacity: 1024,
253 held: Vec::new(),
254 }
255 }
256
257 #[must_use]
262 pub fn with_capacity(mut self, capacity: usize) -> Self {
263 self.capacity = capacity.max(1);
264 self
265 }
266
267 #[must_use]
269 pub fn capacity(&self) -> usize {
270 self.capacity
271 }
272
273 #[must_use]
275 pub fn packages(&self) -> &Packages {
276 &self.packages
277 }
278
279 #[must_use]
281 pub fn active(&self) -> usize {
282 self.held
283 .iter()
284 .filter(|held| held.state != State::Terminated)
285 .count()
286 }
287
288 #[must_use]
290 pub fn all(&self) -> &[Served] {
291 &self.held
292 }
293
294 pub fn on_subscribe(&mut self, request: &Request, now: u64) -> Answer {
296 let cseq = if request.headers.count(&HeaderName::CSeq) == 1 {
297 match request.headers.typed::<CSeq>() {
298 Some(Ok(CSeq {
299 sequence,
300 method: Method::Subscribe,
301 })) => sequence,
302 _ => return Answer::Malformed,
303 }
304 } else {
305 return Answer::Malformed;
306 };
307 if request.headers.count(&HeaderName::Expires) > 1 {
308 return Answer::Malformed;
309 }
310 let Some(id) = Id::from_request(request) else {
311 return Answer::Malformed;
312 };
313 if !self.packages.serves(&id.event) {
314 return Answer::Unserved { status: BAD_EVENT };
317 }
318
319 let requested = match request.headers.typed::<Expires>() {
320 None => self.policy_maximum,
321 Some(Ok(expires)) => Duration::from_secs(u64::from(expires.0)),
322 Some(Err(_)) => return Answer::Malformed,
323 };
324
325 if is_unsubscribe(requested) {
326 if let Some(held) = self.held.iter_mut().find(|held| held.id == id) {
330 if cseq <= held.remote_cseq {
331 return Answer::OutOfOrder { id };
332 }
333 held.remote_cseq = cseq;
334 held.state = State::Terminated;
335 }
336 return Answer::Unsubscribed { id };
337 }
338
339 let expires = granted_expiry(requested, self.policy_maximum);
340 let expires_at = now.saturating_add(expires.as_secs());
341
342 if let Some(held) = self.held.iter_mut().find(|held| held.id == id) {
343 if cseq <= held.remote_cseq {
347 return Answer::OutOfOrder { id };
348 }
349 if held.state == State::Terminated {
350 return Answer::Unserved { status: BAD_EVENT };
351 }
352 held.remote_cseq = cseq;
353 held.expires_at = expires_at;
354 held.state = State::Active;
355 return Answer::Refreshed { id, expires };
356 }
357
358 if self.active() >= self.capacity {
359 return Answer::AtCapacity;
360 }
361
362 self.held.push(Served {
363 id: id.clone(),
364 state: State::Active,
365 expires_at,
366 remote_cseq: cseq,
367 });
368 Answer::Established { id, expires }
369 }
370
371 pub fn terminate(&mut self, id: &Id, reason: Reason) -> Option<Subscription> {
375 let held = self.held.iter_mut().find(|held| &held.id == id)?;
376 if held.state == State::Terminated {
377 return None;
380 }
381 held.state = State::Terminated;
382 Some(Subscription::terminated(reason))
383 }
384
385 pub fn expire(&mut self, now: u64) -> Vec<Id> {
387 let mut expired = Vec::new();
388 for held in &mut self.held {
389 if held.state != State::Terminated && held.expires_at <= now {
390 held.state = State::Terminated;
391 expired.push(held.id.clone());
392 }
393 }
394 expired
395 }
396
397 #[must_use]
404 pub fn notify_state(&self, id: &Id, now: u64) -> Option<Subscription> {
405 let held = self.held.iter().find(|held| &held.id == id)?;
406 match held.state {
407 State::Terminated => None,
408 state => Some(Subscription {
409 state,
410 expires: Some(Duration::from_secs(held.expires_at.saturating_sub(now))),
411 reason: None,
412 retry_after: None,
413 }),
414 }
415 }
416
417 pub fn sweep(&mut self) -> usize {
423 let before = self.held.len();
424 self.held.retain(|held| held.state != State::Terminated);
425 before - self.held.len()
426 }
427}
428
429#[cfg(test)]
430#[allow(
431 clippy::unwrap_used,
432 clippy::expect_used,
433 clippy::panic,
434 clippy::indexing_slicing
435)]
436mod tests {
437 use super::*;
438 use bytes::Bytes;
439 use sipx_sip::{Limits, Message, parse_datagram};
440
441 const NOW: u64 = 1_700_000_000;
442
443 fn subscribe(event: &str, expires: Option<u64>, tag: &str) -> Request {
444 subscribe_with_cseq(event, expires, tag, 1)
445 }
446
447 fn subscribe_with_cseq(event: &str, expires: Option<u64>, tag: &str, cseq: u32) -> Request {
448 let expires_line =
449 expires.map_or_else(String::new, |seconds| format!("Expires: {seconds}\r\n"));
450 let text = format!(
451 "SUBSCRIBE sip:alice@sipx.test SIP/2.0\r\n\
452 Via: SIP/2.0/UDP watcher.example;branch=z9hG4bKx\r\n\
453 To: <sip:alice@sipx.test>\r\n\
454 From: <sip:watcher@example.net>;tag={tag}\r\n\
455 Call-ID: sub-1@watcher\r\n\
456 CSeq: {cseq} SUBSCRIBE\r\n\
457 Event: {event}\r\n\
458 {expires_line}\
459 Max-Forwards: 70\r\n\
460 Content-Length: 0\r\n\r\n"
461 );
462 match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
463 Message::Request(request) => request,
464 Message::Response(_) => panic!("a request"),
465 }
466 }
467
468 fn notifier() -> Subscriptions {
469 Subscriptions::new(
470 Packages::new().with("dialog").with("presence"),
471 Duration::from_secs(3600),
472 )
473 }
474
475 #[test]
476 fn a_new_subscription_is_refused_at_the_bound_but_a_refresh_is_not() {
477 let mut notifier = notifier().with_capacity(1);
478 let first = subscribe("dialog", Some(600), "w1");
479 assert!(matches!(
480 notifier.on_subscribe(&first, NOW),
481 Answer::Established { .. }
482 ));
483
484 let second = subscribe("presence", Some(600), "w2");
485 assert_eq!(notifier.on_subscribe(&second, NOW), Answer::AtCapacity);
486 assert_eq!(notifier.active(), 1);
487
488 let refresh = subscribe_with_cseq("dialog", Some(600), "w1", 2);
489 assert!(matches!(
490 notifier.on_subscribe(&refresh, NOW + 1),
491 Answer::Refreshed { .. }
492 ));
493 }
494
495 #[test]
496 fn malformed_expiry_and_cseq_do_not_mutate_the_store() {
497 let mut notifier = notifier();
498 for (name, value) in [
499 (HeaderName::Expires, "4294967296"),
500 (HeaderName::CSeq, "not-a-cseq"),
501 (HeaderName::CSeq, "2 MESSAGE"),
502 ] {
503 let mut request = subscribe("dialog", Some(600), "w1");
504 request.headers.remove_all(&name);
505 request
506 .headers
507 .push(sipx_sip::Header::build(name, value).expect("syntactic header"));
508 assert_eq!(notifier.on_subscribe(&request, NOW), Answer::Malformed);
509 assert_eq!(notifier.active(), 0);
510 assert!(notifier.all().is_empty());
511 }
512 }
513
514 #[test]
520 fn a_terminated_subscription_stops_notifying() {
521 let mut notifier = notifier();
522 let request = subscribe("dialog", Some(600), "w1");
523 let Answer::Established { id, .. } = notifier.on_subscribe(&request, NOW) else {
524 panic!("a new subscription");
525 };
526
527 assert!(
528 notifier.notify_state(&id, NOW).is_some(),
529 "an active subscription is notified"
530 );
531
532 notifier
533 .terminate(&id, Reason::NoResource)
534 .expect("the terminating state");
535
536 assert!(
537 notifier.notify_state(&id, NOW).is_none(),
538 "a terminated subscription must produce no further notifications"
539 );
540 assert_eq!(notifier.active(), 0);
541
542 let refresh = subscribe_with_cseq("dialog", Some(600), "w1", 2);
545 assert_eq!(
546 notifier.on_subscribe(&refresh, NOW),
547 Answer::Unserved { status: BAD_EVENT },
548 "a terminated subscription must not be refreshed back to life"
549 );
550 assert!(notifier.notify_state(&id, NOW).is_none());
551 }
552
553 #[test]
554 fn a_subscribe_establishes_and_a_second_one_refreshes() {
555 let mut notifier = notifier();
556 let first = notifier.on_subscribe(&subscribe("dialog", Some(600), "w1"), NOW);
557 let Answer::Established { id, expires } = first else {
558 panic!("a new subscription, got {first:?}");
559 };
560 assert_eq!(expires, Duration::from_secs(600));
561 assert_eq!(notifier.active(), 1);
562
563 let again = notifier.on_subscribe(
564 &subscribe_with_cseq("dialog", Some(900), "w1", 2),
565 NOW + 300,
566 );
567 assert_eq!(
568 again,
569 Answer::Refreshed {
570 id: id.clone(),
571 expires: Duration::from_secs(900)
572 }
573 );
574 assert_eq!(
575 notifier.active(),
576 1,
577 "a refresh is not a second subscription"
578 );
579 assert_eq!(
580 notifier
581 .notify_state(&id, NOW + 300)
582 .expect("active")
583 .expires,
584 Some(Duration::from_secs(900)),
585 "the timer was pushed out"
586 );
587 }
588
589 #[test]
591 fn a_notifier_shortens_a_generous_request_to_its_policy() {
592 let mut notifier = notifier();
593 let answer = notifier.on_subscribe(&subscribe("dialog", Some(86400), "w1"), NOW);
594 let Answer::Established { expires, .. } = answer else {
595 panic!("a new subscription");
596 };
597 assert_eq!(expires, Duration::from_secs(3600), "the policy maximum");
598 }
599
600 #[test]
602 fn an_expires_of_zero_ends_the_subscription() {
603 let mut notifier = notifier();
604 let Answer::Established { id, .. } =
605 notifier.on_subscribe(&subscribe("dialog", Some(600), "w1"), NOW)
606 else {
607 panic!("a new subscription");
608 };
609
610 assert_eq!(
611 notifier.on_subscribe(&subscribe_with_cseq("dialog", Some(0), "w1", 2), NOW,),
612 Answer::Unsubscribed { id: id.clone() }
613 );
614 assert_eq!(notifier.active(), 0);
615 assert!(
616 notifier.notify_state(&id, NOW).is_none(),
617 "nothing further is notified after an unsubscribe"
618 );
619 assert!(
620 notifier.all().iter().any(|held| held.id == id),
621 "and it is still findable, so a NOTIFY crossing it finds a terminated subscription \
622 rather than none at all"
623 );
624 }
625
626 #[test]
629 fn a_package_this_notifier_does_not_serve_is_refused_with_489() {
630 let mut notifier = notifier();
631 assert_eq!(
632 notifier.on_subscribe(&subscribe("message-summary", Some(600), "w1"), NOW),
633 Answer::Unserved { status: 489 }
634 );
635 assert_eq!(notifier.active(), 0, "nothing was established");
636 }
637
638 #[test]
639 fn a_subscription_that_runs_out_of_time_is_terminated() {
640 let mut notifier = notifier();
641 let Answer::Established { id, .. } =
642 notifier.on_subscribe(&subscribe("dialog", Some(60), "w1"), NOW)
643 else {
644 panic!("a new subscription");
645 };
646
647 assert!(notifier.expire(NOW + 59).is_empty(), "not yet");
648 assert_eq!(notifier.expire(NOW + 60), vec![id.clone()]);
649 assert_eq!(notifier.active(), 0);
650 assert!(notifier.notify_state(&id, NOW + 60).is_none());
651 }
652
653 #[test]
657 fn two_packages_in_one_dialog_are_two_subscriptions() {
658 let mut notifier = notifier();
659 let first = notifier.on_subscribe(&subscribe("dialog", Some(600), "w1"), NOW);
660 let second = notifier.on_subscribe(&subscribe("presence", Some(600), "w1"), NOW);
661 assert!(matches!(first, Answer::Established { .. }));
662 assert!(
663 matches!(second, Answer::Established { .. }),
664 "a different Event in the same dialog is a new subscription, not a refresh"
665 );
666 assert_eq!(notifier.active(), 2);
667 }
668
669 #[test]
670 fn terminating_twice_reports_the_ending_once() {
671 let mut notifier = notifier();
672 let Answer::Established { id, .. } =
673 notifier.on_subscribe(&subscribe("dialog", Some(600), "w1"), NOW)
674 else {
675 panic!("a new subscription");
676 };
677 assert!(notifier.terminate(&id, Reason::GiveUp).is_some());
678 assert!(
679 notifier.terminate(&id, Reason::GiveUp).is_none(),
680 "one subscription gets one terminating NOTIFY"
681 );
682 }
683
684 #[test]
685 fn sweeping_forgets_only_what_has_ended() {
686 let mut notifier = notifier();
687 let Answer::Established { id, .. } =
688 notifier.on_subscribe(&subscribe("dialog", Some(600), "w1"), NOW)
689 else {
690 panic!("a new subscription");
691 };
692 let _ = notifier.on_subscribe(&subscribe("presence", Some(600), "w1"), NOW);
693 notifier.terminate(&id, Reason::Timeout);
694
695 assert_eq!(notifier.sweep(), 1);
696 assert_eq!(notifier.all().len(), 1);
697 assert_eq!(notifier.active(), 1);
698 }
699
700 #[test]
701 fn a_subscribe_without_an_event_is_malformed() {
702 let text = "SUBSCRIBE sip:alice@sipx.test SIP/2.0\r\n\
703 Via: SIP/2.0/UDP watcher.example;branch=z9hG4bKx\r\n\
704 To: <sip:alice@sipx.test>\r\n\
705 From: <sip:watcher@example.net>;tag=w1\r\n\
706 Call-ID: sub-1@watcher\r\n\
707 CSeq: 1 SUBSCRIBE\r\n\
708 Max-Forwards: 70\r\n\
709 Content-Length: 0\r\n\r\n";
710 let request = match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses")
711 {
712 Message::Request(request) => request,
713 Message::Response(_) => panic!("a request"),
714 };
715 assert_eq!(notifier().on_subscribe(&request, NOW), Answer::Malformed);
716 }
717
718 #[test]
719 fn a_subscribe_with_no_expires_gets_the_policy_maximum() {
720 let mut notifier = notifier();
721 let answer = notifier.on_subscribe(&subscribe("dialog", None, "w1"), NOW);
722 let Answer::Established { expires, .. } = answer else {
723 panic!("a new subscription");
724 };
725 assert_eq!(expires, Duration::from_secs(3600));
726 }
727
728 #[test]
729 fn replayed_subscribe_cseq_cannot_refresh_or_terminate() {
730 let mut notifier = notifier();
731 let Answer::Established { id, .. } =
732 notifier.on_subscribe(&subscribe_with_cseq("dialog", Some(600), "w1", 20), NOW)
733 else {
734 panic!("a new subscription");
735 };
736 let before = notifier.notify_state(&id, NOW).expect("active");
737
738 for request in [
739 subscribe_with_cseq("dialog", Some(900), "w1", 20),
740 subscribe_with_cseq("dialog", Some(0), "w1", 19),
741 ] {
742 assert_eq!(
743 notifier.on_subscribe(&request, NOW + 30),
744 Answer::OutOfOrder { id: id.clone() }
745 );
746 assert_eq!(notifier.notify_state(&id, NOW), Some(before.clone()));
747 assert_eq!(notifier.active(), 1);
748 }
749
750 assert!(matches!(
751 notifier.on_subscribe(
752 &subscribe_with_cseq("dialog", Some(900), "w1", 21),
753 NOW + 30,
754 ),
755 Answer::Refreshed { .. }
756 ));
757 assert_eq!(
758 notifier
759 .all()
760 .iter()
761 .find(|served| served.id == id)
762 .expect("subscription remains")
763 .remote_cseq,
764 21
765 );
766 }
767
768 #[test]
769 fn event_identity_uses_only_exact_type_and_id_tokens() {
770 let reordered = Id::from_request(&subscribe(
771 "dialog;vendor=one;ID=Opaque-A;mode=full",
772 Some(600),
773 "w1",
774 ))
775 .expect("identity");
776 let differently_ordered = Id::from_request(&subscribe(
777 "dialog;mode=partial;id=Opaque-A;vendor=two",
778 Some(600),
779 "w1",
780 ))
781 .expect("identity");
782 assert_eq!(reordered, differently_ordered);
783 assert_eq!(reordered.event, "dialog;id=Opaque-A");
784
785 let changed_type_case =
786 Id::from_request(&subscribe("Dialog;id=Opaque-A", Some(600), "w1")).expect("identity");
787 let changed_id_case =
788 Id::from_request(&subscribe("dialog;id=opaque-a", Some(600), "w1")).expect("identity");
789 assert_ne!(reordered, changed_type_case, "event-type is byte matched");
790 assert_ne!(reordered, changed_id_case, "id is an opaque token");
791
792 assert!(
793 Id::from_request(&subscribe("dialog;id=one;ID=two", Some(600), "w1")).is_none(),
794 "duplicate identity parameters fail closed"
795 );
796 }
797
798 #[test]
799 fn duplicate_identity_headers_fail_before_first_value_selection() {
800 for name in [HeaderName::CallId, HeaderName::From, HeaderName::Event] {
801 let mut request = subscribe("dialog", Some(600), "w1");
802 let value = request.headers.value(&name).expect("header").into_owned();
803 request
804 .headers
805 .push(sipx_sip::Header::build(name, value).expect("syntactic header"));
806 assert!(Id::from_request(&request).is_none());
807 assert_eq!(notifier().on_subscribe(&request, NOW), Answer::Malformed);
808 }
809 }
810}