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, skip_serializing_if = "Vec::is_empty")]
353 pub peers: Vec<String>,
354
355 #[serde(default = "default_config_useragent")]
356 pub useragent: Option<String>,
357 pub register_users: Option<Vec<RegisterOption>>,
358 #[serde(default = "default_graceful_shutdown")]
359 pub graceful_shutdown: Option<bool>,
360 #[serde(default = "default_graceful_shutdown_timeout")]
363 pub graceful_shutdown_timeout: Option<u64>,
364 pub handler: Option<InviteHandlerConfig>,
365 pub accept_timeout: Option<String>,
366 #[serde(default = "default_codecs")]
367 pub codecs: Option<Vec<String>>,
368 pub external_ip: Option<String>,
369 #[serde(default = "default_config_rtp_start_port")]
370 pub rtp_start_port: Option<u16>,
371 #[serde(default = "default_config_rtp_end_port")]
372 pub rtp_end_port: Option<u16>,
373 #[serde(default = "default_config_rtp_latching")]
374 pub enable_rtp_latching: Option<bool>,
375 pub enable_ice_lite: Option<bool>,
376 pub rtp_bind_ip: Option<String>,
377 pub tls_port: Option<u16>,
378 pub tls_cert_file: Option<String>,
379 pub tls_key_file: Option<String>,
380
381 pub enable_srtp: Option<bool>,
382
383 pub callrecord: Option<CallRecordConfig>,
384 #[serde(default = "default_config_media_cache_path")]
385 pub media_cache_path: String,
386 pub ambiance: Option<AmbianceOption>,
387 pub ice_servers: Option<Vec<IceServer>>,
388 #[serde(default)]
389 pub recording: Option<RecordingPolicy>,
390 pub rewrites: Option<Vec<RewriteRule>>,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub trunk_rules: Option<Vec<TrunkRule>>,
393 #[serde(default = "default_enable_options_response")]
394 pub enable_options_response: Option<bool>,
395}
396
397#[derive(Debug, Deserialize, Clone, Serialize)]
398#[serde(rename_all = "snake_case")]
399#[serde(tag = "type")]
400pub enum InviteHandlerConfig {
401 Webhook {
402 url: Option<String>,
403 urls: Option<Vec<String>>,
404 method: Option<String>,
405 headers: Option<Vec<(String, String)>>,
406 },
407 Playbook {
408 rules: Option<Vec<PlaybookRule>>,
409 default: Option<String>,
410 },
411}
412
413#[derive(Debug, Deserialize, Clone, Serialize)]
414#[serde(rename_all = "snake_case")]
415pub struct PlaybookRule {
416 pub caller: Option<String>,
417 pub callee: Option<String>,
418 pub playbook: String,
419}
420
421#[derive(Debug, Deserialize, Clone, Serialize)]
422#[serde(rename_all = "snake_case")]
423pub enum S3Vendor {
424 Aliyun,
425 Tencent,
426 Minio,
427 AWS,
428 GCP,
429 Azure,
430 DigitalOcean,
431}
432
433#[derive(Debug, Deserialize, Clone, Serialize)]
434#[serde(tag = "type")]
435#[serde(rename_all = "snake_case")]
436pub enum CallRecordConfig {
437 Local {
438 root: String,
439 },
440 S3 {
441 vendor: S3Vendor,
442 bucket: String,
443 region: String,
444 access_key: String,
445 secret_key: String,
446 endpoint: String,
447 root: String,
448 with_media: Option<bool>,
449 keep_media_copy: Option<bool>,
450 },
451 Http {
452 url: String,
453 headers: Option<HashMap<String, String>>,
454 with_media: Option<bool>,
455 keep_media_copy: Option<bool>,
456 },
457}
458
459impl Default for CallRecordConfig {
460 fn default() -> Self {
461 Self::Local {
462 #[cfg(target_os = "windows")]
463 root: "./config/cdr".to_string(),
464 #[cfg(not(target_os = "windows"))]
465 root: "./config/cdr".to_string(),
466 }
467 }
468}
469
470impl Default for Config {
471 fn default() -> Self {
472 Self {
473 http_addr: default_config_http_addr(),
474 log_level: None,
475 log_file: None,
476 http_access_skip_paths: Vec::new(),
477 peers: Vec::new(),
478 addr: default_sip_addr(),
479 udp_port: default_sip_port(),
480 auto_learn_public_address: None,
481 useragent: None,
482 register_users: None,
483 graceful_shutdown: Some(true),
484 graceful_shutdown_timeout: default_graceful_shutdown_timeout(),
485 handler: None,
486 accept_timeout: Some("50s".to_string()),
487 media_cache_path: default_config_media_cache_path(),
488 ambiance: None,
489 callrecord: None,
490 ice_servers: None,
491 codecs: None,
492 external_ip: None,
493 rtp_start_port: default_config_rtp_start_port(),
494 rtp_end_port: default_config_rtp_end_port(),
495 enable_rtp_latching: Some(true),
496 rtp_bind_ip: None,
497 enable_ice_lite: None,
498 tls_port: None,
499 tls_cert_file: None,
500 tls_key_file: None,
501 enable_srtp: None,
502 recording: None,
503 rewrites: None,
504 trunk_rules: None,
505 enable_options_response: default_enable_options_response(),
506 }
507 }
508}
509
510impl Clone for Config {
511 fn clone(&self) -> Self {
512 let s = toml::to_string(self).unwrap();
515 toml::from_str(&s).unwrap()
516 }
517}
518
519impl Config {
520 pub fn load(path: &str) -> Result<Self, Error> {
521 let config: Self = toml::from_str(
522 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
523 )?;
524 Ok(config)
525 }
526
527 pub fn recorder_path(&self) -> String {
528 self.recording
529 .as_ref()
530 .map(|policy| policy.recorder_path())
531 .unwrap_or_else(default_config_recorder_path)
532 }
533
534 pub fn recorder_format(&self) -> RecorderFormat {
535 self.recording
536 .as_ref()
537 .map(|policy| policy.recorder_format())
538 .unwrap_or_default()
539 }
540
541 pub fn ensure_recording_defaults(&mut self) -> bool {
542 let mut fallback = false;
543
544 if let Some(policy) = self.recording.as_mut() {
545 fallback |= policy.ensure_defaults();
546 }
547
548 fallback
549 }
550
551 pub fn apply_trunk_rules(&self, invite: &mut InviteOption) {
559 if let Some(rules) = &self.trunk_rules {
560 for rule in rules {
561 if rule.matches(invite) {
562 rule.apply(invite);
563 break;
564 }
565 }
566 }
567 }
568
569 pub fn peer_ws_endpoint(peer: &str) -> Option<String> {
574 let peer = peer.trim();
575 if peer.is_empty() {
576 return None;
577 }
578 if peer.starts_with("wss://") {
579 return Some(peer.to_string());
580 }
581 if peer.starts_with("ws://") {
582 return Some(peer.to_string());
583 }
584 if peer.starts_with("https://") {
585 return Some(peer.replacen("https://", "wss://", 1));
586 }
587 if peer.starts_with("http://") {
588 return Some(peer.replacen("http://", "ws://", 1));
589 }
590 Some(format!("ws://{}", peer))
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn test_playbook_handler_config_parsing() {
600 let toml_config = r#"
601http_addr = "0.0.0.0:8080"
602addr = "0.0.0.0"
603udp_port = 25060
604
605[handler]
606type = "playbook"
607default = "default.md"
608
609[[handler.rules]]
610caller = "^\\+1\\d{10}$"
611callee = "^sip:support@.*"
612playbook = "support.md"
613
614[[handler.rules]]
615caller = "^\\+86\\d+"
616playbook = "chinese.md"
617
618[[handler.rules]]
619callee = "^sip:sales@.*"
620playbook = "sales.md"
621"#;
622
623 let config: Config = toml::from_str(toml_config).unwrap();
624
625 assert!(config.handler.is_some());
626 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
627 assert_eq!(default, Some("default.md".to_string()));
628 let rules = rules.unwrap();
629 assert_eq!(rules.len(), 3);
630
631 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
632 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
633 assert_eq!(rules[0].playbook, "support.md");
634
635 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
636 assert_eq!(rules[1].callee, None);
637 assert_eq!(rules[1].playbook, "chinese.md");
638
639 assert_eq!(rules[2].caller, None);
640 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
641 assert_eq!(rules[2].playbook, "sales.md");
642 } else {
643 panic!("Expected Playbook handler config");
644 }
645 }
646
647 #[test]
648 fn test_playbook_handler_config_without_default() {
649 let toml_config = r#"
650http_addr = "0.0.0.0:8080"
651addr = "0.0.0.0"
652udp_port = 25060
653
654[handler]
655type = "playbook"
656
657[[handler.rules]]
658caller = "^\\+1.*"
659playbook = "us.md"
660"#;
661
662 let config: Config = toml::from_str(toml_config).unwrap();
663
664 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
665 assert_eq!(default, None);
666 let rules = rules.unwrap();
667 assert_eq!(rules.len(), 1);
668 } else {
669 panic!("Expected Playbook handler config");
670 }
671 }
672
673 #[test]
674 fn test_webhook_handler_config_still_works() {
675 let toml_config = r#"
676http_addr = "0.0.0.0:8080"
677addr = "0.0.0.0"
678udp_port = 25060
679
680[handler]
681type = "webhook"
682url = "http://example.com/webhook"
683"#;
684
685 let config: Config = toml::from_str(toml_config).unwrap();
686
687 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
688 assert_eq!(url, Some("http://example.com/webhook".to_string()));
689 } else {
690 panic!("Expected Webhook handler config");
691 }
692 }
693
694 #[test]
699 fn test_trunk_rules_config_parsing() {
700 let toml_config = r#"
701http_addr = "0.0.0.0:8080"
702addr = "0.0.0.0"
703udp_port = 25060
704
705[[trunk_rules]]
706rule.match.to.host = "^172\\.25\\."
707rule.rewrite.contact.host = "172.25.225.2"
708
709[[trunk_rules]]
710rule.match.from.user = "^\\+86.*"
711rule.match.to.host = "^10\\."
712rule.rewrite.from.user = "10086"
713rule.rewrite.to.host = "10.0.0.1"
714rule.rewrite.contact.user = "active-call"
715rule.rewrite.contact.host = "10.0.0.1:25060"
716
717[[trunk_rules]]
718rule.rewrite.contact.host = "116.62.75.161"
719"#;
720
721 let config: Config = toml::from_str(toml_config).unwrap();
722 let rules = config.trunk_rules.expect("trunk_rules should be parsed");
723
724 assert_eq!(rules.len(), 3);
725
726 let m = &rules[0].rule.r#match;
728 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^172\\.25\\."));
729 assert_eq!(m.to.as_ref().unwrap().user, None);
730 assert_eq!(m.from, None);
731 let w = &rules[0].rule.rewrite;
732 assert_eq!(
733 w.contact.as_ref().unwrap().host.as_deref(),
734 Some("172.25.225.2")
735 );
736 assert_eq!(w.from, None);
737 assert_eq!(w.to, None);
738
739 let m = &rules[1].rule.r#match;
741 assert_eq!(m.from.as_ref().unwrap().user.as_deref(), Some("^\\+86.*"));
742 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^10\\."));
743 let w = &rules[1].rule.rewrite;
744 assert_eq!(w.from.as_ref().unwrap().user.as_deref(), Some("10086"));
745 assert_eq!(w.to.as_ref().unwrap().host.as_deref(), Some("10.0.0.1"));
746 assert_eq!(
747 w.contact.as_ref().unwrap().user.as_deref(),
748 Some("active-call")
749 );
750 assert_eq!(
751 w.contact.as_ref().unwrap().host.as_deref(),
752 Some("10.0.0.1:25060")
753 );
754
755 assert_eq!(rules[2].rule.r#match.from, None);
757 assert_eq!(rules[2].rule.r#match.to, None);
758 assert_eq!(
759 rules[2]
760 .rule
761 .rewrite
762 .contact
763 .as_ref()
764 .unwrap()
765 .host
766 .as_deref(),
767 Some("116.62.75.161")
768 );
769 }
770
771 #[test]
772 fn test_trunk_rules_absent_by_default() {
773 let config = Config::default();
774 assert!(config.trunk_rules.is_none());
775 }
776
777 fn invite_option(caller: &str, callee: &str, contact: &str) -> InviteOption {
782 InviteOption {
783 caller: caller.try_into().unwrap(),
784 callee: callee.try_into().unwrap(),
785 contact: contact.try_into().unwrap(),
786 ..Default::default()
787 }
788 }
789
790 #[test]
791 fn test_trunk_rule_matches_on_to_host() {
792 let config: Config = toml::from_str(
793 r#"
794addr = "0.0.0.0"
795udp_port = 25060
796
797[[trunk_rules]]
798rule.match.to.host = "^172\\.25\\."
799rule.rewrite.contact.host = "172.25.225.2"
800"#,
801 )
802 .unwrap();
803
804 let mut invite = invite_option(
806 "sip:ai@116.62.75.161:13050",
807 "sip:agent1@172.25.225.3:15060",
808 "sip:ai@127.0.0.1:13050",
809 );
810 config.apply_trunk_rules(&mut invite);
811 assert_eq!(
812 invite.contact.host_with_port.host.to_string(),
813 "172.25.225.2"
814 );
815
816 let mut invite = invite_option(
818 "sip:ai@116.62.75.161:13050",
819 "sip:+8613800138000@sbc.example.com:5060",
820 "sip:ai@127.0.0.1:13050",
821 );
822 config.apply_trunk_rules(&mut invite);
823 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
824 }
825
826 #[test]
827 fn test_trunk_rule_matches_on_from_user_and_to_host() {
828 let config: Config = toml::from_str(
829 r#"
830addr = "0.0.0.0"
831udp_port = 25060
832
833[[trunk_rules]]
834rule.match.from.user = "^anonymous$"
835rule.match.to.host = "^sbc\\."
836rule.rewrite.contact.host = "116.62.75.161"
837"#,
838 )
839 .unwrap();
840
841 let mut invite = invite_option(
843 "sip:anonymous@116.62.75.161:13050",
844 "sip:+8613800138000@sbc.example.com:5060",
845 "sip:ai@127.0.0.1:13050",
846 );
847 config.apply_trunk_rules(&mut invite);
848 assert_eq!(
849 invite.contact.host_with_port.host.to_string(),
850 "116.62.75.161"
851 );
852
853 let mut invite = invite_option(
855 "sip:alice@116.62.75.161:13050",
856 "sip:+8613800138000@sbc.example.com:5060",
857 "sip:ai@127.0.0.1:13050",
858 );
859 config.apply_trunk_rules(&mut invite);
860 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
861 }
862
863 #[test]
864 fn test_trunk_rule_rewrites_from_to_contact() {
865 let config: Config = toml::from_str(
866 r#"
867addr = "0.0.0.0"
868udp_port = 25060
869
870[[trunk_rules]]
871rule.match.to.host = "^10\\."
872rule.rewrite.from.user = "10086"
873rule.rewrite.from.host = "116.62.75.161"
874rule.rewrite.to.user = "30000"
875rule.rewrite.to.host = "10.0.0.1:25060"
876rule.rewrite.contact.user = "active-call"
877rule.rewrite.contact.host = "10.0.0.1"
878"#,
879 )
880 .unwrap();
881
882 let mut invite = invite_option(
883 "sip:ai@127.0.0.1:13050",
884 "sip:agent1@10.0.0.2:25060",
885 "sip:ai@127.0.0.1:13050",
886 );
887 config.apply_trunk_rules(&mut invite);
888
889 assert_eq!(invite.caller.auth.as_ref().unwrap().user, "10086");
891 assert_eq!(
892 invite.caller.host_with_port.host.to_string(),
893 "116.62.75.161"
894 );
895
896 assert_eq!(invite.callee.auth.as_ref().unwrap().user, "30000");
898 assert_eq!(invite.callee.host_with_port.host.to_string(), "10.0.0.1");
899 assert_eq!(invite.callee.host_with_port.port.unwrap().0, 25060);
900
901 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
903 assert_eq!(invite.contact.host_with_port.host.to_string(), "10.0.0.1");
904 assert_eq!(invite.contact.host_with_port.port.unwrap().0, 13050);
905 }
906
907 #[test]
908 fn test_trunk_rule_catch_all_default() {
909 let config: Config = toml::from_str(
910 r#"
911addr = "0.0.0.0"
912udp_port = 25060
913
914[[trunk_rules]]
915rule.match.to.host = "^172\\.25\\."
916rule.rewrite.contact.host = "172.25.225.2"
917
918[[trunk_rules]]
919rule.rewrite.contact.host = "116.62.75.161"
920"#,
921 )
922 .unwrap();
923
924 let mut invite = invite_option(
926 "sip:ai@116.62.75.161:13050",
927 "sip:+8613800138000@sbc.example.com:5060",
928 "sip:ai@127.0.0.1:13050",
929 );
930 config.apply_trunk_rules(&mut invite);
931 assert_eq!(
932 invite.contact.host_with_port.host.to_string(),
933 "116.62.75.161"
934 );
935 }
936
937 #[test]
938 fn test_trunk_rule_first_match_wins() {
939 let config: Config = toml::from_str(
940 r#"
941addr = "0.0.0.0"
942udp_port = 25060
943
944[[trunk_rules]]
945rule.match.to.host = ".*"
946rule.rewrite.contact.host = "1.1.1.1"
947
948[[trunk_rules]]
949rule.match.to.host = ".*"
950rule.rewrite.contact.host = "2.2.2.2"
951"#,
952 )
953 .unwrap();
954
955 let mut invite = invite_option(
956 "sip:ai@116.62.75.161:13050",
957 "sip:any@example.com:5060",
958 "sip:ai@127.0.0.1:13050",
959 );
960 config.apply_trunk_rules(&mut invite);
961 assert_eq!(invite.contact.host_with_port.host.to_string(), "1.1.1.1");
963 }
964
965 #[test]
966 fn test_trunk_rule_no_config_noop() {
967 let config = Config::default();
968 let mut invite = invite_option(
969 "sip:ai@116.62.75.161:13050",
970 "sip:agent1@172.25.225.3:15060",
971 "sip:ai@127.0.0.1:13050",
972 );
973 let before = invite.contact.to_string();
974 config.apply_trunk_rules(&mut invite);
975 assert_eq!(invite.contact.to_string(), before);
976 }
977
978 #[test]
979 fn test_trunk_rule_rewrite_missing_auth_creates_user() {
980 let config: Config = toml::from_str(
981 r#"
982addr = "0.0.0.0"
983udp_port = 25060
984
985[[trunk_rules]]
986rule.match.to.host = ".*"
987rule.rewrite.contact.user = "active-call"
988"#,
989 )
990 .unwrap();
991
992 let mut invite = InviteOption {
994 caller: "sip:ai@127.0.0.1:13050".try_into().unwrap(),
995 callee: "sip:agent1@172.25.225.3:15060".try_into().unwrap(),
996 contact: "sip:127.0.0.1:13050".try_into().unwrap(),
997 ..Default::default()
998 };
999 config.apply_trunk_rules(&mut invite);
1000 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
1001 }
1002}