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_options_allow_registered_servers() -> Option<bool> {
115 Some(true)
116}
117
118fn default_options_auto_learn() -> Option<bool> {
119 Some(true)
120}
121
122fn default_options_learn_ttl() -> Option<String> {
123 Some("7d".to_string())
124}
125
126fn default_codecs() -> Option<Vec<String>> {
127 let codecs = vec![
128 "pcmu".to_string(),
129 "pcma".to_string(),
130 "g722".to_string(),
131 "g729".to_string(),
132 "opus".to_string(),
133 "telephone_event".to_string(),
134 ];
135 Some(codecs)
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize, Default)]
139#[serde(rename_all = "snake_case")]
140pub struct RecordingPolicy {
141 #[serde(default)]
142 pub enabled: bool,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub auto_start: Option<bool>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub filename_pattern: Option<String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub samplerate: Option<u32>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub ptime: Option<u32>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub path: Option<String>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub format: Option<RecorderFormat>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub native_samplerate: Option<bool>,
159}
160
161impl RecordingPolicy {
162 pub fn recorder_path(&self) -> String {
163 self.path
164 .as_ref()
165 .map(|p| p.trim())
166 .filter(|p| !p.is_empty())
167 .map(|p| p.to_string())
168 .unwrap_or_else(default_config_recorder_path)
169 }
170
171 pub fn recorder_format(&self) -> RecorderFormat {
172 self.format.unwrap_or_default()
173 }
174
175 pub fn ensure_defaults(&mut self) -> bool {
176 if self
177 .path
178 .as_ref()
179 .map(|p| p.trim().is_empty())
180 .unwrap_or(true)
181 {
182 self.path = Some(default_config_recorder_path());
183 }
184
185 false
186 }
187}
188
189#[derive(Debug, Clone, Deserialize, Serialize)]
190pub struct RewriteRule {
191 pub r#match: String,
192 pub rewrite: String,
193}
194
195#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
208#[serde(rename_all = "snake_case")]
209pub struct TrunkRule {
210 pub rule: TrunkRuleDef,
211}
212
213#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
214#[serde(rename_all = "snake_case")]
215pub struct TrunkRuleDef {
216 #[serde(default, rename = "match")]
217 pub r#match: TrunkMatch,
218 pub rewrite: TrunkRewrite,
219}
220
221#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
225#[serde(rename_all = "snake_case")]
226pub struct TrunkMatch {
227 pub from: Option<UriMatch>,
228 pub to: Option<UriMatch>,
229}
230
231#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
234#[serde(rename_all = "snake_case")]
235pub struct UriMatch {
236 pub user: Option<String>,
238 pub host: Option<String>,
240}
241
242#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
244#[serde(rename_all = "snake_case")]
245pub struct TrunkRewrite {
246 pub from: Option<UriRewrite>,
247 pub to: Option<UriRewrite>,
248 pub contact: Option<UriRewrite>,
249}
250
251#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
255#[serde(rename_all = "snake_case")]
256pub struct UriRewrite {
257 pub user: Option<String>,
258 pub host: Option<String>,
259}
260
261impl TrunkRule {
262 fn matches(&self, invite: &InviteOption) -> bool {
263 self.rule.r#match.matches(invite)
264 }
265
266 fn apply(&self, invite: &mut InviteOption) {
267 self.rule.rewrite.apply(invite);
268 }
269}
270
271impl TrunkMatch {
272 fn matches(&self, invite: &InviteOption) -> bool {
273 let from_ok = self
274 .from
275 .as_ref()
276 .map(|from| from.matches(&invite.caller))
277 .unwrap_or(true);
278 let to_ok = self
279 .to
280 .as_ref()
281 .map(|to| to.matches(&invite.callee))
282 .unwrap_or(true);
283 from_ok && to_ok
284 }
285}
286
287impl UriMatch {
288 fn matches(&self, uri: &Uri) -> bool {
289 let user_ok = self
290 .user
291 .as_ref()
292 .map(|user| {
293 uri.auth
294 .as_ref()
295 .map(|auth| regex_is_match(user, &auth.user))
296 .unwrap_or(false)
297 })
298 .unwrap_or(true);
299 let host_ok = self
300 .host
301 .as_ref()
302 .map(|host| regex_is_match(host, &uri.host_with_port.host.to_string()))
303 .unwrap_or(true);
304 user_ok && host_ok
305 }
306}
307
308impl TrunkRewrite {
309 fn apply(&self, invite: &mut InviteOption) {
310 if let Some(from) = &self.from {
311 from.apply(&mut invite.caller);
312 }
313 if let Some(to) = &self.to {
314 to.apply(&mut invite.callee);
315 }
316 if let Some(contact) = &self.contact {
317 contact.apply(&mut invite.contact);
318 }
319 }
320}
321
322impl UriRewrite {
323 fn apply(&self, uri: &mut Uri) {
324 if let Some(user) = &self.user {
325 let auth = uri.auth.get_or_insert_with(|| Auth {
326 user: String::new(),
327 password: None,
328 });
329 auth.user = user.clone();
330 }
331 if let Some(host) = &self.host
332 && let Ok(mut host_with_port) = HostWithPort::try_from(host.as_str())
333 {
334 if host_with_port.port.is_none() {
336 host_with_port.port = uri.host_with_port.port;
337 }
338 uri.host_with_port = host_with_port;
339 }
340 }
341}
342
343fn regex_is_match(pattern: &str, value: &str) -> bool {
344 regex::Regex::new(pattern)
345 .map(|re| re.is_match(value))
346 .unwrap_or(false)
347}
348
349fn host_of_server(server: &str) -> Option<String> {
352 let server = server.trim();
353 if server.is_empty() {
354 return None;
355 }
356 if let Some(rest) = server.strip_prefix('[') {
357 return rest
359 .split(']')
360 .next()
361 .map(|h| h.to_ascii_lowercase());
362 }
363 if server.matches(':').count() > 1 {
365 return Some(server.to_ascii_lowercase());
366 }
367 server.split(':').next().map(|h| h.to_ascii_lowercase())
368}
369
370#[derive(Debug, Clone, PartialEq)]
373pub enum OptionsAclEntry {
374 Ip(std::net::IpAddr),
375 Cidr(ipnet::IpNet),
376 Host(String),
377}
378
379impl OptionsAclEntry {
380 fn parse(raw: &str) -> Option<Self> {
381 let raw = raw.trim();
382 if raw.is_empty() {
383 return None;
384 }
385 if let Ok(ip) = raw.parse::<std::net::IpAddr>() {
386 return Some(Self::Ip(ip));
387 }
388 if let Ok(cidr) = raw.parse::<ipnet::IpNet>() {
389 return Some(Self::Cidr(cidr));
390 }
391 Some(Self::Host(raw.to_ascii_lowercase()))
392 }
393
394 pub fn matches(&self, source_ip: Option<std::net::IpAddr>, source_host: &str) -> bool {
395 match self {
396 Self::Ip(ip) => source_ip == Some(*ip),
397 Self::Cidr(net) => source_ip.map(|ip| net.contains(&ip)).unwrap_or(false),
398 Self::Host(host) => host.eq_ignore_ascii_case(source_host),
399 }
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
411#[serde(rename_all = "snake_case")]
412pub struct OptionsResponseConfig {
413 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub enabled: Option<bool>,
417 #[serde(default, skip_serializing_if = "Vec::is_empty")]
420 pub allow: Vec<String>,
421 #[serde(default = "default_options_allow_registered_servers")]
423 pub allow_registered_servers: Option<bool>,
424 #[serde(default = "default_options_auto_learn")]
429 pub auto_learn: Option<bool>,
430 #[serde(default = "default_options_learn_ttl")]
432 pub learn_ttl: Option<String>,
433 #[serde(default, skip_serializing_if = "Vec::is_empty")]
435 pub extra_headers: Vec<(String, String)>,
436}
437
438#[derive(Debug, Deserialize, Serialize)]
439pub struct Config {
440 #[serde(default = "default_config_http_addr")]
441 pub http_addr: String,
442 pub addr: String,
443 pub udp_port: u16,
444 pub auto_learn_public_address: Option<bool>,
445
446 pub log_level: Option<String>,
447 pub log_file: Option<String>,
448 #[serde(default, skip_serializing_if = "Vec::is_empty")]
449 pub http_access_skip_paths: Vec<String>,
450
451 #[serde(default, skip_serializing_if = "Vec::is_empty")]
458 pub peers: Vec<String>,
459
460 #[serde(default = "default_config_useragent")]
461 pub useragent: Option<String>,
462 pub register_users: Option<Vec<RegisterOption>>,
463 #[serde(default = "default_graceful_shutdown")]
464 pub graceful_shutdown: Option<bool>,
465 #[serde(default = "default_graceful_shutdown_timeout")]
468 pub graceful_shutdown_timeout: Option<u64>,
469 pub handler: Option<InviteHandlerConfig>,
470 pub accept_timeout: Option<String>,
471 #[serde(default = "default_codecs")]
472 pub codecs: Option<Vec<String>>,
473 pub external_ip: Option<String>,
474 #[serde(default = "default_config_rtp_start_port")]
475 pub rtp_start_port: Option<u16>,
476 #[serde(default = "default_config_rtp_end_port")]
477 pub rtp_end_port: Option<u16>,
478 #[serde(default = "default_config_rtp_latching")]
479 pub enable_rtp_latching: Option<bool>,
480 pub enable_ice_lite: Option<bool>,
481 pub rtp_bind_ip: Option<String>,
482 pub tls_port: Option<u16>,
483 pub tls_cert_file: Option<String>,
484 pub tls_key_file: Option<String>,
485
486 pub enable_srtp: Option<bool>,
487
488 pub callrecord: Option<CallRecordConfig>,
489 #[serde(default = "default_config_media_cache_path")]
490 pub media_cache_path: String,
491 pub ambiance: Option<AmbianceOption>,
492 pub ice_servers: Option<Vec<IceServer>>,
493 #[serde(default)]
494 pub recording: Option<RecordingPolicy>,
495 pub rewrites: Option<Vec<RewriteRule>>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub trunk_rules: Option<Vec<TrunkRule>>,
498 #[serde(default = "default_enable_options_response")]
499 pub enable_options_response: Option<bool>,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub options_response: Option<OptionsResponseConfig>,
502}
503
504#[derive(Debug, Deserialize, Clone, Serialize)]
505#[serde(rename_all = "snake_case")]
506#[serde(tag = "type")]
507pub enum InviteHandlerConfig {
508 Webhook {
509 url: Option<String>,
510 urls: Option<Vec<String>>,
511 method: Option<String>,
512 headers: Option<Vec<(String, String)>>,
513 },
514 Playbook {
515 rules: Option<Vec<PlaybookRule>>,
516 default: Option<String>,
517 },
518}
519
520#[derive(Debug, Deserialize, Clone, Serialize)]
521#[serde(rename_all = "snake_case")]
522pub struct PlaybookRule {
523 pub caller: Option<String>,
524 pub callee: Option<String>,
525 pub playbook: String,
526}
527
528#[derive(Debug, Deserialize, Clone, Serialize)]
529#[serde(rename_all = "snake_case")]
530pub enum S3Vendor {
531 Aliyun,
532 Tencent,
533 Minio,
534 AWS,
535 GCP,
536 Azure,
537 DigitalOcean,
538}
539
540#[derive(Debug, Deserialize, Clone, Serialize)]
541#[serde(tag = "type")]
542#[serde(rename_all = "snake_case")]
543pub enum CallRecordConfig {
544 Local {
545 root: String,
546 },
547 S3 {
548 vendor: S3Vendor,
549 bucket: String,
550 region: String,
551 access_key: String,
552 secret_key: String,
553 endpoint: String,
554 root: String,
555 with_media: Option<bool>,
556 keep_media_copy: Option<bool>,
557 },
558 Http {
559 url: String,
560 headers: Option<HashMap<String, String>>,
561 with_media: Option<bool>,
562 keep_media_copy: Option<bool>,
563 },
564}
565
566impl Default for CallRecordConfig {
567 fn default() -> Self {
568 Self::Local {
569 #[cfg(target_os = "windows")]
570 root: "./config/cdr".to_string(),
571 #[cfg(not(target_os = "windows"))]
572 root: "./config/cdr".to_string(),
573 }
574 }
575}
576
577impl Default for Config {
578 fn default() -> Self {
579 Self {
580 http_addr: default_config_http_addr(),
581 log_level: None,
582 log_file: None,
583 http_access_skip_paths: Vec::new(),
584 peers: Vec::new(),
585 addr: default_sip_addr(),
586 udp_port: default_sip_port(),
587 auto_learn_public_address: None,
588 useragent: None,
589 register_users: None,
590 graceful_shutdown: Some(true),
591 graceful_shutdown_timeout: default_graceful_shutdown_timeout(),
592 handler: None,
593 accept_timeout: Some("50s".to_string()),
594 media_cache_path: default_config_media_cache_path(),
595 ambiance: None,
596 callrecord: None,
597 ice_servers: None,
598 codecs: None,
599 external_ip: None,
600 rtp_start_port: default_config_rtp_start_port(),
601 rtp_end_port: default_config_rtp_end_port(),
602 enable_rtp_latching: Some(true),
603 rtp_bind_ip: None,
604 enable_ice_lite: None,
605 tls_port: None,
606 tls_cert_file: None,
607 tls_key_file: None,
608 enable_srtp: None,
609 recording: None,
610 rewrites: None,
611 trunk_rules: None,
612 enable_options_response: default_enable_options_response(),
613 options_response: None,
614 }
615 }
616}
617
618impl Clone for Config {
619 fn clone(&self) -> Self {
620 let s = toml::to_string(self).unwrap();
623 toml::from_str(&s).unwrap()
624 }
625}
626
627impl Config {
628 pub fn load(path: &str) -> Result<Self, Error> {
629 let config: Self = toml::from_str(
630 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
631 )?;
632 Ok(config)
633 }
634
635 pub fn options_response_enabled(&self) -> bool {
637 self.options_response
638 .as_ref()
639 .and_then(|o| o.enabled)
640 .or(self.enable_options_response)
641 .unwrap_or(true)
642 }
643
644 pub fn options_auto_learn(&self) -> bool {
646 self.options_response
647 .as_ref()
648 .and_then(|o| o.auto_learn)
649 .unwrap_or(true)
650 }
651
652 pub fn options_learn_ttl(&self) -> std::time::Duration {
654 const DEFAULT: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 3600);
655 self.options_response
656 .as_ref()
657 .and_then(|o| o.learn_ttl.as_deref())
658 .map(|raw| {
659 humantime::parse_duration(raw).unwrap_or(DEFAULT)
660 })
661 .unwrap_or(DEFAULT)
662 }
663
664 pub fn options_acl_entries(&self) -> Vec<OptionsAclEntry> {
666 self.options_response
667 .as_ref()
668 .map(|o| o.allow.iter().filter_map(|raw| OptionsAclEntry::parse(raw)).collect())
669 .unwrap_or_default()
670 }
671
672 pub fn options_registered_hosts(&self) -> Vec<String> {
675 let enabled = self
676 .options_response
677 .as_ref()
678 .and_then(|o| o.allow_registered_servers)
679 .unwrap_or(true);
680 if !enabled {
681 return vec![];
682 }
683 self.register_users
684 .as_ref()
685 .map(|users| {
686 users
687 .iter()
688 .filter_map(|user| host_of_server(&user.server))
689 .collect()
690 })
691 .unwrap_or_default()
692 }
693
694 pub fn options_extra_headers(&self) -> Vec<(String, String)> {
696 self.options_response
697 .as_ref()
698 .map(|o| o.extra_headers.clone())
699 .unwrap_or_default()
700 }
701
702 pub fn options_matches_static(&self, source_ip: Option<std::net::IpAddr>, source_host: &str) -> bool {
705 self.options_acl_entries()
706 .iter()
707 .any(|entry| entry.matches(source_ip, source_host))
708 || self
709 .options_registered_hosts()
710 .iter()
711 .any(|host| host.eq_ignore_ascii_case(source_host))
712 }
713
714 pub fn recorder_path(&self) -> String {
715 self.recording
716 .as_ref()
717 .map(|policy| policy.recorder_path())
718 .unwrap_or_else(default_config_recorder_path)
719 }
720
721 pub fn recorder_format(&self) -> RecorderFormat {
722 self.recording
723 .as_ref()
724 .map(|policy| policy.recorder_format())
725 .unwrap_or_default()
726 }
727
728 pub fn recorder_native_samplerate(&self) -> bool {
729 self.recording
730 .as_ref()
731 .and_then(|policy| policy.native_samplerate)
732 .unwrap_or(false)
733 }
734
735 pub fn ensure_recording_defaults(&mut self) -> bool {
736 let mut fallback = false;
737
738 if let Some(policy) = self.recording.as_mut() {
739 fallback |= policy.ensure_defaults();
740 }
741
742 fallback
743 }
744
745 pub fn apply_trunk_rules(&self, invite: &mut InviteOption) {
753 if let Some(rules) = &self.trunk_rules {
754 for rule in rules {
755 if rule.matches(invite) {
756 rule.apply(invite);
757 break;
758 }
759 }
760 }
761 }
762
763 pub fn peer_ws_endpoint(peer: &str) -> Option<String> {
768 let peer = peer.trim();
769 if peer.is_empty() {
770 return None;
771 }
772 if peer.starts_with("wss://") {
773 return Some(peer.to_string());
774 }
775 if peer.starts_with("ws://") {
776 return Some(peer.to_string());
777 }
778 if peer.starts_with("https://") {
779 return Some(peer.replacen("https://", "wss://", 1));
780 }
781 if peer.starts_with("http://") {
782 return Some(peer.replacen("http://", "ws://", 1));
783 }
784 Some(format!("ws://{}", peer))
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn test_playbook_handler_config_parsing() {
794 let toml_config = r#"
795http_addr = "0.0.0.0:8080"
796addr = "0.0.0.0"
797udp_port = 25060
798
799[handler]
800type = "playbook"
801default = "default.md"
802
803[[handler.rules]]
804caller = "^\\+1\\d{10}$"
805callee = "^sip:support@.*"
806playbook = "support.md"
807
808[[handler.rules]]
809caller = "^\\+86\\d+"
810playbook = "chinese.md"
811
812[[handler.rules]]
813callee = "^sip:sales@.*"
814playbook = "sales.md"
815"#;
816
817 let config: Config = toml::from_str(toml_config).unwrap();
818
819 assert!(config.handler.is_some());
820 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
821 assert_eq!(default, Some("default.md".to_string()));
822 let rules = rules.unwrap();
823 assert_eq!(rules.len(), 3);
824
825 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
826 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
827 assert_eq!(rules[0].playbook, "support.md");
828
829 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
830 assert_eq!(rules[1].callee, None);
831 assert_eq!(rules[1].playbook, "chinese.md");
832
833 assert_eq!(rules[2].caller, None);
834 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
835 assert_eq!(rules[2].playbook, "sales.md");
836 } else {
837 panic!("Expected Playbook handler config");
838 }
839 }
840
841 #[test]
842 fn test_playbook_handler_config_without_default() {
843 let toml_config = r#"
844http_addr = "0.0.0.0:8080"
845addr = "0.0.0.0"
846udp_port = 25060
847
848[handler]
849type = "playbook"
850
851[[handler.rules]]
852caller = "^\\+1.*"
853playbook = "us.md"
854"#;
855
856 let config: Config = toml::from_str(toml_config).unwrap();
857
858 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
859 assert_eq!(default, None);
860 let rules = rules.unwrap();
861 assert_eq!(rules.len(), 1);
862 } else {
863 panic!("Expected Playbook handler config");
864 }
865 }
866
867 #[test]
868 fn test_webhook_handler_config_still_works() {
869 let toml_config = r#"
870http_addr = "0.0.0.0:8080"
871addr = "0.0.0.0"
872udp_port = 25060
873
874[handler]
875type = "webhook"
876url = "http://example.com/webhook"
877"#;
878
879 let config: Config = toml::from_str(toml_config).unwrap();
880
881 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
882 assert_eq!(url, Some("http://example.com/webhook".to_string()));
883 } else {
884 panic!("Expected Webhook handler config");
885 }
886 }
887
888 #[test]
893 fn test_options_response_defaults() {
894 let config: Config = toml::from_str(
895 r#"
896http_addr = "0.0.0.0:8080"
897addr = "0.0.0.0"
898udp_port = 25060
899"#,
900 )
901 .unwrap();
902
903 assert!(config.options_response.is_none());
904 assert!(config.options_response_enabled());
907 assert!(config.options_auto_learn());
908 assert_eq!(
909 config.options_learn_ttl(),
910 std::time::Duration::from_secs(7 * 24 * 3600)
911 );
912 assert!(config.options_acl_entries().is_empty());
913 assert!(config.options_registered_hosts().is_empty());
914 assert!(config.options_extra_headers().is_empty());
915 }
916
917 #[test]
918 fn test_options_response_config_parsing() {
919 let toml_config = r#"
920http_addr = "0.0.0.0:8080"
921addr = "0.0.0.0"
922udp_port = 25060
923
924[options_response]
925enabled = true
926allow = ["139.224.72.64", "10.0.0.0/8", "SIP.CCC.AliyunCS.com"]
927allow_registered_servers = false
928auto_learn = false
929learn_ttl = "1h"
930extra_headers = [["X-Node-Id", "node-1"], ["User-Agent", "ActiveCall-Test"]]
931"#;
932
933 let config: Config = toml::from_str(toml_config).unwrap();
934 let options = config.options_response.as_ref().unwrap();
935 assert_eq!(options.enabled, Some(true));
936 assert_eq!(options.allow_registered_servers, Some(false));
937 assert_eq!(options.auto_learn, Some(false));
938
939 assert!(!config.options_auto_learn());
940 assert_eq!(
941 config.options_learn_ttl(),
942 std::time::Duration::from_secs(3600)
943 );
944
945 let entries = config.options_acl_entries();
946 assert_eq!(entries.len(), 3);
947 assert_eq!(
948 entries[0],
949 OptionsAclEntry::Ip("139.224.72.64".parse().unwrap())
950 );
951 assert!(matches!(entries[1], OptionsAclEntry::Cidr(_)));
952 assert_eq!(
953 entries[2],
954 OptionsAclEntry::Host("sip.ccc.aliyuncs.com".to_string())
955 );
956
957 let headers = config.options_extra_headers();
958 assert_eq!(headers.len(), 2);
959 assert_eq!(headers[0], ("X-Node-Id".to_string(), "node-1".to_string()));
960 }
961
962 #[test]
963 fn test_options_response_enabled_fallback_chain() {
964 let config: Config = toml::from_str(
966 r#"
967addr = "0.0.0.0"
968udp_port = 25060
969enable_options_response = false
970"#,
971 )
972 .unwrap();
973 assert!(!config.options_response_enabled());
974
975 let config: Config = toml::from_str(
977 r#"
978addr = "0.0.0.0"
979udp_port = 25060
980enable_options_response = false
981
982[options_response]
983enabled = true
984"#,
985 )
986 .unwrap();
987 assert!(config.options_response_enabled());
988
989 let config: Config = toml::from_str(
991 r#"
992addr = "0.0.0.0"
993udp_port = 25060
994enable_options_response = false
995
996[options_response]
997extra_headers = [["X-A", "1"]]
998"#,
999 )
1000 .unwrap();
1001 assert!(!config.options_response_enabled());
1002 }
1003
1004 #[test]
1005 fn test_options_acl_matching() {
1006 let toml_config = r#"
1007addr = "0.0.0.0"
1008udp_port = 25060
1009
1010[options_response]
1011allow = ["139.224.72.64", "10.0.0.0/8", "sip.ccc.aliyuncs.com"]
1012"#;
1013 let config: Config = toml::from_str(toml_config).unwrap();
1014 let ip = |s: &str| Some(s.parse::<std::net::IpAddr>().unwrap());
1015
1016 assert!(config.options_matches_static(ip("139.224.72.64"), "139.224.72.64"));
1018 assert!(!config.options_matches_static(ip("139.224.72.65"), "139.224.72.65"));
1019 assert!(config.options_matches_static(ip("10.1.2.3"), "10.1.2.3"));
1021 assert!(!config.options_matches_static(ip("192.168.1.1"), "192.168.1.1"));
1022 assert!(config
1024 .options_matches_static(None, "SIP.CCC.ALIYUNCS.COM"));
1025 assert!(!config.options_matches_static(None, "sip.example.com"));
1026 assert!(!config.options_matches_static(None, "139.224.72.64"));
1028 }
1029
1030 #[test]
1031 fn test_options_registered_hosts() {
1032 let toml_config = r#"
1033addr = "0.0.0.0"
1034udp_port = 25060
1035
1036[[register_users]]
1037server = "sip.example.com:5060"
1038username = "1001"
1039
1040[[register_users]]
1041server = "10.0.0.5"
1042username = "1002"
1043"#;
1044 let config: Config = toml::from_str(toml_config).unwrap();
1045 assert_eq!(
1047 config.options_registered_hosts(),
1048 vec![
1049 "sip.example.com".to_string(),
1050 "10.0.0.5".to_string()
1051 ]
1052 );
1053 assert!(config.options_matches_static(None, "SIP.EXAMPLE.COM"));
1054 assert!(config.options_matches_static(
1055 Some("10.0.0.5".parse().unwrap()),
1056 "10.0.0.5"
1057 ));
1058
1059 let toml_config_off = r#"
1061addr = "0.0.0.0"
1062udp_port = 25060
1063
1064[[register_users]]
1065server = "sip.example.com"
1066username = "1001"
1067
1068[options_response]
1069allow_registered_servers = false
1070"#;
1071 let config: Config = toml::from_str(toml_config_off).unwrap();
1072 assert!(config.options_registered_hosts().is_empty());
1073 assert!(!config.options_matches_static(None, "sip.example.com"));
1074 }
1075
1076 #[test]
1077 fn test_host_of_server() {
1078 assert_eq!(
1079 host_of_server("sip.example.com:5060"),
1080 Some("sip.example.com".to_string())
1081 );
1082 assert_eq!(
1083 host_of_server("SIP.Example.COM"),
1084 Some("sip.example.com".to_string())
1085 );
1086 assert_eq!(
1087 host_of_server("[2001:db8::1]:5060"),
1088 Some("2001:db8::1".to_string())
1089 );
1090 assert_eq!(
1091 host_of_server("2001:db8::1"),
1092 Some("2001:db8::1".to_string())
1093 );
1094 assert_eq!(host_of_server(""), None);
1095 }
1096
1097 #[test]
1102 fn test_trunk_rules_config_parsing() {
1103 let toml_config = r#"
1104http_addr = "0.0.0.0:8080"
1105addr = "0.0.0.0"
1106udp_port = 25060
1107
1108[[trunk_rules]]
1109rule.match.to.host = "^172\\.25\\."
1110rule.rewrite.contact.host = "172.25.225.2"
1111
1112[[trunk_rules]]
1113rule.match.from.user = "^\\+86.*"
1114rule.match.to.host = "^10\\."
1115rule.rewrite.from.user = "10086"
1116rule.rewrite.to.host = "10.0.0.1"
1117rule.rewrite.contact.user = "active-call"
1118rule.rewrite.contact.host = "10.0.0.1:25060"
1119
1120[[trunk_rules]]
1121rule.rewrite.contact.host = "116.62.75.161"
1122"#;
1123
1124 let config: Config = toml::from_str(toml_config).unwrap();
1125 let rules = config.trunk_rules.expect("trunk_rules should be parsed");
1126
1127 assert_eq!(rules.len(), 3);
1128
1129 let m = &rules[0].rule.r#match;
1131 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^172\\.25\\."));
1132 assert_eq!(m.to.as_ref().unwrap().user, None);
1133 assert_eq!(m.from, None);
1134 let w = &rules[0].rule.rewrite;
1135 assert_eq!(
1136 w.contact.as_ref().unwrap().host.as_deref(),
1137 Some("172.25.225.2")
1138 );
1139 assert_eq!(w.from, None);
1140 assert_eq!(w.to, None);
1141
1142 let m = &rules[1].rule.r#match;
1144 assert_eq!(m.from.as_ref().unwrap().user.as_deref(), Some("^\\+86.*"));
1145 assert_eq!(m.to.as_ref().unwrap().host.as_deref(), Some("^10\\."));
1146 let w = &rules[1].rule.rewrite;
1147 assert_eq!(w.from.as_ref().unwrap().user.as_deref(), Some("10086"));
1148 assert_eq!(w.to.as_ref().unwrap().host.as_deref(), Some("10.0.0.1"));
1149 assert_eq!(
1150 w.contact.as_ref().unwrap().user.as_deref(),
1151 Some("active-call")
1152 );
1153 assert_eq!(
1154 w.contact.as_ref().unwrap().host.as_deref(),
1155 Some("10.0.0.1:25060")
1156 );
1157
1158 assert_eq!(rules[2].rule.r#match.from, None);
1160 assert_eq!(rules[2].rule.r#match.to, None);
1161 assert_eq!(
1162 rules[2]
1163 .rule
1164 .rewrite
1165 .contact
1166 .as_ref()
1167 .unwrap()
1168 .host
1169 .as_deref(),
1170 Some("116.62.75.161")
1171 );
1172 }
1173
1174 #[test]
1175 fn test_trunk_rules_absent_by_default() {
1176 let config = Config::default();
1177 assert!(config.trunk_rules.is_none());
1178 }
1179
1180 fn invite_option(caller: &str, callee: &str, contact: &str) -> InviteOption {
1185 InviteOption {
1186 caller: caller.try_into().unwrap(),
1187 callee: callee.try_into().unwrap(),
1188 contact: contact.try_into().unwrap(),
1189 ..Default::default()
1190 }
1191 }
1192
1193 #[test]
1194 fn test_trunk_rule_matches_on_to_host() {
1195 let config: Config = toml::from_str(
1196 r#"
1197addr = "0.0.0.0"
1198udp_port = 25060
1199
1200[[trunk_rules]]
1201rule.match.to.host = "^172\\.25\\."
1202rule.rewrite.contact.host = "172.25.225.2"
1203"#,
1204 )
1205 .unwrap();
1206
1207 let mut invite = invite_option(
1209 "sip:ai@116.62.75.161:13050",
1210 "sip:agent1@172.25.225.3:15060",
1211 "sip:ai@127.0.0.1:13050",
1212 );
1213 config.apply_trunk_rules(&mut invite);
1214 assert_eq!(
1215 invite.contact.host_with_port.host.to_string(),
1216 "172.25.225.2"
1217 );
1218
1219 let mut invite = invite_option(
1221 "sip:ai@116.62.75.161:13050",
1222 "sip:+8613800138000@sbc.example.com:5060",
1223 "sip:ai@127.0.0.1:13050",
1224 );
1225 config.apply_trunk_rules(&mut invite);
1226 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
1227 }
1228
1229 #[test]
1230 fn test_trunk_rule_matches_on_from_user_and_to_host() {
1231 let config: Config = toml::from_str(
1232 r#"
1233addr = "0.0.0.0"
1234udp_port = 25060
1235
1236[[trunk_rules]]
1237rule.match.from.user = "^anonymous$"
1238rule.match.to.host = "^sbc\\."
1239rule.rewrite.contact.host = "116.62.75.161"
1240"#,
1241 )
1242 .unwrap();
1243
1244 let mut invite = invite_option(
1246 "sip:anonymous@116.62.75.161:13050",
1247 "sip:+8613800138000@sbc.example.com:5060",
1248 "sip:ai@127.0.0.1:13050",
1249 );
1250 config.apply_trunk_rules(&mut invite);
1251 assert_eq!(
1252 invite.contact.host_with_port.host.to_string(),
1253 "116.62.75.161"
1254 );
1255
1256 let mut invite = invite_option(
1258 "sip:alice@116.62.75.161:13050",
1259 "sip:+8613800138000@sbc.example.com:5060",
1260 "sip:ai@127.0.0.1:13050",
1261 );
1262 config.apply_trunk_rules(&mut invite);
1263 assert_eq!(invite.contact.host_with_port.host.to_string(), "127.0.0.1");
1264 }
1265
1266 #[test]
1267 fn test_trunk_rule_rewrites_from_to_contact() {
1268 let config: Config = toml::from_str(
1269 r#"
1270addr = "0.0.0.0"
1271udp_port = 25060
1272
1273[[trunk_rules]]
1274rule.match.to.host = "^10\\."
1275rule.rewrite.from.user = "10086"
1276rule.rewrite.from.host = "116.62.75.161"
1277rule.rewrite.to.user = "30000"
1278rule.rewrite.to.host = "10.0.0.1:25060"
1279rule.rewrite.contact.user = "active-call"
1280rule.rewrite.contact.host = "10.0.0.1"
1281"#,
1282 )
1283 .unwrap();
1284
1285 let mut invite = invite_option(
1286 "sip:ai@127.0.0.1:13050",
1287 "sip:agent1@10.0.0.2:25060",
1288 "sip:ai@127.0.0.1:13050",
1289 );
1290 config.apply_trunk_rules(&mut invite);
1291
1292 assert_eq!(invite.caller.auth.as_ref().unwrap().user, "10086");
1294 assert_eq!(
1295 invite.caller.host_with_port.host.to_string(),
1296 "116.62.75.161"
1297 );
1298
1299 assert_eq!(invite.callee.auth.as_ref().unwrap().user, "30000");
1301 assert_eq!(invite.callee.host_with_port.host.to_string(), "10.0.0.1");
1302 assert_eq!(invite.callee.host_with_port.port.unwrap().0, 25060);
1303
1304 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
1306 assert_eq!(invite.contact.host_with_port.host.to_string(), "10.0.0.1");
1307 assert_eq!(invite.contact.host_with_port.port.unwrap().0, 13050);
1308 }
1309
1310 #[test]
1311 fn test_trunk_rule_catch_all_default() {
1312 let config: Config = toml::from_str(
1313 r#"
1314addr = "0.0.0.0"
1315udp_port = 25060
1316
1317[[trunk_rules]]
1318rule.match.to.host = "^172\\.25\\."
1319rule.rewrite.contact.host = "172.25.225.2"
1320
1321[[trunk_rules]]
1322rule.rewrite.contact.host = "116.62.75.161"
1323"#,
1324 )
1325 .unwrap();
1326
1327 let mut invite = invite_option(
1329 "sip:ai@116.62.75.161:13050",
1330 "sip:+8613800138000@sbc.example.com:5060",
1331 "sip:ai@127.0.0.1:13050",
1332 );
1333 config.apply_trunk_rules(&mut invite);
1334 assert_eq!(
1335 invite.contact.host_with_port.host.to_string(),
1336 "116.62.75.161"
1337 );
1338 }
1339
1340 #[test]
1341 fn test_trunk_rule_first_match_wins() {
1342 let config: Config = toml::from_str(
1343 r#"
1344addr = "0.0.0.0"
1345udp_port = 25060
1346
1347[[trunk_rules]]
1348rule.match.to.host = ".*"
1349rule.rewrite.contact.host = "1.1.1.1"
1350
1351[[trunk_rules]]
1352rule.match.to.host = ".*"
1353rule.rewrite.contact.host = "2.2.2.2"
1354"#,
1355 )
1356 .unwrap();
1357
1358 let mut invite = invite_option(
1359 "sip:ai@116.62.75.161:13050",
1360 "sip:any@example.com:5060",
1361 "sip:ai@127.0.0.1:13050",
1362 );
1363 config.apply_trunk_rules(&mut invite);
1364 assert_eq!(invite.contact.host_with_port.host.to_string(), "1.1.1.1");
1366 }
1367
1368 #[test]
1369 fn test_trunk_rule_no_config_noop() {
1370 let config = Config::default();
1371 let mut invite = invite_option(
1372 "sip:ai@116.62.75.161:13050",
1373 "sip:agent1@172.25.225.3:15060",
1374 "sip:ai@127.0.0.1:13050",
1375 );
1376 let before = invite.contact.to_string();
1377 config.apply_trunk_rules(&mut invite);
1378 assert_eq!(invite.contact.to_string(), before);
1379 }
1380
1381 #[test]
1382 fn test_trunk_rule_rewrite_missing_auth_creates_user() {
1383 let config: Config = toml::from_str(
1384 r#"
1385addr = "0.0.0.0"
1386udp_port = 25060
1387
1388[[trunk_rules]]
1389rule.match.to.host = ".*"
1390rule.rewrite.contact.user = "active-call"
1391"#,
1392 )
1393 .unwrap();
1394
1395 let mut invite = InviteOption {
1397 caller: "sip:ai@127.0.0.1:13050".try_into().unwrap(),
1398 callee: "sip:agent1@172.25.225.3:15060".try_into().unwrap(),
1399 contact: "sip:127.0.0.1:13050".try_into().unwrap(),
1400 ..Default::default()
1401 };
1402 config.apply_trunk_rules(&mut invite);
1403 assert_eq!(invite.contact.auth.as_ref().unwrap().user, "active-call");
1404 }
1405}