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 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub native_samplerate: Option<bool>,
147}
148
149impl RecordingPolicy {
150 pub fn recorder_path(&self) -> String {
151 self.path
152 .as_ref()
153 .map(|p| p.trim())
154 .filter(|p| !p.is_empty())
155 .map(|p| p.to_string())
156 .unwrap_or_else(default_config_recorder_path)
157 }
158
159 pub fn recorder_format(&self) -> RecorderFormat {
160 self.format.unwrap_or_default()
161 }
162
163 pub fn ensure_defaults(&mut self) -> bool {
164 if self
165 .path
166 .as_ref()
167 .map(|p| p.trim().is_empty())
168 .unwrap_or(true)
169 {
170 self.path = Some(default_config_recorder_path());
171 }
172
173 false
174 }
175}
176
177#[derive(Debug, Clone, Deserialize, Serialize)]
178pub struct RewriteRule {
179 pub r#match: String,
180 pub rewrite: String,
181}
182
183#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
196#[serde(rename_all = "snake_case")]
197pub struct TrunkRule {
198 pub rule: TrunkRuleDef,
199}
200
201#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
202#[serde(rename_all = "snake_case")]
203pub struct TrunkRuleDef {
204 #[serde(default, rename = "match")]
205 pub r#match: TrunkMatch,
206 pub rewrite: TrunkRewrite,
207}
208
209#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
213#[serde(rename_all = "snake_case")]
214pub struct TrunkMatch {
215 pub from: Option<UriMatch>,
216 pub to: Option<UriMatch>,
217}
218
219#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
222#[serde(rename_all = "snake_case")]
223pub struct UriMatch {
224 pub user: Option<String>,
226 pub host: Option<String>,
228}
229
230#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
232#[serde(rename_all = "snake_case")]
233pub struct TrunkRewrite {
234 pub from: Option<UriRewrite>,
235 pub to: Option<UriRewrite>,
236 pub contact: Option<UriRewrite>,
237}
238
239#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
243#[serde(rename_all = "snake_case")]
244pub struct UriRewrite {
245 pub user: Option<String>,
246 pub host: Option<String>,
247}
248
249impl TrunkRule {
250 fn matches(&self, invite: &InviteOption) -> bool {
251 self.rule.r#match.matches(invite)
252 }
253
254 fn apply(&self, invite: &mut InviteOption) {
255 self.rule.rewrite.apply(invite);
256 }
257}
258
259impl TrunkMatch {
260 fn matches(&self, invite: &InviteOption) -> bool {
261 let from_ok = self
262 .from
263 .as_ref()
264 .map(|from| from.matches(&invite.caller))
265 .unwrap_or(true);
266 let to_ok = self
267 .to
268 .as_ref()
269 .map(|to| to.matches(&invite.callee))
270 .unwrap_or(true);
271 from_ok && to_ok
272 }
273}
274
275impl UriMatch {
276 fn matches(&self, uri: &Uri) -> bool {
277 let user_ok = self
278 .user
279 .as_ref()
280 .map(|user| {
281 uri.auth
282 .as_ref()
283 .map(|auth| regex_is_match(user, &auth.user))
284 .unwrap_or(false)
285 })
286 .unwrap_or(true);
287 let host_ok = self
288 .host
289 .as_ref()
290 .map(|host| regex_is_match(host, &uri.host_with_port.host.to_string()))
291 .unwrap_or(true);
292 user_ok && host_ok
293 }
294}
295
296impl TrunkRewrite {
297 fn apply(&self, invite: &mut InviteOption) {
298 if let Some(from) = &self.from {
299 from.apply(&mut invite.caller);
300 }
301 if let Some(to) = &self.to {
302 to.apply(&mut invite.callee);
303 }
304 if let Some(contact) = &self.contact {
305 contact.apply(&mut invite.contact);
306 }
307 }
308}
309
310impl UriRewrite {
311 fn apply(&self, uri: &mut Uri) {
312 if let Some(user) = &self.user {
313 let auth = uri.auth.get_or_insert_with(|| Auth {
314 user: String::new(),
315 password: None,
316 });
317 auth.user = user.clone();
318 }
319 if let Some(host) = &self.host
320 && let Ok(mut host_with_port) = HostWithPort::try_from(host.as_str())
321 {
322 if host_with_port.port.is_none() {
324 host_with_port.port = uri.host_with_port.port;
325 }
326 uri.host_with_port = host_with_port;
327 }
328 }
329}
330
331fn regex_is_match(pattern: &str, value: &str) -> bool {
332 regex::Regex::new(pattern)
333 .map(|re| re.is_match(value))
334 .unwrap_or(false)
335}
336
337#[derive(Debug, Deserialize, Serialize)]
338pub struct Config {
339 #[serde(default = "default_config_http_addr")]
340 pub http_addr: String,
341 pub addr: String,
342 pub udp_port: u16,
343 pub auto_learn_public_address: Option<bool>,
344
345 pub log_level: Option<String>,
346 pub log_file: Option<String>,
347 #[serde(default, skip_serializing_if = "Vec::is_empty")]
348 pub http_access_skip_paths: Vec<String>,
349
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub peers: Vec<String>,
358
359 #[serde(default = "default_config_useragent")]
360 pub useragent: Option<String>,
361 pub register_users: Option<Vec<RegisterOption>>,
362 #[serde(default = "default_graceful_shutdown")]
363 pub graceful_shutdown: Option<bool>,
364 #[serde(default = "default_graceful_shutdown_timeout")]
367 pub graceful_shutdown_timeout: Option<u64>,
368 pub handler: Option<InviteHandlerConfig>,
369 pub accept_timeout: Option<String>,
370 #[serde(default = "default_codecs")]
371 pub codecs: Option<Vec<String>>,
372 pub external_ip: Option<String>,
373 #[serde(default = "default_config_rtp_start_port")]
374 pub rtp_start_port: Option<u16>,
375 #[serde(default = "default_config_rtp_end_port")]
376 pub rtp_end_port: Option<u16>,
377 #[serde(default = "default_config_rtp_latching")]
378 pub enable_rtp_latching: Option<bool>,
379 pub enable_ice_lite: Option<bool>,
380 pub rtp_bind_ip: Option<String>,
381 pub tls_port: Option<u16>,
382 pub tls_cert_file: Option<String>,
383 pub tls_key_file: Option<String>,
384
385 pub enable_srtp: Option<bool>,
386
387 pub callrecord: Option<CallRecordConfig>,
388 #[serde(default = "default_config_media_cache_path")]
389 pub media_cache_path: String,
390 pub ambiance: Option<AmbianceOption>,
391 pub ice_servers: Option<Vec<IceServer>>,
392 #[serde(default)]
393 pub recording: Option<RecordingPolicy>,
394 pub rewrites: Option<Vec<RewriteRule>>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub trunk_rules: Option<Vec<TrunkRule>>,
397 #[serde(default = "default_enable_options_response")]
398 pub enable_options_response: Option<bool>,
399}
400
401#[derive(Debug, Deserialize, Clone, Serialize)]
402#[serde(rename_all = "snake_case")]
403#[serde(tag = "type")]
404pub enum InviteHandlerConfig {
405 Webhook {
406 url: Option<String>,
407 urls: Option<Vec<String>>,
408 method: Option<String>,
409 headers: Option<Vec<(String, String)>>,
410 },
411 Playbook {
412 rules: Option<Vec<PlaybookRule>>,
413 default: Option<String>,
414 },
415}
416
417#[derive(Debug, Deserialize, Clone, Serialize)]
418#[serde(rename_all = "snake_case")]
419pub struct PlaybookRule {
420 pub caller: Option<String>,
421 pub callee: Option<String>,
422 pub playbook: String,
423}
424
425#[derive(Debug, Deserialize, Clone, Serialize)]
426#[serde(rename_all = "snake_case")]
427pub enum S3Vendor {
428 Aliyun,
429 Tencent,
430 Minio,
431 AWS,
432 GCP,
433 Azure,
434 DigitalOcean,
435}
436
437#[derive(Debug, Deserialize, Clone, Serialize)]
438#[serde(tag = "type")]
439#[serde(rename_all = "snake_case")]
440pub enum CallRecordConfig {
441 Local {
442 root: String,
443 },
444 S3 {
445 vendor: S3Vendor,
446 bucket: String,
447 region: String,
448 access_key: String,
449 secret_key: String,
450 endpoint: String,
451 root: String,
452 with_media: Option<bool>,
453 keep_media_copy: Option<bool>,
454 },
455 Http {
456 url: String,
457 headers: Option<HashMap<String, String>>,
458 with_media: Option<bool>,
459 keep_media_copy: Option<bool>,
460 },
461}
462
463impl Default for CallRecordConfig {
464 fn default() -> Self {
465 Self::Local {
466 #[cfg(target_os = "windows")]
467 root: "./config/cdr".to_string(),
468 #[cfg(not(target_os = "windows"))]
469 root: "./config/cdr".to_string(),
470 }
471 }
472}
473
474impl Default for Config {
475 fn default() -> Self {
476 Self {
477 http_addr: default_config_http_addr(),
478 log_level: None,
479 log_file: None,
480 http_access_skip_paths: Vec::new(),
481 peers: Vec::new(),
482 addr: default_sip_addr(),
483 udp_port: default_sip_port(),
484 auto_learn_public_address: None,
485 useragent: None,
486 register_users: None,
487 graceful_shutdown: Some(true),
488 graceful_shutdown_timeout: default_graceful_shutdown_timeout(),
489 handler: None,
490 accept_timeout: Some("50s".to_string()),
491 media_cache_path: default_config_media_cache_path(),
492 ambiance: None,
493 callrecord: None,
494 ice_servers: None,
495 codecs: None,
496 external_ip: None,
497 rtp_start_port: default_config_rtp_start_port(),
498 rtp_end_port: default_config_rtp_end_port(),
499 enable_rtp_latching: Some(true),
500 rtp_bind_ip: None,
501 enable_ice_lite: None,
502 tls_port: None,
503 tls_cert_file: None,
504 tls_key_file: None,
505 enable_srtp: None,
506 recording: None,
507 rewrites: None,
508 trunk_rules: None,
509 enable_options_response: default_enable_options_response(),
510 }
511 }
512}
513
514impl Clone for Config {
515 fn clone(&self) -> Self {
516 let s = toml::to_string(self).unwrap();
519 toml::from_str(&s).unwrap()
520 }
521}
522
523impl Config {
524 pub fn load(path: &str) -> Result<Self, Error> {
525 let config: Self = toml::from_str(
526 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
527 )?;
528 Ok(config)
529 }
530
531 pub fn recorder_path(&self) -> String {
532 self.recording
533 .as_ref()
534 .map(|policy| policy.recorder_path())
535 .unwrap_or_else(default_config_recorder_path)
536 }
537
538 pub fn recorder_format(&self) -> RecorderFormat {
539 self.recording
540 .as_ref()
541 .map(|policy| policy.recorder_format())
542 .unwrap_or_default()
543 }
544
545 pub fn recorder_native_samplerate(&self) -> bool {
546 self.recording
547 .as_ref()
548 .and_then(|policy| policy.native_samplerate)
549 .unwrap_or(false)
550 }
551
552 pub fn ensure_recording_defaults(&mut self) -> bool {
553 let mut fallback = false;
554
555 if let Some(policy) = self.recording.as_mut() {
556 fallback |= policy.ensure_defaults();
557 }
558
559 fallback
560 }
561
562 pub fn apply_trunk_rules(&self, invite: &mut InviteOption) {
570 if let Some(rules) = &self.trunk_rules {
571 for rule in rules {
572 if rule.matches(invite) {
573 rule.apply(invite);
574 break;
575 }
576 }
577 }
578 }
579
580 pub fn peer_ws_endpoint(peer: &str) -> Option<String> {
585 let peer = peer.trim();
586 if peer.is_empty() {
587 return None;
588 }
589 if peer.starts_with("wss://") {
590 return Some(peer.to_string());
591 }
592 if peer.starts_with("ws://") {
593 return Some(peer.to_string());
594 }
595 if peer.starts_with("https://") {
596 return Some(peer.replacen("https://", "wss://", 1));
597 }
598 if peer.starts_with("http://") {
599 return Some(peer.replacen("http://", "ws://", 1));
600 }
601 Some(format!("ws://{}", peer))
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn test_playbook_handler_config_parsing() {
611 let toml_config = r#"
612http_addr = "0.0.0.0:8080"
613addr = "0.0.0.0"
614udp_port = 25060
615
616[handler]
617type = "playbook"
618default = "default.md"
619
620[[handler.rules]]
621caller = "^\\+1\\d{10}$"
622callee = "^sip:support@.*"
623playbook = "support.md"
624
625[[handler.rules]]
626caller = "^\\+86\\d+"
627playbook = "chinese.md"
628
629[[handler.rules]]
630callee = "^sip:sales@.*"
631playbook = "sales.md"
632"#;
633
634 let config: Config = toml::from_str(toml_config).unwrap();
635
636 assert!(config.handler.is_some());
637 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
638 assert_eq!(default, Some("default.md".to_string()));
639 let rules = rules.unwrap();
640 assert_eq!(rules.len(), 3);
641
642 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
643 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
644 assert_eq!(rules[0].playbook, "support.md");
645
646 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
647 assert_eq!(rules[1].callee, None);
648 assert_eq!(rules[1].playbook, "chinese.md");
649
650 assert_eq!(rules[2].caller, None);
651 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
652 assert_eq!(rules[2].playbook, "sales.md");
653 } else {
654 panic!("Expected Playbook handler config");
655 }
656 }
657
658 #[test]
659 fn test_playbook_handler_config_without_default() {
660 let toml_config = r#"
661http_addr = "0.0.0.0:8080"
662addr = "0.0.0.0"
663udp_port = 25060
664
665[handler]
666type = "playbook"
667
668[[handler.rules]]
669caller = "^\\+1.*"
670playbook = "us.md"
671"#;
672
673 let config: Config = toml::from_str(toml_config).unwrap();
674
675 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
676 assert_eq!(default, None);
677 let rules = rules.unwrap();
678 assert_eq!(rules.len(), 1);
679 } else {
680 panic!("Expected Playbook handler config");
681 }
682 }
683
684 #[test]
685 fn test_webhook_handler_config_still_works() {
686 let toml_config = r#"
687http_addr = "0.0.0.0:8080"
688addr = "0.0.0.0"
689udp_port = 25060
690
691[handler]
692type = "webhook"
693url = "http://example.com/webhook"
694"#;
695
696 let config: Config = toml::from_str(toml_config).unwrap();
697
698 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
699 assert_eq!(url, Some("http://example.com/webhook".to_string()));
700 } else {
701 panic!("Expected Webhook handler config");
702 }
703 }
704
705 #[test]
710 fn test_trunk_rules_config_parsing() {
711 let toml_config = r#"
712http_addr = "0.0.0.0:8080"
713addr = "0.0.0.0"
714udp_port = 25060
715
716[[trunk_rules]]
717rule.match.to.host = "^172\\.25\\."
718rule.rewrite.contact.host = "172.25.225.2"
719
720[[trunk_rules]]
721rule.match.from.user = "^\\+86.*"
722rule.match.to.host = "^10\\."
723rule.rewrite.from.user = "10086"
724rule.rewrite.to.host = "10.0.0.1"
725rule.rewrite.contact.user = "active-call"
726rule.rewrite.contact.host = "10.0.0.1:25060"
727
728[[trunk_rules]]
729rule.rewrite.contact.host = "116.62.75.161"
730"#;
731
732 let config: Config = toml::from_str(toml_config).unwrap();
733 let rules = config.trunk_rules.expect("trunk_rules should be parsed");
734
735 assert_eq!(rules.len(), 3);
736
737 let m = &rules[0].rule.r#match;
739 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^172\\.25\\."));
740 assert_eq!(m.to.as_ref().unwrap().user, None);
741 assert_eq!(m.from, None);
742 let w = &rules[0].rule.rewrite;
743 assert_eq!(
744 w.contact.as_ref().unwrap().host.as_deref(),
745 Some("172.25.225.2")
746 );
747 assert_eq!(w.from, None);
748 assert_eq!(w.to, None);
749
750 let m = &rules[1].rule.r#match;
752 assert_eq!(m.from.as_ref().unwrap().user.as_deref(), Some("^\\+86.*"));
753 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^10\\."));
754 let w = &rules[1].rule.rewrite;
755 assert_eq!(w.from.as_ref().unwrap().user.as_deref(), Some("10086"));
756 assert_eq!(w.to.as_ref().unwrap().host.as_deref(), Some("10.0.0.1"));
757 assert_eq!(
758 w.contact.as_ref().unwrap().user.as_deref(),
759 Some("active-call")
760 );
761 assert_eq!(
762 w.contact.as_ref().unwrap().host.as_deref(),
763 Some("10.0.0.1:25060")
764 );
765
766 assert_eq!(rules[2].rule.r#match.from, None);
768 assert_eq!(rules[2].rule.r#match.to, None);
769 assert_eq!(
770 rules[2]
771 .rule
772 .rewrite
773 .contact
774 .as_ref()
775 .unwrap()
776 .host
777 .as_deref(),
778 Some("116.62.75.161")
779 );
780 }
781
782 #[test]
783 fn test_trunk_rules_absent_by_default() {
784 let config = Config::default();
785 assert!(config.trunk_rules.is_none());
786 }
787
788 fn invite_option(caller: &str, callee: &str, contact: &str) -> InviteOption {
793 InviteOption {
794 caller: caller.try_into().unwrap(),
795 callee: callee.try_into().unwrap(),
796 contact: contact.try_into().unwrap(),
797 ..Default::default()
798 }
799 }
800
801 #[test]
802 fn test_trunk_rule_matches_on_to_host() {
803 let config: Config = toml::from_str(
804 r#"
805addr = "0.0.0.0"
806udp_port = 25060
807
808[[trunk_rules]]
809rule.match.to.host = "^172\\.25\\."
810rule.rewrite.contact.host = "172.25.225.2"
811"#,
812 )
813 .unwrap();
814
815 let mut invite = invite_option(
817 "sip:ai@116.62.75.161:13050",
818 "sip:agent1@172.25.225.3:15060",
819 "sip:ai@127.0.0.1:13050",
820 );
821 config.apply_trunk_rules(&mut invite);
822 assert_eq!(
823 invite.contact.host_with_port.host.to_string(),
824 "172.25.225.2"
825 );
826
827 let mut invite = invite_option(
829 "sip:ai@116.62.75.161:13050",
830 "sip:+8613800138000@sbc.example.com:5060",
831 "sip:ai@127.0.0.1:13050",
832 );
833 config.apply_trunk_rules(&mut invite);
834 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
835 }
836
837 #[test]
838 fn test_trunk_rule_matches_on_from_user_and_to_host() {
839 let config: Config = toml::from_str(
840 r#"
841addr = "0.0.0.0"
842udp_port = 25060
843
844[[trunk_rules]]
845rule.match.from.user = "^anonymous$"
846rule.match.to.host = "^sbc\\."
847rule.rewrite.contact.host = "116.62.75.161"
848"#,
849 )
850 .unwrap();
851
852 let mut invite = invite_option(
854 "sip:anonymous@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!(
860 invite.contact.host_with_port.host.to_string(),
861 "116.62.75.161"
862 );
863
864 let mut invite = invite_option(
866 "sip:alice@116.62.75.161:13050",
867 "sip:+8613800138000@sbc.example.com:5060",
868 "sip:ai@127.0.0.1:13050",
869 );
870 config.apply_trunk_rules(&mut invite);
871 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
872 }
873
874 #[test]
875 fn test_trunk_rule_rewrites_from_to_contact() {
876 let config: Config = toml::from_str(
877 r#"
878addr = "0.0.0.0"
879udp_port = 25060
880
881[[trunk_rules]]
882rule.match.to.host = "^10\\."
883rule.rewrite.from.user = "10086"
884rule.rewrite.from.host = "116.62.75.161"
885rule.rewrite.to.user = "30000"
886rule.rewrite.to.host = "10.0.0.1:25060"
887rule.rewrite.contact.user = "active-call"
888rule.rewrite.contact.host = "10.0.0.1"
889"#,
890 )
891 .unwrap();
892
893 let mut invite = invite_option(
894 "sip:ai@127.0.0.1:13050",
895 "sip:agent1@10.0.0.2:25060",
896 "sip:ai@127.0.0.1:13050",
897 );
898 config.apply_trunk_rules(&mut invite);
899
900 assert_eq!(invite.caller.auth.as_ref().unwrap().user, "10086");
902 assert_eq!(
903 invite.caller.host_with_port.host.to_string(),
904 "116.62.75.161"
905 );
906
907 assert_eq!(invite.callee.auth.as_ref().unwrap().user, "30000");
909 assert_eq!(invite.callee.host_with_port.host.to_string(), "10.0.0.1");
910 assert_eq!(invite.callee.host_with_port.port.unwrap().0, 25060);
911
912 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
914 assert_eq!(invite.contact.host_with_port.host.to_string(), "10.0.0.1");
915 assert_eq!(invite.contact.host_with_port.port.unwrap().0, 13050);
916 }
917
918 #[test]
919 fn test_trunk_rule_catch_all_default() {
920 let config: Config = toml::from_str(
921 r#"
922addr = "0.0.0.0"
923udp_port = 25060
924
925[[trunk_rules]]
926rule.match.to.host = "^172\\.25\\."
927rule.rewrite.contact.host = "172.25.225.2"
928
929[[trunk_rules]]
930rule.rewrite.contact.host = "116.62.75.161"
931"#,
932 )
933 .unwrap();
934
935 let mut invite = invite_option(
937 "sip:ai@116.62.75.161:13050",
938 "sip:+8613800138000@sbc.example.com:5060",
939 "sip:ai@127.0.0.1:13050",
940 );
941 config.apply_trunk_rules(&mut invite);
942 assert_eq!(
943 invite.contact.host_with_port.host.to_string(),
944 "116.62.75.161"
945 );
946 }
947
948 #[test]
949 fn test_trunk_rule_first_match_wins() {
950 let config: Config = toml::from_str(
951 r#"
952addr = "0.0.0.0"
953udp_port = 25060
954
955[[trunk_rules]]
956rule.match.to.host = ".*"
957rule.rewrite.contact.host = "1.1.1.1"
958
959[[trunk_rules]]
960rule.match.to.host = ".*"
961rule.rewrite.contact.host = "2.2.2.2"
962"#,
963 )
964 .unwrap();
965
966 let mut invite = invite_option(
967 "sip:ai@116.62.75.161:13050",
968 "sip:any@example.com:5060",
969 "sip:ai@127.0.0.1:13050",
970 );
971 config.apply_trunk_rules(&mut invite);
972 assert_eq!(invite.contact.host_with_port.host.to_string(), "1.1.1.1");
974 }
975
976 #[test]
977 fn test_trunk_rule_no_config_noop() {
978 let config = Config::default();
979 let mut invite = invite_option(
980 "sip:ai@116.62.75.161:13050",
981 "sip:agent1@172.25.225.3:15060",
982 "sip:ai@127.0.0.1:13050",
983 );
984 let before = invite.contact.to_string();
985 config.apply_trunk_rules(&mut invite);
986 assert_eq!(invite.contact.to_string(), before);
987 }
988
989 #[test]
990 fn test_trunk_rule_rewrite_missing_auth_creates_user() {
991 let config: Config = toml::from_str(
992 r#"
993addr = "0.0.0.0"
994udp_port = 25060
995
996[[trunk_rules]]
997rule.match.to.host = ".*"
998rule.rewrite.contact.user = "active-call"
999"#,
1000 )
1001 .unwrap();
1002
1003 let mut invite = InviteOption {
1005 caller: "sip:ai@127.0.0.1:13050".try_into().unwrap(),
1006 callee: "sip:agent1@172.25.225.3:15060".try_into().unwrap(),
1007 contact: "sip:127.0.0.1:13050".try_into().unwrap(),
1008 ..Default::default()
1009 };
1010 config.apply_trunk_rules(&mut invite);
1011 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
1012 }
1013}