1use 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
14pub fn get_default_config_path(config_file: &str) -> Option<PathBuf> {
16 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 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 if let Some(project_dirs) = ProjectDirs::from("org", "shadowsocks", "shadowsocks-rust") {
38 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 #[cfg(unix)]
56 {
57 let base_directories = xdg::BaseDirectories::with_prefix("shadowsocks-rust");
58 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 #[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#[derive(thiserror::Error, Debug)]
85pub enum ConfigError {
86 #[error("{0}")]
88 IoError(#[from] io::Error),
89 #[error("{0}")]
91 JsonError(#[from] json5::Error),
92 #[error("Invalid value: {0}")]
94 InvalidValue(String),
95}
96
97#[derive(Deserialize, Debug, Clone, Default)]
99#[serde(default)]
100pub struct Config {
101 #[cfg(feature = "logging")]
103 pub log: LogConfig,
104
105 pub runtime: RuntimeConfig,
107}
108
109impl Config {
110 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 pub fn load_from_str(s: &str) -> Result<Self, ConfigError> {
123 json5::from_str(s).map_err(ConfigError::from)
124 }
125
126 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 let _ = matches;
156 }
157}
158
159#[cfg(feature = "logging")]
161#[derive(Deserialize, Debug, Clone)]
162#[serde(default)]
163pub struct LogConfig {
164 pub level: u32,
166 pub format: LogFormatConfig,
168 pub writers: Vec<LogWriterConfig>,
170 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#[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#[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#[cfg(feature = "logging")]
207#[derive(Deserialize, Debug, Clone, Default)]
208pub struct LogConsoleWriterConfig {
209 #[serde(default)]
211 pub level: Option<u32>,
212 #[serde(default)]
214 pub format: LogFormatConfigOverride,
215}
216
217#[cfg(feature = "logging")]
219#[derive(Deserialize, Debug, Clone, Default)]
220#[serde(default)]
221pub struct LogFormatConfigOverride {
222 pub without_time: Option<bool>,
223}
224
225#[cfg(feature = "logging")]
227#[derive(Deserialize, Debug, Clone)]
228pub struct LogFileWriterConfig {
229 #[serde(default)]
231 pub level: Option<u32>,
232 #[serde(default)]
234 pub format: LogFormatConfigOverride,
235
236 pub directory: PathBuf,
238 #[serde(default)]
240 pub rotation: LogRotation,
241 #[serde(default)]
243 pub prefix: Option<String>,
244 #[serde(default)]
246 pub suffix: Option<String>,
247 #[serde(default)]
249 pub max_files: Option<usize>,
250}
251
252#[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#[cfg(all(feature = "logging", unix))]
276#[derive(Deserialize, Debug, Clone)]
277pub struct LogSyslogWriterConfig {
278 #[serde(default)]
280 pub level: Option<u32>,
281 #[serde(default)]
283 pub format: LogFormatConfigOverride,
284
285 #[serde(default)]
287 pub identity: Option<String>,
288 #[serde(default)]
290 pub facility: Option<i32>,
291}
292
293#[derive(Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq)]
295#[serde(rename_all = "snake_case")]
296pub enum RuntimeMode {
297 #[cfg_attr(not(feature = "multi-threaded"), default)]
299 SingleThread,
300 #[cfg(feature = "multi-threaded")]
302 #[cfg_attr(feature = "multi-threaded", default)]
303 MultiThread,
304}
305
306#[derive(Deserialize, Debug, Clone, Default)]
308#[serde(default)]
309pub struct RuntimeConfig {
310 #[cfg(feature = "multi-threaded")]
312 pub worker_count: Option<usize>,
313 pub mode: RuntimeMode,
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_deser_empty() {
323 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 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 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 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 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}