Expand README with architecture and setup details

This commit is contained in:
2026-07-08 09:58:26 +03:00
parent 81be7e186c
commit c5120669d2
109 changed files with 22311 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
use crate::models::{SubscriptionCache, SubscriptionServer};
use base64::{engine::general_purpose, Engine};
use serde_json::{json, Map, Value};
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;
const SUPPORTED_PROXY_TYPES: &[&str] = &["vless", "vmess", "trojan", "shadowsocks", "hysteria2"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubscriptionError {
pub message: String,
}
impl SubscriptionError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for SubscriptionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for SubscriptionError {}
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedSubscription {
pub config: Value,
pub servers: Vec<SubscriptionServer>,
}
pub fn parse_subscription_body(body: &str) -> Result<ParsedSubscription, SubscriptionError> {
let config = match serde_json::from_str::<Value>(body) {
Ok(value) => value,
Err(_) => parse_link_subscription(body)?,
};
let servers = servers_from_config(&config)?;
Ok(ParsedSubscription { config, servers })
}
pub fn parse_user_info(header_value: Option<&str>) -> Map<String, Value> {
let mut result = Map::new();
let Some(header_value) = header_value else {
return result;
};
for part in header_value.split(';') {
let Some((key, value)) = part.trim().split_once('=') else {
continue;
};
let key = key.trim();
if key.is_empty() {
continue;
}
if let Ok(parsed) = value.trim().parse::<i64>() {
result.insert(key.to_string(), Value::Number(parsed.into()));
}
}
result
}
pub fn fetch_subscription(url: &str) -> Result<SubscriptionCache, SubscriptionError> {
let parsed_url =
Url::parse(url).map_err(|_| SubscriptionError::new("Invalid subscription URL"))?;
if !matches!(parsed_url.scheme(), "http" | "https") {
return Err(SubscriptionError::new(
"Subscription URL must use http or https",
));
}
let response = reqwest::blocking::Client::new()
.get(parsed_url)
.header("user-agent", "singbox")
.header("x-device-os", std::env::consts::OS)
.header("x-device-model", "proxywarden")
.send()
.map_err(|error| SubscriptionError::new(format!("Subscription request failed: {error}")))?;
let status = response.status();
if !status.is_success() {
return Err(SubscriptionError::new(format!(
"Subscription request failed: HTTP {}",
status.as_u16()
)));
}
let user_info = parse_user_info(
response
.headers()
.get("subscription-userinfo")
.and_then(|value| value.to_str().ok()),
);
let body = response.text().map_err(|error| {
SubscriptionError::new(format!("Subscription body read failed: {error}"))
})?;
let parsed = parse_subscription_body(&body)?;
Ok(SubscriptionCache {
config: parsed.config,
servers: parsed.servers,
user_info,
fetched_at: now_timestamp(),
})
}
fn parse_link_subscription(body: &str) -> Result<Value, SubscriptionError> {
let decoded = maybe_decode_base64(body);
let links = decoded
.lines()
.map(str::trim)
.filter(|line| line.starts_with("vless://"))
.collect::<Vec<_>>();
if links.is_empty() {
return Err(SubscriptionError::new(
"Subscription does not contain JSON config or VLESS links",
));
}
let outbounds = links
.into_iter()
.map(parse_vless_url)
.collect::<Result<Vec<_>, _>>()?;
Ok(json!({ "outbounds": outbounds }))
}
fn parse_vless_url(raw_url: &str) -> Result<Value, SubscriptionError> {
if !raw_url.starts_with("vless://") {
return Err(SubscriptionError::new("VLESS URL must start with vless://"));
}
let parsed = Url::parse(raw_url).map_err(|_| SubscriptionError::new("Invalid VLESS URL"))?;
let tag = parsed.fragment().unwrap_or("vless-out").to_string();
let uuid = parsed.username().trim().to_string();
let server = parsed.host_str().map(str::to_string).unwrap_or_default();
let server_port = parsed.port_or_known_default().unwrap_or(443);
let public_key = query_value(&parsed, "pbk").unwrap_or_default();
let short_id = query_value(&parsed, "sid").unwrap_or_default();
let server_name = query_value(&parsed, "sni").unwrap_or_else(|| server.clone());
let fingerprint = query_value(&parsed, "fp").unwrap_or_else(|| "chrome".to_string());
let flow = query_value(&parsed, "flow").unwrap_or_default();
if uuid.is_empty() || server.is_empty() {
return Err(SubscriptionError::new(
"VLESS URL misses uuid, host or port",
));
}
if public_key.is_empty() || short_id.is_empty() {
return Err(SubscriptionError::new(
"VLESS REALITY parameters pbk and sid are required",
));
}
Ok(json!({
"type": "vless",
"tag": tag,
"server": server,
"server_port": server_port,
"uuid": uuid,
"flow": flow,
"tls": {
"enabled": true,
"server_name": server_name,
"utls": {
"enabled": true,
"fingerprint": fingerprint
},
"reality": {
"enabled": true,
"public_key": public_key,
"short_id": short_id
}
},
"packet_encoding": "xudp"
}))
}
fn servers_from_config(config: &Value) -> Result<Vec<SubscriptionServer>, SubscriptionError> {
let servers = config
.get("outbounds")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(server_from_outbound)
.collect::<Vec<_>>();
if servers.is_empty() {
return Err(SubscriptionError::new(
"No supported proxy outbounds found in subscription",
));
}
Ok(servers)
}
fn server_from_outbound(outbound: &Value) -> Option<SubscriptionServer> {
let server_type = outbound.get("type")?.as_str()?.to_string();
if !SUPPORTED_PROXY_TYPES.contains(&server_type.as_str()) {
return None;
}
let server = outbound
.get("server")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
let server_port = outbound
.get("server_port")
.and_then(Value::as_u64)
.and_then(|value| u16::try_from(value).ok())
.unwrap_or(443);
let tag = outbound
.get("tag")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("{server_type}-{server}"));
Some(SubscriptionServer {
tag,
server_type,
server,
server_port,
})
}
fn maybe_decode_base64(content: &str) -> String {
let compact = content.split_whitespace().collect::<String>();
if compact.is_empty()
|| !compact
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '+' | '/' | '=' | '-' | '_'))
{
return content.to_string();
}
for engine in [general_purpose::STANDARD, general_purpose::URL_SAFE] {
if let Ok(decoded) = engine.decode(compact.as_bytes()) {
if let Ok(decoded) = String::from_utf8(decoded) {
if decoded.contains("vless://") || decoded.contains('{') {
return decoded;
}
}
}
}
content.to_string()
}
fn query_value(url: &Url, key: &str) -> Option<String> {
url.query_pairs()
.find(|(name, _)| name == key)
.map(|(_, value)| value.into_owned())
}
fn now_timestamp() -> String {
let seconds = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
format!("unix:{seconds}")
}