shadowsocks_rust/
config.rs

1//! Common configuration utilities
2
3use std::{
4    env,
5    fs::OpenOptions,
6    io::{self, Read},
7    path::{Path, PathBuf},
8};
9
10use clap::ArgMatches;
11use directories::ProjectDirs;
12use serde::Deserialize;
13
14/// Default configuration file path
15pub fn get_default_config_path(config_file: &str) -> Option<PathBuf> {
16    // config.json in the current working directory ($PWD)
17    let config_files = vec![config_file, "config.json"];
18    if let Ok(mut path) = env::current_dir() {
19        for filename in &config_files {
20            path.push(filename);
21            if path.exists() {
22                return Some(path);
23            }
24            path.pop();
25        }
26    } else {
27        // config.json in the current working directory (relative path)
28        for filename in &config_files {
29            let relative_path = PathBuf::from(filename);
30            if relative_path.exists() {
31                return Some(relative_path);
32            }
33        }
34    }
35
36    // System standard directories
37    if let Some(project_dirs) = ProjectDirs::from("org", "shadowsocks", "shadowsocks-rust") {
38        // Linux: $XDG_CONFIG_HOME/shadowsocks-rust/config.json
39        //        $HOME/.config/shadowsocks-rust/config.json
40        // macOS: $HOME/Library/Application Support/org.shadowsocks.shadowsocks-rust/config.json
41        // Windows: {FOLDERID_RoamingAppData}/shadowsocks/shadowsocks-rust/config/config.json
42
43        let mut config_path = project_dirs.config_dir().to_path_buf();
44        for filename in &config_files {
45            config_path.push(filename);
46            if config_path.exists() {
47                return Some(config_path);
48            }
49            config_path.pop();
50        }
51    }
52
53    // UNIX systems, XDG Base Directory
54    // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
55    #[cfg(unix)]
56    {
57        let base_directories = xdg::BaseDirectories::with_prefix("shadowsocks-rust");
58        // $XDG_CONFIG_HOME/shadowsocks-rust/config.json
59        // for dir in $XDG_CONFIG_DIRS; $dir/shadowsocks-rust/config.json
60        for filename in &config_files {
61            if let Some(config_path) = base_directories.find_config_file(filename) {
62                return Some(config_path);
63            }
64        }
65    }
66
67    // UNIX global configuration file
68    #[cfg(unix)]
69    {
70        let mut global_config_path = PathBuf::from("/etc/shadowsocks-rust");
71        for filename in &config_files {
72            global_config_path.push(filename);
73            if global_config_path.exists() {
74                return Some(global_config_path.to_path_buf());
75            }
76            global_config_path.pop();
77        }
78    }
79
80    None
81}
82
83/// Error while reading `Config`
84#[derive(thiserror::Error, Debug)]
85pub enum ConfigError {
86    /// Input/Output error
87    #[error("{0}")]
88    IoError(#[from] io::Error),
89    /// JSON parsing error
90    #[error("{0}")]
91    JsonError(#[from] json5::Error),
92    /// Invalid value
93    #[error("Invalid value: {0}")]
94    InvalidValue(String),
95}
96
97/// Configuration Options for shadowsocks service runnables
98#[derive(Deserialize, Debug, Clone, Default)]
99#[serde(default)]
100pub struct Config {
101    /// Logger configuration
102    #[cfg(feature = "logging")]
103    pub log: LogConfig,
104
105    /// Runtime configuration
106    pub runtime: RuntimeConfig,
107}
108
109impl Config {
110    /// Load `Config` from file
111    pub fn load_from_file<P: AsRef<Path>>(filename: &P) -> Result<Self, ConfigError> {
112        let filename = filename.as_ref();
113
114        let mut reader = OpenOptions::new().read(true).open(filename)?;
115        let mut content = String::new();
116        reader.read_to_string(&mut content)?;
117
118        Self::load_from_str(&content)
119    }
120
121    /// Load `Config` from string
122    pub fn load_from_str(s: &str) -> Result<Self, ConfigError> {
123        json5::from_str(s).map_err(ConfigError::from)
124    }
125
126    /// Set by command line options
127    pub fn set_options(&mut self, matches: &ArgMatches) {
128        #[cfg(feature = "logging")]
129        {
130            let debug_level = matches.get_count("VERBOSE");
131            if debug_level > 0 {
132                self.log.level = debug_level as u32;
133            }
134
135            if matches.get_flag("LOG_WITHOUT_TIME") {
136                self.log.format.without_time = true;
137            }
138
139            if let Some(log_config) = matches.get_one::<PathBuf>("LOG_CONFIG").cloned() {
140                self.log.config_path = Some(log_config);
141            }
142        }
143
144        #[cfg(feature = "multi-threaded")]
145        if matches.get_flag("SINGLE_THREADED") {
146            self.runtime.mode = RuntimeMode::SingleThread;
147        }
148
149        #[cfg(feature = "multi-threaded")]
150        if let Some(worker_count) = matches.get_one::<usize>("WORKER_THREADS") {
151            self.runtime.worker_count = Some(*worker_count);
152        }
153
154        // suppress unused warning
155        let _ = matches;
156    }
157}
158
159/// Logger configuration
160#[cfg(feature = "logging")]
161#[derive(Deserialize, Debug, Clone)]
162#[serde(default)]
163pub struct LogConfig {
164    /// Default log level for all writers, [0, 3]
165    pub level: u32,
166    /// Default format configuration for all writers
167    pub format: LogFormatConfig,
168    /// Log writers configuration
169    pub writers: Vec<LogWriterConfig>,
170    /// Deprecated: Path to the `log4rs` config file
171    pub config_path: Option<PathBuf>,
172}
173
174#[cfg(feature = "logging")]
175impl Default for LogConfig {
176    fn default() -> Self {
177        LogConfig {
178            level: 0,
179            format: LogFormatConfig::default(),
180            writers: vec![LogWriterConfig::Console(LogConsoleWriterConfig::default())],
181            config_path: None,
182        }
183    }
184}
185
186/// Logger format configuration
187#[cfg(feature = "logging")]
188#[derive(Deserialize, Debug, Clone, Default, Eq, PartialEq)]
189#[serde(default)]
190pub struct LogFormatConfig {
191    pub without_time: bool,
192}
193
194/// Holds writer-specific configuration for logging
195#[cfg(feature = "logging")]
196#[derive(Deserialize, Debug, Clone)]
197#[serde(rename_all = "snake_case")]
198pub enum LogWriterConfig {
199    Console(LogConsoleWriterConfig),
200    File(LogFileWriterConfig),
201    #[cfg(unix)]
202    Syslog(LogSyslogWriterConfig),
203}
204
205/// Console appender configuration for logging
206#[cfg(feature = "logging")]
207#[derive(Deserialize, Debug, Clone, Default)]
208pub struct LogConsoleWriterConfig {
209    /// Level override
210    #[serde(default)]
211    pub level: Option<u32>,
212    /// Format override
213    #[serde(default)]
214    pub format: LogFormatConfigOverride,
215}
216
217/// Logger format override
218#[cfg(feature = "logging")]
219#[derive(Deserialize, Debug, Clone, Default)]
220#[serde(default)]
221pub struct LogFormatConfigOverride {
222    pub without_time: Option<bool>,
223}
224
225/// File appender configuration for logging
226#[cfg(feature = "logging")]
227#[derive(Deserialize, Debug, Clone)]
228pub struct LogFileWriterConfig {
229    /// Level override
230    #[serde(default)]
231    pub level: Option<u32>,
232    /// Format override
233    #[serde(default)]
234    pub format: LogFormatConfigOverride,
235
236    /// Directory to store log files
237    pub directory: PathBuf,
238    /// Rotation strategy for log files. Default is `Rotation::NEVER`.
239    #[serde(default)]
240    pub rotation: LogRotation,
241    /// Prefix for log file names. Default is the binary name.
242    #[serde(default)]
243    pub prefix: Option<String>,
244    /// Suffix for log file names. Default is "log".
245    #[serde(default)]
246    pub suffix: Option<String>,
247    /// Maximum number of log files to keep. Default is `None`, meaning no limit.
248    #[serde(default)]
249    pub max_files: Option<usize>,
250}
251
252/// Log rotation frequency
253#[cfg(feature = "logging")]
254#[derive(Deserialize, Debug, Copy, Clone, Default, Eq, PartialEq)]
255#[serde(rename_all = "snake_case")]
256pub enum LogRotation {
257    #[default]
258    Never,
259    Hourly,
260    Daily,
261}
262
263#[cfg(feature = "logging")]
264impl From<LogRotation> for tracing_appender::rolling::Rotation {
265    fn from(rotation: LogRotation) -> Self {
266        match rotation {
267            LogRotation::Never => Self::NEVER,
268            LogRotation::Hourly => Self::HOURLY,
269            LogRotation::Daily => Self::DAILY,
270        }
271    }
272}
273
274/// File appender configuration for logging
275#[cfg(all(feature = "logging", unix))]
276#[derive(Deserialize, Debug, Clone)]
277pub struct LogSyslogWriterConfig {
278    /// Level override
279    #[serde(default)]
280    pub level: Option<u32>,
281    /// Format override
282    #[serde(default)]
283    pub format: LogFormatConfigOverride,
284
285    /// syslog identity, process name by default
286    #[serde(default)]
287    pub identity: Option<String>,
288    /// Facility, 1 (USER) by default
289    #[serde(default)]
290    pub facility: Option<i32>,
291}
292
293/// Runtime mode (Tokio)
294#[derive(Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq)]
295#[serde(rename_all = "snake_case")]
296pub enum RuntimeMode {
297    /// Single-Thread Runtime
298    #[cfg_attr(not(feature = "multi-threaded"), default)]
299    SingleThread,
300    /// Multi-Thread Runtime
301    #[cfg(feature = "multi-threaded")]
302    #[cfg_attr(feature = "multi-threaded", default)]
303    MultiThread,
304}
305
306/// Runtime configuration
307#[derive(Deserialize, Debug, Clone, Default)]
308#[serde(default)]
309pub struct RuntimeConfig {
310    /// Multithread runtime worker count, CPU count if not configured
311    #[cfg(feature = "multi-threaded")]
312    pub worker_count: Option<usize>,
313    /// Runtime Mode, single-thread, multi-thread
314    pub mode: RuntimeMode,
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_deser_empty() {
323        // empty config should load successfully
324        let config: Config = Config::load_from_str("{}").unwrap();
325        assert_eq!(config.runtime.mode, RuntimeMode::default());
326        #[cfg(feature = "multi-threaded")]
327        {
328            assert!(config.runtime.worker_count.is_none());
329        }
330        #[cfg(feature = "logging")]
331        {
332            assert_eq!(config.log.level, 0);
333            assert!(!config.log.format.without_time);
334            // default writer configuration should contain a stdout writer
335            assert_eq!(config.log.writers.len(), 1);
336            if let LogWriterConfig::Console(stdout_config) = &config.log.writers[0] {
337                assert_eq!(stdout_config.level, None);
338                assert_eq!(stdout_config.format.without_time, None);
339            } else {
340                panic!("Expected a stdout writer configuration");
341            }
342        }
343    }
344
345    #[test]
346    fn test_deser_disable_logging() {
347        // allow user explicitly disable logging by providing an empty writers array
348        let config_str = r#"
349            {
350                "log": {
351                    "writers": []
352                }
353            }
354        "#;
355        let config: Config = Config::load_from_str(config_str).unwrap();
356        #[cfg(feature = "logging")]
357        {
358            assert_eq!(config.log.level, 0);
359            assert!(!config.log.format.without_time);
360            assert!(config.log.writers.is_empty());
361        }
362    }
363
364    #[test]
365    fn test_deser_file_writer_full() {
366        let config_str = r#"
367            {
368                "log": {
369                    "writers": [
370                        {
371                            "file": {
372                                "level": 2,
373                                "format": {
374                                    "without_time": true
375                                },
376                                "directory": "/var/log/shadowsocks",
377                                "rotation": "daily",
378                                "prefix": "ss-rust",
379                                "suffix": "log",
380                                "max_files": 5
381                            }
382                        }
383                    ]
384                }
385            }
386        "#;
387        let config: Config = Config::load_from_str(config_str).unwrap();
388        #[cfg(feature = "logging")]
389        {
390            assert_eq!(config.log.writers.len(), 1);
391            if let LogWriterConfig::File(file_config) = &config.log.writers[0] {
392                assert_eq!(file_config.level, Some(2));
393                assert_eq!(file_config.format.without_time, Some(true));
394                assert_eq!(file_config.directory, PathBuf::from("/var/log/shadowsocks"));
395                assert_eq!(file_config.rotation, LogRotation::Daily);
396                assert_eq!(file_config.prefix.as_deref(), Some("ss-rust"));
397                assert_eq!(file_config.suffix.as_deref(), Some("log"));
398                assert_eq!(file_config.max_files, Some(5));
399            } else {
400                panic!("Expected a file writer configuration");
401            }
402        }
403    }
404
405    #[test]
406    fn test_deser_file_writer_minimal() {
407        // Minimal valid file writer configuration
408        let config_str = r#"
409            {
410                "log": {
411                    "writers": [
412                        {
413                            "file": {
414                                "directory": "/var/log/shadowsocks"
415                            }
416                        }
417                    ]
418                }
419            }
420        "#;
421        let config: Config = Config::load_from_str(config_str).unwrap();
422        #[cfg(feature = "logging")]
423        {
424            assert_eq!(config.log.writers.len(), 1);
425            if let LogWriterConfig::File(file_config) = &config.log.writers[0] {
426                assert_eq!(file_config.level, None);
427                assert_eq!(file_config.format.without_time, None);
428                assert_eq!(file_config.directory, PathBuf::from("/var/log/shadowsocks"));
429                assert_eq!(file_config.rotation, LogRotation::Never);
430                assert!(file_config.prefix.is_none());
431                assert!(file_config.suffix.is_none());
432                assert!(file_config.max_files.is_none());
433            } else {
434                panic!("Expected a file writer configuration");
435            }
436        }
437    }
438    #[test]
439    fn test_deser_console_writer_full() {
440        let config_str = r#"
441            {
442                "log": {
443                    "writers": [
444                        {
445                            "console": {
446                                "level": 1,
447                                "format": {
448                                    "without_time": false
449                                }
450                            }
451                        }
452                    ]
453                }
454            }
455        "#;
456        let config: Config = Config::load_from_str(config_str).unwrap();
457        #[cfg(feature = "logging")]
458        {
459            assert_eq!(config.log.writers.len(), 1);
460            if let LogWriterConfig::Console(stdout_config) = &config.log.writers[0] {
461                assert_eq!(stdout_config.level, Some(1));
462                assert_eq!(stdout_config.format.without_time, Some(false));
463            } else {
464                panic!("Expected a console writer configuration");
465            }
466        }
467    }
468
469    #[test]
470    fn test_deser_console_writer_minimal() {
471        // Minimal valid console writer configuration
472        let config_str = r#"
473            {
474                "log": {
475                    "writers": [
476                        {
477                            "console": {}
478                        }
479                    ]
480                }
481            }
482        "#;
483        let config: Config = Config::load_from_str(config_str).unwrap();
484        #[cfg(feature = "logging")]
485        {
486            assert_eq!(config.log.writers.len(), 1);
487            if let LogWriterConfig::Console(stdout_config) = &config.log.writers[0] {
488                assert_eq!(stdout_config.level, None);
489                assert_eq!(stdout_config.format.without_time, None);
490            } else {
491                panic!("Expected a console writer configuration");
492            }
493        }
494    }
495}