Skip to main content

sipx_call/
publication.rs

1//! Live endpoint driver for RFC 3903 event-state publication.
2//!
3//! The protocol contract is `docs/specs/publication-endpoint.md`. The inbound role mutates the
4//! exact injected [`Compositor`]; the outbound role drives [`Publisher`] through real transactions.
5
6use std::collections::{HashMap, HashSet};
7use std::net::SocketAddr;
8use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
9use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
10use std::time::Duration;
11
12use bytes::Bytes;
13use sipx_sip::build::ResponseBuilder;
14use sipx_sip::headers::{CSeq, Expires};
15use sipx_sip::{HeaderName, Method, Request, Response, StatusCode};
16use sipx_transport::{Handle, Incoming, Target, TransportKind};
17use sipx_ua::auth::new_cnonce;
18use sipx_ua::presence::{Compositor, PIDF_TYPE, Publish, Published};
19use sipx_ua::publication_client::{
20    CommandError, Config as PublisherConfig, Output, Publisher, Start, StartError, StateChange,
21    Timer,
22};
23use thiserror::Error;
24use tokio::sync::{mpsc, oneshot, watch};
25use tokio::task::JoinHandle;
26use tokio::time::Instant;
27use tokio_util::sync::CancellationToken;
28
29use crate::dispatch::with_to_tag;
30
31const DRIVER_QUEUE: usize = 32;
32const RETRY_AFTER: Duration = Duration::from_secs(1);
33
34/// Inbound publication admission and expiry policy.
35#[derive(Debug, Clone)]
36pub struct PublicationConfig {
37    /// Minimum positive expiry; smaller requests receive 423.
38    pub minimum_expiry: Duration,
39    /// Expiry used when the request omits Expires.
40    pub default_expiry: Duration,
41    /// Maximum active resources held by the endpoint driver.
42    pub capacity: usize,
43    /// Maximum accepted PUBLISH body.
44    pub body_limit: usize,
45    /// Outbound publisher policy.
46    pub publisher: PublisherConfig,
47}
48
49impl Default for PublicationConfig {
50    fn default() -> Self {
51        Self {
52            minimum_expiry: Duration::from_secs(60),
53            default_expiry: Duration::from_secs(3_600),
54            capacity: 1_024,
55            body_limit: 65_536,
56            publisher: PublisherConfig::default(),
57        }
58    }
59}
60
61impl PublicationConfig {
62    fn validate(&self) -> Result<(), PublicationError> {
63        self.publisher.validate()?;
64        if self.minimum_expiry.is_zero()
65            || self.default_expiry < self.minimum_expiry
66            || self.default_expiry.as_secs() > u64::from(u32::MAX)
67            || self.capacity == 0
68            || self.body_limit == 0
69        {
70            return Err(PublicationError::InvalidConfiguration);
71        }
72        Ok(())
73    }
74}
75
76/// Inbound authorization is an explicit application decision.
77pub trait PublicationAuthorization: Send + Sync + 'static {
78    /// Whether this source may mutate this resource and event package.
79    fn authorize(&self, request: &Request, source: SocketAddr, transport: TransportKind) -> bool;
80}
81
82/// Application-owned composition decision around the exact publication store.
83pub trait PublicationComposition: Send + Sync + 'static {
84    /// Apply one authorized operation, optionally composing it with existing state.
85    fn apply(
86        &self,
87        compositor: &mut Compositor,
88        entity: &str,
89        publication: Publish,
90        now: u64,
91    ) -> Published;
92}
93
94/// Explicit policy that uses the compositor's one-document-per-resource semantics.
95#[derive(Debug, Default)]
96pub struct ReplacePublicationState;
97
98impl PublicationComposition for ReplacePublicationState {
99    fn apply(
100        &self,
101        compositor: &mut Compositor,
102        entity: &str,
103        publication: Publish,
104        now: u64,
105    ) -> Published {
106        compositor.apply(entity, publication, now)
107    }
108}
109
110/// Explicit permissive policy for development and already-authenticated frontends.
111#[derive(Debug, Default)]
112pub struct AllowPublications;
113
114impl PublicationAuthorization for AllowPublications {
115    fn authorize(
116        &self,
117        _request: &Request,
118        _source: SocketAddr,
119        _transport: TransportKind,
120    ) -> bool {
121        true
122    }
123}
124
125/// Runtime resource snapshot.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct PublicationCounts {
128    /// Live publisher and inbound expiry tasks.
129    pub active_tasks: usize,
130    /// Live timer tasks.
131    pub active_timers: usize,
132    /// Live outbound client transactions.
133    pub active_transactions: usize,
134    /// Live outbound publisher handles.
135    pub active_publishers: usize,
136    /// Live inbound compositor publications.
137    pub active_publications: usize,
138    /// Inbound or outbound capacity refusals.
139    pub shed: u64,
140}
141
142#[derive(Debug, Default)]
143struct Counters {
144    tasks: AtomicUsize,
145    timers: AtomicUsize,
146    transactions: AtomicUsize,
147    publishers: AtomicUsize,
148    shed: AtomicU64,
149}
150
151/// Failure before a runtime operation is admitted.
152#[derive(Debug, Error)]
153#[non_exhaustive]
154pub enum PublicationError {
155    /// The publisher driver is not attached to a dispatcher.
156    #[error("publications are not attached to a dispatcher")]
157    NotAttached,
158    /// Dispatcher shutdown has atomically closed admission.
159    #[error("publication shutdown has closed admission")]
160    ShuttingDown,
161    /// Runtime configuration is invalid.
162    #[error("invalid publication configuration")]
163    InvalidConfiguration,
164    /// The configured publisher capacity is full.
165    #[error("publisher capacity exceeded")]
166    CapacityExceeded,
167    /// Another local publisher already serializes this resource.
168    #[error("a publisher already owns this resource")]
169    DuplicateResource,
170    /// The sans-I/O client refused a lifecycle command.
171    #[error(transparent)]
172    Command(#[from] CommandError),
173    /// The sans-I/O client rejected the initial fields.
174    #[error(transparent)]
175    Start(#[from] StartError),
176}
177
178#[derive(Debug)]
179struct Shared {
180    endpoint: Mutex<Option<Handle>>,
181    resources: Mutex<HashSet<Vec<u8>>>,
182    drivers: Mutex<HashMap<Vec<u8>, JoinHandle<()>>>,
183    config: PublisherConfig,
184    counters: Arc<Counters>,
185    shutdown: CancellationToken,
186}
187
188/// Application ownership of one outbound publication.
189#[derive(Debug)]
190pub struct Publication {
191    commands: mpsc::Sender<Command>,
192    states: watch::Receiver<Option<StateChange>>,
193    body_limit: usize,
194}
195
196impl Publication {
197    /// Wait for the next authoritative or terminal state.
198    pub async fn next_state(&mut self) -> Option<StateChange> {
199        self.states.changed().await.ok()?;
200        self.states.borrow_and_update().clone()
201    }
202
203    /// Conditionally replace the publication body.
204    pub async fn modify(
205        &self,
206        body: Bytes,
207        content_type: impl Into<String>,
208    ) -> Result<(), PublicationError> {
209        if body.is_empty() || body.len() > self.body_limit {
210            return Err(CommandError::InvalidBody.into());
211        }
212        let (reply, result) = oneshot::channel();
213        self.commands
214            .send(Command::Modify(body, content_type.into(), reply))
215            .await
216            .map_err(|_| PublicationError::NotAttached)?;
217        result.await.map_err(|_| PublicationError::NotAttached)??;
218        Ok(())
219    }
220
221    /// Conditionally remove the publication.
222    pub async fn remove(&self) -> Result<(), PublicationError> {
223        let (reply, result) = oneshot::channel();
224        self.commands
225            .send(Command::Remove(reply))
226            .await
227            .map_err(|_| PublicationError::NotAttached)?;
228        result.await.map_err(|_| PublicationError::NotAttached)??;
229        Ok(())
230    }
231}
232
233impl Drop for Publication {
234    fn drop(&mut self) {
235        let (reply, _) = oneshot::channel();
236        // discard: Drop cannot wait; the finite lease and dispatcher shutdown remain backstops.
237        let _ = self.commands.try_send(Command::Remove(reply));
238    }
239}
240
241/// Cloneable application handle for inbound observation and outbound starts.
242#[derive(Debug, Clone)]
243pub struct PublicationsHandle {
244    shared: Arc<Shared>,
245    compositor: Arc<Mutex<Compositor>>,
246}
247
248impl PublicationsHandle {
249    /// Start one serialized outbound publication.
250    pub fn publish(&self, start: Start) -> Result<Publication, PublicationError> {
251        let resource = start.resource.to_bytes().to_vec();
252        let mut drivers = lock(&self.shared.drivers);
253        drivers.retain(|_, task| !task.is_finished());
254        if self.shared.shutdown.is_cancelled() {
255            return Err(PublicationError::ShuttingDown);
256        }
257        if drivers.contains_key(&resource) {
258            return Err(PublicationError::DuplicateResource);
259        }
260        let endpoint = lock(&self.shared.endpoint)
261            .clone()
262            .ok_or(PublicationError::NotAttached)?;
263        reserve(&self.shared)?;
264        let mut resources = lock(&self.shared.resources);
265        if resources.contains(&resource) {
266            release(&self.shared.counters);
267            return Err(PublicationError::DuplicateResource);
268        }
269        let (publisher, initial) = match Publisher::start(self.shared.config.clone(), start) {
270            Ok(started) => started,
271            Err(error) => {
272                release(&self.shared.counters);
273                return Err(error.into());
274            }
275        };
276        resources.insert(resource.clone());
277        drop(resources);
278        let (commands, command_rx) = mpsc::channel(DRIVER_QUEUE);
279        let (states, state_rx) = watch::channel(None);
280        let driver = Driver {
281            endpoint,
282            publisher,
283            resource: resource.clone(),
284            commands: command_rx,
285            states,
286            events: None,
287            response: None,
288            timers: HashMap::new(),
289            shared: Arc::clone(&self.shared),
290        };
291        let task = tokio::spawn(driver.run(initial));
292        drivers.insert(resource, task);
293        drop(drivers);
294        Ok(Publication {
295            commands,
296            states: state_rx,
297            body_limit: self.shared.config.body_limit,
298        })
299    }
300
301    /// The exact compositor allocation used by inbound PUBLISH.
302    #[must_use]
303    pub fn compositor(&self) -> Arc<Mutex<Compositor>> {
304        Arc::clone(&self.compositor)
305    }
306
307    /// Point-in-time owned-work counts.
308    #[must_use]
309    pub fn counts(&self) -> PublicationCounts {
310        PublicationCounts {
311            active_tasks: self.shared.counters.tasks.load(Ordering::Relaxed),
312            active_timers: self.shared.counters.timers.load(Ordering::Relaxed),
313            active_transactions: self.shared.counters.transactions.load(Ordering::Relaxed),
314            active_publishers: self.shared.counters.publishers.load(Ordering::Relaxed),
315            active_publications: lock(&self.compositor).len(),
316            shed: self.shared.counters.shed.load(Ordering::Relaxed),
317        }
318    }
319}
320
321/// Dispatcher-owned inbound compositor and outbound publisher runtime.
322pub struct Publications {
323    endpoint: Option<Handle>,
324    compositor: Arc<Mutex<Compositor>>,
325    composition: Arc<dyn PublicationComposition>,
326    authorization: Arc<dyn PublicationAuthorization>,
327    config: PublicationConfig,
328    expiry_tasks: HashMap<String, JoinHandle<()>>,
329    origin: Instant,
330    shared: Arc<Shared>,
331}
332
333impl std::fmt::Debug for Publications {
334    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        formatter
336            .debug_struct("Publications")
337            .field("config", &self.config)
338            .field("inbound_tasks", &self.expiry_tasks.len())
339            .finish_non_exhaustive()
340    }
341}
342
343impl Publications {
344    /// Construct a bounded service around the application's exact compositor and policies.
345    pub fn new(
346        config: PublicationConfig,
347        compositor: Compositor,
348        composition: Arc<dyn PublicationComposition>,
349        authorization: Arc<dyn PublicationAuthorization>,
350    ) -> Result<Self, PublicationError> {
351        config.validate()?;
352        let compositor = Arc::new(Mutex::new(compositor));
353        let counters = Arc::new(Counters::default());
354        Ok(Self {
355            endpoint: None,
356            compositor: Arc::clone(&compositor),
357            composition,
358            authorization,
359            config: config.clone(),
360            expiry_tasks: HashMap::new(),
361            origin: Instant::now(),
362            shared: Arc::new(Shared {
363                endpoint: Mutex::new(None),
364                resources: Mutex::new(HashSet::new()),
365                drivers: Mutex::new(HashMap::new()),
366                config: config.publisher,
367                counters,
368                shutdown: CancellationToken::new(),
369            }),
370        })
371    }
372
373    /// Application handle retained across dispatcher ownership transfer.
374    #[must_use]
375    pub fn handle(&self) -> PublicationsHandle {
376        PublicationsHandle {
377            shared: Arc::clone(&self.shared),
378            compositor: Arc::clone(&self.compositor),
379        }
380    }
381
382    pub(crate) fn attach(&mut self, endpoint: Handle) {
383        self.endpoint = Some(endpoint.clone());
384        *lock(&self.shared.endpoint) = Some(endpoint);
385    }
386
387    /// Consume one PUBLISH selected by the dispatcher.
388    #[allow(
389        clippy::too_many_lines,
390        reason = "the fail-closed inbound decision table stays in one visible protocol order"
391    )]
392    pub(crate) async fn receive(&mut self, incoming: &Incoming) {
393        self.expiry_tasks.retain(|_, task| !task.is_finished());
394        let Some(endpoint) = self.endpoint.clone() else {
395            return;
396        };
397        if !valid_request(&incoming.request) {
398            answer(&endpoint, incoming, 400, "Bad Request", None, None, None).await;
399            return;
400        }
401        if !self
402            .authorization
403            .authorize(&incoming.request, incoming.source, incoming.transport)
404        {
405            answer(&endpoint, incoming, 403, "Forbidden", None, None, None).await;
406            return;
407        }
408        if event(&incoming.request).as_deref() != Some("presence") {
409            answer(
410                &endpoint,
411                incoming,
412                489,
413                "Bad Event",
414                Some((HeaderName::AllowEvents, Bytes::from_static(b"presence"))),
415                None,
416                None,
417            )
418            .await;
419            return;
420        }
421        let Ok(tag) = conditional_tag(&incoming.request) else {
422            answer(&endpoint, incoming, 400, "Bad Request", None, None, None).await;
423            return;
424        };
425        let Some(expires) = requested_expiry(&incoming.request, self.config.default_expiry) else {
426            answer(&endpoint, incoming, 400, "Bad Request", None, None, None).await;
427            return;
428        };
429        if !expires.is_zero() && expires < self.config.minimum_expiry {
430            answer(
431                &endpoint,
432                incoming,
433                423,
434                "Interval Too Brief",
435                Some((
436                    HeaderName::MinExpires,
437                    Bytes::from(self.config.minimum_expiry.as_secs().to_string()),
438                )),
439                None,
440                None,
441            )
442            .await;
443            return;
444        }
445        if incoming.request.body().len() > self.config.body_limit {
446            answer(
447                &endpoint,
448                incoming,
449                413,
450                "Content Too Large",
451                None,
452                None,
453                None,
454            )
455            .await;
456            return;
457        }
458        let body = if incoming.request.body().is_empty() {
459            None
460        } else {
461            if incoming.request.headers.count(&HeaderName::ContentType) != 1
462                || incoming
463                    .request
464                    .headers
465                    .value(&HeaderName::ContentType)
466                    .as_deref()
467                    != Some(PIDF_TYPE.as_bytes())
468            {
469                answer(
470                    &endpoint,
471                    incoming,
472                    415,
473                    "Unsupported Media Type",
474                    None,
475                    None,
476                    None,
477                )
478                .await;
479                return;
480            }
481            let Ok(body) = std::str::from_utf8(incoming.request.body()) else {
482                answer(&endpoint, incoming, 400, "Bad Request", None, None, None).await;
483                return;
484            };
485            Some(body.to_owned())
486        };
487
488        let entity = String::from_utf8_lossy(&incoming.request.uri.to_bytes()).into_owned();
489        let now = self.origin.elapsed().as_secs();
490        let publication = Publish::read(tag, body, expires);
491        let outcome = {
492            let mut compositor = lock(&self.compositor);
493            compositor.expire(now);
494            let is_new = matches!(publication, Publish::Initial { .. })
495                && compositor.document(&entity).is_none();
496            if is_new && compositor.len() >= self.config.capacity {
497                None
498            } else {
499                Some(
500                    self.composition
501                        .apply(&mut compositor, &entity, publication, now),
502                )
503            }
504        };
505        let Some(outcome) = outcome else {
506            self.shared.counters.shed.fetch_add(1, Ordering::Relaxed);
507            answer(
508                &endpoint,
509                incoming,
510                503,
511                "Service Unavailable",
512                Some((
513                    HeaderName::RetryAfter,
514                    Bytes::from(RETRY_AFTER.as_secs().to_string()),
515                )),
516                None,
517                None,
518            )
519            .await;
520            return;
521        };
522        match outcome {
523            Published::Accepted { tag, expires } => {
524                answer(
525                    &endpoint,
526                    incoming,
527                    200,
528                    "OK",
529                    None,
530                    Some((&tag, expires)),
531                    None,
532                )
533                .await;
534                self.arm_expiry(entity, expires).await;
535            }
536            Published::Removed { tag } => {
537                answer(
538                    &endpoint,
539                    incoming,
540                    200,
541                    "OK",
542                    None,
543                    Some((&tag, Duration::ZERO)),
544                    None,
545                )
546                .await;
547                if let Some(task) = self.expiry_tasks.remove(&entity) {
548                    abort_and_join(task).await;
549                }
550            }
551            Published::ConditionFailed => {
552                answer(
553                    &endpoint,
554                    incoming,
555                    412,
556                    "Conditional Request Failed",
557                    None,
558                    None,
559                    None,
560                )
561                .await;
562            }
563            Published::Invalid => {
564                answer(&endpoint, incoming, 400, "Bad Request", None, None, None).await;
565            }
566            Published::Unavailable => {
567                answer(
568                    &endpoint,
569                    incoming,
570                    500,
571                    "Server Internal Error",
572                    None,
573                    None,
574                    None,
575                )
576                .await;
577            }
578        }
579    }
580
581    async fn arm_expiry(&mut self, entity: String, expires: Duration) {
582        if let Some(previous) = self.expiry_tasks.remove(&entity) {
583            abort_and_join(previous).await;
584        }
585        let compositor = Arc::clone(&self.compositor);
586        let counters = Arc::clone(&self.shared.counters);
587        let origin = self.origin;
588        self.expiry_tasks.insert(
589            entity,
590            tokio::spawn(async move {
591                let _guard = WorkGuard::timer(counters);
592                // Protocol timer: the duration is the publication expiry being asserted.
593                tokio::time::sleep(expires).await;
594                lock(&compositor).expire(origin.elapsed().as_secs());
595            }),
596        );
597    }
598
599    /// Cancel the dispatcher-owned service and join every task before returning.
600    pub(crate) async fn shutdown(&mut self) {
601        let drivers: Vec<_> = {
602            let mut drivers = lock(&self.shared.drivers);
603            self.shared.shutdown.cancel();
604            drivers.drain().map(|(_, task)| task).collect()
605        };
606        let expiry_tasks: Vec<_> = self.expiry_tasks.drain().map(|(_, task)| task).collect();
607        for task in expiry_tasks {
608            abort_and_join(task).await;
609        }
610        for task in drivers {
611            if let Err(error) = task.await {
612                tracing::warn!(%error, "publication driver did not join cleanly");
613            }
614        }
615    }
616}
617
618impl Drop for Publications {
619    fn drop(&mut self) {
620        let _drivers = lock(&self.shared.drivers);
621        self.shared.shutdown.cancel();
622        for task in self.expiry_tasks.values() {
623            task.abort();
624        }
625    }
626}
627
628#[derive(Debug)]
629enum Command {
630    Modify(Bytes, String, oneshot::Sender<Result<(), CommandError>>),
631    Remove(oneshot::Sender<Result<(), CommandError>>),
632}
633
634#[derive(Debug)]
635enum RuntimeEvent {
636    Response(Option<Response>),
637    Timer(Timer, u64),
638}
639
640struct Driver {
641    endpoint: Handle,
642    publisher: Publisher,
643    resource: Vec<u8>,
644    commands: mpsc::Receiver<Command>,
645    states: watch::Sender<Option<StateChange>>,
646    events: Option<(mpsc::Sender<RuntimeEvent>, mpsc::Receiver<RuntimeEvent>)>,
647    response: Option<JoinHandle<()>>,
648    timers: HashMap<Timer, JoinHandle<()>>,
649    shared: Arc<Shared>,
650}
651
652impl Driver {
653    async fn run(mut self, initial: Vec<Output>) {
654        let _guard = WorkGuard::publisher(Arc::clone(&self.shared.counters));
655        let (events, event_rx) = mpsc::channel(DRIVER_QUEUE);
656        self.events = Some((events, event_rx));
657        self.apply(initial).await;
658        while self.publisher.is_active() {
659            let input = {
660                let Some((_, events)) = self.events.as_mut() else {
661                    break;
662                };
663                tokio::select! {
664                    biased;
665                    () = self.shared.shutdown.cancelled() => DriverInput::Shutdown,
666                    command = self.commands.recv() => DriverInput::Command(command),
667                    event = events.recv() => DriverInput::Event(event),
668                }
669            };
670            let outputs = match input {
671                DriverInput::Command(Some(Command::Modify(body, content_type, reply))) => {
672                    command_result(self.publisher.modify(body, content_type), reply)
673                }
674                DriverInput::Command(Some(Command::Remove(reply))) => {
675                    command_result(self.publisher.remove(), reply)
676                }
677                DriverInput::Event(Some(RuntimeEvent::Response(response))) => {
678                    self.publisher.response(response.as_ref(), &new_cnonce())
679                }
680                DriverInput::Event(Some(RuntimeEvent::Timer(timer, generation))) => {
681                    self.publisher.timer_fired(timer, generation)
682                }
683                DriverInput::Shutdown | DriverInput::Command(None) | DriverInput::Event(None) => {
684                    self.publisher.shutdown_deadline()
685                }
686            };
687            self.apply(outputs).await;
688        }
689        if let Some(response) = self.response.take() {
690            abort_and_join(response).await;
691        }
692        for (_, timer) in self.timers.drain() {
693            abort_and_join(timer).await;
694        }
695        lock(&self.shared.resources).remove(&self.resource);
696    }
697
698    async fn apply(&mut self, outputs: Vec<Output>) {
699        for output in outputs {
700            match output {
701                Output::SendPublish { request, target } => self.send(*request, target).await,
702                Output::ArmTimer {
703                    timer,
704                    generation,
705                    after,
706                } => self.arm(timer, generation, after).await,
707                Output::CancelTimer { timer, .. } => {
708                    if let Some(task) = self.timers.remove(&timer) {
709                        abort_and_join(task).await;
710                    }
711                }
712                Output::StateChanged(change) => {
713                    // discard: no receiver means the application released its observation handle.
714                    let _ = self.states.send(Some(change));
715                }
716            }
717        }
718    }
719
720    async fn send(&mut self, request: Request, peer: sipx_ua::event_client::Peer) {
721        if let Some(previous) = self.response.take() {
722            abort_and_join(previous).await;
723        }
724        let Some((events, _)) = self.events.as_ref() else {
725            return;
726        };
727        match self.endpoint.send(request, transport_target(peer)).await {
728            Ok(mut responses) => {
729                let events = events.clone();
730                let counters = Arc::clone(&self.shared.counters);
731                self.response = Some(tokio::spawn(async move {
732                    let _guard = WorkGuard::transaction(counters);
733                    let response = responses.final_response().await;
734                    // discard: closure means the owning driver is already tearing down.
735                    let _ = events.send(RuntimeEvent::Response(response)).await;
736                }));
737            }
738            Err(error) => {
739                tracing::warn!(%error, "could not send PUBLISH");
740                // discard: closure means the owning driver is already tearing down.
741                let _ = events.try_send(RuntimeEvent::Response(None));
742            }
743        }
744    }
745
746    async fn arm(&mut self, timer: Timer, generation: u64, after: Duration) {
747        if let Some(previous) = self.timers.remove(&timer) {
748            abort_and_join(previous).await;
749        }
750        let Some((events, _)) = self.events.as_ref() else {
751            return;
752        };
753        let events = events.clone();
754        let counters = Arc::clone(&self.shared.counters);
755        self.timers.insert(
756            timer,
757            tokio::spawn(async move {
758                let _guard = WorkGuard::timer(counters);
759                // Protocol timer: the duration is the state-machine input this task represents.
760                tokio::time::sleep(after).await;
761                // discard: closure means the owning driver has cancelled this timer's state.
762                let _ = events.send(RuntimeEvent::Timer(timer, generation)).await;
763            }),
764        );
765    }
766}
767
768async fn abort_and_join(task: JoinHandle<()>) {
769    task.abort();
770    // discard: cancellation is the requested outcome; the await is solely the ownership barrier.
771    let _ = task.await;
772}
773
774enum DriverInput {
775    Command(Option<Command>),
776    Event(Option<RuntimeEvent>),
777    Shutdown,
778}
779
780fn command_result(
781    result: Result<Vec<Output>, CommandError>,
782    reply: oneshot::Sender<Result<(), CommandError>>,
783) -> Vec<Output> {
784    match result {
785        Ok(outputs) => {
786            // discard: the command still took effect if its caller stopped waiting for the reply.
787            let _ = reply.send(Ok(()));
788            outputs
789        }
790        Err(error) => {
791            // discard: the typed failure has no observer after its caller cancels the command.
792            let _ = reply.send(Err(error));
793            Vec::new()
794        }
795    }
796}
797
798enum WorkKind {
799    Publisher,
800    Timer,
801    Transaction,
802}
803
804struct WorkGuard {
805    counters: Arc<Counters>,
806    kind: WorkKind,
807}
808
809impl WorkGuard {
810    fn publisher(counters: Arc<Counters>) -> Self {
811        counters.tasks.fetch_add(1, Ordering::Relaxed);
812        Self {
813            counters,
814            kind: WorkKind::Publisher,
815        }
816    }
817
818    fn timer(counters: Arc<Counters>) -> Self {
819        counters.tasks.fetch_add(1, Ordering::Relaxed);
820        counters.timers.fetch_add(1, Ordering::Relaxed);
821        Self {
822            counters,
823            kind: WorkKind::Timer,
824        }
825    }
826
827    fn transaction(counters: Arc<Counters>) -> Self {
828        counters.transactions.fetch_add(1, Ordering::Relaxed);
829        Self {
830            counters,
831            kind: WorkKind::Transaction,
832        }
833    }
834}
835
836impl Drop for WorkGuard {
837    fn drop(&mut self) {
838        match self.kind {
839            WorkKind::Publisher => {
840                self.counters.tasks.fetch_sub(1, Ordering::Relaxed);
841                self.counters.publishers.fetch_sub(1, Ordering::Relaxed);
842            }
843            WorkKind::Timer => {
844                self.counters.tasks.fetch_sub(1, Ordering::Relaxed);
845                self.counters.timers.fetch_sub(1, Ordering::Relaxed);
846            }
847            WorkKind::Transaction => {
848                self.counters.transactions.fetch_sub(1, Ordering::Relaxed);
849            }
850        }
851    }
852}
853
854// `fetch_update` remains the spelling available at the workspace MSRV; current nightly deprecates
855// it before the replacement is available on that supported toolchain.
856#[allow(deprecated)]
857fn reserve(shared: &Shared) -> Result<(), PublicationError> {
858    shared
859        .counters
860        .publishers
861        .fetch_update(Ordering::AcqRel, Ordering::Relaxed, |current| {
862            (current < shared.config.capacity).then_some(current.saturating_add(1))
863        })
864        .map_err(|_| {
865            shared.counters.shed.fetch_add(1, Ordering::Relaxed);
866            PublicationError::CapacityExceeded
867        })?;
868    Ok(())
869}
870
871fn release(counters: &Counters) {
872    counters.publishers.fetch_sub(1, Ordering::Relaxed);
873}
874
875fn valid_request(request: &Request) -> bool {
876    request.method == Method::Publish
877        && request.headers.count(&HeaderName::CallId) == 1
878        && request.headers.count(&HeaderName::From) == 1
879        && request.headers.count(&HeaderName::To) == 1
880        && request.headers.count(&HeaderName::CSeq) == 1
881        && matches!(
882            request.headers.typed::<CSeq>(),
883            Some(Ok(CSeq {
884                method: Method::Publish,
885                ..
886            }))
887        )
888}
889
890fn event(request: &Request) -> Option<String> {
891    if request.headers.count(&HeaderName::Event) != 1 {
892        return None;
893    }
894    let value = request.headers.value(&HeaderName::Event)?;
895    let token = std::str::from_utf8(&value).ok()?.split(';').next()?.trim();
896    (!token.is_empty()).then(|| token.to_ascii_lowercase())
897}
898
899fn conditional_tag(request: &Request) -> Result<Option<String>, ()> {
900    match request.headers.count(&HeaderName::SipIfMatch) {
901        0 => Ok(None),
902        1 => {
903            let value = request.headers.value(&HeaderName::SipIfMatch).ok_or(())?;
904            if opaque_token(&value) {
905                Ok(Some(String::from_utf8_lossy(&value).into_owned()))
906            } else {
907                Err(())
908            }
909        }
910        _ => Err(()),
911    }
912}
913
914fn requested_expiry(request: &Request, default: Duration) -> Option<Duration> {
915    match request.headers.count(&HeaderName::Expires) {
916        0 => Some(default),
917        1 => request
918            .headers
919            .typed::<Expires>()?
920            .ok()
921            .map(|expires| Duration::from_secs(u64::from(expires.0))),
922        _ => None,
923    }
924}
925
926fn opaque_token(value: &[u8]) -> bool {
927    !value.is_empty()
928        && value.iter().all(|byte| {
929            byte.is_ascii_alphanumeric()
930                || matches!(
931                    byte,
932                    b'-' | b'.' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
933                )
934        })
935}
936
937async fn answer(
938    endpoint: &Handle,
939    incoming: &Incoming,
940    status: u16,
941    reason: &'static str,
942    extra: Option<(HeaderName, Bytes)>,
943    authority: Option<(&str, Duration)>,
944    to_tag: Option<&str>,
945) {
946    let Some(status) = StatusCode::new(status) else {
947        return;
948    };
949    let built = ResponseBuilder::to_request(&incoming.request, status, reason)
950        .and_then(|builder| with_to_tag(builder, &incoming.request, to_tag))
951        .and_then(|builder| match extra {
952            Some((name, value)) => builder.header(name, value),
953            None => Ok(builder),
954        })
955        .and_then(|builder| match authority {
956            Some((tag, expires)) => builder
957                .header(HeaderName::SipETag, Bytes::from(tag.to_owned()))?
958                .header(
959                    HeaderName::Expires,
960                    Bytes::from(expires.as_secs().to_string()),
961                ),
962            None => Ok(builder),
963        });
964    let Ok(builder) = built else {
965        return;
966    };
967    if let Err(error) = endpoint.respond(&incoming.key, builder.build()).await {
968        tracing::warn!(%error, "could not answer PUBLISH");
969    }
970}
971
972fn transport(value: sipx_ua::event_client::Transport) -> TransportKind {
973    match value {
974        sipx_ua::event_client::Transport::Udp => TransportKind::Udp,
975        sipx_ua::event_client::Transport::Tcp => TransportKind::Tcp,
976        sipx_ua::event_client::Transport::Tls => TransportKind::Tls,
977        sipx_ua::event_client::Transport::Ws => TransportKind::Ws,
978        sipx_ua::event_client::Transport::Wss => TransportKind::Wss,
979        sipx_ua::event_client::Transport::Quic => TransportKind::Quic,
980    }
981}
982
983fn transport_target(peer: sipx_ua::event_client::Peer) -> Target {
984    let mut target = Target::new(peer.address, transport(peer.transport));
985    if let Some(identity) = peer.identity {
986        target = target.verifying(identity);
987    }
988    if let Some(path) = peer.path {
989        target = target.at_path(path);
990    }
991    target
992}
993
994fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
995    mutex.lock().unwrap_or_else(PoisonError::into_inner)
996}
997
998#[cfg(test)]
999#[allow(
1000    clippy::unwrap_used,
1001    clippy::expect_used,
1002    clippy::panic,
1003    clippy::indexing_slicing
1004)]
1005mod admission_tests {
1006    use std::sync::{Arc, Barrier};
1007    use std::time::Duration;
1008
1009    use bytes::Bytes;
1010    use sipx_transport::{Config as TransportConfig, bind};
1011    use sipx_ua::event_client::{Peer, Transport};
1012    use sipx_ua::presence::Compositor;
1013    use sipx_ua::publication_client::Start;
1014
1015    use super::*;
1016
1017    fn start(target: std::net::SocketAddr) -> Start {
1018        Start {
1019            resource: sipx_sip::Uri::parse(Bytes::from_static(b"sip:resource@example.test"))
1020                .expect("URI"),
1021            local_identity: "<sip:client@example.test>".to_owned(),
1022            target: Peer::new(target, Transport::Udp),
1023            event: "presence".to_owned(),
1024            expires: Duration::from_secs(60),
1025            body: Bytes::from_static(b"<presence/>"),
1026            content_type: "application/pidf+xml".to_owned(),
1027            credentials: None,
1028            call_id: "admission@example.test".to_owned(),
1029            from_tag: "admission".to_owned(),
1030            initial_cseq: 1,
1031        }
1032    }
1033
1034    #[tokio::test]
1035    async fn racing_shutdown_closes_admission_before_any_spawn() {
1036        let (endpoint, _) = bind(TransportConfig::new(
1037            "127.0.0.1:0".parse().expect("address"),
1038        ))
1039        .await
1040        .expect("endpoint");
1041        let mut runtime = Publications::new(
1042            PublicationConfig::default(),
1043            Compositor::new(Duration::from_secs(60)),
1044            Arc::new(ReplacePublicationState),
1045            Arc::new(AllowPublications),
1046        )
1047        .expect("runtime");
1048        runtime.attach(endpoint.clone());
1049        let handle = runtime.handle();
1050        let post_shutdown = handle.clone();
1051        let shared = Arc::clone(&runtime.shared);
1052        let drivers = lock(&shared.drivers);
1053        let barrier = Arc::new(Barrier::new(2));
1054        let contender = Arc::clone(&barrier);
1055        let target = endpoint.local_addr();
1056        let attempt = std::thread::spawn(move || {
1057            contender.wait();
1058            handle.publish(start(target))
1059        });
1060        barrier.wait();
1061        shared.shutdown.cancel();
1062        drop(drivers);
1063        assert!(matches!(
1064            attempt.join().expect("thread"),
1065            Err(PublicationError::ShuttingDown)
1066        ));
1067        assert!(matches!(
1068            post_shutdown.publish(start(target)),
1069            Err(PublicationError::ShuttingDown)
1070        ));
1071        assert!(lock(&shared.drivers).is_empty());
1072        endpoint.shutdown().await;
1073    }
1074}
1075
1076#[cfg(test)]
1077#[allow(
1078    clippy::unwrap_used,
1079    clippy::expect_used,
1080    clippy::panic,
1081    clippy::indexing_slicing
1082)]
1083mod tests {
1084    use super::*;
1085    use sipx_ua::event_client::{Peer, Transport};
1086
1087    #[test]
1088    fn publication_driver_preserves_secure_target_identity_and_resource() {
1089        let peer = Peer::new("192.0.2.20:7443".parse().expect("peer"), Transport::Wss)
1090            .verifying("compositor.example.test")
1091            .at_path("/publish");
1092        let target = transport_target(peer);
1093        assert_eq!(target.transport, TransportKind::Wss);
1094        assert_eq!(target.verify_as.as_deref(), Some("compositor.example.test"));
1095        assert_eq!(target.path.as_deref(), Some("/publish"));
1096    }
1097}