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