Skip to content

Commit 69a5061

Browse files
committed
fix: apply protocol-matched User-Agent for subscription fetches (#5)
Subscription panels (v2board/xboard/sspanel) route responses by UA. The previous client sent no UA (default reqwest/0.12.x) so these panels rejected the request, surfacing as "address unreachable" even though the host was reachable (sing-box SFM with a proper UA worked fine). - loader: apply user_agent to reqwest client; derive per-protocol default (sing-box / mihomo / v2rayN) from source_type or flag - config: default user_agent to empty so the per-protocol default kicks in; non-empty config value still wins as an explicit override
1 parent bf9d174 commit 69a5061

2 files changed

Lines changed: 38 additions & 8 deletions

File tree

src/core/config.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ pub struct AppConfig {
4949
}
5050

5151
fn default_user_agent() -> String {
52-
"Mozilla/5.0 (compatible; ProxyConvert/2.0)".to_string()
52+
// Empty by default -> SourceLoader picks a protocol-matched UA per-request
53+
// (sing-box/mihomo/v2rayN). Subscription panels route by UA, so a generic
54+
// UA like "Mozilla/... ProxyConvert/..." gets rejected.
55+
String::new()
5356
}
5457

5558
fn default_timeout() -> u64 {
@@ -229,10 +232,7 @@ mod tests {
229232
fn test_default_config() {
230233
let config = AppConfig::default();
231234

232-
assert_eq!(
233-
config.user_agent,
234-
"Mozilla/5.0 (compatible; ProxyConvert/2.0)"
235-
);
235+
assert_eq!(config.user_agent, "");
236236
assert_eq!(config.timeout_seconds, 30);
237237
assert_eq!(config.retry_count, 3);
238238
assert_eq!(config.log_level, "info");

src/utils/source/loader.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,43 @@ impl SourceLoader {
4646
// Use source flag if set (empty = &flag=), else protocol default
4747
let url_with_flag =
4848
Self::append_flag_to_url(source, &source_meta.source_type, source_meta.flag.as_deref());
49-
Self::load_from_url(&url_with_flag, config).await
49+
// Pick a User-Agent that subscription panels recognize.
50+
// Why: v2board/xboard/sspanel-style panels route responses by UA;
51+
// the default reqwest UA is rejected or silently dropped, surfacing
52+
// as "address unreachable" even when the host is reachable.
53+
let ua = Self::effective_user_agent(&source_meta.source_type, source_meta.flag.as_deref(), config);
54+
Self::load_from_url(&url_with_flag, &ua, config).await
5055
} else {
5156
// File path: use only the part before ? (query params are kept in source string for reference)
5257
let path = source.find('?').map(|i| &source[..i]).unwrap_or(source.as_str());
5358
Self::load_from_file(path)
5459
}
5560
}
5661

62+
/// Choose the User-Agent to send with subscription requests.
63+
/// Precedence: explicit `config.user_agent` (non-empty) > protocol-matched default.
64+
fn effective_user_agent(
65+
source_type: &Protocol,
66+
flag_override: Option<&str>,
67+
config: &AppConfig,
68+
) -> String {
69+
let ua = config.user_agent.trim();
70+
if !ua.is_empty() {
71+
return ua.to_string();
72+
}
73+
// Derive from flag if the user overrode it, otherwise from source_type.
74+
let kind = flag_override
75+
.and_then(Protocol::from_str)
76+
.unwrap_or(*source_type);
77+
// Name-only; subscription panels typically match on the keyword, not the version.
78+
// Users who hit a version-strict panel can override via config.user_agent.
79+
match kind {
80+
Protocol::SingBox => "sing-box".to_string(),
81+
Protocol::Clash => "mihomo".to_string(),
82+
Protocol::V2Ray => "v2rayN".to_string(),
83+
}
84+
}
85+
5786
/// Append or update flag query parameter to URL.
5887
/// Use flag_override if set (empty string = &flag=), else source_type default.
5988
fn append_flag_to_url(
@@ -151,11 +180,12 @@ impl SourceLoader {
151180
}
152181

153182
/// Load content from URL (uses NetworkError for fetch failures).
154-
async fn load_from_url(url: &str, config: &AppConfig) -> Result<String> {
155-
tracing::info!("Fetching URL: {}", url);
183+
async fn load_from_url(url: &str, user_agent: &str, config: &AppConfig) -> Result<String> {
184+
tracing::info!("Fetching URL: {} (UA: {})", url, user_agent);
156185

157186
let client = reqwest::Client::builder()
158187
.timeout(std::time::Duration::from_secs(config.timeout_seconds))
188+
.user_agent(user_agent)
159189
.build()
160190
.map_err(|e| ConvertError::network_error(e.to_string().as_str()))?;
161191

0 commit comments

Comments
 (0)