Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 49 additions & 25 deletions dragonfly-client/src/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use lazy_static::lazy_static;
use rcgen::Certificate;
use rustls::{RootCertStore, ServerConfig};
use rustls_pki_types::CertificateDer;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncWriteExt, BufReader, BufWriter};
Expand Down Expand Up @@ -420,17 +420,11 @@ pub async fn http_handler(
}

if request.uri().scheme().cloned() == Some(http::uri::Scheme::HTTPS) {
info!(
"proxy HTTPS request directly to remote server: {:?}",
request
);
log_request(&request, "proxy HTTPS request directly to remote server:");
return proxy_via_https(request, registry_cert).await;
}

info!(
"proxy HTTP request directly to remote server: {:?}",
request
);
log_request(&request, "proxy HTTP request directly to remote server:");
return proxy_via_http(request).await;
}

Expand All @@ -445,7 +439,7 @@ pub async fn https_handler(
registry_cert: Arc<Option<Vec<CertificateDer<'static>>>>,
server_ca_cert: Arc<Option<Certificate>>,
) -> ClientResult<Response> {
info!("handle HTTPS request: {:?}", request);
log_request(&request, "handle HTTPS request:");

// Proxy the request directly to the remote server.
if let Some(host) = request.uri().host() {
Expand Down Expand Up @@ -622,10 +616,7 @@ pub async fn upgraded_handler(
config.proxy.rules.as_deref(),
url::Url::parse(&request_uri.to_string()).or_err(ErrorType::ParseError)?,
) {
info!(
"proxy HTTPS request via dfdaemon by rule config: {:?}",
request,
);
log_request(&request, "proxy HTTPS request via dfdaemon by rule config:");
return proxy_via_dfdaemon(
config,
task,
Expand All @@ -640,9 +631,9 @@ pub async fn upgraded_handler(
// If the request header contains the X-Dragonfly-Use-P2P header, proxy the request via the
// dfdaemon.
if header::get_use_p2p(request.headers()) {
info!(
"proxy HTTP request via dfdaemon by X-Dragonfly-Use-P2P header: {:?}",
request,
log_request(
&request,
"proxy HTTP request via dfdaemon by X-Dragonfly-Use-P2P header:",
);
return proxy_via_dfdaemon(
config,
Expand All @@ -656,17 +647,11 @@ pub async fn upgraded_handler(
}

if request.uri().scheme().cloned() == Some(http::uri::Scheme::HTTPS) {
info!(
"proxy HTTPS request directly to remote server: {:?}",
request,
);
log_request(&request, "proxy HTTPS request directly to remote server:");
return proxy_via_https(request, registry_cert).await;
}

info!(
"proxy HTTP request directly to remote server: {:?}",
request,
);
log_request(&request, "proxy HTTP request directly to remote server:");
return proxy_via_http(request).await;
}

Expand Down Expand Up @@ -1327,3 +1312,42 @@ fn empty() -> BoxBody<Bytes, ClientError> {
.map_err(|never| match never {})
.boxed()
}

/// log_request safely output information from the request through logs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capitalize first word

fn log_request(request: &Request<hyper::body::Incoming>, log_info: &str) {
const HEADER_BLACKLIST: &[&str] = &[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing commonly sensitive headers:

  • x-api-key
  • x-csrf-token / x-xsrf-token
  • www-authenticate (may leak auth scheme details)
  • x-forwarded-for (PII — client IP)

Consider whether an allowlist approach (log only known-safe headers) would be more secure than a blocklist, which requires ongoing maintenance.

"authorization",
"cookie",
"set-cookie",
"proxy-authorization",
"x-access-token",
"x-auth-token",
];

let blacklist_set: HashSet<&str> = HEADER_BLACKLIST.iter().cloned().collect();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allocates a new HashSet per invocation. For a proxy that handles high request volume, this is unnecessary overhead.

With only 6 entries, a linear scan over the const slice is actually faster.


let (mut safe_headers, mut sensitive_headers) = (Vec::new(), Vec::new());
for (name, value) in request.headers().iter() {
if blacklist_set.contains(&name.as_str()) {
sensitive_headers.push((name, value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to allocate a Vec, you can use a bool flag like this for better performance:

let sensitive_names: Vec<&str> = request.headers().keys()
    .filter(|name| SENSITIVE_HEADERS.contains(&name.as_str()))
    .map(|name| name.as_str())
    .collect();

if !sensitive_names.is_empty() {
    debug!("{} | redacted sensitive headers: {:?}", log_info, sensitive_names);
}

} else {
safe_headers.push((name, value));
}
}

info!(
"{} | method={}, uri={}, version={:?}, safe_headers={:?}",
log_info,
request.method(),
request.uri(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query strings often carry tokens (?token=xxx, ?access_key=xxx). This should either be sanitized or at least called out as a known limitation.

request.version(),
safe_headers,
);

if !sensitive_headers.is_empty() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logs the actual values of Authorization, Cookie, etc.

Recommendation: Log only the header names, not their values. Or redact values to something like [REDACTED].

debug!(
"{} | BLACKLISTED_HEADERS (SENSITIVE): {:?}",
log_info, sensitive_headers,
);
}
}
Loading