Skip to main content

trojan_client/
config.rs

1//! Client configuration.
2
3use serde::Deserialize;
4use trojan_config::{LoggingConfig, TcpConfig};
5
6use crate::error::ClientError;
7
8/// Top-level client configuration.
9#[derive(Debug, Clone, Deserialize)]
10pub struct ClientConfig {
11    pub client: ClientSettings,
12    #[serde(default)]
13    pub logging: LoggingConfig,
14}
15
16/// Core client settings.
17#[derive(Debug, Clone, Deserialize)]
18pub struct ClientSettings {
19    /// Local SOCKS5 listen address, e.g. "127.0.0.1:1080".
20    pub listen: String,
21
22    /// Remote trojan server address, e.g. "example.com:443".
23    pub remote: String,
24
25    /// Password (plaintext, SHA-224 computed at runtime).
26    pub password: String,
27
28    /// TLS configuration.
29    #[serde(default)]
30    pub tls: ClientTlsConfig,
31
32    /// TCP socket options.
33    #[serde(default)]
34    pub tcp: TcpConfig,
35}
36
37/// Client-side TLS configuration.
38#[derive(Debug, Clone, Deserialize)]
39pub struct ClientTlsConfig {
40    /// TLS SNI hostname. Defaults to the host portion of `remote`.
41    pub sni: Option<String>,
42
43    /// ALPN protocol list.
44    #[serde(default = "default_alpn")]
45    pub alpn: Vec<String>,
46
47    /// Skip certificate verification (for testing only).
48    #[serde(default)]
49    pub skip_verify: bool,
50
51    /// Custom CA certificate path (PEM).
52    pub ca: Option<String>,
53}
54
55impl Default for ClientTlsConfig {
56    fn default() -> Self {
57        Self {
58            sni: None,
59            alpn: default_alpn(),
60            skip_verify: false,
61            ca: None,
62        }
63    }
64}
65
66fn default_alpn() -> Vec<String> {
67    vec!["h2".into(), "http/1.1".into()]
68}
69
70/// Load client configuration from a file path.
71///
72/// Supports TOML, JSON, and JSONC formats (detected by extension).
73pub fn load_client_config(path: &std::path::Path) -> Result<ClientConfig, ClientError> {
74    let content = std::fs::read_to_string(path)
75        .map_err(|e| ClientError::Config(format!("failed to read config: {e}")))?;
76
77    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("toml");
78
79    match ext {
80        "toml" => toml::from_str(&content)
81            .map_err(|e| ClientError::Config(format!("TOML parse error: {e}"))),
82        "json" | "jsonc" => {
83            // Strip single-line comments for JSONC support
84            let stripped: String = content
85                .lines()
86                .map(|line| {
87                    if let Some(idx) = line.find("//") {
88                        // Only strip if not inside a string (simple heuristic)
89                        let before = &line[..idx];
90                        let quotes = before.chars().filter(|&c| c == '"').count();
91                        if quotes % 2 == 0 { before } else { line }
92                    } else {
93                        line
94                    }
95                })
96                .collect::<Vec<_>>()
97                .join("\n");
98            serde_json::from_str(&stripped)
99                .map_err(|e| ClientError::Config(format!("JSON parse error: {e}")))
100        }
101        _ => toml::from_str(&content)
102            .map_err(|e| ClientError::Config(format!("config parse error: {e}"))),
103    }
104}