1use tokio::sync::mpsc::Sender;
2
3pub trait Phone: Send {
4 fn register(&self, aor: &str, regint: u32);
5 fn unregister(&self) {}
10
11 fn dial(&self, number: &str);
12 fn hangup(&self);
13 fn hangup_all(&self);
14 fn accept(&self);
15 fn hold(&self);
16 fn resume(&self);
17 fn mute(&self);
18 fn set_speaker_muted(&self, muted: bool) {
26 let _ = muted;
27 }
28
29 fn silence_alert(&self) {}
34 fn send_dtmf(&self, digit: char);
35 fn switch_line(&self, line: usize);
36 fn select_call(&self, call_id: &str) {
40 let _ = call_id;
41 }
42 fn transfer(&self, uri: &str);
43 fn attended_transfer_start(&self, uri: &str);
44 fn attended_transfer_exec(&self);
45 fn attended_transfer_abort(&self);
46 fn add_header(&self, key: &str, value: &str);
47 fn rm_header(&self, key: &str);
48 fn set_audio_source(&self, spec: &str);
52
53 fn media_stats(&self) -> Option<crate::event::MediaStats> {
57 None
58 }
59
60 fn audio_codec(&self) -> Option<crate::event::CodecInfo> {
63 None
64 }
65
66 fn received_dtmf(&self) -> String {
69 String::new()
70 }
71
72 fn inbound_headers(&self, call_id: &str) -> Vec<(String, String)> {
76 let _ = call_id;
77 Vec::new()
78 }
79
80 fn arm_invite_response(&self, scode: u16, reason: &str, headers: Vec<String>);
86
87 fn disarm_invite_response(&self);
89
90 fn deflect_incoming(&self, contact: &str, diversion: Option<&str>) {
94 let mut headers = vec![format!("Contact: <{contact}>")];
95 if let Some(div) = diversion {
96 headers.push(format!("Diversion: <{div}>"));
97 }
98 self.arm_invite_response(302, "Moved Temporarily", headers);
99 }
100}
101
102pub struct MockPhone {
110 cmd_tx: Sender<(String, String)>,
111}
112
113impl MockPhone {
114 pub fn new(cmd_tx: Sender<(String, String)>) -> Self {
115 Self { cmd_tx }
116 }
117
118 fn send(&self, cmd: &str, params: &str) {
119 if let Err(e) = self.cmd_tx.try_send((cmd.to_string(), params.to_string())) {
120 crate::rlog!(Warn, "cmd dropped: {} ({})", cmd, e);
121 }
122 }
123}
124
125impl Phone for MockPhone {
126 fn register(&self, _aor: &str, regint: u32) {
127 self.send("uareg", &format!("{} 0", regint));
128 }
129 fn unregister(&self) {
130 self.send("unregister", "");
131 }
132 fn dial(&self, number: &str) {
133 self.send("dial", number);
134 }
135 fn hangup(&self) {
136 self.send("hangup", "");
137 }
138 fn hangup_all(&self) {
139 self.send("hangupall", "");
140 }
141 fn accept(&self) {
142 self.send("accept", "");
143 }
144 fn hold(&self) {
145 self.send("hold", "");
146 }
147 fn resume(&self) {
148 self.send("resume", "");
149 }
150 fn mute(&self) {
151 self.send("mute", "");
152 }
153 fn silence_alert(&self) {
154 self.send("silence", "");
155 }
156 fn set_speaker_muted(&self, muted: bool) {
157 self.send("speaker", if muted { "off" } else { "on" });
158 }
159 fn send_dtmf(&self, digit: char) {
160 self.send("sndcode", &digit.to_string());
161 }
162 fn switch_line(&self, line: usize) {
163 self.send("line", &line.to_string());
164 }
165 fn transfer(&self, uri: &str) {
166 self.send("transfer", uri);
167 }
168 fn attended_transfer_start(&self, uri: &str) {
169 self.send("atransferstart", uri);
170 }
171 fn attended_transfer_exec(&self) {
172 self.send("atransferexec", "");
173 }
174 fn attended_transfer_abort(&self) {
175 self.send("atransferabort", "");
176 }
177 fn add_header(&self, key: &str, value: &str) {
178 self.send(
179 "uaaddheader",
180 &format!("{}={} 0", key, uri_header_escape(value)),
181 );
182 }
183 fn rm_header(&self, key: &str) {
184 self.send("uarmheader", &format!("{} 0", key));
185 }
186 fn set_audio_source(&self, spec: &str) {
187 self.send("ausrc", spec);
188 }
189 fn arm_invite_response(&self, scode: u16, reason: &str, headers: Vec<String>) {
190 self.send(
191 "armresponse",
192 &format!("{scode} {reason} [{}]", headers.join("; ")),
193 );
194 }
195 fn disarm_invite_response(&self) {
196 self.send("disarmresponse", "");
197 }
198}
199
200fn uri_header_escape(s: &str) -> String {
208 let mut out = String::with_capacity(s.len());
209 for &b in s.as_bytes() {
210 if is_hvalue(b) {
211 out.push(b as char);
212 } else {
213 out.push_str(&format!("%{:02X}", b));
214 }
215 }
216 out
217}
218
219fn is_hvalue(b: u8) -> bool {
220 b.is_ascii_alphanumeric()
221 || matches!(
222 b,
223 b'-' | b'_'
224 | b'.'
225 | b'!'
226 | b'~'
227 | b'*'
228 | b'\''
229 | b'('
230 | b')'
231 | b'['
232 | b']'
233 | b'/'
234 | b'?'
235 | b':'
236 | b'+'
237 | b'$'
238 )
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn passes_through_hvalue_chars() {
247 assert_eq!(uri_header_escape("Foo-Bar_1.0"), "Foo-Bar_1.0");
248 assert_eq!(uri_header_escape("a[b]/c?d:e+f$"), "a[b]/c?d:e+f$");
249 }
250
251 #[test]
252 fn escapes_spaces_and_specials() {
253 assert_eq!(uri_header_escape("Foo Bar"), "Foo%20Bar");
254 assert_eq!(uri_header_escape("a=b;c,d"), "a%3Db%3Bc%2Cd");
255 assert_eq!(uri_header_escape("100%"), "100%25");
256 }
257
258 #[test]
259 fn escapes_non_ascii() {
260 assert_eq!(uri_header_escape("ä"), "%C3%A4");
261 }
262
263 fn make_phone() -> (MockPhone, tokio::sync::mpsc::Receiver<(String, String)>) {
264 let (tx, rx) = tokio::sync::mpsc::channel(8);
265 (MockPhone::new(tx), rx)
266 }
267
268 #[test]
269 fn add_header_emits_uaaddheader_with_ua_index_zero() {
270 let (phone, mut rx) = make_phone();
271 phone.add_header("X-Foo", "bar");
272 let (cmd, params) = rx.try_recv().expect("one message");
273 assert_eq!(cmd, "uaaddheader");
274 assert_eq!(params, "X-Foo=bar 0");
275 }
276
277 #[test]
278 fn add_header_encodes_unsafe_chars_in_value() {
279 let (phone, mut rx) = make_phone();
280 phone.add_header("History-Info", "<sip:1@x.com>;index=1");
281 let (_, params) = rx.try_recv().expect("one message");
282 assert_eq!(params, "History-Info=%3Csip:1%40x.com%3E%3Bindex%3D1 0");
283 }
284
285 #[test]
286 fn add_header_preserves_order_across_multiple_calls() {
287 let (phone, mut rx) = make_phone();
288 phone.add_header("History-Info", "<sip:1@x.com>;index=1");
289 phone.add_header("History-Info", "<sip:2@x.com>;index=2");
290 phone.add_header("X-Other", "hi");
291
292 let msgs: Vec<_> = std::iter::from_fn(|| rx.try_recv().ok()).collect();
293 assert_eq!(msgs.len(), 3);
294 assert_eq!(msgs[0].1, "History-Info=%3Csip:1%40x.com%3E%3Bindex%3D1 0");
295 assert_eq!(msgs[1].1, "History-Info=%3Csip:2%40x.com%3E%3Bindex%3D2 0");
296 assert_eq!(msgs[2].1, "X-Other=hi 0");
297 }
298}