1use crate::media::{ambiance::AmbianceOption, recorder::RecorderFormat};
2use crate::useragent::RegisterOption;
3use anyhow::{Error, Result};
4use clap::Parser;
5use rustrtc::IceServer;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Parser, Debug)]
10#[command(version)]
11pub struct Cli {
12 #[clap(long)]
13 pub conf: Option<String>,
14
15 #[clap(long)]
16 pub http: Option<String>,
17
18 #[clap(long)]
19 pub sip: Option<String>,
20
21 #[clap(long)]
23 pub handler: Option<String>,
24}
25
26pub(crate) fn default_config_recorder_path() -> String {
27 #[cfg(target_os = "windows")]
28 return "./config/recorders".to_string();
29 #[cfg(not(target_os = "windows"))]
30 return "./config/recorders".to_string();
31}
32
33fn default_config_media_cache_path() -> String {
34 #[cfg(target_os = "windows")]
35 return "./config/mediacache".to_string();
36 #[cfg(not(target_os = "windows"))]
37 return "./config/mediacache".to_string();
38}
39
40fn default_config_http_addr() -> String {
41 "0.0.0.0:8080".to_string()
42}
43
44fn default_sip_addr() -> String {
45 "0.0.0.0".to_string()
46}
47
48fn default_sip_port() -> u16 {
49 25060
50}
51
52fn default_config_rtp_start_port() -> Option<u16> {
53 Some(12000)
54}
55
56fn default_config_rtp_end_port() -> Option<u16> {
57 Some(42000)
58}
59
60fn default_codecs() -> Option<Vec<String>> {
61 let mut codecs = vec![
62 "pcmu".to_string(),
63 "pcma".to_string(),
64 "g722".to_string(),
65 "g729".to_string(),
66 "telephone_event".to_string(),
67 ];
68
69 #[cfg(feature = "opus")]
70 {
71 codecs.push("opus".to_string());
72 }
73
74 Some(codecs)
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize, Default)]
78#[serde(rename_all = "snake_case")]
79pub struct RecordingPolicy {
80 #[serde(default)]
81 pub enabled: bool,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub auto_start: Option<bool>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub filename_pattern: Option<String>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub samplerate: Option<u32>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub ptime: Option<u32>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub path: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub format: Option<RecorderFormat>,
94}
95
96impl RecordingPolicy {
97 pub fn recorder_path(&self) -> String {
98 self.path
99 .as_ref()
100 .map(|p| p.trim())
101 .filter(|p| !p.is_empty())
102 .map(|p| p.to_string())
103 .unwrap_or_else(default_config_recorder_path)
104 }
105
106 pub fn recorder_format(&self) -> RecorderFormat {
107 self.format.unwrap_or_default()
108 }
109
110 pub fn ensure_defaults(&mut self) -> bool {
111 if self
112 .path
113 .as_ref()
114 .map(|p| p.trim().is_empty())
115 .unwrap_or(true)
116 {
117 self.path = Some(default_config_recorder_path());
118 }
119
120 false
121 }
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize)]
125pub struct RewriteRule {
126 pub r#match: String,
127 pub rewrite: String,
128}
129
130#[derive(Debug, Deserialize, Serialize)]
131pub struct Config {
132 #[serde(default = "default_config_http_addr")]
133 pub http_addr: String,
134 pub addr: String,
135 pub udp_port: u16,
136
137 pub log_level: Option<String>,
138 pub log_file: Option<String>,
139 #[serde(default, skip_serializing_if = "Vec::is_empty")]
140 pub http_access_skip_paths: Vec<String>,
141
142 pub useragent: Option<String>,
143 pub register_users: Option<Vec<RegisterOption>>,
144 pub graceful_shutdown: Option<bool>,
145 pub handler: Option<InviteHandlerConfig>,
146 pub accept_timeout: Option<String>,
147 #[serde(default = "default_codecs")]
148 pub codecs: Option<Vec<String>>,
149 pub external_ip: Option<String>,
150 #[serde(default = "default_config_rtp_start_port")]
151 pub rtp_start_port: Option<u16>,
152 #[serde(default = "default_config_rtp_end_port")]
153 pub rtp_end_port: Option<u16>,
154
155 pub callrecord: Option<CallRecordConfig>,
156 #[serde(default = "default_config_media_cache_path")]
157 pub media_cache_path: String,
158 pub ambiance: Option<AmbianceOption>,
159 pub ice_servers: Option<Vec<IceServer>>,
160 #[serde(default)]
161 pub recording: Option<RecordingPolicy>,
162 pub rewrites: Option<Vec<RewriteRule>>,
163}
164
165#[derive(Debug, Deserialize, Clone, Serialize)]
166#[serde(rename_all = "snake_case")]
167#[serde(tag = "type")]
168pub enum InviteHandlerConfig {
169 Webhook {
170 url: Option<String>,
171 urls: Option<Vec<String>>,
172 method: Option<String>,
173 headers: Option<Vec<(String, String)>>,
174 },
175 Playbook {
176 rules: Option<Vec<PlaybookRule>>,
177 default: Option<String>,
178 },
179}
180
181#[derive(Debug, Deserialize, Clone, Serialize)]
182#[serde(rename_all = "snake_case")]
183pub struct PlaybookRule {
184 pub caller: Option<String>,
185 pub callee: Option<String>,
186 pub playbook: String,
187}
188
189#[derive(Debug, Deserialize, Clone, Serialize)]
190#[serde(rename_all = "snake_case")]
191pub enum S3Vendor {
192 Aliyun,
193 Tencent,
194 Minio,
195 AWS,
196 GCP,
197 Azure,
198 DigitalOcean,
199}
200
201#[derive(Debug, Deserialize, Clone, Serialize)]
202#[serde(tag = "type")]
203#[serde(rename_all = "snake_case")]
204pub enum CallRecordConfig {
205 Local {
206 root: String,
207 },
208 S3 {
209 vendor: S3Vendor,
210 bucket: String,
211 region: String,
212 access_key: String,
213 secret_key: String,
214 endpoint: String,
215 root: String,
216 with_media: Option<bool>,
217 keep_media_copy: Option<bool>,
218 },
219 Http {
220 url: String,
221 headers: Option<HashMap<String, String>>,
222 with_media: Option<bool>,
223 keep_media_copy: Option<bool>,
224 },
225}
226
227impl Default for CallRecordConfig {
228 fn default() -> Self {
229 Self::Local {
230 #[cfg(target_os = "windows")]
231 root: "./config/cdr".to_string(),
232 #[cfg(not(target_os = "windows"))]
233 root: "./config/cdr".to_string(),
234 }
235 }
236}
237
238impl Default for Config {
239 fn default() -> Self {
240 Self {
241 http_addr: default_config_http_addr(),
242 log_level: None,
243 log_file: None,
244 http_access_skip_paths: Vec::new(),
245 addr: default_sip_addr(),
246 udp_port: default_sip_port(),
247 useragent: None,
248 register_users: None,
249 graceful_shutdown: Some(true),
250 handler: None,
251 accept_timeout: Some("50s".to_string()),
252 media_cache_path: default_config_media_cache_path(),
253 ambiance: None,
254 callrecord: None,
255 ice_servers: None,
256 codecs: None,
257 external_ip: None,
258 rtp_start_port: default_config_rtp_start_port(),
259 rtp_end_port: default_config_rtp_end_port(),
260 recording: None,
261 rewrites: None,
262 }
263 }
264}
265
266impl Clone for Config {
267 fn clone(&self) -> Self {
268 let s = toml::to_string(self).unwrap();
271 toml::from_str(&s).unwrap()
272 }
273}
274
275impl Config {
276 pub fn load(path: &str) -> Result<Self, Error> {
277 let config: Self = toml::from_str(
278 &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
279 )?;
280 Ok(config)
281 }
282
283 pub fn recorder_path(&self) -> String {
284 self.recording
285 .as_ref()
286 .map(|policy| policy.recorder_path())
287 .unwrap_or_else(default_config_recorder_path)
288 }
289
290 pub fn recorder_format(&self) -> RecorderFormat {
291 self.recording
292 .as_ref()
293 .map(|policy| policy.recorder_format())
294 .unwrap_or_default()
295 }
296
297 pub fn ensure_recording_defaults(&mut self) -> bool {
298 let mut fallback = false;
299
300 if let Some(policy) = self.recording.as_mut() {
301 fallback |= policy.ensure_defaults();
302 }
303
304 fallback
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_playbook_handler_config_parsing() {
314 let toml_config = r#"
315http_addr = "0.0.0.0:8080"
316addr = "0.0.0.0"
317udp_port = 25060
318
319[handler]
320type = "playbook"
321default = "default.md"
322
323[[handler.rules]]
324caller = "^\\+1\\d{10}$"
325callee = "^sip:support@.*"
326playbook = "support.md"
327
328[[handler.rules]]
329caller = "^\\+86\\d+"
330playbook = "chinese.md"
331
332[[handler.rules]]
333callee = "^sip:sales@.*"
334playbook = "sales.md"
335"#;
336
337 let config: Config = toml::from_str(toml_config).unwrap();
338
339 assert!(config.handler.is_some());
340 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
341 assert_eq!(default, Some("default.md".to_string()));
342 let rules = rules.unwrap();
343 assert_eq!(rules.len(), 3);
344
345 assert_eq!(rules[0].caller, Some(r"^\+1\d{10}$".to_string()));
346 assert_eq!(rules[0].callee, Some("^sip:support@.*".to_string()));
347 assert_eq!(rules[0].playbook, "support.md");
348
349 assert_eq!(rules[1].caller, Some(r"^\+86\d+".to_string()));
350 assert_eq!(rules[1].callee, None);
351 assert_eq!(rules[1].playbook, "chinese.md");
352
353 assert_eq!(rules[2].caller, None);
354 assert_eq!(rules[2].callee, Some("^sip:sales@.*".to_string()));
355 assert_eq!(rules[2].playbook, "sales.md");
356 } else {
357 panic!("Expected Playbook handler config");
358 }
359 }
360
361 #[test]
362 fn test_playbook_handler_config_without_default() {
363 let toml_config = r#"
364http_addr = "0.0.0.0:8080"
365addr = "0.0.0.0"
366udp_port = 25060
367
368[handler]
369type = "playbook"
370
371[[handler.rules]]
372caller = "^\\+1.*"
373playbook = "us.md"
374"#;
375
376 let config: Config = toml::from_str(toml_config).unwrap();
377
378 if let Some(InviteHandlerConfig::Playbook { rules, default }) = config.handler {
379 assert_eq!(default, None);
380 let rules = rules.unwrap();
381 assert_eq!(rules.len(), 1);
382 } else {
383 panic!("Expected Playbook handler config");
384 }
385 }
386
387 #[test]
388 fn test_webhook_handler_config_still_works() {
389 let toml_config = r#"
390http_addr = "0.0.0.0:8080"
391addr = "0.0.0.0"
392udp_port = 25060
393
394[handler]
395type = "webhook"
396url = "http://example.com/webhook"
397"#;
398
399 let config: Config = toml::from_str(toml_config).unwrap();
400
401 if let Some(InviteHandlerConfig::Webhook { url, .. }) = config.handler {
402 assert_eq!(url, Some("http://example.com/webhook".to_string()));
403 } else {
404 panic!("Expected Webhook handler config");
405 }
406 }
407}