mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 05:44:35 +03:00
217 lines
6.7 KiB
Rust
217 lines
6.7 KiB
Rust
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ConfigError {
|
|
#[error("failed to read config file {0}: {1}")]
|
|
Read(String, #[source] std::io::Error),
|
|
#[error("failed to parse config json: {0}")]
|
|
Parse(#[from] serde_json::Error),
|
|
#[error("invalid config: {0}")]
|
|
Invalid(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum ScriptId {
|
|
One(String),
|
|
Many(Vec<String>),
|
|
}
|
|
|
|
impl ScriptId {
|
|
pub fn into_vec(self) -> Vec<String> {
|
|
match self {
|
|
ScriptId::One(s) => vec![s],
|
|
ScriptId::Many(v) => v,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Config {
|
|
pub mode: String,
|
|
#[serde(default = "default_google_ip")]
|
|
pub google_ip: String,
|
|
#[serde(default = "default_front_domain")]
|
|
pub front_domain: String,
|
|
#[serde(default)]
|
|
pub script_id: Option<ScriptId>,
|
|
#[serde(default)]
|
|
pub script_ids: Option<ScriptId>,
|
|
pub auth_key: String,
|
|
#[serde(default = "default_listen_host")]
|
|
pub listen_host: String,
|
|
#[serde(default = "default_listen_port")]
|
|
pub listen_port: u16,
|
|
#[serde(default)]
|
|
pub socks5_port: Option<u16>,
|
|
#[serde(default = "default_log_level")]
|
|
pub log_level: String,
|
|
#[serde(default = "default_verify_ssl")]
|
|
pub verify_ssl: bool,
|
|
#[serde(default)]
|
|
pub hosts: HashMap<String, String>,
|
|
#[serde(default)]
|
|
pub enable_batching: bool,
|
|
/// Optional upstream SOCKS5 proxy for non-HTTP / raw-TCP traffic
|
|
/// (e.g. `"127.0.0.1:50529"` pointing at a local xray / v2ray instance).
|
|
/// When set, the SOCKS5 listener forwards raw-TCP flows through it
|
|
/// instead of connecting directly. HTTP/HTTPS traffic (which goes
|
|
/// through the Apps Script relay) and SNI-rewrite tunnels are
|
|
/// unaffected.
|
|
#[serde(default)]
|
|
pub upstream_socks5: Option<String>,
|
|
/// Fan-out factor for non-cached relay requests when multiple
|
|
/// `script_id`s are configured. `0` or `1` = off (round-robin, the
|
|
/// default). `2` or more = fire that many Apps Script instances in
|
|
/// parallel per request and return the first successful response —
|
|
/// kills long-tail latency caused by a single slow Apps Script
|
|
/// instance, at the cost of using that much more daily quota.
|
|
/// Value is clamped to the number of available (non-blacklisted)
|
|
/// script IDs.
|
|
#[serde(default)]
|
|
pub parallel_relay: u8,
|
|
/// Optional explicit SNI rotation pool for outbound TLS to `google_ip`.
|
|
/// Empty / missing = auto-expand from `front_domain` (current default of
|
|
/// {www, mail, drive, docs, calendar}.google.com). Set to an explicit list
|
|
/// to pick exactly which SNI names get rotated through — useful when one
|
|
/// of the defaults is locally blocked (e.g. mail.google.com in Iran at
|
|
/// various times). Can be tested per-name via the UI or `mhrv-rs test-sni`.
|
|
#[serde(default)]
|
|
pub sni_hosts: Option<Vec<String>>,
|
|
#[serde(default = "default_fetch_ips_from_api")]
|
|
pub fetch_ips_from_api: bool,
|
|
|
|
#[serde(default = "default_max_ips_to_scan")]
|
|
pub max_ips_to_scan: usize,
|
|
|
|
#[serde(default = "default_scan_batch_size")]
|
|
pub scan_batch_size:usize,
|
|
|
|
#[serde(default = "default_google_ip_validation")]
|
|
pub google_ip_validation: bool
|
|
}
|
|
|
|
fn default_fetch_ips_from_api() -> bool { false }
|
|
fn default_max_ips_to_scan() -> usize { 100 }
|
|
fn default_scan_batch_size() -> usize {500}
|
|
fn default_google_ip_validation() -> bool {true}
|
|
|
|
fn default_google_ip() -> String {
|
|
"216.239.38.120".into()
|
|
}
|
|
fn default_front_domain() -> String {
|
|
"www.google.com".into()
|
|
}
|
|
fn default_listen_host() -> String {
|
|
"127.0.0.1".into()
|
|
}
|
|
fn default_listen_port() -> u16 {
|
|
8085
|
|
}
|
|
fn default_log_level() -> String {
|
|
"warn".into()
|
|
}
|
|
fn default_verify_ssl() -> bool {
|
|
true
|
|
}
|
|
|
|
impl Config {
|
|
pub fn load(path: &Path) -> Result<Self, ConfigError> {
|
|
let data = std::fs::read_to_string(path)
|
|
.map_err(|e| ConfigError::Read(path.display().to_string(), e))?;
|
|
let cfg: Config = serde_json::from_str(&data)?;
|
|
cfg.validate()?;
|
|
Ok(cfg)
|
|
}
|
|
|
|
fn validate(&self) -> Result<(), ConfigError> {
|
|
if self.mode != "apps_script" {
|
|
return Err(ConfigError::Invalid(format!(
|
|
"only 'apps_script' mode is supported in this build (got '{}')",
|
|
self.mode
|
|
)));
|
|
}
|
|
if self.auth_key.trim().is_empty() || self.auth_key == "CHANGE_ME_TO_A_STRONG_SECRET" {
|
|
return Err(ConfigError::Invalid(
|
|
"auth_key must be set to a strong secret".into(),
|
|
));
|
|
}
|
|
let ids = self.script_ids_resolved();
|
|
if ids.is_empty() {
|
|
return Err(ConfigError::Invalid(
|
|
"script_id (or script_ids) is required".into(),
|
|
));
|
|
}
|
|
for id in &ids {
|
|
if id.is_empty() || id == "YOUR_APPS_SCRIPT_DEPLOYMENT_ID" {
|
|
return Err(ConfigError::Invalid(
|
|
"script_id is not set — deploy Code.gs and paste its Deployment ID".into(),
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn script_ids_resolved(&self) -> Vec<String> {
|
|
if let Some(s) = &self.script_ids {
|
|
return s.clone().into_vec();
|
|
}
|
|
if let Some(s) = &self.script_id {
|
|
return s.clone().into_vec();
|
|
}
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_single_script_id() {
|
|
let s = r#"{
|
|
"mode": "apps_script",
|
|
"auth_key": "MY_SECRET_KEY_123",
|
|
"script_id": "ABCDEF"
|
|
}"#;
|
|
let cfg: Config = serde_json::from_str(s).unwrap();
|
|
assert_eq!(cfg.script_ids_resolved(), vec!["ABCDEF".to_string()]);
|
|
cfg.validate().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn parses_multi_script_id() {
|
|
let s = r#"{
|
|
"mode": "apps_script",
|
|
"auth_key": "MY_SECRET_KEY_123",
|
|
"script_id": ["A", "B", "C"]
|
|
}"#;
|
|
let cfg: Config = serde_json::from_str(s).unwrap();
|
|
assert_eq!(cfg.script_ids_resolved(), vec!["A", "B", "C"]);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_placeholder_script_id() {
|
|
let s = r#"{
|
|
"mode": "apps_script",
|
|
"auth_key": "SECRET",
|
|
"script_id": "YOUR_APPS_SCRIPT_DEPLOYMENT_ID"
|
|
}"#;
|
|
let cfg: Config = serde_json::from_str(s).unwrap();
|
|
assert!(cfg.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_wrong_mode() {
|
|
let s = r#"{
|
|
"mode": "domain_fronting",
|
|
"auth_key": "SECRET",
|
|
"script_id": "X"
|
|
}"#;
|
|
let cfg: Config = serde_json::from_str(s).unwrap();
|
|
assert!(cfg.validate().is_err());
|
|
}
|
|
}
|