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    str::FromStr,
9};
10
11use clap::ArgMatches;
12use directories::ProjectDirs;
13use serde::Deserialize;
14
15/// Default configuration file path
16pub fn get_default_config_path(config_file: &str) -> Option<PathBuf> {
17    // config.json in the current working directory ($PWD)
18    let config_files = vec![config_file, "config.json"];
19    if let Ok(mut path) = env::current_dir() {
20        for filename in &config_files {
21            path.push(filename);
22            if path.exists() {
23                return Some(path);
24            }
25            path.pop();
26        }
27    } else {
28        // config.json in the current working directory (relative path)
29        for filename in &config_files {
30            let relative_path = PathBuf::from(filename);
31            if relative_path.exists() {
32                return Some(relative_path);
33            }
34        }
35    }
36
37    // System standard directories
38    if let Some(project_dirs) = ProjectDirs::from("org", "shadowsocks", "shadowsocks-rust") {
39        // Linux: $XDG_CONFIG_HOME/shadowsocks-rust/config.json
40        //        $HOME/.config/shadowsocks-rust/config.json
41        // macOS: $HOME/Library/Application Support/org.shadowsocks.shadowsocks-rust/config.json
42        // Windows: {FOLDERID_RoamingAppData}/shadowsocks/shadowsocks-rust/config/config.json
43
44        let mut config_path = project_dirs.config_dir().to_path_buf();
45        for filename in &config_files {
46            config_path.push(filename);
47            if config_path.exists() {
48                return Some(config_path);
49            }
50            config_path.pop();
51        }
52    }
53
54    // UNIX systems, XDG Base Directory
55    // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
56    #[cfg(unix)]
57    {
58        let base_directories = xdg::BaseDirectories::with_prefix("shadowsocks-rust");
59        // $XDG_CONFIG_HOME/shadowsocks-rust/config.json
60        // for dir in $XDG_CONFIG_DIRS; $dir/shadowsocks-rust/config.json
61        for filename in &config_files {
62            if let Some(config_path) = base_directories.find_config_file(filename) {
63                return Some(config_path);
64            }
65        }
66    }
67
68    // UNIX global configuration file
69    #[cfg(unix)]
70    {
71        let mut global_config_path = PathBuf::from("/etc/shadowsocks-rust");
72        for filename in &config_files {
73            global_config_path.push(filename);
74            if global_config_path.exists() {
75                return Some(global_config_path.to_path_buf());
76            }
77            global_config_path.pop();
78        }
79    }
80
81    None
82}
83
84/// Error while reading `Config`
85#[derive(thiserror::Error, Debug)]
86pub enum ConfigError {
87    /// Input/Output error
88    #[error("{0}")]
89    IoError(#[from] io::Error),
90    /// JSON parsing error
91    #[error("{0}")]
92    JsonError(#[from] json5::Error),
93    /// Invalid value
94    #[error("Invalid value: {0}")]
95    InvalidValue(String),
96}
97
98/// Configuration Options for shadowsocks service runnables
99#[derive(Debug, Clone, 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        let ssconfig = json5::from_str(s)?;
124        Self::load_from_ssconfig(ssconfig)
125    }
126
127    fn load_from_ssconfig(ssconfig: SSConfig) -> Result<Self, ConfigError> {
128        let mut config = Self::default();
129
130        #[cfg(feature = "logging")]
131        if let Some(log) = ssconfig.log {
132            let mut nlog = LogConfig::default();
133            if let Some(level) = log.level {
134                nlog.level = level;
135            }
136
137            if let Some(format) = log.format {
138                let mut nformat = LogFormatConfig::default();
139                if let Some(without_time) = format.without_time {
140                    nformat.without_time = without_time;
141                }
142                nlog.format = nformat;
143            }
144
145            if let Some(config_path) = log.config_path {
146                nlog.config_path = Some(PathBuf::from(config_path));
147            }
148
149            config.log = nlog;
150        }
151
152        if let Some(runtime) = ssconfig.runtime {
153            let mut nruntime = RuntimeConfig::default();
154
155            #[cfg(feature = "multi-threaded")]
156            if let Some(worker_count) = runtime.worker_count {
157                nruntime.worker_count = Some(worker_count);
158            }
159
160            if let Some(mode) = runtime.mode {
161                match mode.parse::<RuntimeMode>() {
162                    Ok(m) => nruntime.mode = m,
163                    Err(..) => return Err(ConfigError::InvalidValue(mode)),
164                }
165            }
166
167            config.runtime = nruntime;
168        }
169
170        Ok(config)
171    }
172
173    /// Set by command line options
174    pub fn set_options(&mut self, matches: &ArgMatches) {
175        #[cfg(feature = "logging")]
176        {
177            let debug_level = matches.get_count("VERBOSE");
178            if debug_level > 0 {
179                self.log.level = debug_level as u32;
180            }
181
182            if matches.get_flag("LOG_WITHOUT_TIME") {
183                self.log.format.without_time = true;
184            }
185
186            if let Some(log_config) = matches.get_one::<PathBuf>("LOG_CONFIG").cloned() {
187                self.log.config_path = Some(log_config);
188            }
189        }
190
191        #[cfg(feature = "multi-threaded")]
192        if matches.get_flag("SINGLE_THREADED") {
193            self.runtime.mode = RuntimeMode::SingleThread;
194        }
195
196        #[cfg(feature = "multi-threaded")]
197        if let Some(worker_count) = matches.get_one::<usize>("WORKER_THREADS") {
198            self.runtime.worker_count = Some(*worker_count);
199        }
200
201        let _ = matches;
202    }
203}
204
205/// Logger configuration
206#[cfg(feature = "logging")]
207#[derive(Debug, Clone, Default)]
208pub struct LogConfig {
209    /// Default logger log level, [0, 3]
210    pub level: u32,
211    /// Default logger format configuration
212    pub format: LogFormatConfig,
213    /// Logging configuration file path
214    pub config_path: Option<PathBuf>,
215}
216
217/// Logger format configuration
218#[cfg(feature = "logging")]
219#[derive(Debug, Clone, Default)]
220pub struct LogFormatConfig {
221    pub without_time: bool,
222}
223
224/// Runtime mode (Tokio)
225#[derive(Debug, Clone, Copy, Default)]
226pub enum RuntimeMode {
227    /// Single-Thread Runtime
228    #[cfg_attr(not(feature = "multi-threaded"), default)]
229    SingleThread,
230    /// Multi-Thread Runtime
231    #[cfg(feature = "multi-threaded")]
232    #[cfg_attr(feature = "multi-threaded", default)]
233    MultiThread,
234}
235
236/// Parse `RuntimeMode` from string error
237#[derive(Debug)]
238pub struct RuntimeModeError;
239
240impl FromStr for RuntimeMode {
241    type Err = RuntimeModeError;
242
243    fn from_str(s: &str) -> Result<Self, Self::Err> {
244        match s {
245            "single_thread" => Ok(Self::SingleThread),
246            #[cfg(feature = "multi-threaded")]
247            "multi_thread" => Ok(Self::MultiThread),
248            _ => Err(RuntimeModeError),
249        }
250    }
251}
252
253/// Runtime configuration
254#[derive(Debug, Clone, Default)]
255pub struct RuntimeConfig {
256    /// Multithread runtime worker count, CPU count if not configured
257    #[cfg(feature = "multi-threaded")]
258    pub worker_count: Option<usize>,
259    /// Runtime Mode, single-thread, multi-thread
260    pub mode: RuntimeMode,
261}
262
263#[derive(Deserialize)]
264struct SSConfig {
265    #[cfg(feature = "logging")]
266    log: Option<SSLogConfig>,
267    runtime: Option<SSRuntimeConfig>,
268}
269
270#[cfg(feature = "logging")]
271#[derive(Deserialize)]
272struct SSLogConfig {
273    level: Option<u32>,
274    format: Option<SSLogFormat>,
275    config_path: Option<String>,
276}
277
278#[cfg(feature = "logging")]
279#[derive(Deserialize)]
280struct SSLogFormat {
281    without_time: Option<bool>,
282}
283
284#[derive(Deserialize)]
285struct SSRuntimeConfig {
286    #[cfg(feature = "multi-threaded")]
287    worker_count: Option<usize>,
288    mode: Option<String>,
289}