mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-05 18:58:40 +02:00
feat: full ui refresh
This commit is contained in:
@@ -1115,149 +1115,6 @@ impl ProxyManager {
|
||||
self.load_proxy_check_cache(proxy_id)
|
||||
}
|
||||
|
||||
pub async fn fetch_proxy_from_url(
|
||||
&self,
|
||||
url: &str,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<Option<ProxySettings>, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {e}"))?;
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch launch hook: {e}"))?;
|
||||
|
||||
if response.status() == reqwest::StatusCode::NO_CONTENT {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Launch hook returned status {}", response.status()));
|
||||
}
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read launch hook response: {e}"))?;
|
||||
|
||||
let body = body.trim();
|
||||
if body.is_empty() {
|
||||
return Err("Launch hook returned empty response".to_string());
|
||||
}
|
||||
|
||||
if let Ok(settings) = Self::parse_dynamic_proxy_json(body) {
|
||||
return Ok(Some(settings));
|
||||
}
|
||||
|
||||
match Self::parse_dynamic_proxy_text(body) {
|
||||
Ok(settings) => Ok(Some(settings)),
|
||||
Err(text_error) => Err(format!(
|
||||
"Failed to parse launch hook response: {text_error}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JSON proxy payload: { "ip"/"host": "...", "port": ..., "username": "...", "password": "..." }
|
||||
fn parse_dynamic_proxy_json(body: &str) -> Result<ProxySettings, String> {
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(body).map_err(|e| format!("Invalid JSON response: {e}"))?;
|
||||
|
||||
let obj = json
|
||||
.as_object()
|
||||
.ok_or_else(|| "JSON response is not an object".to_string())?;
|
||||
|
||||
let raw_host = obj
|
||||
.get("ip")
|
||||
.or_else(|| obj.get("host"))
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "Missing 'ip' or 'host' field in JSON response".to_string())?;
|
||||
|
||||
// Strip protocol prefix from host if present (e.g. "socks5://1.2.3.4" -> "1.2.3.4")
|
||||
// and extract the proxy type from it if no explicit type field is provided
|
||||
let (host, protocol_from_host) = if let Some(rest) = raw_host.strip_prefix("://") {
|
||||
(rest.to_string(), None)
|
||||
} else if let Some((proto, rest)) = raw_host.split_once("://") {
|
||||
(rest.to_string(), Some(proto.to_lowercase()))
|
||||
} else {
|
||||
(raw_host.to_string(), None)
|
||||
};
|
||||
|
||||
let port = obj
|
||||
.get("port")
|
||||
.and_then(|v| {
|
||||
v.as_u64()
|
||||
.or_else(|| v.as_str().and_then(|s| s.parse().ok()))
|
||||
})
|
||||
.ok_or_else(|| "Missing or invalid 'port' field in JSON response".to_string())?
|
||||
as u16;
|
||||
|
||||
let proxy_type = obj
|
||||
.get("type")
|
||||
.or_else(|| obj.get("proxy_type"))
|
||||
.or_else(|| obj.get("protocol"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_lowercase())
|
||||
.or(protocol_from_host)
|
||||
.unwrap_or_else(|| "http".to_string());
|
||||
|
||||
let username = obj
|
||||
.get("username")
|
||||
.or_else(|| obj.get("user"))
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let password = obj
|
||||
.get("password")
|
||||
.or_else(|| obj.get("pass"))
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
Ok(ProxySettings {
|
||||
proxy_type,
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
// Parse plain text proxy payload using the same logic as proxy import
|
||||
fn parse_dynamic_proxy_text(body: &str) -> Result<ProxySettings, String> {
|
||||
let line = body
|
||||
.lines()
|
||||
.find(|l| !l.trim().is_empty())
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
if line.is_empty() {
|
||||
return Err("Empty text response".to_string());
|
||||
}
|
||||
|
||||
match Self::parse_single_proxy_line(line) {
|
||||
ProxyParseResult::Parsed(parsed) => Ok(ProxySettings {
|
||||
proxy_type: parsed.proxy_type,
|
||||
host: parsed.host,
|
||||
port: parsed.port,
|
||||
username: parsed.username,
|
||||
password: parsed.password,
|
||||
}),
|
||||
ProxyParseResult::Ambiguous {
|
||||
possible_formats, ..
|
||||
} => Err(format!(
|
||||
"Ambiguous proxy format. Could be: {}",
|
||||
possible_formats.join(" or ")
|
||||
)),
|
||||
ProxyParseResult::Invalid { reason, .. } => {
|
||||
Err(format!("Failed to parse proxy response: {reason}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export all proxies as JSON
|
||||
pub fn export_proxies_json(&self) -> Result<String, String> {
|
||||
let stored_proxies = self.stored_proxies.lock().unwrap();
|
||||
@@ -2317,8 +2174,6 @@ mod tests {
|
||||
use hyper::Response;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpListener;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
// Helper function to build donut-proxy binary for testing
|
||||
async fn ensure_donut_proxy_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
@@ -3587,263 +3442,4 @@ mod tests {
|
||||
|
||||
delete_proxy_config(&id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_standard_format() {
|
||||
let body = r#"{"ip": "1.2.3.4", "port": 8080, "username": "user1", "password": "pass1"}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.host, "1.2.3.4");
|
||||
assert_eq!(result.port, 8080);
|
||||
assert_eq!(result.proxy_type, "http");
|
||||
assert_eq!(result.username.as_deref(), Some("user1"));
|
||||
assert_eq!(result.password.as_deref(), Some("pass1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_host_alias() {
|
||||
let body = r#"{"host": "proxy.example.com", "port": 3128}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.host, "proxy.example.com");
|
||||
assert_eq!(result.port, 3128);
|
||||
assert!(result.username.is_none());
|
||||
assert!(result.password.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_user_pass_aliases() {
|
||||
let body = r#"{"ip": "10.0.0.1", "port": 1080, "user": "u", "pass": "p"}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.username.as_deref(), Some("u"));
|
||||
assert_eq!(result.password.as_deref(), Some("p"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_port_as_string() {
|
||||
let body = r#"{"ip": "1.2.3.4", "port": "9090"}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.port, 9090);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_with_proxy_type() {
|
||||
let body = r#"{"ip": "1.2.3.4", "port": 1080, "type": "socks5"}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.proxy_type, "socks5");
|
||||
|
||||
let body2 = r#"{"ip": "1.2.3.4", "port": 1080, "proxy_type": "socks4"}"#;
|
||||
let result2 = ProxyManager::parse_dynamic_proxy_json(body2).unwrap();
|
||||
assert_eq!(result2.proxy_type, "socks4");
|
||||
|
||||
// "protocol" field alias
|
||||
let body3 = r#"{"ip": "1.2.3.4", "port": 1080, "protocol": "socks5"}"#;
|
||||
let result3 = ProxyManager::parse_dynamic_proxy_json(body3).unwrap();
|
||||
assert_eq!(result3.proxy_type, "socks5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_normalizes_case() {
|
||||
let body = r#"{"ip": "1.2.3.4", "port": 1080, "type": "SOCKS5"}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.proxy_type, "socks5");
|
||||
|
||||
let body2 = r#"{"ip": "1.2.3.4", "port": 8080, "protocol": "HTTP"}"#;
|
||||
let result2 = ProxyManager::parse_dynamic_proxy_json(body2).unwrap();
|
||||
assert_eq!(result2.proxy_type, "http");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_strips_protocol_from_host() {
|
||||
// User's API returns "ip": "socks5://1.2.3.4" with protocol embedded in host
|
||||
let body = r#"{"ip": "socks5://1.2.3.4", "port": 1080, "username": "u", "password": "p"}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert_eq!(result.host, "1.2.3.4");
|
||||
assert_eq!(result.proxy_type, "socks5");
|
||||
assert_eq!(result.port, 1080);
|
||||
|
||||
// Protocol in host should be used as proxy_type when no explicit type field
|
||||
let body2 = r#"{"ip": "http://10.0.0.1", "port": 8080}"#;
|
||||
let result2 = ProxyManager::parse_dynamic_proxy_json(body2).unwrap();
|
||||
assert_eq!(result2.host, "10.0.0.1");
|
||||
assert_eq!(result2.proxy_type, "http");
|
||||
|
||||
// Explicit type field takes precedence over protocol in host
|
||||
let body3 = r#"{"ip": "http://10.0.0.1", "port": 1080, "type": "socks5"}"#;
|
||||
let result3 = ProxyManager::parse_dynamic_proxy_json(body3).unwrap();
|
||||
assert_eq!(result3.host, "10.0.0.1");
|
||||
assert_eq!(result3.proxy_type, "socks5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_empty_credentials_treated_as_none() {
|
||||
let body = r#"{"ip": "1.2.3.4", "port": 8080, "username": "", "password": ""}"#;
|
||||
let result = ProxyManager::parse_dynamic_proxy_json(body).unwrap();
|
||||
assert!(result.username.is_none());
|
||||
assert!(result.password.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_missing_ip() {
|
||||
let body = r#"{"port": 8080}"#;
|
||||
let err = ProxyManager::parse_dynamic_proxy_json(body).unwrap_err();
|
||||
assert!(err.contains("ip") || err.contains("host"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_missing_port() {
|
||||
let body = r#"{"ip": "1.2.3.4"}"#;
|
||||
let err = ProxyManager::parse_dynamic_proxy_json(body).unwrap_err();
|
||||
assert!(err.contains("port"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_invalid_json() {
|
||||
let err = ProxyManager::parse_dynamic_proxy_json("not json").unwrap_err();
|
||||
assert!(err.contains("Invalid JSON"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_json_not_object() {
|
||||
let err = ProxyManager::parse_dynamic_proxy_json("[1,2,3]").unwrap_err();
|
||||
assert!(err.contains("not an object"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_text_host_port_user_pass() {
|
||||
let body = "proxy.example.com:8080:user1:pass1";
|
||||
let result = ProxyManager::parse_dynamic_proxy_text(body).unwrap();
|
||||
assert_eq!(result.host, "proxy.example.com");
|
||||
assert_eq!(result.port, 8080);
|
||||
assert_eq!(result.username.as_deref(), Some("user1"));
|
||||
assert_eq!(result.password.as_deref(), Some("pass1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_text_protocol_url_format() {
|
||||
let body = "http://user:pass@proxy.example.com:3128";
|
||||
let result = ProxyManager::parse_dynamic_proxy_text(body).unwrap();
|
||||
assert_eq!(result.host, "proxy.example.com");
|
||||
assert_eq!(result.port, 3128);
|
||||
assert_eq!(result.proxy_type, "http");
|
||||
assert_eq!(result.username.as_deref(), Some("user"));
|
||||
assert_eq!(result.password.as_deref(), Some("pass"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_text_with_whitespace() {
|
||||
let body = " \n proxy.example.com:8080:user:pass \n ";
|
||||
let result = ProxyManager::parse_dynamic_proxy_text(body).unwrap();
|
||||
assert_eq!(result.host, "proxy.example.com");
|
||||
assert_eq!(result.port, 8080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_text_empty() {
|
||||
let err = ProxyManager::parse_dynamic_proxy_text("").unwrap_err();
|
||||
assert!(err.contains("Empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dynamic_proxy_text_whitespace_only() {
|
||||
let err = ProxyManager::parse_dynamic_proxy_text(" \n \n ").unwrap_err();
|
||||
assert!(err.contains("Empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_proxy_from_url_parses_json_response() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/hook"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_string(
|
||||
r#"{"host":"proxy.example.com","port":3128,"type":"socks5","username":"user","password":"pass"}"#,
|
||||
),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let pm = ProxyManager::new();
|
||||
let result = pm
|
||||
.fetch_proxy_from_url(
|
||||
&format!("{}/hook", server.uri()),
|
||||
Duration::from_millis(500),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.host, "proxy.example.com");
|
||||
assert_eq!(result.port, 3128);
|
||||
assert_eq!(result.proxy_type, "socks5");
|
||||
assert_eq!(result.username.as_deref(), Some("user"));
|
||||
assert_eq!(result.password.as_deref(), Some("pass"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_proxy_from_url_parses_text_response() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/hook"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("socks5://user:pass@1.2.3.4:1080"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let pm = ProxyManager::new();
|
||||
let result = pm
|
||||
.fetch_proxy_from_url(
|
||||
&format!("{}/hook", server.uri()),
|
||||
Duration::from_millis(500),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.host, "1.2.3.4");
|
||||
assert_eq!(result.port, 1080);
|
||||
assert_eq!(result.proxy_type, "socks5");
|
||||
assert_eq!(result.username.as_deref(), Some("user"));
|
||||
assert_eq!(result.password.as_deref(), Some("pass"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_proxy_from_url_returns_none_for_no_content() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/hook"))
|
||||
.respond_with(ResponseTemplate::new(204))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let pm = ProxyManager::new();
|
||||
let result = pm
|
||||
.fetch_proxy_from_url(
|
||||
&format!("{}/hook", server.uri()),
|
||||
Duration::from_millis(500),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_proxy_from_url_respects_timeout() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/hook"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_delay(Duration::from_millis(200))
|
||||
.set_body_string(r#"{"host":"1.2.3.4","port":8080}"#),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let pm = ProxyManager::new();
|
||||
let err = pm
|
||||
.fetch_proxy_from_url(&format!("{}/hook", server.uri()), Duration::from_millis(50))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.contains("Failed to fetch launch hook"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user