use std::{
env,
fs::OpenOptions,
io::{self, Read},
path::{Path, PathBuf},
};
use clap::ArgMatches;
use directories::ProjectDirs;
use serde::Deserialize;
pub fn get_default_config_path(config_file: &str) -> Option<PathBuf> {
let config_files = vec![config_file, "config.json"];
if let Ok(mut path) = env::current_dir() {
for filename in &config_files {
path.push(filename);
if path.exists() {
return Some(path);
}
path.pop();
}
} else {
for filename in &config_files {
let relative_path = PathBuf::from(filename);
if relative_path.exists() {
return Some(relative_path);
}
}
}
if let Some(project_dirs) = ProjectDirs::from("org", "shadowsocks", "shadowsocks-rust") {
let mut config_path = project_dirs.config_dir().to_path_buf();
for filename in &config_files {
config_path.push(filename);
if config_path.exists() {
return Some(config_path);
}
config_path.pop();
}
}
#[cfg(unix)]
{
let base_directories = xdg::BaseDirectories::with_prefix("shadowsocks-rust");
for filename in &config_files {
if let Some(config_path) = base_directories.find_config_file(filename) {
return Some(config_path);
}
}
}
#[cfg(unix)]
{
let mut global_config_path = PathBuf::from("/etc/shadowsocks-rust");
for filename in &config_files {
global_config_path.push(filename);
if global_config_path.exists() {
return Some(global_config_path.to_path_buf());
}
global_config_path.pop();
}
}
None
}
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
#[error("{0}")]
IoError(#[from] io::Error),
#[error("{0}")]
JsonError(#[from] json5::Error),
#[error("Invalid value: {0}")]
InvalidValue(String),
}
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct Config {
#[cfg(feature = "logging")]
pub log: LogConfig,
pub runtime: RuntimeConfig,
}
impl Config {
pub fn load_from_file<P: AsRef<Path>>(filename: &P) -> Result<Self, ConfigError> {
let filename = filename.as_ref();
let mut reader = OpenOptions::new().read(true).open(filename)?;
let mut content = String::new();
reader.read_to_string(&mut content)?;
Self::load_from_str(&content)
}
pub fn load_from_str(s: &str) -> Result<Self, ConfigError> {
json5::from_str(s).map_err(ConfigError::from)
}
pub fn set_options(&mut self, matches: &ArgMatches) {
#[cfg(feature = "logging")]
{
let debug_level = matches.get_count("VERBOSE");
if debug_level > 0 {
self.log.level = debug_level as u32;
}
if matches.get_flag("LOG_WITHOUT_TIME") {
self.log.format.without_time = true;
}
if let Some(log_config) = matches.get_one::<PathBuf>("LOG_CONFIG").cloned() {
self.log.config_path = Some(log_config);
}
}
#[cfg(feature = "multi-threaded")]
if matches.get_flag("SINGLE_THREADED") {
self.runtime.mode = RuntimeMode::SingleThread;
}
#[cfg(feature = "multi-threaded")]
if let Some(worker_count) = matches.get_one::<usize>("WORKER_THREADS") {
self.runtime.worker_count = Some(*worker_count);
}
let _ = matches;
}
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Clone)]
#[serde(default)]
pub struct LogConfig {
pub level: u32,
pub format: LogFormatConfig,
pub writers: Vec<LogWriterConfig>,
pub config_path: Option<PathBuf>,
}
#[cfg(feature = "logging")]
impl Default for LogConfig {
fn default() -> Self {
LogConfig {
level: 0,
format: LogFormatConfig::default(),
writers: vec![LogWriterConfig::Console(LogConsoleWriterConfig::default())],
config_path: None,
}
}
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Clone, Default, Eq, PartialEq)]
#[serde(default)]
pub struct LogFormatConfig {
pub without_time: bool,
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum LogWriterConfig {
Console(LogConsoleWriterConfig),
File(LogFileWriterConfig),
#[cfg(unix)]
Syslog(LogSyslogWriterConfig),
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Clone, Default)]
pub struct LogConsoleWriterConfig {
#[serde(default)]
pub level: Option<u32>,
#[serde(default)]
pub format: LogFormatConfigOverride,
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct LogFormatConfigOverride {
pub without_time: Option<bool>,
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Clone)]
pub struct LogFileWriterConfig {
#[serde(default)]
pub level: Option<u32>,
#[serde(default)]
pub format: LogFormatConfigOverride,
pub directory: PathBuf,
#[serde(default)]
pub rotation: LogRotation,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub suffix: Option<String>,
#[serde(default)]
pub max_files: Option<usize>,
}
#[cfg(feature = "logging")]
#[derive(Deserialize, Debug, Copy, Clone, Default, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum LogRotation {
#[default]
Never,
Hourly,
Daily,
}
#[cfg(feature = "logging")]
impl From<LogRotation> for tracing_appender::rolling::Rotation {
fn from(rotation: LogRotation) -> Self {
match rotation {
LogRotation::Never => Self::NEVER,
LogRotation::Hourly => Self::HOURLY,
LogRotation::Daily => Self::DAILY,
}
}
}
#[cfg(all(feature = "logging", unix))]
#[derive(Deserialize, Debug, Clone)]
pub struct LogSyslogWriterConfig {
#[serde(default)]
pub level: Option<u32>,
#[serde(default)]
pub format: LogFormatConfigOverride,
#[serde(default)]
pub identity: Option<String>,
#[serde(default)]
pub facility: Option<i32>,
}
#[derive(Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMode {
#[cfg_attr(not(feature = "multi-threaded"), default)]
SingleThread,
#[cfg(feature = "multi-threaded")]
#[cfg_attr(feature = "multi-threaded", default)]
MultiThread,
}
#[derive(Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct RuntimeConfig {
#[cfg(feature = "multi-threaded")]
pub worker_count: Option<usize>,
pub mode: RuntimeMode,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deser_empty() {
let config: Config = Config::load_from_str("{}").unwrap();
assert_eq!(config.runtime.mode, RuntimeMode::default());
#[cfg(feature = "multi-threaded")]
{
assert!(config.runtime.worker_count.is_none());
}
#[cfg(feature = "logging")]
{
assert_eq!(config.log.level, 0);
assert!(!config.log.format.without_time);
assert_eq!(config.log.writers.len(), 1);
if let LogWriterConfig::Console(stdout_config) = &config.log.writers[0] {
assert_eq!(stdout_config.level, None);
assert_eq!(stdout_config.format.without_time, None);
} else {
panic!("Expected a stdout writer configuration");
}
}
}
#[test]
fn test_deser_disable_logging() {
let config_str = r#"
{
"log": {
"writers": []
}
}
"#;
let config: Config = Config::load_from_str(config_str).unwrap();
#[cfg(feature = "logging")]
{
assert_eq!(config.log.level, 0);
assert!(!config.log.format.without_time);
assert!(config.log.writers.is_empty());
}
}
#[test]
fn test_deser_file_writer_full() {
let config_str = r#"
{
"log": {
"writers": [
{
"file": {
"level": 2,
"format": {
"without_time": true
},
"directory": "/var/log/shadowsocks",
"rotation": "daily",
"prefix": "ss-rust",
"suffix": "log",
"max_files": 5
}
}
]
}
}
"#;
let config: Config = Config::load_from_str(config_str).unwrap();
#[cfg(feature = "logging")]
{
assert_eq!(config.log.writers.len(), 1);
if let LogWriterConfig::File(file_config) = &config.log.writers[0] {
assert_eq!(file_config.level, Some(2));
assert_eq!(file_config.format.without_time, Some(true));
assert_eq!(file_config.directory, PathBuf::from("/var/log/shadowsocks"));
assert_eq!(file_config.rotation, LogRotation::Daily);
assert_eq!(file_config.prefix.as_deref(), Some("ss-rust"));
assert_eq!(file_config.suffix.as_deref(), Some("log"));
assert_eq!(file_config.max_files, Some(5));
} else {
panic!("Expected a file writer configuration");
}
}
}
#[test]
fn test_deser_file_writer_minimal() {
let config_str = r#"
{
"log": {
"writers": [
{
"file": {
"directory": "/var/log/shadowsocks"
}
}
]
}
}
"#;
let config: Config = Config::load_from_str(config_str).unwrap();
#[cfg(feature = "logging")]
{
assert_eq!(config.log.writers.len(), 1);
if let LogWriterConfig::File(file_config) = &config.log.writers[0] {
assert_eq!(file_config.level, None);
assert_eq!(file_config.format.without_time, None);
assert_eq!(file_config.directory, PathBuf::from("/var/log/shadowsocks"));
assert_eq!(file_config.rotation, LogRotation::Never);
assert!(file_config.prefix.is_none());
assert!(file_config.suffix.is_none());
assert!(file_config.max_files.is_none());
} else {
panic!("Expected a file writer configuration");
}
}
}
#[test]
fn test_deser_console_writer_full() {
let config_str = r#"
{
"log": {
"writers": [
{
"console": {
"level": 1,
"format": {
"without_time": false
}
}
}
]
}
}
"#;
let config: Config = Config::load_from_str(config_str).unwrap();
#[cfg(feature = "logging")]
{
assert_eq!(config.log.writers.len(), 1);
if let LogWriterConfig::Console(stdout_config) = &config.log.writers[0] {
assert_eq!(stdout_config.level, Some(1));
assert_eq!(stdout_config.format.without_time, Some(false));
} else {
panic!("Expected a console writer configuration");
}
}
}
#[test]
fn test_deser_console_writer_minimal() {
let config_str = r#"
{
"log": {
"writers": [
{
"console": {}
}
]
}
}
"#;
let config: Config = Config::load_from_str(config_str).unwrap();
#[cfg(feature = "logging")]
{
assert_eq!(config.log.writers.len(), 1);
if let LogWriterConfig::Console(stdout_config) = &config.log.writers[0] {
assert_eq!(stdout_config.level, None);
assert_eq!(stdout_config.format.without_time, None);
} else {
panic!("Expected a console writer configuration");
}
}
}
}