Files
opsx-vpn/client/src-tauri/src/server.rs
T

118 lines
3.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::time::Duration;
use reqwest::{redirect::Policy, Url};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct HealthResponse {
ok: bool,
}
#[derive(Serialize)]
pub struct ServerHealth {
address: String,
reachable: bool,
}
fn health_url(input: &str) -> Result<Url, String> {
let mut url = Url::parse(input.trim())
.map_err(|_| "请输入完整的服务端地址(含 http:// 或 https://)".to_string())?;
if url.scheme() != "https" && url.scheme() != "http" {
return Err("服务端地址须以 http:// 或 https:// 开头".to_string());
}
if url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| url.path() != "/"
{
return Err("请填写服务端根地址,不包含账号、路径、查询参数或片段".to_string());
}
url.set_path("/api/health");
Ok(url)
}
#[tauri::command]
pub async fn check_server(base_url: String) -> Result<ServerHealth, String> {
let url = health_url(&base_url)?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.redirect(Policy::none())
.build()
.map_err(|err| format!("无法创建网络客户端:{err}"))?;
let response = client
.get(url.clone())
.send()
.await
.map_err(|err| format!("连接服务端失败:{err}"))?;
if !response.status().is_success() {
return Err(format!("服务端健康检查返回 HTTP {}", response.status()));
}
let body: HealthResponse = response
.json()
.await
.map_err(|_| "服务端响应不是有效的 opsx-vpn 健康结果".to_string())?;
if !body.ok {
return Err("服务端报告未就绪".to_string());
}
let mut root = url;
root.set_path("");
Ok(ServerHealth {
address: root.as_str().trim_end_matches('/').to_string(),
reachable: true,
})
}
#[cfg(test)]
mod tests {
use super::{check_server, health_url};
use std::io::{Read, Write};
use std::net::TcpListener;
#[test]
fn accepts_http_and_https_server_addresses() {
assert_eq!(
health_url("https://vpn.example.com:443").unwrap().as_str(),
"https://vpn.example.com/api/health"
);
assert!(health_url("http://127.0.0.1:18080/").is_ok());
assert!(health_url("http://localhost:18080").is_ok());
assert_eq!(
health_url("http://121.4.111.208:18080").unwrap().as_str(),
"http://121.4.111.208:18080/api/health"
);
}
#[test]
fn rejects_unsupported_schemes_and_non_root_addresses() {
for input in [
"ftp://vpn.example.com",
"https://user:pass@vpn.example.com",
"https://vpn.example.com/admin",
"https://vpn.example.com/?token=abc",
"file:///etc/passwd",
] {
assert!(health_url(input).is_err(), "accepted {input}");
}
}
#[test]
fn checks_the_existing_health_contract_over_http() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local test listener");
let address = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0; 512];
let read = stream.read(&mut request).unwrap();
assert!(String::from_utf8_lossy(&request[..read]).starts_with("GET /api/health "));
stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}").unwrap();
});
let result =
tauri::async_runtime::block_on(check_server(format!("http://{address}"))).unwrap();
assert!(result.reachable);
assert_eq!(result.address, format!("http://{address}"));
server.join().unwrap();
}
}