use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RelayNetwork {
Tcp,
Grpc,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default = "default::log_level")]
pub log_level: String,
#[serde(default)]
pub relay: RelayConfig,
#[serde(default)]
pub http: HttpConfig,
#[serde(default)]
pub subscription: SubscriptionConfig,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(default)]
pub struct RelayConfig {
#[serde(default = "default::relay::listen")]
pub listen: String,
#[serde(default = "default::relay::port")]
pub port: u16,
#[serde(default = "default::relay::network")]
pub network: RelayNetwork,
#[serde(default = "default::relay::service_name")]
pub service_name: String,
#[serde(default)]
pub idle_timeout: u64,
#[serde(default)]
pub grpc_pool_idle_timeout: u64,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(default)]
pub struct HttpConfig {
#[serde(default = "default::http::listen")]
pub listen: String,
#[serde(default = "default::http::port")]
pub port: u16,
#[serde(default)]
pub users: Vec<HttpUser>,
#[serde(default)]
pub outputs: Vec<OutputConfig>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct HttpUser {
pub username: String,
pub password: String,
pub outputs: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct OutputConfig {
pub name: String,
pub host: String,
pub port: u16,
#[serde(default)]
pub sni: Option<String>,
#[serde(default, rename = "skip-cert-verify", alias = "skip_cert_verify")]
pub skip_cert_verify: bool,
#[serde(default)]
pub process: Vec<ProcessStep>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(default)]
pub struct SubscriptionConfig {
pub cache_file: Option<String>,
#[serde(default)]
pub update_interval: u64,
#[serde(default)]
pub sources: Vec<SubscriptionSource>,
#[serde(default = "default::deduplication")]
pub deduplication: String,
}
impl Default for SubscriptionConfig {
fn default() -> Self {
Self {
cache_file: None,
update_interval: 0,
sources: Vec::new(),
deduplication: default::deduplication(),
}
}
}
impl Default for RelayConfig {
fn default() -> Self {
Self {
listen: default::relay::listen(),
port: default::relay::port(),
network: default::relay::network(),
service_name: default::relay::service_name(),
idle_timeout: 0,
grpc_pool_idle_timeout: 0,
}
}
}
impl Default for HttpConfig {
fn default() -> Self {
Self {
listen: default::http::listen(),
port: default::http::port(),
users: Vec::new(),
outputs: Vec::new(),
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct SubscriptionSource {
pub name: String,
pub url: String,
#[serde(default = "default::subscription::user_agent")]
pub user_agent: String,
#[serde(default)]
pub process: Vec<ProcessStep>,
}
#[derive(Debug, Deserialize, Clone, Serialize, Default)]
pub struct ProcessStep {
#[serde(default)]
pub filter: Vec<String>,
#[serde(default)]
pub filter_source: Vec<String>,
#[serde(default)]
pub invert: bool,
#[serde(default)]
pub remove: bool,
#[serde(default)]
pub rename: Vec<[String; 2]>,
#[serde(default)]
pub remove_emoji: bool,
#[serde(default)]
pub override_security: Option<String>,
}
mod default {
pub fn log_level() -> String {
"info".to_string()
}
pub fn deduplication() -> String {
"rename".to_string()
}
pub mod subscription {
pub fn user_agent() -> String {
concat!(
"tobira/",
env!("CARGO_PKG_VERSION_MAJOR"),
".",
env!("CARGO_PKG_VERSION_MINOR"),
" (like dae/1.0) (like v2rayA/1.0 WebRequestHelper) (like v2rayN/1.0 WebRequestHelper)"
)
.to_string()
}
}
pub mod relay {
pub fn listen() -> String {
"[::]".to_string()
}
pub fn port() -> u16 {
10808
}
pub fn network() -> super::super::RelayNetwork {
super::super::RelayNetwork::Tcp
}
pub fn service_name() -> String {
"GunService".to_string()
}
}
pub mod http {
pub fn listen() -> String {
"[::]".to_string()
}
pub fn port() -> u16 {
8080
}
}
}
pub fn load(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
#[cfg(test)]
mod tests {
use super::Config;
#[test]
fn parse_defaults_for_omitted_listen_and_http_port() {
let text = r#"
[relay]
port = 12204
[http]
[subscription]
"#;
let cfg: Config = toml::from_str(text).expect("config should parse");
assert_eq!(cfg.relay.listen, "[::]");
assert_eq!(cfg.relay.port, 12204);
assert_eq!(cfg.relay.network, super::RelayNetwork::Tcp);
assert_eq!(cfg.relay.service_name, "GunService");
assert_eq!(cfg.relay.idle_timeout, 0);
assert_eq!(cfg.relay.grpc_pool_idle_timeout, 0);
assert_eq!(cfg.http.listen, "[::]");
assert_eq!(cfg.http.port, 8080);
}
#[test]
fn parse_defaults_when_sections_omitted() {
let text = r#"
[subscription]
"#;
let cfg: Config = toml::from_str(text).expect("config should parse");
assert_eq!(cfg.relay.listen, "[::]");
assert_eq!(cfg.relay.port, 10808);
assert_eq!(cfg.relay.network, super::RelayNetwork::Tcp);
assert_eq!(cfg.relay.service_name, "GunService");
assert_eq!(cfg.http.listen, "[::]");
assert_eq!(cfg.http.port, 8080);
assert_eq!(cfg.subscription.update_interval, 0);
assert!(cfg.subscription.sources.is_empty());
assert_eq!(cfg.relay.grpc_pool_idle_timeout, 0);
}
#[test]
fn parse_grpc_relay_network_with_default_service_name() {
let text = r#"
[relay]
network = "grpc"
port = 1443
"#;
let cfg: Config = toml::from_str(text).expect("config should parse");
assert_eq!(cfg.relay.network, super::RelayNetwork::Grpc);
assert_eq!(cfg.relay.port, 1443);
assert_eq!(cfg.relay.service_name, "GunService");
assert_eq!(cfg.relay.idle_timeout, 0);
assert_eq!(cfg.relay.grpc_pool_idle_timeout, 0);
}
#[test]
fn parse_relay_timeouts() {
let text = r#"
[relay]
idle_timeout = 300
grpc_pool_idle_timeout = 60
"#;
let cfg: Config = toml::from_str(text).expect("config should parse");
assert_eq!(cfg.relay.idle_timeout, 300);
assert_eq!(cfg.relay.grpc_pool_idle_timeout, 60);
}
#[test]
fn output_rejects_transport_overrides() {
let text = r#"
[[http.outputs]]
name = "main"
host = "relay.example.com"
port = 10808
network = "grpc"
tls = true
"#;
let err = toml::from_str::<Config>(text).expect_err("output transport is relay-scoped");
let message = err.to_string();
assert!(message.contains("unknown field"));
}
#[test]
fn output_parses_skip_cert_verify() {
let text = r#"
[[http.outputs]]
name = "main"
host = "relay.example.com"
port = 443
skip-cert-verify = true
"#;
let cfg: Config = toml::from_str(text).expect("config should parse");
assert!(cfg.http.outputs[0].skip_cert_verify);
}
}