Compare commits

...

6 Commits

Author SHA1 Message Date
21pages
43df9fb7a1 copilot review: normalize HTTP method before direct request dispatch
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-29 17:20:23 +08:00
21pages
8d65f21f23 copilot review: format IPv6 tcp proxy log targets correctly
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-29 16:41:48 +08:00
21pages
91ebbbd31d copilot review: test function name
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-29 16:31:41 +08:00
21pages
a965e8cf8f format common.rs
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-29 16:22:21 +08:00
21pages
c737611538 copilot review: redact tcp proxy logs, dedupe headers, and avoid body clone
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-29 16:21:18 +08:00
21pages
9f2ce33a6c Suppress secure_tcp info logs for TCP proxy requests
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-29 14:44:52 +08:00

View File

@@ -1160,6 +1160,25 @@ fn is_tcp_proxy_api_target(url: &str) -> bool {
should_use_tcp_proxy_for_api_url(url, &ui_get_api_server()) should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
} }
fn tcp_proxy_log_target(url: &str) -> String {
url::Url::parse(url)
.ok()
.map(|parsed| {
let mut redacted = format!("{}://", parsed.scheme());
let Some(host) = parsed.host() else {
return "<invalid-url>".to_owned();
};
redacted.push_str(&host.to_string());
if let Some(port) = parsed.port() {
redacted.push(':');
redacted.push_str(&port.to_string());
}
redacted.push_str(parsed.path());
redacted
})
.unwrap_or_else(|| "<invalid-url>".to_owned())
}
#[inline] #[inline]
fn get_tcp_proxy_addr() -> String { fn get_tcp_proxy_addr() -> String {
check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT) check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
@@ -1186,11 +1205,16 @@ async fn tcp_proxy_request(
parsed.path().to_string() parsed.path().to_string()
}; };
log::debug!("Sending {} {} via TCP proxy to {}", method, path, tcp_addr); log::debug!(
"Sending {} {} via TCP proxy to {}",
method,
parsed.path(),
tcp_addr
);
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?; let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
let key = crate::get_key(true).await; let key = crate::get_key(true).await;
secure_tcp(&mut conn, &key).await?; secure_tcp_silent(&mut conn, &key).await?;
let mut req = HttpProxyRequest::new(); let mut req = HttpProxyRequest::new();
req.method = method.to_uppercase(); req.method = method.to_uppercase();
@@ -1225,6 +1249,7 @@ fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
if !header.is_empty() { if !header.is_empty() {
let tmp: Vec<&str> = header.splitn(2, ": ").collect(); let tmp: Vec<&str> = header.splitn(2, ": ").collect();
if tmp.len() == 2 { if tmp.len() == 2 {
if !tmp[0].eq_ignore_ascii_case("Content-Type") {
entries.push(HeaderEntry { entries.push(HeaderEntry {
name: tmp[0].into(), name: tmp[0].into(),
value: tmp[1].into(), value: tmp[1].into(),
@@ -1232,15 +1257,12 @@ fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
}); });
} }
} }
}
entries entries
} }
/// POST request via TCP proxy. /// POST request via TCP proxy.
async fn post_request_via_tcp_proxy( async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType<String> {
url: &str,
body: &str,
header: &str,
) -> ResultType<String> {
let headers = parse_simple_header(header); let headers = parse_simple_header(header);
let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?; let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
if !resp.error.is_empty() { if !resp.error.is_empty() {
@@ -1256,10 +1278,7 @@ fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType<String> {
let mut response_headers = Map::new(); let mut response_headers = Map::new();
for entry in resp.headers.iter() { for entry in resp.headers.iter() {
response_headers.insert( response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
entry.name.to_lowercase(),
json!(entry.value),
);
} }
let mut result = Map::new(); let mut result = Map::new();
@@ -1295,11 +1314,7 @@ fn tcp_proxy_fallback_log_condition() -> &'static str {
} }
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback. /// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
async fn post_request_http( async fn post_request_http(url: String, body: String, header: &str) -> ResultType<(u16, String)> {
url: String,
body: String,
header: &str,
) -> ResultType<(u16, String)> {
let proxy_conf = Config::get_socks(); let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(&url, &proxy_conf); let tls_url = get_url_for_tls(&url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url); let tls_type = get_cached_tls_type(tls_url);
@@ -1339,9 +1354,12 @@ pub async fn post_request(url: String, body: String, header: &str) -> ResultType
if should_fallback && can_fallback_to_raw_tcp(&url) { if should_fallback && can_fallback_to_raw_tcp(&url) {
log::warn!( log::warn!(
"HTTP POST to {} {} (result: {:?}), trying TCP proxy fallback", "HTTP POST to {} {} (result: {:?}), trying TCP proxy fallback",
url, tcp_proxy_log_target(&url),
tcp_proxy_fallback_log_condition(), tcp_proxy_fallback_log_condition(),
http_result.as_ref().map(|(s, _)| *s).map_err(|e| e.to_string()), http_result
.as_ref()
.map(|(s, _)| *s)
.map_err(|e| e.to_string()),
); );
match post_request_via_tcp_proxy(&url, &body, header).await { match post_request_via_tcp_proxy(&url, &body, header).await {
Ok(resp) => return Ok(resp), Ok(resp) => return Ok(resp),
@@ -1464,7 +1482,8 @@ async fn get_http_response_async(
tls_type.unwrap_or(TlsType::Rustls), tls_type.unwrap_or(TlsType::Rustls),
danger_accept_invalid_cert.unwrap_or(false), danger_accept_invalid_cert.unwrap_or(false),
); );
let mut http_client = match method { let normalized_method = method.to_ascii_lowercase();
let mut http_client = match normalized_method.as_str() {
"get" => http_client.get(url), "get" => http_client.get(url),
"post" => http_client.post(url), "post" => http_client.post(url),
"put" => http_client.put(url), "put" => http_client.put(url),
@@ -1568,7 +1587,7 @@ async fn http_request_http(
url, url,
tls_url, tls_url,
method, method,
body.clone(), body,
header, header,
tls_type, tls_type,
danger_accept_invalid_cert, danger_accept_invalid_cert,
@@ -1578,10 +1597,7 @@ async fn http_request_http(
// Serialize response headers // Serialize response headers
let mut response_headers = Map::new(); let mut response_headers = Map::new();
for (key, value) in response.headers() { for (key, value) in response.headers() {
response_headers.insert( response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or("")));
key.to_string(),
json!(value.to_str().unwrap_or("")),
);
} }
let status_code = response.status().as_u16(); let status_code = response.status().as_u16();
@@ -1590,15 +1606,12 @@ async fn http_request_http(
// Construct the JSON object // Construct the JSON object
let mut result = Map::new(); let mut result = Map::new();
result.insert("status_code".to_string(), json!(status_code)); result.insert("status_code".to_string(), json!(status_code));
result.insert( result.insert("headers".to_string(), Value::Object(response_headers));
"headers".to_string(),
Value::Object(response_headers),
);
result.insert("body".to_string(), json!(response_body)); result.insert("body".to_string(), json!(response_body));
// Convert map to JSON string // Convert map to JSON string
let json_str = let json_str = serde_json::to_string(&result)
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))?; .map_err(|e| anyhow!("Failed to serialize response: {}", e))?;
Ok((status_code, json_str)) Ok((status_code, json_str))
} }
@@ -1624,7 +1637,7 @@ pub async fn http_request_sync(
log::warn!( log::warn!(
"HTTP {} to {} {}, trying TCP proxy fallback", "HTTP {} to {} {}, trying TCP proxy fallback",
method, method,
url, tcp_proxy_log_target(&url),
tcp_proxy_fallback_log_condition() tcp_proxy_fallback_log_condition()
); );
match http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header).await { match http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header).await {
@@ -1913,7 +1926,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool {
false false
} }
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> {
// Skip additional encryption when using WebSocket connections (wss://) // Skip additional encryption when using WebSocket connections (wss://)
// as WebSocket Secure (wss://) already provides transport layer encryption. // as WebSocket Secure (wss://) already provides transport layer encryption.
// This doesn't affect the end-to-end encryption between clients, // This doesn't affect the end-to-end encryption between clients,
@@ -1946,8 +1959,10 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
}); });
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
conn.set_key(key); conn.set_key(key);
if log_on_success {
log::info!("Connection secured"); log::info!("Connection secured");
} }
}
_ => {} _ => {}
} }
} }
@@ -1957,6 +1972,14 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
Ok(()) Ok(())
} }
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, true).await
}
async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, false).await
}
#[inline] #[inline]
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> { fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
if pk.len() == 32 { if pk.len() == 32 {
@@ -2792,8 +2815,9 @@ mod tests {
} }
} }
let _restore = let _restore = RestoreCustomRendezvousServer(Config::get_option(
RestoreCustomRendezvousServer(Config::get_option(keys::OPTION_CUSTOM_RENDEZVOUS_SERVER)); keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
));
Config::set_option( Config::set_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
"1:2".to_string(), "1:2".to_string(),
@@ -2804,11 +2828,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() { async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
let err = http_request_via_tcp_proxy("not a url", "get", None, "{") let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
.await assert!(result.is_err());
.unwrap_err()
.to_string();
assert!(err.contains("EOF while parsing an object"));
} }
#[tokio::test] #[tokio::test]
@@ -2852,11 +2873,71 @@ mod tests {
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))); .any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
} }
#[test]
fn test_parse_simple_header_ignores_custom_content_type() {
let headers = parse_simple_header("Content-Type: text/plain");
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("application/json")
);
}
#[test]
fn test_parse_simple_header_preserves_non_content_type_header() {
let headers = parse_simple_header("Authorization: Bearer token");
assert!(headers.iter().any(|entry| {
entry.name.eq_ignore_ascii_case("Authorization")
&& entry.value.as_str() == "Bearer token"
}));
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("application/json")
);
}
#[test] #[test]
fn test_tcp_proxy_fallback_log_condition() { fn test_tcp_proxy_fallback_log_condition() {
assert_eq!(tcp_proxy_fallback_log_condition(), "failed or 5xx"); assert_eq!(tcp_proxy_fallback_log_condition(), "failed or 5xx");
} }
#[test]
fn test_tcp_proxy_log_target_redacts_query_only() {
assert_eq!(
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
"https://example.com/api/heartbeat"
);
}
#[test]
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
assert_eq!(
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
"https://[2001:db8::1]:21114/api/heartbeat"
);
}
#[test] #[test]
fn test_http_proxy_response_to_json() { fn test_http_proxy_response_to_json() {
let mut resp = HttpProxyResponse { let mut resp = HttpProxyResponse {