mirror of
https://github.com/shadowsocks/shadowsocks-rust.git
synced 2026-11-06 16:14:11 +00:00
496 lines
15 KiB
Rust
496 lines
15 KiB
Rust
//! Common configuration utilities
|
|
|
|
use std::{
|
|
env,
|
|
fs::OpenOptions,
|
|
io::{self, Read},
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use clap::ArgMatches;
|
|
use directories::ProjectDirs;
|
|
use serde::Deserialize;
|
|
|
|
/// Default configuration file path
|
|
pub fn get_default_config_path(config_file: &str) -> Option<PathBuf> {
|
|
// config.json in the current working directory ($PWD)
|
|
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 {
|
|
// config.json in the current working directory (relative path)
|
|
for filename in &config_files {
|
|
let relative_path = PathBuf::from(filename);
|
|
if relative_path.exists() {
|
|
return Some(relative_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// System standard directories
|
|
if let Some(project_dirs) = ProjectDirs::from("org", "shadowsocks", "shadowsocks-rust") {
|
|
// Linux: $XDG_CONFIG_HOME/shadowsocks-rust/config.json
|
|
// $HOME/.config/shadowsocks-rust/config.json
|
|
// macOS: $HOME/Library/Application Support/org.shadowsocks.shadowsocks-rust/config.json
|
|
// Windows: {FOLDERID_RoamingAppData}/shadowsocks/shadowsocks-rust/config/config.json
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
// UNIX systems, XDG Base Directory
|
|
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
#[cfg(unix)]
|
|
{
|
|
let base_directories = xdg::BaseDirectories::with_prefix("shadowsocks-rust");
|
|
// $XDG_CONFIG_HOME/shadowsocks-rust/config.json
|
|
// for dir in $XDG_CONFIG_DIRS; $dir/shadowsocks-rust/config.json
|
|
for filename in &config_files {
|
|
if let Some(config_path) = base_directories.find_config_file(filename) {
|
|
return Some(config_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// UNIX global configuration file
|
|
#[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
|
|
}
|
|
|
|
/// Error while reading `Config`
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum ConfigError {
|
|
/// Input/Output error
|
|
#[error("{0}")]
|
|
IoError(#[from] io::Error),
|
|
/// JSON parsing error
|
|
#[error("{0}")]
|
|
JsonError(#[from] json5::Error),
|
|
/// Invalid value
|
|
#[error("Invalid value: {0}")]
|
|
InvalidValue(String),
|
|
}
|
|
|
|
/// Configuration Options for shadowsocks service runnables
|
|
#[derive(Deserialize, Debug, Clone, Default)]
|
|
#[serde(default)]
|
|
pub struct Config {
|
|
/// Logger configuration
|
|
#[cfg(feature = "logging")]
|
|
pub log: LogConfig,
|
|
|
|
/// Runtime configuration
|
|
pub runtime: RuntimeConfig,
|
|
}
|
|
|
|
impl Config {
|
|
/// Load `Config` from file
|
|
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)
|
|
}
|
|
|
|
/// Load `Config` from string
|
|
pub fn load_from_str(s: &str) -> Result<Self, ConfigError> {
|
|
json5::from_str(s).map_err(ConfigError::from)
|
|
}
|
|
|
|
/// Set by command line options
|
|
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);
|
|
}
|
|
|
|
// suppress unused warning
|
|
let _ = matches;
|
|
}
|
|
}
|
|
|
|
/// Logger configuration
|
|
#[cfg(feature = "logging")]
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
#[serde(default)]
|
|
pub struct LogConfig {
|
|
/// Default log level for all writers, [0, 3]
|
|
pub level: u32,
|
|
/// Default format configuration for all writers
|
|
pub format: LogFormatConfig,
|
|
/// Log writers configuration
|
|
pub writers: Vec<LogWriterConfig>,
|
|
/// Deprecated: Path to the `log4rs` config file
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Logger format configuration
|
|
#[cfg(feature = "logging")]
|
|
#[derive(Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
|
#[serde(default)]
|
|
pub struct LogFormatConfig {
|
|
pub without_time: bool,
|
|
}
|
|
|
|
/// Holds writer-specific configuration for logging
|
|
#[cfg(feature = "logging")]
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum LogWriterConfig {
|
|
Console(LogConsoleWriterConfig),
|
|
File(LogFileWriterConfig),
|
|
#[cfg(unix)]
|
|
Syslog(LogSyslogWriterConfig),
|
|
}
|
|
|
|
/// Console appender configuration for logging
|
|
#[cfg(feature = "logging")]
|
|
#[derive(Deserialize, Debug, Clone, Default)]
|
|
pub struct LogConsoleWriterConfig {
|
|
/// Level override
|
|
#[serde(default)]
|
|
pub level: Option<u32>,
|
|
/// Format override
|
|
#[serde(default)]
|
|
pub format: LogFormatConfigOverride,
|
|
}
|
|
|
|
/// Logger format override
|
|
#[cfg(feature = "logging")]
|
|
#[derive(Deserialize, Debug, Clone, Default)]
|
|
#[serde(default)]
|
|
pub struct LogFormatConfigOverride {
|
|
pub without_time: Option<bool>,
|
|
}
|
|
|
|
/// File appender configuration for logging
|
|
#[cfg(feature = "logging")]
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct LogFileWriterConfig {
|
|
/// Level override
|
|
#[serde(default)]
|
|
pub level: Option<u32>,
|
|
/// Format override
|
|
#[serde(default)]
|
|
pub format: LogFormatConfigOverride,
|
|
|
|
/// Directory to store log files
|
|
pub directory: PathBuf,
|
|
/// Rotation strategy for log files. Default is `Rotation::NEVER`.
|
|
#[serde(default)]
|
|
pub rotation: LogRotation,
|
|
/// Prefix for log file names. Default is the binary name.
|
|
#[serde(default)]
|
|
pub prefix: Option<String>,
|
|
/// Suffix for log file names. Default is "log".
|
|
#[serde(default)]
|
|
pub suffix: Option<String>,
|
|
/// Maximum number of log files to keep. Default is `None`, meaning no limit.
|
|
#[serde(default)]
|
|
pub max_files: Option<usize>,
|
|
}
|
|
|
|
/// Log rotation frequency
|
|
#[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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// File appender configuration for logging
|
|
#[cfg(all(feature = "logging", unix))]
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
pub struct LogSyslogWriterConfig {
|
|
/// Level override
|
|
#[serde(default)]
|
|
pub level: Option<u32>,
|
|
/// Format override
|
|
#[serde(default)]
|
|
pub format: LogFormatConfigOverride,
|
|
|
|
/// syslog identity, process name by default
|
|
#[serde(default)]
|
|
pub identity: Option<String>,
|
|
/// Facility, 1 (USER) by default
|
|
#[serde(default)]
|
|
pub facility: Option<i32>,
|
|
}
|
|
|
|
/// Runtime mode (Tokio)
|
|
#[derive(Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum RuntimeMode {
|
|
/// Single-Thread Runtime
|
|
#[cfg_attr(not(feature = "multi-threaded"), default)]
|
|
SingleThread,
|
|
/// Multi-Thread Runtime
|
|
#[cfg(feature = "multi-threaded")]
|
|
#[cfg_attr(feature = "multi-threaded", default)]
|
|
MultiThread,
|
|
}
|
|
|
|
/// Runtime configuration
|
|
#[derive(Deserialize, Debug, Clone, Default)]
|
|
#[serde(default)]
|
|
pub struct RuntimeConfig {
|
|
/// Multithread runtime worker count, CPU count if not configured
|
|
#[cfg(feature = "multi-threaded")]
|
|
pub worker_count: Option<usize>,
|
|
/// Runtime Mode, single-thread, multi-thread
|
|
pub mode: RuntimeMode,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_deser_empty() {
|
|
// empty config should load successfully
|
|
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);
|
|
// default writer configuration should contain a stdout writer
|
|
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() {
|
|
// allow user explicitly disable logging by providing an empty writers array
|
|
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() {
|
|
// Minimal valid file writer configuration
|
|
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() {
|
|
// Minimal valid console writer configuration
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
}
|