Refactor ProxyWarden routing and settings flow
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
use crate::models::{decode_percent_encoded_utf8, SubscriptionCache, SubscriptionServer};
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use reqwest::redirect;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::net::{IpAddr, Ipv6Addr};
|
||||
use std::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use url::Url;
|
||||
|
||||
const SUPPORTED_PROXY_TYPES: &[&str] = &["vless", "vmess", "trojan", "shadowsocks", "hysteria2"];
|
||||
const DEFAULT_APP_NAME: &str = "ProxyWarden";
|
||||
const SUBSCRIPTION_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SUBSCRIPTION_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubscriptionError {
|
||||
@@ -34,6 +39,11 @@ pub struct ParsedSubscription {
|
||||
pub servers: Vec<SubscriptionServer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct SubscriptionFetchPolicy {
|
||||
pub allow_unsafe_local_urls: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SubscriptionFetchIdentity {
|
||||
pub device_hwid: Option<String>,
|
||||
@@ -134,16 +144,35 @@ pub fn fetch_subscription(url: &str) -> Result<SubscriptionCache, SubscriptionEr
|
||||
pub fn fetch_subscription_with_identity(
|
||||
url: &str,
|
||||
identity: &SubscriptionFetchIdentity,
|
||||
) -> Result<SubscriptionCache, SubscriptionError> {
|
||||
fetch_subscription_with_identity_and_policy(url, identity, SubscriptionFetchPolicy::default())
|
||||
}
|
||||
|
||||
pub fn fetch_subscription_with_identity_and_policy(
|
||||
url: &str,
|
||||
identity: &SubscriptionFetchIdentity,
|
||||
policy: SubscriptionFetchPolicy,
|
||||
) -> 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",
|
||||
));
|
||||
}
|
||||
validate_subscription_fetch_url(&parsed_url, policy)?;
|
||||
|
||||
let mut request = reqwest::blocking::Client::new().get(parsed_url);
|
||||
let redirect_policy = redirect::Policy::custom(move |attempt| {
|
||||
if validate_subscription_fetch_url(attempt.url(), policy).is_ok() {
|
||||
attempt.follow()
|
||||
} else {
|
||||
attempt.stop()
|
||||
}
|
||||
});
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.connect_timeout(SUBSCRIPTION_CONNECT_TIMEOUT)
|
||||
.timeout(SUBSCRIPTION_REQUEST_TIMEOUT)
|
||||
.redirect(redirect_policy)
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
SubscriptionError::new(format!("Subscription client setup failed: {error}"))
|
||||
})?;
|
||||
let mut request = client.get(parsed_url);
|
||||
|
||||
for (name, value) in identity.request_headers_without_device_hwid() {
|
||||
request = request.header(name, value);
|
||||
@@ -189,6 +218,69 @@ pub fn fetch_subscription_with_identity(
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_subscription_fetch_url(
|
||||
parsed_url: &Url,
|
||||
policy: SubscriptionFetchPolicy,
|
||||
) -> Result<(), SubscriptionError> {
|
||||
if !matches!(parsed_url.scheme(), "http" | "https") {
|
||||
return Err(SubscriptionError::new(
|
||||
"Subscription URL must use http or https",
|
||||
));
|
||||
}
|
||||
|
||||
if !policy.allow_unsafe_local_urls && is_unsafe_subscription_host(parsed_url) {
|
||||
return Err(SubscriptionError::new(
|
||||
"Subscription URL host is local, private, link-local, multicast, or metadata-only",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_unsafe_subscription_host(parsed_url: &Url) -> bool {
|
||||
let Some(host) = parsed_url.host_str() else {
|
||||
return true;
|
||||
};
|
||||
let host = host.trim_matches(['[', ']']).to_ascii_lowercase();
|
||||
|
||||
if matches!(host.as_str(), "localhost" | "metadata.google.internal")
|
||||
|| host.ends_with(".localhost")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
host.parse::<IpAddr>().is_ok_and(is_unsafe_ip)
|
||||
}
|
||||
|
||||
fn is_unsafe_ip(ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(ip) => {
|
||||
ip.is_loopback()
|
||||
|| ip.is_private()
|
||||
|| ip.is_link_local()
|
||||
|| ip.is_multicast()
|
||||
|| ip.is_broadcast()
|
||||
|| ip.is_unspecified()
|
||||
|| ip.octets() == [169, 254, 169, 254]
|
||||
}
|
||||
IpAddr::V6(ip) => {
|
||||
ip.is_loopback()
|
||||
|| ip.is_unspecified()
|
||||
|| ip.is_multicast()
|
||||
|| is_unique_local_ipv6(ip)
|
||||
|| is_unicast_link_local_ipv6(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_unique_local_ipv6(ip: Ipv6Addr) -> bool {
|
||||
(ip.segments()[0] & 0xfe00) == 0xfc00
|
||||
}
|
||||
|
||||
fn is_unicast_link_local_ipv6(ip: Ipv6Addr) -> bool {
|
||||
(ip.segments()[0] & 0xffc0) == 0xfe80
|
||||
}
|
||||
|
||||
fn parse_link_subscription(body: &str) -> Result<Value, SubscriptionError> {
|
||||
let decoded = maybe_decode_base64(body);
|
||||
let links = decoded
|
||||
|
||||
Reference in New Issue
Block a user