Skip to main content

sipx_ua/
history.rs

1//! User-agent retargeting with RFC 7044 diversion history.
2
3use bytes::Bytes;
4use sipx_sip::{
5    BuildError, Header, HeaderError, HeaderName, HistoryInfo, ReasonValue, Request,
6    TargetChangeKind, Uri, UriError,
7};
8use thiserror::Error;
9
10/// Why a request could not be safely retargeted.
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum RetargetError {
14    /// The received History-Info cache is malformed.
15    #[error("cannot read diversion history: {0}")]
16    History(#[from] HeaderError),
17    /// A generated header could not be represented safely.
18    #[error("cannot build retargeted request: {0}")]
19    Build(#[from] BuildError),
20    /// A replacement target could not be represented as a URI.
21    #[error("cannot replace request target: {0}")]
22    Uri(#[from] UriError),
23}
24
25/// Retarget a request and extend its diversion history.
26///
27/// This is the UA operation from RFC 7044 ยงยง9.1-9.2. It retains every received entry, exposes a
28/// missing previous target with a `.0` index, embeds the reason in the previous SIP/SIPS URI,
29/// appends the new target at `.1`, and applies history privacy before the request is emitted.
30pub fn retarget(
31    request: &Request,
32    next: Uri,
33    reason: &ReasonValue,
34    kind: TargetChangeKind,
35) -> Result<Request, RetargetError> {
36    let history = HistoryInfo::from_headers(&request.headers)
37        .transpose()?
38        .unwrap_or_default()
39        .retargeted(request.uri.clone(), next.clone(), reason, kind);
40    let mut history = history;
41    history.apply_message_privacy(&request.headers)?;
42
43    let mut retargeted = request.clone();
44    retargeted.set_uri(next)?;
45    retargeted.headers.remove_all(&HeaderName::HistoryInfo);
46    retargeted
47        .headers
48        .push(Header::build(HeaderName::HistoryInfo, history.to_bytes())?);
49    let advertises_history = retargeted
50        .headers
51        .typed_all::<sipx_sip::headers::Supported>()
52        .filter_map(Result::ok)
53        .any(|tags| tags.contains("histinfo"));
54    if !advertises_history {
55        retargeted.headers.push(Header::build(
56            HeaderName::Supported,
57            Bytes::from_static(b"histinfo"),
58        )?);
59    }
60    Ok(retargeted)
61}
62
63#[cfg(test)]
64#[allow(
65    clippy::unwrap_used,
66    clippy::expect_used,
67    clippy::panic,
68    clippy::indexing_slicing
69)]
70mod tests {
71    use super::*;
72    use sipx_sip::{Limits, StatusCode, parse_datagram};
73
74    #[test]
75    fn a_retargeted_request_carries_the_previous_target_and_the_reason_it_moved() {
76        let request = parse_datagram(
77            Bytes::from_static(
78                b"INVITE sip:alice@example.test SIP/2.0\r\n\
79                  Supported: timer\r\n\
80                  Content-Length: 0\r\n\r\n",
81            ),
82            &Limits::datagram(),
83        )
84        .unwrap()
85        .as_request()
86        .unwrap()
87        .clone();
88        let next = Uri::parse(Bytes::from_static(b"sip:bob@example.test")).unwrap();
89        let moved = retarget(
90            &request,
91            next,
92            &ReasonValue::sip(StatusCode::new(302).unwrap(), None),
93            TargetChangeKind::Mp,
94        )
95        .unwrap();
96
97        assert_eq!(
98            moved.uri.to_bytes(),
99            Bytes::from_static(b"sip:bob@example.test")
100        );
101        assert_eq!(
102            moved.headers.value(&HeaderName::HistoryInfo).as_deref(),
103            Some(
104                &b"<sip:alice@example.test?Reason=SIP%3Bcause%3D302>;index=1, <sip:bob@example.test>;index=1.1;mp=1"[..]
105            )
106        );
107        let mut wire = Vec::new();
108        moved.write_to(&mut wire);
109        assert!(
110            String::from_utf8_lossy(&wire).starts_with("INVITE sip:bob@example.test SIP/2.0\r\n")
111        );
112    }
113
114    #[test]
115    fn retargeting_applies_message_history_privacy() {
116        let request = parse_datagram(
117            Bytes::from_static(
118                b"INVITE sip:alice@example.test SIP/2.0\r\n\
119                  Privacy: history\r\n\
120                  Content-Length: 0\r\n\r\n",
121            ),
122            &Limits::datagram(),
123        )
124        .unwrap()
125        .as_request()
126        .unwrap()
127        .clone();
128        let next = Uri::parse(Bytes::from_static(b"sip:bob@example.test")).unwrap();
129        let moved = retarget(
130            &request,
131            next,
132            &ReasonValue::sip(StatusCode::new(302).unwrap(), None),
133            TargetChangeKind::Mp,
134        )
135        .unwrap();
136        assert_eq!(
137            moved.headers.value(&HeaderName::HistoryInfo).as_deref(),
138            Some(
139                &b"<sip:anonymous@anonymous.invalid>;index=1, <sip:anonymous@anonymous.invalid>;index=1.1;mp=1"[..]
140            )
141        );
142    }
143}