1use bytes::Bytes;
4use sipx_sip::{
5 BuildError, Header, HeaderError, HeaderName, HistoryInfo, ReasonValue, Request,
6 TargetChangeKind, Uri, UriError,
7};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum RetargetError {
14 #[error("cannot read diversion history: {0}")]
16 History(#[from] HeaderError),
17 #[error("cannot build retargeted request: {0}")]
19 Build(#[from] BuildError),
20 #[error("cannot replace request target: {0}")]
22 Uri(#[from] UriError),
23}
24
25pub 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}