turn_server/
config.rs

1use std::{collections::HashMap, fs::read_to_string, net::SocketAddr, str::FromStr};
2
3use anyhow::anyhow;
4use clap::Parser;
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7
8#[repr(C)]
9#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum Transport {
12    TCP = 0,
13    UDP = 1,
14}
15
16impl FromStr for Transport {
17    type Err = anyhow::Error;
18
19    fn from_str(value: &str) -> Result<Self, Self::Err> {
20        Ok(match value {
21            "udp" => Self::UDP,
22            "tcp" => Self::TCP,
23            _ => return Err(anyhow!("unknown transport: {value}")),
24        })
25    }
26}
27
28#[derive(Deserialize, Serialize, Debug, Clone)]
29pub struct Interface {
30    pub transport: Transport,
31    /// turn server listen address
32    pub bind: SocketAddr,
33    /// external address
34    ///
35    /// specify the node external address and port.
36    /// for the case of exposing the service to the outside,
37    /// you need to manually specify the server external IP
38    /// address and service listening port.
39    pub external: SocketAddr,
40}
41
42impl FromStr for Interface {
43    type Err = anyhow::Error;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        let (transport, addrs) = s
47            .split('@')
48            .collect_tuple()
49            .ok_or_else(|| anyhow!("invalid interface transport: {}", s))?;
50
51        let (bind, external) = addrs
52            .split('/')
53            .collect_tuple()
54            .ok_or_else(|| anyhow!("invalid interface address: {}", s))?;
55
56        Ok(Interface {
57            external: external.parse::<SocketAddr>()?,
58            bind: bind.parse::<SocketAddr>()?,
59            transport: transport.parse()?,
60        })
61    }
62}
63
64#[derive(Deserialize, Debug)]
65pub struct Turn {
66    /// turn server realm
67    ///
68    /// specify the domain where the server is located.
69    /// for a single node, this configuration is fixed,
70    /// but each node can be configured as a different domain.
71    /// this is a good idea to divide the nodes by namespace.
72    #[serde(default = "Turn::realm")]
73    pub realm: String,
74
75    /// turn server listen interfaces
76    ///
77    /// The address and port to which the UDP Server is bound. Multiple
78    /// addresses can be bound at the same time. The binding address supports
79    /// ipv4 and ipv6.
80    #[serde(default = "Turn::interfaces")]
81    pub interfaces: Vec<Interface>,
82}
83
84impl Turn {
85    pub fn get_externals(&self) -> Vec<SocketAddr> {
86        self.interfaces.iter().map(|item| item.external).collect()
87    }
88}
89
90impl Turn {
91    fn realm() -> String {
92        "localhost".to_string()
93    }
94
95    fn interfaces() -> Vec<Interface> {
96        vec![]
97    }
98}
99
100impl Default for Turn {
101    fn default() -> Self {
102        Self {
103            realm: Self::realm(),
104            interfaces: Self::interfaces(),
105        }
106    }
107}
108
109#[derive(Deserialize, Debug)]
110pub struct Api {
111    /// api bind
112    ///
113    /// This option specifies the http server binding address used to control
114    /// the turn server.
115    ///
116    /// Warn: This http server does not contain any means of authentication,
117    /// and sensitive information and dangerous operations can be obtained
118    /// through this service, please do not expose it directly to an unsafe
119    /// environment.
120    #[serde(default = "Api::bind")]
121    pub bind: SocketAddr,
122    /// hooks server url
123    ///
124    /// This option is used to specify the http address of the hooks service.
125    ///
126    /// Warn: This http server does not contain any means of authentication,
127    /// and sensitive information and dangerous operations can be obtained
128    /// through this service, please do not expose it directly to an unsafe
129    /// environment.
130    pub hooks: Option<String>,
131}
132
133impl Api {
134    fn bind() -> SocketAddr {
135        "127.0.0.1:3000".parse().unwrap()
136    }
137}
138
139impl Default for Api {
140    fn default() -> Self {
141        Self {
142            hooks: None,
143            bind: Self::bind(),
144        }
145    }
146}
147
148#[derive(Deserialize, Debug, Clone, Copy)]
149#[serde(rename_all = "lowercase")]
150pub enum LogLevel {
151    Error,
152    Warn,
153    Info,
154    Debug,
155    Trace,
156}
157
158impl FromStr for LogLevel {
159    type Err = String;
160
161    fn from_str(value: &str) -> Result<Self, Self::Err> {
162        Ok(match value {
163            "trace" => Self::Trace,
164            "debug" => Self::Debug,
165            "info" => Self::Info,
166            "warn" => Self::Warn,
167            "error" => Self::Error,
168            _ => return Err(format!("unknown log level: {value}")),
169        })
170    }
171}
172
173impl Default for LogLevel {
174    fn default() -> Self {
175        Self::Info
176    }
177}
178
179impl LogLevel {
180    pub fn as_level(&self) -> log::Level {
181        match *self {
182            Self::Error => log::Level::Error,
183            Self::Debug => log::Level::Debug,
184            Self::Trace => log::Level::Trace,
185            Self::Warn => log::Level::Warn,
186            Self::Info => log::Level::Info,
187        }
188    }
189}
190
191#[derive(Deserialize, Debug, Default)]
192pub struct Log {
193    /// log level
194    ///
195    /// An enum representing the available verbosity levels of the logger.
196    #[serde(default)]
197    pub level: LogLevel,
198}
199
200#[derive(Deserialize, Debug, Default)]
201pub struct Auth {
202    /// static user password
203    ///
204    /// This option can be used to specify the
205    /// static identity authentication information used by the turn server for
206    /// verification. Note: this is a high-priority authentication method, turn
207    /// The server will try to use static authentication first, and then use
208    /// external control service authentication.
209    #[serde(default)]
210    pub static_credentials: HashMap<String, String>,
211    /// Static authentication key value (string) that applies only to the TURN
212    /// REST API.
213    ///
214    /// If set, the turn server will not request external services via the HTTP
215    /// Hooks API to obtain the key.
216    pub static_auth_secret: Option<String>,
217}
218
219#[derive(Deserialize, Debug)]
220pub struct Config {
221    #[serde(default)]
222    pub turn: Turn,
223    #[serde(default)]
224    pub api: Api,
225    #[serde(default)]
226    pub log: Log,
227    #[serde(default)]
228    pub auth: Auth,
229}
230
231#[derive(Parser, Debug)]
232#[command(
233    about = env!("CARGO_PKG_DESCRIPTION"),
234    version = env!("CARGO_PKG_VERSION"),
235    author = env!("CARGO_PKG_AUTHORS"),
236)]
237struct Cli {
238    /// Specify the configuration file path
239    ///
240    /// Example: --config /etc/turn-rs/config.toml
241    #[arg(long, short)]
242    config: Option<String>,
243    /// Static user password
244    ///
245    /// Example: --auth-static-credentials test=test
246    #[arg(long, value_parser = Cli::parse_credential)]
247    auth_static_credentials: Option<Vec<(String, String)>>,
248    /// Static authentication key value (string) that applies only to the TURN
249    /// REST API
250    #[arg(long)]
251    auth_static_auth_secret: Option<String>,
252    /// An enum representing the available verbosity levels of the logger
253    #[arg(
254        long,
255        value_parser = clap::value_parser!(LogLevel),
256    )]
257    log_level: Option<LogLevel>,
258    /// This option specifies the http server binding address used to control
259    /// the turn server
260    #[arg(long)]
261    api_bind: Option<SocketAddr>,
262    /// This option is used to specify the http address of the hooks service
263    ///
264    /// Example: --api-hooks http://localhost:8080/turn
265    #[arg(long)]
266    api_hooks: Option<String>,
267    /// TURN server realm
268    #[arg(long)]
269    turn_realm: Option<String>,
270    /// TURN server listen interfaces
271    ///
272    /// Example: --turn-interfaces udp@127.0.0.1:3478/127.0.0.1:3478
273    #[arg(long)]
274    turn_interfaces: Option<Vec<Interface>>,
275}
276
277impl Cli {
278    // [username]:[password]
279    fn parse_credential(s: &str) -> Result<(String, String), anyhow::Error> {
280        let (username, password) = s
281            .split('=')
282            .collect_tuple()
283            .ok_or_else(|| anyhow!("invalid credential str: {}", s))?;
284        Ok((username.to_string(), password.to_string()))
285    }
286}
287
288impl Config {
289    /// Load configure from config file and command line parameters.
290    ///
291    /// Load command line parameters, if the configuration file path is
292    /// specified, the configuration is read from the configuration file,
293    /// otherwise the default configuration is used.
294    pub fn load() -> anyhow::Result<Self> {
295        let cli = Cli::parse();
296        let mut config = toml::from_str::<Self>(
297            &cli.config
298                .and_then(|path| read_to_string(path).ok())
299                .unwrap_or("".to_string()),
300        )?;
301
302        // Command line arguments have a high priority and override configuration file
303        // options; here they are used to replace the configuration parsed out of the
304        // configuration file.
305        {
306            if let Some(credentials) = cli.auth_static_credentials {
307                for (k, v) in credentials {
308                    config.auth.static_credentials.insert(k, v);
309                }
310            }
311
312            if let Some(secret) = cli.auth_static_auth_secret {
313                config.auth.static_auth_secret.replace(secret);
314            }
315
316            if let Some(level) = cli.log_level {
317                config.log.level = level;
318            }
319
320            if let Some(bind) = cli.api_bind {
321                config.api.bind = bind;
322            }
323
324            if let Some(hooks) = cli.api_hooks {
325                config.api.hooks.replace(hooks);
326            }
327
328            if let Some(realm) = cli.turn_realm {
329                config.turn.realm = realm;
330            }
331
332            if let Some(interfaces) = cli.turn_interfaces {
333                for interface in interfaces {
334                    config.turn.interfaces.push(interface);
335                }
336            }
337        }
338
339        // Filters out transport protocols that are not enabled.
340        {
341            let mut interfaces = Vec::with_capacity(config.turn.interfaces.len());
342
343            {
344                for it in &config.turn.interfaces {
345                    #[cfg(feature = "udp")]
346                    if it.transport == Transport::UDP {
347                        interfaces.push(it.clone());
348                    }
349
350                    #[cfg(feature = "tcp")]
351                    if it.transport == Transport::TCP {
352                        interfaces.push(it.clone());
353                    }
354                }
355            }
356
357            config.turn.interfaces = interfaces;
358        }
359
360        Ok(config)
361    }
362}