1use std::time::Duration;
7
8use bytes::Bytes;
9use sipx_sip::auth::{Challenge, Credentials, respond, strongest};
10use sipx_sip::build::RequestBuilder;
11use sipx_sip::headers::Expires;
12use sipx_sip::{Header, HeaderName, Method, Request, Response, Uri};
13use thiserror::Error;
14
15use crate::event_client::Peer;
16
17pub const DEFAULT_CAPACITY: usize = 1_024;
19pub const DEFAULT_BODY_LIMIT: usize = 65_536;
21
22#[derive(Debug, Clone)]
24pub struct Config {
25 pub capacity: usize,
27 pub body_limit: usize,
29 pub authentication_retries: u8,
31 pub interval_retries: u8,
33 pub maximum_expiry: Duration,
35}
36
37impl Default for Config {
38 fn default() -> Self {
39 Self {
40 capacity: DEFAULT_CAPACITY,
41 body_limit: DEFAULT_BODY_LIMIT,
42 authentication_retries: 2,
43 interval_retries: 1,
44 maximum_expiry: Duration::from_secs(u64::from(u32::MAX)),
45 }
46 }
47}
48
49impl Config {
50 pub fn validate(&self) -> Result<(), StartError> {
52 if self.capacity == 0
53 || self.body_limit == 0
54 || self.maximum_expiry.is_zero()
55 || self.maximum_expiry.as_secs() > u64::from(u32::MAX)
56 {
57 return Err(StartError::InvalidConfiguration);
58 }
59 Ok(())
60 }
61}
62
63#[derive(Debug)]
65pub struct Start {
66 pub resource: Uri,
68 pub local_identity: String,
70 pub target: Peer,
72 pub event: String,
74 pub expires: Duration,
76 pub body: Bytes,
78 pub content_type: String,
80 pub credentials: Option<Credentials>,
82 pub call_id: String,
84 pub from_tag: String,
86 pub initial_cseq: u32,
88}
89
90#[derive(Debug, Error, Clone, PartialEq, Eq)]
92#[non_exhaustive]
93pub enum StartError {
94 #[error("invalid publisher configuration")]
96 InvalidConfiguration,
97 #[error("invalid publication start")]
99 InvalidStart,
100 #[error("publication body exceeds configured maximum")]
102 BodyTooLarge,
103 #[error("could not build PUBLISH")]
105 Build,
106}
107
108#[derive(Debug, Error, Clone, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum CommandError {
112 #[error("publication is no longer active")]
114 Terminated,
115 #[error("another publication operation is in flight")]
117 Busy,
118 #[error("invalid publication body")]
120 InvalidBody,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum Timer {
126 Refresh,
128 Expiry,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct PublishedState {
135 pub tag: String,
137 pub expires: Duration,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum Termination {
144 StaleTag,
146 MalformedResponse,
148 AuthenticationExhausted,
150 IntervalRejected,
152 Rejected(u16),
154 TransactionFailed,
156 LocalCSeqExhausted,
158 LocalExpiry,
160 Removed,
162 Shutdown,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum StateChange {
169 Published(PublishedState),
171 Terminated(Termination),
173}
174
175#[derive(Debug)]
177pub enum Output {
178 SendPublish {
180 request: Box<Request>,
182 target: Peer,
184 },
185 ArmTimer {
187 timer: Timer,
189 generation: u64,
191 after: Duration,
193 },
194 CancelTimer {
196 timer: Timer,
198 generation: u64,
200 },
201 StateChanged(StateChange),
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum OperationKind {
207 Initial,
208 Refresh,
209 Modify,
210 Remove,
211}
212
213struct Operation {
214 kind: OperationKind,
215 attempted: Duration,
216 request: Request,
217 auth_retries: u8,
218 interval_retries: u8,
219}
220
221#[derive(Default)]
222struct Timers {
223 refresh: Option<u64>,
224 expiry: Option<u64>,
225 next: u64,
226}
227
228pub struct Publisher {
230 config: Config,
231 resource: Uri,
232 local_identity: String,
233 target: Peer,
234 event: String,
235 desired: Duration,
236 body: Bytes,
237 content_type: String,
238 credentials: Option<Credentials>,
239 call_id: String,
240 from_tag: String,
241 cseq: u32,
242 tag: Option<String>,
243 granted: Option<Duration>,
244 operation: Option<Operation>,
245 pending_remove: bool,
246 timers: Timers,
247 active: bool,
248}
249
250impl std::fmt::Debug for Publisher {
251 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 formatter
253 .debug_struct("Publisher")
254 .field("resource", &self.resource)
255 .field("event", &self.event)
256 .field("cseq", &self.cseq)
257 .field("has_tag", &self.tag.is_some())
258 .field("granted", &self.granted)
259 .field("active", &self.active)
260 .finish_non_exhaustive()
261 }
262}
263
264impl Publisher {
265 pub fn start(config: Config, start: Start) -> Result<(Self, Vec<Output>), StartError> {
267 config.validate()?;
268 if start.body.len() > config.body_limit {
269 return Err(StartError::BodyTooLarge);
270 }
271 if start.body.is_empty()
272 || start.event.trim().is_empty()
273 || !token(start.event.as_bytes())
274 || start.content_type.trim().is_empty()
275 || start.local_identity.trim().is_empty()
276 || start.call_id.trim().is_empty()
277 || start.from_tag.trim().is_empty()
278 || !token(start.from_tag.as_bytes())
279 || start.initial_cseq == 0
280 || start.expires.is_zero()
281 || start.expires > config.maximum_expiry
282 || start.expires.as_secs() > u64::from(u32::MAX)
283 {
284 return Err(StartError::InvalidStart);
285 }
286 let request = build_request(
287 &start.resource,
288 &start.local_identity,
289 &start.call_id,
290 &start.from_tag,
291 start.initial_cseq,
292 &start.event,
293 start.expires,
294 None,
295 Some((&start.content_type, start.body.clone())),
296 )?;
297 let output = Output::SendPublish {
298 request: Box::new(request.clone()),
299 target: start.target.clone(),
300 };
301 Ok((
302 Self {
303 config,
304 resource: start.resource,
305 local_identity: start.local_identity,
306 target: start.target,
307 event: start.event,
308 desired: start.expires,
309 body: start.body,
310 content_type: start.content_type,
311 credentials: start.credentials,
312 call_id: start.call_id,
313 from_tag: start.from_tag,
314 cseq: start.initial_cseq,
315 tag: None,
316 granted: None,
317 operation: Some(Operation {
318 kind: OperationKind::Initial,
319 attempted: start.expires,
320 request,
321 auth_retries: 0,
322 interval_retries: 0,
323 }),
324 pending_remove: false,
325 timers: Timers::default(),
326 active: true,
327 },
328 vec![output],
329 ))
330 }
331
332 #[must_use]
334 pub fn is_active(&self) -> bool {
335 self.active
336 }
337
338 #[must_use]
340 pub fn entity_tag(&self) -> Option<&str> {
341 self.tag.as_deref()
342 }
343
344 #[must_use]
346 pub fn granted_expiry(&self) -> Option<Duration> {
347 self.granted
348 }
349
350 #[allow(
352 clippy::too_many_lines,
353 reason = "the RFC response table remains in protocol order"
354 )]
355 pub fn response(&mut self, response: Option<&Response>, cnonce: &str) -> Vec<Output> {
356 let mut outputs = Vec::new();
357 let Some(mut operation) = self.operation.take() else {
358 return outputs;
359 };
360 let Some(response) = response else {
361 terminate(self, Termination::TransactionFailed, &mut outputs);
362 return outputs;
363 };
364
365 if matches!(response.status.code(), 401 | 407) {
366 let proxy = response.status.code() == 407;
367 let header = if proxy {
368 HeaderName::ProxyAuthenticate
369 } else {
370 HeaderName::WwwAuthenticate
371 };
372 let challenge = strongest(
373 response
374 .headers
375 .get_all(&header)
376 .filter_map(|value| Challenge::parse(&value.value(), proxy))
377 .collect(),
378 );
379 if operation.auth_retries >= self.config.authentication_retries
380 || challenge.is_none()
381 || self.credentials.is_none()
382 || !increment_cseq(self, &mut outputs)
383 {
384 if self.active {
385 terminate(self, Termination::AuthenticationExhausted, &mut outputs);
386 }
387 return outputs;
388 }
389 if let (Some(challenge), Some(credentials)) = (challenge, self.credentials.as_ref()) {
390 operation.auth_retries = operation.auth_retries.saturating_add(1);
391 let mut retry = operation.request.clone();
392 replace_cseq(&mut retry, self.cseq);
393 let uri = String::from_utf8_lossy(&retry.uri.to_bytes()).into_owned();
394 let authorization = respond(
395 &challenge,
396 credentials,
397 "PUBLISH",
398 &uri,
399 u32::from(operation.auth_retries),
400 cnonce,
401 );
402 retry.headers.remove_all(&challenge.response_header());
403 if let Ok(header) =
404 Header::build(challenge.response_header(), Bytes::from(authorization))
405 {
406 retry.headers.push(header);
407 operation.request = retry.clone();
408 self.operation = Some(operation);
409 outputs.push(Output::SendPublish {
410 request: Box::new(retry),
411 target: self.target.clone(),
412 });
413 return outputs;
414 }
415 }
416 terminate(self, Termination::AuthenticationExhausted, &mut outputs);
417 return outputs;
418 }
419
420 if response.status.code() == 423 {
421 let minimum = strict_duration(response, &HeaderName::MinExpires);
422 let valid = operation.kind != OperationKind::Remove
423 && operation.interval_retries < self.config.interval_retries
424 && minimum.is_some_and(|minimum| {
425 minimum > operation.attempted
426 && minimum <= self.config.maximum_expiry
427 && u32::try_from(minimum.as_secs()).is_ok()
428 });
429 if valid
430 && increment_cseq(self, &mut outputs)
431 && let Some(minimum) = minimum
432 {
433 operation.interval_retries = operation.interval_retries.saturating_add(1);
434 operation.attempted = minimum;
435 let mut retry = operation.request.clone();
436 replace_cseq(&mut retry, self.cseq);
437 replace_duration(&mut retry, minimum);
438 operation.request = retry.clone();
439 self.operation = Some(operation);
440 outputs.push(Output::SendPublish {
441 request: Box::new(retry),
442 target: self.target.clone(),
443 });
444 return outputs;
445 }
446 if self.active {
447 terminate(self, Termination::IntervalRejected, &mut outputs);
448 }
449 return outputs;
450 }
451
452 if response.status.code() == 412 && operation.kind != OperationKind::Initial {
453 self.tag = None;
454 terminate(self, Termination::StaleTag, &mut outputs);
455 return outputs;
456 }
457
458 if response.status.is_success() {
459 let tag = strict_tag(response);
460 let expires = strict_duration(response, &HeaderName::Expires);
461 let valid_expiry = if operation.kind == OperationKind::Remove {
462 expires == Some(Duration::ZERO)
463 } else {
464 expires.is_some_and(|value| !value.is_zero() && value <= operation.attempted)
465 };
466 if tag.is_none() || !valid_expiry {
467 terminate(self, Termination::MalformedResponse, &mut outputs);
468 return outputs;
469 }
470 if operation.kind == OperationKind::Remove {
471 self.tag = None;
472 terminate(self, Termination::Removed, &mut outputs);
473 return outputs;
474 }
475 if let (Some(tag), Some(expires)) = (tag, expires) {
476 self.tag = Some(tag.clone());
477 self.granted = Some(expires);
478 self.operation = None;
479 arm(self, Timer::Expiry, expires, &mut outputs);
480 arm_refresh(self, expires, &mut outputs);
481 outputs.push(Output::StateChanged(StateChange::Published(
482 PublishedState { tag, expires },
483 )));
484 maybe_begin_remove(self, &mut outputs);
485 }
486 return outputs;
487 }
488
489 terminate(
490 self,
491 Termination::Rejected(response.status.code()),
492 &mut outputs,
493 );
494 outputs
495 }
496
497 pub fn modify(
499 &mut self,
500 body: Bytes,
501 content_type: String,
502 ) -> Result<Vec<Output>, CommandError> {
503 if !self.active || self.tag.is_none() {
504 return Err(CommandError::Terminated);
505 }
506 if self.operation.is_some() {
507 return Err(CommandError::Busy);
508 }
509 if body.is_empty() || body.len() > self.config.body_limit || content_type.trim().is_empty()
510 {
511 return Err(CommandError::InvalidBody);
512 }
513 let mut outputs = Vec::new();
514 cancel(self, Timer::Refresh, &mut outputs);
515 if !increment_cseq(self, &mut outputs) {
516 return Ok(outputs);
517 }
518 self.body = body;
519 self.content_type = content_type;
520 let expires = self.granted.unwrap_or(self.desired);
521 begin(self, OperationKind::Modify, expires, true, &mut outputs);
522 Ok(outputs)
523 }
524
525 pub fn remove(&mut self) -> Result<Vec<Output>, CommandError> {
527 if !self.active || self.tag.is_none() {
528 return Err(CommandError::Terminated);
529 }
530 let mut outputs = Vec::new();
531 cancel(self, Timer::Refresh, &mut outputs);
532 if self.operation.is_some() {
533 self.pending_remove = true;
534 } else {
535 self.pending_remove = true;
536 maybe_begin_remove(self, &mut outputs);
537 }
538 Ok(outputs)
539 }
540
541 pub fn timer_fired(&mut self, timer: Timer, generation: u64) -> Vec<Output> {
543 let mut outputs = Vec::new();
544 if timer_generation(&self.timers, timer) != Some(generation) {
545 return outputs;
546 }
547 set_timer(&mut self.timers, timer, None);
548 match timer {
549 Timer::Expiry => terminate(self, Termination::LocalExpiry, &mut outputs),
550 Timer::Refresh => {
551 if self.operation.is_none()
552 && self.tag.is_some()
553 && increment_cseq(self, &mut outputs)
554 {
555 let expires = self.granted.unwrap_or(self.desired);
556 begin(self, OperationKind::Refresh, expires, false, &mut outputs);
557 }
558 }
559 }
560 outputs
561 }
562
563 pub fn shutdown_deadline(&mut self) -> Vec<Output> {
565 let mut outputs = Vec::new();
566 if self.active {
567 terminate(self, Termination::Shutdown, &mut outputs);
568 }
569 outputs
570 }
571}
572
573fn begin(
574 publisher: &mut Publisher,
575 kind: OperationKind,
576 expires: Duration,
577 with_body: bool,
578 outputs: &mut Vec<Output>,
579) {
580 let Some(tag) = publisher.tag.as_deref() else {
581 terminate(publisher, Termination::StaleTag, outputs);
582 return;
583 };
584 let body = with_body.then(|| (publisher.content_type.as_str(), publisher.body.clone()));
585 match build_request(
586 &publisher.resource,
587 &publisher.local_identity,
588 &publisher.call_id,
589 &publisher.from_tag,
590 publisher.cseq,
591 &publisher.event,
592 expires,
593 Some(tag),
594 body,
595 ) {
596 Ok(request) => {
597 publisher.operation = Some(Operation {
598 kind,
599 attempted: expires,
600 request: request.clone(),
601 auth_retries: 0,
602 interval_retries: 0,
603 });
604 outputs.push(Output::SendPublish {
605 request: Box::new(request),
606 target: publisher.target.clone(),
607 });
608 }
609 Err(_) => terminate(publisher, Termination::MalformedResponse, outputs),
610 }
611}
612
613fn maybe_begin_remove(publisher: &mut Publisher, outputs: &mut Vec<Output>) {
614 if !publisher.pending_remove || publisher.operation.is_some() || publisher.tag.is_none() {
615 return;
616 }
617 publisher.pending_remove = false;
618 cancel(publisher, Timer::Refresh, outputs);
619 if increment_cseq(publisher, outputs) {
620 begin(
621 publisher,
622 OperationKind::Remove,
623 Duration::ZERO,
624 false,
625 outputs,
626 );
627 }
628}
629
630#[allow(
631 clippy::too_many_arguments,
632 reason = "the arguments are the complete PUBLISH identity"
633)]
634fn build_request(
635 resource: &Uri,
636 local_identity: &str,
637 call_id: &str,
638 from_tag: &str,
639 cseq: u32,
640 event: &str,
641 expires: Duration,
642 tag: Option<&str>,
643 body: Option<(&str, Bytes)>,
644) -> Result<Request, StartError> {
645 let mut builder = RequestBuilder::new(Method::Publish, resource.clone())
646 .header(HeaderName::To, Bytes::from(format!("<{resource}>")))
647 .map_err(|_| StartError::Build)?
648 .header(
649 HeaderName::From,
650 Bytes::from(format!("{local_identity};tag={from_tag}")),
651 )
652 .map_err(|_| StartError::Build)?
653 .header(HeaderName::CallId, Bytes::from(call_id.to_owned()))
654 .map_err(|_| StartError::Build)?
655 .cseq(cseq, &Method::Publish)
656 .map_err(|_| StartError::Build)?
657 .header(HeaderName::Event, Bytes::from(event.to_owned()))
658 .map_err(|_| StartError::Build)?
659 .header(
660 HeaderName::Expires,
661 Bytes::from(expires.as_secs().to_string()),
662 )
663 .map_err(|_| StartError::Build)?
664 .max_forwards(70);
665 if let Some(tag) = tag {
666 builder = builder
667 .header(HeaderName::SipIfMatch, Bytes::from(tag.to_owned()))
668 .map_err(|_| StartError::Build)?;
669 }
670 let payload = match body {
671 Some((content_type, body)) => {
672 builder = builder
673 .header(
674 HeaderName::ContentType,
675 Bytes::from(content_type.to_owned()),
676 )
677 .map_err(|_| StartError::Build)?;
678 body
679 }
680 None => Bytes::new(),
681 };
682 Ok(builder.body(payload).build())
683}
684
685fn strict_tag(response: &Response) -> Option<String> {
686 if response.headers.count(&HeaderName::SipETag) != 1 {
687 return None;
688 }
689 let value = response.headers.value(&HeaderName::SipETag)?;
690 token(&value).then(|| String::from_utf8_lossy(&value).into_owned())
691}
692
693fn strict_duration(response: &Response, name: &HeaderName) -> Option<Duration> {
694 if response.headers.count(name) != 1 {
695 return None;
696 }
697 if name == &HeaderName::Expires {
698 return response
699 .headers
700 .typed::<Expires>()?
701 .ok()
702 .map(|value| Duration::from_secs(u64::from(value.0)));
703 }
704 let value = response.headers.value(name)?;
705 let seconds = std::str::from_utf8(&value)
706 .ok()?
707 .trim()
708 .parse::<u32>()
709 .ok()?;
710 Some(Duration::from_secs(u64::from(seconds)))
711}
712
713fn token(value: &[u8]) -> bool {
714 !value.is_empty()
715 && value.iter().all(|byte| {
716 byte.is_ascii_alphanumeric()
717 || matches!(
718 byte,
719 b'-' | b'.' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
720 )
721 })
722}
723
724fn replace_cseq(request: &mut Request, sequence: u32) {
725 request.headers.remove_all(&HeaderName::CSeq);
726 if let Ok(header) = Header::build(HeaderName::CSeq, Bytes::from(format!("{sequence} PUBLISH")))
727 {
728 request.headers.push(header);
729 }
730}
731
732fn replace_duration(request: &mut Request, expires: Duration) {
733 request.headers.remove_all(&HeaderName::Expires);
734 if let Ok(header) = Header::build(
735 HeaderName::Expires,
736 Bytes::from(expires.as_secs().to_string()),
737 ) {
738 request.headers.push(header);
739 }
740}
741
742fn increment_cseq(publisher: &mut Publisher, outputs: &mut Vec<Output>) -> bool {
743 let Some(next) = publisher.cseq.checked_add(1) else {
744 terminate(publisher, Termination::LocalCSeqExhausted, outputs);
745 return false;
746 };
747 publisher.cseq = next;
748 true
749}
750
751fn arm_refresh(publisher: &mut Publisher, expires: Duration, outputs: &mut Vec<Output>) {
752 let seconds = expires.as_secs();
753 let refresh = if seconds <= 1 {
754 Duration::from_secs(1)
757 } else {
758 Duration::from_secs((seconds.saturating_mul(4) / 5).clamp(1, seconds - 1))
759 };
760 arm(publisher, Timer::Refresh, refresh, outputs);
761}
762
763fn arm(publisher: &mut Publisher, timer: Timer, after: Duration, outputs: &mut Vec<Output>) {
764 cancel(publisher, timer, outputs);
765 publisher.timers.next = publisher.timers.next.saturating_add(1);
766 let generation = publisher.timers.next;
767 set_timer(&mut publisher.timers, timer, Some(generation));
768 outputs.push(Output::ArmTimer {
769 timer,
770 generation,
771 after,
772 });
773}
774
775fn cancel(publisher: &mut Publisher, timer: Timer, outputs: &mut Vec<Output>) {
776 if let Some(generation) = timer_generation(&publisher.timers, timer) {
777 set_timer(&mut publisher.timers, timer, None);
778 outputs.push(Output::CancelTimer { timer, generation });
779 }
780}
781
782fn timer_generation(timers: &Timers, timer: Timer) -> Option<u64> {
783 match timer {
784 Timer::Refresh => timers.refresh,
785 Timer::Expiry => timers.expiry,
786 }
787}
788
789fn set_timer(timers: &mut Timers, timer: Timer, generation: Option<u64>) {
790 match timer {
791 Timer::Refresh => timers.refresh = generation,
792 Timer::Expiry => timers.expiry = generation,
793 }
794}
795
796fn terminate(publisher: &mut Publisher, reason: Termination, outputs: &mut Vec<Output>) {
797 cancel(publisher, Timer::Refresh, outputs);
798 cancel(publisher, Timer::Expiry, outputs);
799 publisher.operation = None;
800 publisher.pending_remove = false;
801 publisher.active = false;
802 publisher.tag = None;
803 publisher.granted = None;
804 outputs.push(Output::StateChanged(StateChange::Terminated(reason)));
805}