1use crate::media::{ambiance::AmbianceOption, recorder::RecorderFormat};
2use crate::useragent::RegisterOption;
3use anyhow::{Error, Result};
4use clap::Parser;
5use rsipstack::dialog::invitation::InviteOption;
6use rsipstack::rsip::uri::{Auth, HostWithPort, Uri};
7use rustrtc::IceServer;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Parser, Debug)]
12#[command(version)]
13pub struct Cli {
14 #[clap(long)]
16 pub conf: Option<String>,
17 #[clap(long)]
19 pub http: Option<String>,
20
21 #[clap(long)]
23 pub sip: Option<String>,
24
25 #[clap(long)]
27 pub handler: Option<String>,
28
29 #[clap(long)]
31 pub call: Option<String>,
32
33 #[clap(long)]
35 pub external_ip: Option<String>,
36
37 #[clap(long, value_delimiter = ',')]
39 pub codecs: Option<Vec<String>>,
40
41 #[cfg(feature = "offline")]
43 #[clap(long)]
44 pub download_models: Option<String>,
45
46 #[cfg(feature = "offline")]
48 #[clap(long, default_value = "./models")]
49 pub models_dir: String,
50
51 #[cfg(feature = "offline")]
53 #[clap(long)]
54 pub exit_after_download: bool,
55}
56
57pub(crate) fn default_config_recorder_path() -> String {
58 #[cfg(target_os = "windows")]
59 return "./config/recorders".to_string();
60 #[cfg(not(target_os = "windows"))]
61 return "./config/recorders".to_string();
62}
63
64fn default_config_media_cache_path() -> String {
65 #[cfg(target_os = "windows")]
66 return "./config/mediacache".to_string();
67 #[cfg(not(target_os = "windows"))]
68 return "./config/mediacache".to_string();
69}
70
71fn default_config_http_addr() -> String {
72 "0.0.0.0:8080".to_string()
73}
74
75fn default_sip_addr() -> String {
76 "0.0.0.0".to_string()
77}
78
79fn default_sip_port() -> u16 {
80 25060
81}
82
83fn default_config_rtp_start_port() -> Option<u16> {
84 Some(12000)
85}
86
87fn default_config_rtp_end_port() -> Option<u16> {
88 Some(42000)
89}
90
91fn default_config_rtp_latching() -> Option<bool> {
92 Some(true)
93}
94
95fn default_graceful_shutdown() -> Option<bool> {
96 Some(true)
97}
98
99fn default_graceful_shutdown_timeout() -> Option<u64> {
100 Some(30)
101}
102
103fn default_config_useragent() -> Option<String> {
104 Some(format!(
105 "active-call({} miuda.ai)",
106 env!("CARGO_PKG_VERSION")
107 ))
108}
109
110fn default_enable_options_response() -> Option<bool> {
111 Some(true)
112}
113
114fn default_codecs() -> Option<Vec<String>> {
115 let codecs = vec![
116 "pcmu".to_string(),
117 "pcma".to_string(),
118 "g722".to_string(),
119 "g729".to_string(),
120 "opus".to_string(),
121 "telephone_event".to_string(),
122 ];
123 Some(codecs)
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize, Default)]
127#[serde(rename_all = "snake_case")]
128pub struct RecordingPolicy {
129 #[serde(default)]
130 pub enabled: bool,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub auto_start: Option<bool>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub filename_pattern: Option<String>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub samplerate: Option<u32>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub ptime: Option<u32>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub path: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub format: Option<RecorderFormat>,
143}
144
145impl RecordingPolicy {
146 pub fn recorder_path(&self) -> String {
147 self.path
148 .as_ref()
149 .map(|p| p.trim())
150 .filter(|p| !p.is_empty())
151 .map(|p| p.to_string())
152 .unwrap_or_else(default_config_recorder_path)
153 }
154
155 pub fn recorder_format(&self) -> RecorderFormat {
156 self.format.unwrap_or_default()
157 }
158
159 pub fn ensure_defaults(&mut self) -> bool {
160 if self
161 .path
162 .as_ref()
163 .map(|p| p.trim().is_empty())
164 .unwrap_or(true)
165 {
166 self.path = Some(default_config_recorder_path());
167 }
168
169 false
170 }
171}
172
173#[derive(Debug, Clone, Deserialize, Serialize)]
174pub struct RewriteRule {
175 pub r#match: String,
176 pub rewrite: String,
177}
178
179#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
192#[serde(rename_all = "snake_case")]
193pub struct TrunkRule {
194 pub rule: TrunkRuleDef,
195}
196
197#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
198#[serde(rename_all = "snake_case")]
199pub struct TrunkRuleDef {
200 #[serde(default, rename = "match")]
201 pub r#match: TrunkMatch,
202 pub rewrite: TrunkRewrite,
203}
204
205#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
209#[serde(rename_all = "snake_case")]
210pub struct TrunkMatch {
211 pub from: Option<UriMatch>,
212 pub to: Option<UriMatch>,
213}
214
215#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
218#[serde(rename_all = "snake_case")]
219pub struct UriMatch {
220 pub user: Option<String>,
222 pub host: Option<String>,
224}
225
226#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
228#[serde(rename_all = "snake_case")]
229pub struct TrunkRewrite {
230 pub from: Option<UriRewrite>,
231 pub to: Option<UriRewrite>,
232 pub contact: Option<UriRewrite>,
233}
234
235#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
239#[serde(rename_all = "snake_case")]
240pub struct UriRewrite {
241 pub user: Option<String>,
242 pub host: Option<String>,
243}
244
245impl TrunkRule {
246 fn matches(&self, invite: &InviteOption) -> bool {
247 self.rule.r#match.matches(invite)
248 }
249
250 fn apply(&self, invite: &mut InviteOption) {
251 self.rule.rewrite.apply(invite);
252 }
253}
254
255impl TrunkMatch {
256 fn matches(&self, invite: &InviteOption) -> bool {
257 let from_ok = self
258 .from
259 .as_ref()
260 .map(|from| from.matches(&invite.caller))
261 .unwrap_or(true);
262 let to_ok = self
263 .to
264 .as_ref()
265 .map(|to| to.matches(&invite.callee))
266 .unwrap_or(true);
267 from_ok && to_ok
268 }
269}
270
271impl UriMatch {
272 fn matches(&self, uri: &Uri) -> bool {
273 let user_ok = self
274 .user
275 .as_ref()
276 .map(|user| {
277 uri.auth
278 .as_ref()
279 .map(|auth| regex_is_match(user, &auth.user))
280 .unwrap_or(false)
281 })
282 .unwrap_or(true);
283 let host_ok = self
284 .host
285 .as_ref()
286 .map(|host| regex_is_match(host, &uri.host_with_port.host.to_string()))
287 .unwrap_or(true);
288 user_ok && host_ok
289 }
290}
291
292impl TrunkRewrite {
293 fn apply(&self, invite: &mut InviteOption) {
294 if let Some(from) = &self.from {
295 from.apply(&mut invite.caller);
296 }
297 if let Some(to) = &self.to {
298 to.apply(&mut invite.callee);
299 }
300 if let Some(contact) = &self.contact {
301 contact.apply(&mut invite.contact);
302 }
303 }
304}
305
306impl UriRewrite {
307 fn apply(&self, uri: &mut Uri) {
308 if let Some(user) = &self.user {
309 let auth = uri.auth.get_or_insert_with(|| Auth {
310 user: String::new(),
311 password: None,
312 });
313 auth.user = user.clone();
314 }
315 if let Some(host) = &self.host
316 && let Ok(mut host_with_port) = HostWithPort::try_from(host.as_str())
317 {
318 if host_with_port.port.is_none() {
320 host_with_port.port = uri.host_with_port.port;
321 }
322 uri.host_with_port = host_with_port;
323 }
324 }
325}
326
327fn regex_is_match(pattern: &str, value: &str) -> bool {
328 regex::Regex::new(pattern)
329 .map(|re| re.is_match(value))
330 .unwrap_or(false)
331}
332
333#[derive(Debug, Deserialize, Serialize)]
334pub struct Config {
335 #[serde(default = "default_config_http_addr")]
336 pub http_addr: String,
337 pub addr: String,
338 pub udp_port: u16,
339 pub auto_learn_public_address: Option<bool>,
340
341 pub log_level: Option<String>,
342 pub log_file: Option<String>,
343 #[serde(default, skip_serializing_if = "Vec::is_empty")]
344 pub http_access_skip_paths: Vec<String>,
345
346 #[serde(default = "default_config_useragent")]
347 pub useragent: Option<String>,
348 pub register_users: Option<Vec<RegisterOption>>,
349 #[serde(default = "default_graceful_shutdown")]
350 pub graceful_shutdown: Option<bool>,
351 #[serde(default = "default_graceful_shutdown_timeout")]
354 pub graceful_shutdown_timeout: Option<u64>,
355 pub handler: Option<InviteHandlerConfig>,
356 pub accept_timeout: Option<String>,
357 #[serde(default = "default_codecs")]
358 pub codecs: Option<Vec<String>>,
359 pub external_ip: Option<String>,
360 #[serde(default = "default_config_rtp_start_port")]
361 pub rtp_start_port: Option<u16>,
362 #[serde(default = "default_config_rtp_end_port")]
363 pub rtp_end_port: Option<u16>,
364 #[serde(default = "default_config_rtp_latching")]
365 pub enable_rtp_latching: Option<bool>,
366 pub enable_ice_lite: Option<bool>,
367 pub rtp_bind_ip: Option<String>,
368 pub tls_port: Option<u16>,
369 pub tls_cert_file: Option<String>,
370 pub tls_key_file: Option<String>,
371
372 pub enable_srtp: Option<bool>,
373
374 pub callrecord: Option<CallRecordConfig>,
375 #[serde(default = "default_config_media_cache_path")]
376 pub media_cache_path: String,
377 pub ambiance: Option<AmbianceOption>,
378 pub ice_servers: Option<Vec<IceServer>>,
379 #[serde(default)]
380 pub recording: Option<RecordingPolicy>,
381 pub rewrites: Option<Vec<RewriteRule>>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub trunk_rules: Option<Vec<TrunkRule>>,
384 #[serde(default = "default_enable_options_response")]
385 pub enable_options_response: Option<bool>,
386}
387
388#[derive(Debug, Deserialize, Clone, Serialize)]
389#[serde(rename_all = "snake_case")]
390#[serde(tag = "type")]
391pub enum InviteHandlerConfig {
392 Webhook {
393 url: Option<String>,
394 urls: Option<Vec<String>>,
395 method: Option<String>,
396 headers: Option<Vec<(String, String)>>,
397 },
398 Playbook {
399 rules: Option<Vec<PlaybookRule>>,
400 default: Option<String>,
401 },
402}
403
404#[derive(Debug, Deserialize, Clone, Serialize)]
405#[serde(rename_all = "snake_case")]
406pub struct PlaybookRule {
407 pub caller: Option<String>,
408 pub callee: Option<String>,
409 pub playbook: String,
410}
411
412#[derive(Debug, Deserialize, Clone, Serialize)]
413#[serde(rename_all = "snake_case")]
414pub enum S3Vendor {
415 Aliyun,
416 Tencent,
417 Minio,
418 AWS,
419 GCP,
420 Azure,
421 DigitalOcean,
422}
423
424#[derive(Debug, Deserialize, Clone, Serialize)]
425#[serde(tag = "type")]
426#[serde(rename_all = "snake_case")]
427pub enum CallRecordConfig {
428 Local {
429 root: String,
430 },
431 S3 {
432 vendor: S3Vendor,
433 bucket: String,
434 region: String,
435 access_key: String,
436 secret_key: String,
437 endpoint: String,
438 root: String,
439 with_media: Option<bool>,
440 keep_media_copy: Option<bool>,
441 },
442 Http {
443 url: String,
444 headers: Option<HashMap<String, String>>,
445 with_media: Option<bool>,
446 keep_media_copy: Option<bool>,
447 },
448}
449
450impl Default for CallRecordConfig {
451 fn default() -> Self {
452 Self::Local {
453 #[cfg(target_os = "windows")]
454 root: "./config/cdr".to_string(),
455 #[cfg(not(target_os = "windows"))]
456 root: "./config/cdr".to_string(),
457 }
458 }
459}
460
461impl Default for Config {
462 fn default() -> Self {
463 Self {
464 http_addr: default_config_http_addr(),
465 log_level: None,
466 log_file: None,
467 http_access_skip_paths: Vec::new(),
468 addr: default_sip_addr(),
469 udp_port: default_sip_port(),
470 auto_learn_public_address: None,
471 useragent: None,
472 register_users: None,
473 graceful_shutdown: Some(true),
474 graceful_shutdown_timeout: default_graceful_shutdown_timeout(),
475 handler: None,
476 accept_timeout: Some("50s".to_string()),
477 media_cache_path: default_config_media_cache_path(),
478 ambiance: None,
479 callrecord: None,
480 ice_servers: None,
481 codecs: None,
482 external_ip: None,
483 rtp_start_port: default_config_rtp_start_port(),
484 rtp_end_port: default_config_rtp_end_port(),
485 enable_rtp_latching: Some(true),
486 rtp_bind_ip: None,
487 enable_ice_lite: None,
488 tls_port: None,
489 tls_cert_file: None,
490 tls_key_file: None,
491 enable_srtp: None,
492 recording: None,
493 rewrites: None,
494 trunk_rules: None,
495 enable_options_response: default_enable_options_response(),
496 }
497 }
498}
499
500impl Clone for Config {
501 fn clone(&self) -> Self {
502 let s = toml::to_string(self).unwrap();
505 toml::from_str(&s).unwrap()
506 }
507}
508
509impl Config {
510 pub fn load(path: &str) -> Result<Self, Error> {
511 let config: Self = toml::from_str(
512 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
513 )?;
514 Ok(config)
515 }
516
517 pub fn recorder_path(&self) -> String {
518 self.recording
519 .as_ref()
520 .map(|policy| policy.recorder_path())
521 .unwrap_or_else(default_config_recorder_path)
522 }
523
524 pub fn recorder_format(&self) -> RecorderFormat {
525 self.recording
526 .as_ref()
527 .map(|policy| policy.recorder_format())
528 .unwrap_or_default()
529 }
530
531 pub fn ensure_recording_defaults(&mut self) -> bool {
532 let mut fallback = false;
533
534 if let Some(policy) = self.recording.as_mut() {
535 fallback |= policy.ensure_defaults();
536 }
537
538 fallback
539 }
540
541 pub fn apply_trunk_rules(&self, invite: &mut InviteOption) {
549 if let Some(rules) = &self.trunk_rules {
550 for rule in rules {
551 if rule.matches(invite) {
552 rule.apply(invite);
553 break;
554 }
555 }
556 }
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn test_playbook_handler_config_parsing() {
566 let toml_config = r#"
567http_addr = "0.0.0.0:8080"
568addr = "0.0.0.0"
569udp_port = 25060
570
571[handler]
572type = "playbook"
573default = "default.md"
574
575[[handler.rules]]
576caller = "^\\+1\\d{10}$"
577callee = "^sip:support@.*"
578playbook = "support.md"
579
580[[handler.rules]]
581caller = "^\\+86\\d+"
582playbook = "chinese.md"
583
584[[handler.rules]]
585callee = "^sip:sales@.*"
586playbook = "sales.md"
587"#;
588
589 let config: Config = toml::from_str(toml_config).unwrap();
590
591 assert!(config.handler.is_some());
592 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
593 assert_eq!(default, Some("default.md".to_string()));
594 let rules = rules.unwrap();
595 assert_eq!(rules.len(), 3);
596
597 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
598 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
599 assert_eq!(rules[0].playbook, "support.md");
600
601 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
602 assert_eq!(rules[1].callee, None);
603 assert_eq!(rules[1].playbook, "chinese.md");
604
605 assert_eq!(rules[2].caller, None);
606 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
607 assert_eq!(rules[2].playbook, "sales.md");
608 } else {
609 panic!("Expected Playbook handler config");
610 }
611 }
612
613 #[test]
614 fn test_playbook_handler_config_without_default() {
615 let toml_config = r#"
616http_addr = "0.0.0.0:8080"
617addr = "0.0.0.0"
618udp_port = 25060
619
620[handler]
621type = "playbook"
622
623[[handler.rules]]
624caller = "^\\+1.*"
625playbook = "us.md"
626"#;
627
628 let config: Config = toml::from_str(toml_config).unwrap();
629
630 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
631 assert_eq!(default, None);
632 let rules = rules.unwrap();
633 assert_eq!(rules.len(), 1);
634 } else {
635 panic!("Expected Playbook handler config");
636 }
637 }
638
639 #[test]
640 fn test_webhook_handler_config_still_works() {
641 let toml_config = r#"
642http_addr = "0.0.0.0:8080"
643addr = "0.0.0.0"
644udp_port = 25060
645
646[handler]
647type = "webhook"
648url = "http://example.com/webhook"
649"#;
650
651 let config: Config = toml::from_str(toml_config).unwrap();
652
653 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
654 assert_eq!(url, Some("http://example.com/webhook".to_string()));
655 } else {
656 panic!("Expected Webhook handler config");
657 }
658 }
659
660 #[test]
665 fn test_trunk_rules_config_parsing() {
666 let toml_config = r#"
667http_addr = "0.0.0.0:8080"
668addr = "0.0.0.0"
669udp_port = 25060
670
671[[trunk_rules]]
672rule.match.to.host = "^172\\.25\\."
673rule.rewrite.contact.host = "172.25.225.2"
674
675[[trunk_rules]]
676rule.match.from.user = "^\\+86.*"
677rule.match.to.host = "^10\\."
678rule.rewrite.from.user = "10086"
679rule.rewrite.to.host = "10.0.0.1"
680rule.rewrite.contact.user = "active-call"
681rule.rewrite.contact.host = "10.0.0.1:25060"
682
683[[trunk_rules]]
684rule.rewrite.contact.host = "116.62.75.161"
685"#;
686
687 let config: Config = toml::from_str(toml_config).unwrap();
688 let rules = config.trunk_rules.expect("trunk_rules should be parsed");
689
690 assert_eq!(rules.len(), 3);
691
692 let m = &rules[0].rule.r#match;
694 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^172\\.25\\."));
695 assert_eq!(m.to.as_ref().unwrap().user, None);
696 assert_eq!(m.from, None);
697 let w = &rules[0].rule.rewrite;
698 assert_eq!(
699 w.contact.as_ref().unwrap().host.as_deref(),
700 Some("172.25.225.2")
701 );
702 assert_eq!(w.from, None);
703 assert_eq!(w.to, None);
704
705 let m = &rules[1].rule.r#match;
707 assert_eq!(m.from.as_ref().unwrap().user.as_deref(), Some("^\\+86.*"));
708 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^10\\."));
709 let w = &rules[1].rule.rewrite;
710 assert_eq!(w.from.as_ref().unwrap().user.as_deref(), Some("10086"));
711 assert_eq!(w.to.as_ref().unwrap().host.as_deref(), Some("10.0.0.1"));
712 assert_eq!(
713 w.contact.as_ref().unwrap().user.as_deref(),
714 Some("active-call")
715 );
716 assert_eq!(
717 w.contact.as_ref().unwrap().host.as_deref(),
718 Some("10.0.0.1:25060")
719 );
720
721 assert_eq!(rules[2].rule.r#match.from, None);
723 assert_eq!(rules[2].rule.r#match.to, None);
724 assert_eq!(
725 rules[2]
726 .rule
727 .rewrite
728 .contact
729 .as_ref()
730 .unwrap()
731 .host
732 .as_deref(),
733 Some("116.62.75.161")
734 );
735 }
736
737 #[test]
738 fn test_trunk_rules_absent_by_default() {
739 let config = Config::default();
740 assert!(config.trunk_rules.is_none());
741 }
742
743 fn invite_option(caller: &str, callee: &str, contact: &str) -> InviteOption {
748 InviteOption {
749 caller: caller.try_into().unwrap(),
750 callee: callee.try_into().unwrap(),
751 contact: contact.try_into().unwrap(),
752 ..Default::default()
753 }
754 }
755
756 #[test]
757 fn test_trunk_rule_matches_on_to_host() {
758 let config: Config = toml::from_str(
759 r#"
760addr = "0.0.0.0"
761udp_port = 25060
762
763[[trunk_rules]]
764rule.match.to.host = "^172\\.25\\."
765rule.rewrite.contact.host = "172.25.225.2"
766"#,
767 )
768 .unwrap();
769
770 let mut invite = invite_option(
772 "sip:ai@116.62.75.161:13050",
773 "sip:agent1@172.25.225.3:15060",
774 "sip:ai@127.0.0.1:13050",
775 );
776 config.apply_trunk_rules(&mut invite);
777 assert_eq!(
778 invite.contact.host_with_port.host.to_string(),
779 "172.25.225.2"
780 );
781
782 let mut invite = invite_option(
784 "sip:ai@116.62.75.161:13050",
785 "sip:+8613800138000@sbc.example.com:5060",
786 "sip:ai@127.0.0.1:13050",
787 );
788 config.apply_trunk_rules(&mut invite);
789 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
790 }
791
792 #[test]
793 fn test_trunk_rule_matches_on_from_user_and_to_host() {
794 let config: Config = toml::from_str(
795 r#"
796addr = "0.0.0.0"
797udp_port = 25060
798
799[[trunk_rules]]
800rule.match.from.user = "^anonymous$"
801rule.match.to.host = "^sbc\\."
802rule.rewrite.contact.host = "116.62.75.161"
803"#,
804 )
805 .unwrap();
806
807 let mut invite = invite_option(
809 "sip:anonymous@116.62.75.161:13050",
810 "sip:+8613800138000@sbc.example.com:5060",
811 "sip:ai@127.0.0.1:13050",
812 );
813 config.apply_trunk_rules(&mut invite);
814 assert_eq!(
815 invite.contact.host_with_port.host.to_string(),
816 "116.62.75.161"
817 );
818
819 let mut invite = invite_option(
821 "sip:alice@116.62.75.161:13050",
822 "sip:+8613800138000@sbc.example.com:5060",
823 "sip:ai@127.0.0.1:13050",
824 );
825 config.apply_trunk_rules(&mut invite);
826 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
827 }
828
829 #[test]
830 fn test_trunk_rule_rewrites_from_to_contact() {
831 let config: Config = toml::from_str(
832 r#"
833addr = "0.0.0.0"
834udp_port = 25060
835
836[[trunk_rules]]
837rule.match.to.host = "^10\\."
838rule.rewrite.from.user = "10086"
839rule.rewrite.from.host = "116.62.75.161"
840rule.rewrite.to.user = "30000"
841rule.rewrite.to.host = "10.0.0.1:25060"
842rule.rewrite.contact.user = "active-call"
843rule.rewrite.contact.host = "10.0.0.1"
844"#,
845 )
846 .unwrap();
847
848 let mut invite = invite_option(
849 "sip:ai@127.0.0.1:13050",
850 "sip:agent1@10.0.0.2:25060",
851 "sip:ai@127.0.0.1:13050",
852 );
853 config.apply_trunk_rules(&mut invite);
854
855 assert_eq!(invite.caller.auth.as_ref().unwrap().user, "10086");
857 assert_eq!(
858 invite.caller.host_with_port.host.to_string(),
859 "116.62.75.161"
860 );
861
862 assert_eq!(invite.callee.auth.as_ref().unwrap().user, "30000");
864 assert_eq!(invite.callee.host_with_port.host.to_string(), "10.0.0.1");
865 assert_eq!(invite.callee.host_with_port.port.unwrap().0, 25060);
866
867 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
869 assert_eq!(invite.contact.host_with_port.host.to_string(), "10.0.0.1");
870 assert_eq!(invite.contact.host_with_port.port.unwrap().0, 13050);
871 }
872
873 #[test]
874 fn test_trunk_rule_catch_all_default() {
875 let config: Config = toml::from_str(
876 r#"
877addr = "0.0.0.0"
878udp_port = 25060
879
880[[trunk_rules]]
881rule.match.to.host = "^172\\.25\\."
882rule.rewrite.contact.host = "172.25.225.2"
883
884[[trunk_rules]]
885rule.rewrite.contact.host = "116.62.75.161"
886"#,
887 )
888 .unwrap();
889
890 let mut invite = invite_option(
892 "sip:ai@116.62.75.161:13050",
893 "sip:+8613800138000@sbc.example.com:5060",
894 "sip:ai@127.0.0.1:13050",
895 );
896 config.apply_trunk_rules(&mut invite);
897 assert_eq!(
898 invite.contact.host_with_port.host.to_string(),
899 "116.62.75.161"
900 );
901 }
902
903 #[test]
904 fn test_trunk_rule_first_match_wins() {
905 let config: Config = toml::from_str(
906 r#"
907addr = "0.0.0.0"
908udp_port = 25060
909
910[[trunk_rules]]
911rule.match.to.host = ".*"
912rule.rewrite.contact.host = "1.1.1.1"
913
914[[trunk_rules]]
915rule.match.to.host = ".*"
916rule.rewrite.contact.host = "2.2.2.2"
917"#,
918 )
919 .unwrap();
920
921 let mut invite = invite_option(
922 "sip:ai@116.62.75.161:13050",
923 "sip:any@example.com:5060",
924 "sip:ai@127.0.0.1:13050",
925 );
926 config.apply_trunk_rules(&mut invite);
927 assert_eq!(invite.contact.host_with_port.host.to_string(), "1.1.1.1");
929 }
930
931 #[test]
932 fn test_trunk_rule_no_config_noop() {
933 let config = Config::default();
934 let mut invite = invite_option(
935 "sip:ai@116.62.75.161:13050",
936 "sip:agent1@172.25.225.3:15060",
937 "sip:ai@127.0.0.1:13050",
938 );
939 let before = invite.contact.to_string();
940 config.apply_trunk_rules(&mut invite);
941 assert_eq!(invite.contact.to_string(), before);
942 }
943
944 #[test]
945 fn test_trunk_rule_rewrite_missing_auth_creates_user() {
946 let config: Config = toml::from_str(
947 r#"
948addr = "0.0.0.0"
949udp_port = 25060
950
951[[trunk_rules]]
952rule.match.to.host = ".*"
953rule.rewrite.contact.user = "active-call"
954"#,
955 )
956 .unwrap();
957
958 let mut invite = InviteOption {
960 caller: "sip:ai@127.0.0.1:13050".try_into().unwrap(),
961 callee: "sip:agent1@172.25.225.3:15060".try_into().unwrap(),
962 contact: "sip:127.0.0.1:13050".try_into().unwrap(),
963 ..Default::default()
964 };
965 config.apply_trunk_rules(&mut invite);
966 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
967 }
968}