Skip to main content

wavekat_sip/
inbound.rs

1//! Inbound in-dialog requests surfaced to the consumer.
2//!
3//! By default [`SipEndpoint`](crate::SipEndpoint) auto-answers every in-dialog
4//! request (`BYE` / `OPTIONS` / `INFO` / re-`INVITE`) with `200 OK`. A consumer
5//! that needs to *handle* a peer's re-`INVITE` (e.g. answer an RFC 4028 session
6//! refresh with a fresh SDP answer, or apply a peer-initiated hold) or read an
7//! inbound `INFO` (e.g. SIP-INFO DTMF) opts in via
8//! [`Call::inbound_requests`](crate::Call::inbound_requests). From then on the
9//! endpoint routes that dialog's re-`INVITE` / `INFO` here instead of
10//! auto-answering them; the consumer answers each [`InboundRequest`] itself.
11//!
12//! `BYE` and `OPTIONS` are always auto-answered by the endpoint, even after
13//! opting in — call teardown and keepalive don't need consumer involvement.
14
15use std::sync::Arc;
16
17use rsip::{Headers, Method, Request, StatusCode};
18
19use crate::stack::response::{build_response, ResponseBody};
20use crate::stack::transaction::TransactionKey;
21use crate::stack::ua::Ua;
22
23/// A single inbound in-dialog request (a peer re-`INVITE` or `INFO`) awaiting a
24/// response from the consumer.
25///
26/// Inspect it with [`method`](Self::method) / [`headers`](Self::headers) /
27/// [`body`](Self::body), then answer exactly once with
28/// [`respond`](Self::respond) or [`ok`](Self::ok). Dropping it without
29/// answering leaves the peer's transaction to time out.
30pub struct InboundRequest {
31    ua: Arc<Ua>,
32    key: TransactionKey,
33    request: Request,
34}
35
36impl InboundRequest {
37    pub(crate) fn new(ua: Arc<Ua>, key: TransactionKey, request: Request) -> Self {
38        Self { ua, key, request }
39    }
40
41    /// The request method — [`Method::Invite`] for a re-INVITE (re-offer /
42    /// session refresh), [`Method::Info`] for SIP INFO, [`Method::Notify`] for a
43    /// transfer-progress sipfrag, or [`Method::Refer`] for a peer-initiated
44    /// transfer.
45    pub fn method(&self) -> &Method {
46        self.request.method()
47    }
48
49    /// The request's headers (e.g. to read `Session-Expires` on a refresh).
50    pub fn headers(&self) -> &Headers {
51        &self.request.headers
52    }
53
54    /// The request body (e.g. an SDP re-offer or a `application/dtmf-relay`
55    /// INFO payload).
56    pub fn body(&self) -> &[u8] {
57        &self.request.body
58    }
59
60    /// The `CSeq` method+number is in [`headers`](Self::headers); this is the
61    /// raw request for callers that need full access.
62    pub fn request(&self) -> &Request {
63        &self.request
64    }
65
66    /// Answer with `status`, optional `extra_headers`, and an optional SDP body
67    /// (sent as `application/sdp`). Returns `true` if the engine sent it.
68    ///
69    /// Use this to answer a re-INVITE: `200 OK` with the SDP answer for a
70    /// session refresh or a peer hold, or a non-2xx to decline.
71    pub async fn respond(
72        self,
73        status: StatusCode,
74        extra_headers: Vec<rsip::Header>,
75        sdp: Option<Vec<u8>>,
76    ) -> bool {
77        let body = sdp.map(|bytes| ResponseBody {
78            content_type: "application/sdp",
79            bytes,
80        });
81        let Some(mut response) = build_response(&self.request, status, None, None, body) else {
82            return false;
83        };
84        for header in extra_headers {
85            response.headers.push(header);
86        }
87        self.ua.answer(self.key, response).await
88    }
89
90    /// Answer `200 OK` with no body — the right reply for an `INFO`, or a
91    /// re-INVITE you accept without renegotiating media.
92    pub async fn ok(self) -> bool {
93        self.respond(StatusCode::OK, Vec::new(), None).await
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use rsip::message::HeadersExt;
101
102    fn info_request() -> Request {
103        let raw = "INFO sip:bob@10.0.0.1:5060 SIP/2.0\r\n\
104             Via: SIP/2.0/UDP 1.2.3.4:5060;branch=z9hG4bK-info\r\n\
105             From: <sip:alice@atlanta.example.com>;tag=alice\r\n\
106             To: <sip:bob@biloxi.example.com>;tag=ourtag\r\n\
107             Call-ID: call-dlg\r\n\
108             CSeq: 2 INFO\r\n\
109             Content-Type: application/dtmf-relay\r\n\
110             Content-Length: 21\r\n\r\n\
111             Signal=5\nDuration=160";
112        Request::try_from(raw.as_bytes()).unwrap()
113    }
114
115    #[test]
116    fn exposes_method_headers_and_body() {
117        // Construction needs a Ua, which needs a runtime; the accessors are
118        // pure on the request, so test them on the parsed request directly.
119        let req = info_request();
120        assert_eq!(*req.method(), Method::Info);
121        assert_eq!(req.body, b"Signal=5\nDuration=160");
122        assert!(req.cseq_header().is_ok());
123    }
124}