mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-30 19:33:20 +03:00
Compare commits
9 Commits
27c0cd4f9b
...
tcp-proxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28cca601b5 | ||
|
|
93cfd56954 | ||
|
|
5e7484c51b | ||
|
|
43df9fb7a1 | ||
|
|
8d65f21f23 | ||
|
|
91ebbbd31d | ||
|
|
a965e8cf8f | ||
|
|
c737611538 | ||
|
|
9f2ce33a6c |
344
src/common.rs
344
src/common.rs
@@ -1086,6 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_public(url: &str) -> bool {
|
pub fn is_public(url: &str) -> bool {
|
||||||
|
let url = url.to_ascii_lowercase();
|
||||||
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,6 +1161,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)
|
||||||
@@ -1168,6 +1188,10 @@ fn get_tcp_proxy_addr() -> String {
|
|||||||
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
|
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
|
||||||
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
|
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
|
||||||
/// receives `HttpProxyResponse`.
|
/// receives `HttpProxyResponse`.
|
||||||
|
///
|
||||||
|
/// The entire operation (connect + handshake + send + receive) is wrapped in
|
||||||
|
/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
|
||||||
|
/// any stage cannot block the caller indefinitely.
|
||||||
async fn tcp_proxy_request(
|
async fn tcp_proxy_request(
|
||||||
method: &str,
|
method: &str,
|
||||||
url: &str,
|
url: &str,
|
||||||
@@ -1186,45 +1210,55 @@ 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 overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
|
||||||
let key = crate::get_key(true).await;
|
timeout(overall_timeout, async {
|
||||||
secure_tcp(&mut conn, &key).await?;
|
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
|
||||||
|
let key = crate::get_key(true).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();
|
||||||
req.path = path;
|
req.path = path;
|
||||||
req.headers = headers.into();
|
req.headers = headers.into();
|
||||||
req.body = Bytes::from(body.to_vec());
|
req.body = Bytes::from(body.to_vec());
|
||||||
|
|
||||||
let mut msg_out = RendezvousMessage::new();
|
let mut msg_out = RendezvousMessage::new();
|
||||||
msg_out.set_http_proxy_request(req);
|
msg_out.set_http_proxy_request(req);
|
||||||
conn.send(&msg_out).await?;
|
conn.send(&msg_out).await?;
|
||||||
|
|
||||||
match timeout(READ_TIMEOUT, conn.next()).await? {
|
match conn.next().await {
|
||||||
Some(Ok(bytes)) => {
|
Some(Ok(bytes)) => {
|
||||||
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
|
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
|
||||||
match msg_in.union {
|
match msg_in.union {
|
||||||
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
|
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
|
||||||
_ => bail!("Unexpected response from TCP proxy"),
|
_ => bail!("Unexpected response from TCP proxy"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
|
||||||
|
None => bail!("TCP proxy connection closed without response"),
|
||||||
}
|
}
|
||||||
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
|
})
|
||||||
None => bail!("TCP proxy connection closed without response"),
|
.await?
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
|
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
|
||||||
|
/// If the caller supplies a Content-Type header it overrides the default `application/json`.
|
||||||
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
|
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
|
||||||
let mut entries = vec![HeaderEntry {
|
let mut entries = Vec::new();
|
||||||
name: "Content-Type".into(),
|
let mut has_content_type = false;
|
||||||
value: "application/json".into(),
|
|
||||||
..Default::default()
|
|
||||||
}];
|
|
||||||
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") {
|
||||||
|
has_content_type = true;
|
||||||
|
}
|
||||||
entries.push(HeaderEntry {
|
entries.push(HeaderEntry {
|
||||||
name: tmp[0].into(),
|
name: tmp[0].into(),
|
||||||
value: tmp[1].into(),
|
value: tmp[1].into(),
|
||||||
@@ -1232,15 +1266,21 @@ fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !has_content_type {
|
||||||
|
entries.insert(
|
||||||
|
0,
|
||||||
|
HeaderEntry {
|
||||||
|
name: "Content-Type".into(),
|
||||||
|
value: "application/json".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
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 +1296,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();
|
||||||
@@ -1289,25 +1326,16 @@ fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn tcp_proxy_fallback_log_condition() -> &'static str {
|
|
||||||
"failed or 5xx"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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: &str, body: &str, 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);
|
||||||
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||||
let response = post_request_(
|
let response = post_request_(
|
||||||
&url,
|
url,
|
||||||
tls_url,
|
tls_url,
|
||||||
body.clone(),
|
body.to_owned(),
|
||||||
header,
|
header,
|
||||||
tls_type,
|
tls_type,
|
||||||
danger_accept_invalid_cert,
|
danger_accept_invalid_cert,
|
||||||
@@ -1319,6 +1347,49 @@ async fn post_request_http(
|
|||||||
Ok((status, text))
|
Ok((status, text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
|
||||||
|
/// if the URL is eligible. 4xx responses are returned as-is.
|
||||||
|
async fn with_tcp_proxy_fallback<HttpFut, TcpFut>(
|
||||||
|
url: &str,
|
||||||
|
method: &str,
|
||||||
|
http_fn: HttpFut,
|
||||||
|
tcp_fn: TcpFut,
|
||||||
|
) -> ResultType<String>
|
||||||
|
where
|
||||||
|
HttpFut: Future<Output = ResultType<(u16, String)>>,
|
||||||
|
TcpFut: Future<Output = ResultType<String>>,
|
||||||
|
{
|
||||||
|
if should_use_raw_tcp_for_api(url) {
|
||||||
|
return tcp_fn.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let http_result = http_fn.await;
|
||||||
|
let should_fallback = match &http_result {
|
||||||
|
Err(_) => true,
|
||||||
|
Ok((status, _)) => *status >= 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_fallback && can_fallback_to_raw_tcp(url) {
|
||||||
|
log::warn!(
|
||||||
|
"HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback",
|
||||||
|
method,
|
||||||
|
tcp_proxy_log_target(url),
|
||||||
|
http_result
|
||||||
|
.as_ref()
|
||||||
|
.map(|(s, _)| *s)
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
|
);
|
||||||
|
match tcp_fn.await {
|
||||||
|
Ok(resp) => return Ok(resp),
|
||||||
|
Err(tcp_err) => {
|
||||||
|
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http_result.map(|(_status, text)| text)
|
||||||
|
}
|
||||||
|
|
||||||
/// POST request with raw TCP proxy support.
|
/// POST request with raw TCP proxy support.
|
||||||
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
|
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
|
||||||
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
|
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
|
||||||
@@ -1326,37 +1397,13 @@ async fn post_request_http(
|
|||||||
/// - 4xx responses are returned as-is (server is reachable, business logic error).
|
/// - 4xx responses are returned as-is (server is reachable, business logic error).
|
||||||
/// - If fallback also fails, returns the original HTTP result (text or error).
|
/// - If fallback also fails, returns the original HTTP result (text or error).
|
||||||
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
|
||||||
if should_use_raw_tcp_for_api(&url) {
|
with_tcp_proxy_fallback(
|
||||||
return post_request_via_tcp_proxy(&url, &body, header).await;
|
&url,
|
||||||
}
|
"POST",
|
||||||
|
post_request_http(&url, &body, header),
|
||||||
let http_result = post_request_http(url.clone(), body.clone(), header).await;
|
post_request_via_tcp_proxy(&url, &body, header),
|
||||||
let should_fallback = match &http_result {
|
)
|
||||||
Err(_) => true,
|
.await
|
||||||
Ok((status, _)) => *status >= 500,
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_fallback && can_fallback_to_raw_tcp(&url) {
|
|
||||||
log::warn!(
|
|
||||||
"HTTP POST to {} {} (result: {:?}), trying TCP proxy fallback",
|
|
||||||
url,
|
|
||||||
tcp_proxy_fallback_log_condition(),
|
|
||||||
http_result.as_ref().map(|(s, _)| *s).map_err(|e| e.to_string()),
|
|
||||||
);
|
|
||||||
match post_request_via_tcp_proxy(&url, &body, header).await {
|
|
||||||
Ok(resp) => return Ok(resp),
|
|
||||||
Err(tcp_err) => {
|
|
||||||
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
|
||||||
// Fall through to return original HTTP result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return original HTTP result
|
|
||||||
match http_result {
|
|
||||||
Ok((_status, text)) => Ok(text),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
@@ -1464,7 +1511,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 +1616,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 +1626,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 +1635,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1610,32 +1652,13 @@ pub async fn http_request_sync(
|
|||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
header: String,
|
header: String,
|
||||||
) -> ResultType<String> {
|
) -> ResultType<String> {
|
||||||
if should_use_raw_tcp_for_api(&url) {
|
with_tcp_proxy_fallback(
|
||||||
return http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header).await;
|
&url,
|
||||||
}
|
&method,
|
||||||
|
http_request_http(&url, &method, body.clone(), &header),
|
||||||
let http_result = http_request_http(&url, &method, body.clone(), &header).await;
|
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
|
||||||
let should_fallback = match &http_result {
|
)
|
||||||
Err(_) => true,
|
.await
|
||||||
Ok((status, _)) => *status >= 500,
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_fallback && can_fallback_to_raw_tcp(&url) {
|
|
||||||
log::warn!(
|
|
||||||
"HTTP {} to {} {}, trying TCP proxy fallback",
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
tcp_proxy_fallback_log_condition()
|
|
||||||
);
|
|
||||||
match http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header).await {
|
|
||||||
Ok(resp) => return Ok(resp),
|
|
||||||
Err(tcp_err) => {
|
|
||||||
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
http_result.map(|(_status, json_str)| json_str)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
|
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
|
||||||
@@ -1913,7 +1936,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,7 +1969,9 @@ 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);
|
||||||
log::info!("Connection secured");
|
if log_on_success {
|
||||||
|
log::info!("Connection secured");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1957,6 +1982,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 {
|
||||||
@@ -2734,11 +2767,13 @@ mod tests {
|
|||||||
assert!(is_public("https://rustdesk.com/"));
|
assert!(is_public("https://rustdesk.com/"));
|
||||||
assert!(is_public("https://www.rustdesk.com/"));
|
assert!(is_public("https://www.rustdesk.com/"));
|
||||||
assert!(is_public("https://api.rustdesk.com/v1"));
|
assert!(is_public("https://api.rustdesk.com/v1"));
|
||||||
|
assert!(is_public("https://API.RUSTDESK.COM/v1"));
|
||||||
assert!(is_public("https://rustdesk.com/path"));
|
assert!(is_public("https://rustdesk.com/path"));
|
||||||
|
|
||||||
// Test URLs ending with "rustdesk.com"
|
// Test URLs ending with "rustdesk.com"
|
||||||
assert!(is_public("rustdesk.com"));
|
assert!(is_public("rustdesk.com"));
|
||||||
assert!(is_public("https://rustdesk.com"));
|
assert!(is_public("https://rustdesk.com"));
|
||||||
|
assert!(is_public("https://RustDesk.com"));
|
||||||
assert!(is_public("http://www.rustdesk.com"));
|
assert!(is_public("http://www.rustdesk.com"));
|
||||||
assert!(is_public("https://api.rustdesk.com"));
|
assert!(is_public("https://api.rustdesk.com"));
|
||||||
|
|
||||||
@@ -2792,8 +2827,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 +2840,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]
|
||||||
@@ -2853,8 +2886,63 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tcp_proxy_fallback_log_condition() {
|
fn test_parse_simple_header_respects_custom_content_type() {
|
||||||
assert_eq!(tcp_proxy_fallback_log_condition(), "failed or 5xx");
|
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("text/plain")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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]
|
||||||
|
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]
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::HbbHttpResponse;
|
use super::HbbHttpResponse;
|
||||||
use crate::hbbs_http::create_http_client_with_url;
|
use crate::hbbs_http::create_http_client_with_url;
|
||||||
use hbb_common::{config::LocalConfig, log, ResultType};
|
use hbb_common::{config::LocalConfig, log, ResultType};
|
||||||
use reqwest::blocking::Client;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
@@ -109,7 +110,7 @@ pub struct AuthBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct OidcSession {
|
pub struct OidcSession {
|
||||||
client: Option<Client>,
|
warmed_api_server: Option<String>,
|
||||||
state_msg: &'static str,
|
state_msg: &'static str,
|
||||||
failed_msg: String,
|
failed_msg: String,
|
||||||
code_url: Option<OidcAuthUrl>,
|
code_url: Option<OidcAuthUrl>,
|
||||||
@@ -136,7 +137,7 @@ impl Default for UserStatus {
|
|||||||
impl OidcSession {
|
impl OidcSession {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: None,
|
warmed_api_server: None,
|
||||||
state_msg: REQUESTING_ACCOUNT_AUTH,
|
state_msg: REQUESTING_ACCOUNT_AUTH,
|
||||||
failed_msg: "".to_owned(),
|
failed_msg: "".to_owned(),
|
||||||
code_url: None,
|
code_url: None,
|
||||||
@@ -149,11 +150,28 @@ impl OidcSession {
|
|||||||
|
|
||||||
fn ensure_client(api_server: &str) {
|
fn ensure_client(api_server: &str) {
|
||||||
let mut write_guard = OIDC_SESSION.write().unwrap();
|
let mut write_guard = OIDC_SESSION.write().unwrap();
|
||||||
if write_guard.client.is_none() {
|
if write_guard.warmed_api_server.as_deref() == Some(api_server) {
|
||||||
// This URL is used to detect the appropriate TLS implementation for the server.
|
return;
|
||||||
let login_option_url = format!("{}/api/login-options", &api_server);
|
}
|
||||||
let client = create_http_client_with_url(&login_option_url);
|
// This URL is used to detect the appropriate TLS implementation for the server.
|
||||||
write_guard.client = Some(client);
|
let login_option_url = format!("{}/api/login-options", api_server);
|
||||||
|
let _ = create_http_client_with_url(&login_option_url);
|
||||||
|
write_guard.warmed_api_server = Some(api_server.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hbb_http_response<T: DeserializeOwned>(body: &str) -> ResultType<HbbHttpResponse<T>> {
|
||||||
|
let map = serde_json::from_str::<Map<String, Value>>(body)?;
|
||||||
|
if let Some(error) = map.get("error") {
|
||||||
|
if let Some(err) = error.as_str() {
|
||||||
|
Ok(HbbHttpResponse::Error(err.to_owned()))
|
||||||
|
} else {
|
||||||
|
Ok(HbbHttpResponse::ErrorFormat)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match serde_json::from_value(Value::Object(map)) {
|
||||||
|
Ok(v) => Ok(HbbHttpResponse::Data(v)),
|
||||||
|
Err(_) => Ok(HbbHttpResponse::DataTypeFormat),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,26 +182,15 @@ impl OidcSession {
|
|||||||
uuid: &str,
|
uuid: &str,
|
||||||
) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
|
) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
|
||||||
Self::ensure_client(api_server);
|
Self::ensure_client(api_server);
|
||||||
let resp = if let Some(client) = &OIDC_SESSION.read().unwrap().client {
|
let body = serde_json::json!({
|
||||||
client
|
"op": op,
|
||||||
.post(format!("{}/api/oidc/auth", api_server))
|
"id": id,
|
||||||
.json(&serde_json::json!({
|
"uuid": uuid,
|
||||||
"op": op,
|
"deviceInfo": crate::ui_interface::get_login_device_info(),
|
||||||
"id": id,
|
})
|
||||||
"uuid": uuid,
|
.to_string();
|
||||||
"deviceInfo": crate::ui_interface::get_login_device_info(),
|
let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?;
|
||||||
}))
|
Self::parse_hbb_http_response(&resp)
|
||||||
.send()?
|
|
||||||
} else {
|
|
||||||
hbb_common::bail!("http client not initialized");
|
|
||||||
};
|
|
||||||
let status = resp.status();
|
|
||||||
match resp.try_into() {
|
|
||||||
Ok(v) => Ok(v),
|
|
||||||
Err(err) => {
|
|
||||||
hbb_common::bail!("Http status: {}, err: {}", status, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query(
|
fn query(
|
||||||
@@ -197,11 +204,19 @@ impl OidcSession {
|
|||||||
&[("code", code), ("id", id), ("uuid", uuid)],
|
&[("code", code), ("id", id), ("uuid", uuid)],
|
||||||
)?;
|
)?;
|
||||||
Self::ensure_client(api_server);
|
Self::ensure_client(api_server);
|
||||||
if let Some(client) = &OIDC_SESSION.read().unwrap().client {
|
#[derive(Deserialize)]
|
||||||
Ok(client.get(url).send()?.try_into()?)
|
struct HttpResponseBody {
|
||||||
} else {
|
body: String,
|
||||||
hbb_common::bail!("http client not initialized")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resp = crate::http_request_sync(
|
||||||
|
url.to_string(),
|
||||||
|
"GET".to_owned(),
|
||||||
|
None,
|
||||||
|
"{}".to_owned(),
|
||||||
|
)?;
|
||||||
|
let resp = serde_json::from_str::<HttpResponseBody>(&resp)?;
|
||||||
|
Self::parse_hbb_http_response(&resp.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
|
|||||||
Reference in New Issue
Block a user