Repository: denoland/deno
Pull request: feat: Add support for DENO_TRUST_PROXY_HEADERS env var

If this env var is specified, then `x-deno-client-address` headers are removed
from the served HTTP traffic.

Changed files:
filename: cli/args/flags.rs
patch: @@ -1225,7 +1225,8 @@ static ENV_VARIABLES_HELP: &str = cstr!(
   <g>NO_COLOR</>               Set to disable color
   <g>NO_PROXY</>               Comma-separated list of hosts which do not use a proxy
                           <p(245)>(module downloads, fetch)</>
-  <g>NPM_CONFIG_REGISTRY</>    URL to use for the npm registry."#
+  <g>NPM_CONFIG_REGISTRY</>    URL to use for the npm registry.
+  <g>DENO_TRUST_PROXY_HEADERS</>  If specified, removes X-deno-client-address header when serving HTTP."#
 );
 
 static DENO_HELP: &str = cstr!(

filename: ext/http/00_serve.ts
patch: @@ -44,6 +44,8 @@ const {
   SafePromiseAll,
   PromisePrototypeThen,
   StringPrototypeIncludes,
+  StringPrototypeSlice,
+  StringPrototypeStartsWith,
   Symbol,
   TypeError,
   TypedArrayPrototypeGetSymbolToStringTag,
@@ -331,23 +333,23 @@ class InnerRequest {
   }
 
   get remoteAddr() {
-    const transport = this.#context.listener?.addr.transport;
-    if (transport === "unix" || transport === "unixpacket") {
-      return {
-        transport,
-        path: this.#context.listener.addr.path,
-      };
-    }
     if (this.#methodAndUri === undefined) {
       if (this.#external === null) {
         throw new TypeError("Request closed");
       }
       this.#methodAndUri = op_http_get_request_method_and_url(this.#external);
     }
-    if (transport === "vsock") {
+    const transport = this.#context.listener?.addr.transport;
+    if (this.#methodAndUri[3] === "unix") {
+      return {
+        transport,
+        path: this.#context.listener.addr.path,
+      };
+    }
+    if (StringPrototypeStartsWith(this.#methodAndUri[3], "vsock:")) {
       return {
         transport,
-        cid: Number(this.#methodAndUri[3]),
+        cid: Number(StringPrototypeSlice(this.#methodAndUri[3], 6)),
         port: this.#methodAndUri[4],
       };
     }

filename: ext/http/http_next.rs
patch: @@ -397,20 +397,28 @@ where
   .unwrap()
   .into();
 
-  let peer_address: v8::Local<v8::Value> = v8::String::new_from_utf8(
+  let (peer_ip, peer_port) = if let Some(client_addr) = &*http.client_addr() {
+    let addr: std::net::SocketAddr =
+      client_addr.to_str().unwrap().parse().unwrap();
+    (Rc::from(format!("{}", addr.ip())), Some(addr.port() as u32))
+  } else {
+    (request_info.peer_address.clone(), request_info.peer_port)
+  };
+
+  let peer_ip: v8::Local<v8::Value> = v8::String::new_from_utf8(
     scope,
-    request_info.peer_address.as_bytes(),
+    peer_ip.as_bytes(),
     v8::NewStringType::Normal,
   )
   .unwrap()
   .into();
 
-  let port: v8::Local<v8::Value> = match request_info.peer_port {
+  let peer_port: v8::Local<v8::Value> = match peer_port {
     Some(port) => v8::Number::new(scope, port.into()).into(),
     None => v8::undefined(scope).into(),
   };
 
-  let vec = [method, authority, path, peer_address, port, scheme];
+  let vec = [method, authority, path, peer_ip, peer_port, scheme];
   v8::Array::new_with_elements(scope, vec.as_slice())
 }
 

filename: ext/http/request_properties.rs
patch: @@ -173,7 +173,9 @@ impl HttpPropertyExtractor for DefaultHttpPropertyExtractor {
       #[cfg(unix)]
       NetworkStreamAddress::Unix(_) => Rc::from("unix"),
       #[cfg(unix)]
-      NetworkStreamAddress::Vsock(addr) => Rc::from(addr.cid().to_string()),
+      NetworkStreamAddress::Vsock(addr) => {
+        Rc::from(format!("vsock:{}", addr.cid()))
+      }
     };
     let local_port = listen_properties.local_port;
     let stream_type = listen_properties.stream_type;

filename: ext/http/service.rs
patch: @@ -9,6 +9,7 @@ use std::future::Future;
 use std::mem::ManuallyDrop;
 use std::pin::Pin;
 use std::rc::Rc;
+use std::sync::OnceLock;
 use std::task::ready;
 use std::task::Context;
 use std::task::Poll;
@@ -297,6 +298,7 @@ struct HttpRecordInner {
   needs_close_after_finish: bool,
   legacy_abort: bool,
   otel_info: Option<OtelInfo>,
+  client_addr: Option<http::HeaderValue>,
 }
 
 pub struct HttpRecord(RefCell<Option<HttpRecordInner>>);
@@ -312,6 +314,21 @@ impl Drop for HttpRecord {
   }
 }
 
+fn trust_proxy_headers() -> bool {
+  static TRUST_PROXY_HEADERS: OnceLock<bool> = OnceLock::new();
+
+  static VAR_NAME: &str = "DENO_TRUST_PROXY_HEADERS";
+
+  *TRUST_PROXY_HEADERS.get_or_init(|| {
+    if let Some(v) = std::env::var_os(VAR_NAME) {
+      std::env::remove_var(VAR_NAME);
+      v == "1"
+    } else {
+      false
+    }
+  })
+}
+
 impl HttpRecord {
   fn new(
     request: Request,
@@ -320,7 +337,12 @@ impl HttpRecord {
     otel_info: Option<OtelInfo>,
     legacy_abort: bool,
   ) -> Rc<Self> {
-    let (request_parts, request_body) = request.into_parts();
+    let (mut request_parts, request_body) = request.into_parts();
+    let client_addr = if trust_proxy_headers() {
+      request_parts.headers.remove("x-deno-client-address")
+    } else {
+      None
+    };
     let request_body = Some(request_body.into());
     let (mut response_parts, _) = http::Response::new(()).into_parts();
     let record =
@@ -357,6 +379,7 @@ impl HttpRecord {
       legacy_abort,
       needs_close_after_finish: false,
       otel_info,
+      client_addr,
     });
     record
   }
@@ -616,6 +639,10 @@ impl HttpRecord {
       info.handle_duration_and_request_size();
     }
   }
+
+  pub fn client_addr(&self) -> Ref<'_, Option<http::HeaderValue>> {
+    Ref::map(self.self_ref(), |inner| &inner.client_addr)
+  }
 }
 
 // `None` variant used when no body is present, for example