From 074f53234a161b3ef02d3d28e3ff16053fa69a5a Mon Sep 17 00:00:00 2001 From: ylxdzsw Date: Wed, 16 Feb 2022 07:16:12 +0800 Subject: [PATCH] feat(ext/http): add support for unix domain sockets (#13628) --- Cargo.lock | 1 + cli/tests/unit/http_test.ts | 43 ++++++++++++++++ ext/http/Cargo.toml | 1 + ext/http/lib.rs | 99 ++++++++++++++++++++++++++----------- runtime/ops/http.rs | 16 ++++++ 5 files changed, 130 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 113529785f..d78f552306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -966,6 +966,7 @@ dependencies = [ "deno_core", "deno_websocket", "hyper", + "percent-encoding", "ring", "serde", "tokio", diff --git a/cli/tests/unit/http_test.ts b/cli/tests/unit/http_test.ts index 75e0d5505f..6a57c2fd10 100644 --- a/cli/tests/unit/http_test.ts +++ b/cli/tests/unit/http_test.ts @@ -1142,6 +1142,49 @@ Deno.test( }, ); +// https://github.com/denoland/deno/pull/13628 +Deno.test( + { + ignore: Deno.build.os === "windows", + permissions: { read: true, write: true }, + }, + async function httpServerOnUnixSocket() { + const filePath = Deno.makeTempFileSync(); + + const promise = (async () => { + const listener = Deno.listen({ path: filePath, transport: "unix" }); + for await (const conn of listener) { + const httpConn = Deno.serveHttp(conn); + for await (const { request, respondWith } of httpConn) { + const url = new URL(request.url); + assertEquals(url.protocol, "http+unix:"); + assertEquals(decodeURIComponent(url.host), filePath); + assertEquals(url.pathname, "/path/name"); + await respondWith(new Response("", { headers: {} })); + httpConn.close(); + } + break; + } + })(); + + // fetch() does not supports unix domain sockets yet https://github.com/denoland/deno/issues/8821 + const conn = await Deno.connect({ path: filePath, transport: "unix" }); + const encoder = new TextEncoder(); + // The Host header must be present and empty if it is not a Internet host name (RFC2616, Section 14.23) + const body = `GET /path/name HTTP/1.1\r\nHost:\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + const resp = new Uint8Array(200); + const readResult = await conn.read(resp); + assertEquals(readResult, 115); + + conn.close(); + + await promise; + }, +); + function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 const tp = new TextProtoReader(r); diff --git a/ext/http/Cargo.toml b/ext/http/Cargo.toml index 1554aecec6..9bbe1c1ded 100644 --- a/ext/http/Cargo.toml +++ b/ext/http/Cargo.toml @@ -19,6 +19,7 @@ bytes = "1" deno_core = { version = "0.118.0", path = "../../core" } deno_websocket = { version = "0.41.0", path = "../websocket" } hyper = { version = "0.14.9", features = ["server", "stream", "http1", "http2", "runtime"] } +percent-encoding = "2.1.0" ring = "0.16.20" serde = { version = "1.0.129", features = ["derive"] } tokio = { version = "1.10.1", features = ["full"] } diff --git a/ext/http/lib.rs b/ext/http/lib.rs index 24dd77c92b..e11d42da17 100644 --- a/ext/http/lib.rs +++ b/ext/http/lib.rs @@ -39,6 +39,7 @@ use hyper::service::Service; use hyper::Body; use hyper::Request; use hyper::Response; +use percent_encoding::percent_encode; use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; @@ -49,7 +50,6 @@ use std::future::Future; use std::io; use std::mem::replace; use std::mem::take; -use std::net::SocketAddr; use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; @@ -83,8 +83,27 @@ pub fn init() -> Extension { .build() } +pub enum HttpSocketAddr { + IpSocket(std::net::SocketAddr), + #[cfg(unix)] + UnixSocket(tokio::net::unix::SocketAddr), +} + +impl From for HttpSocketAddr { + fn from(addr: std::net::SocketAddr) -> Self { + Self::IpSocket(addr) + } +} + +#[cfg(unix)] +impl From for HttpSocketAddr { + fn from(addr: tokio::net::unix::SocketAddr) -> Self { + Self::UnixSocket(addr) + } +} + struct HttpConnResource { - addr: SocketAddr, + addr: HttpSocketAddr, scheme: &'static str, acceptors_tx: mpsc::UnboundedSender, closed_fut: Shared>>>, @@ -92,7 +111,7 @@ struct HttpConnResource { } impl HttpConnResource { - fn new(io: S, scheme: &'static str, addr: SocketAddr) -> Self + fn new(io: S, scheme: &'static str, addr: HttpSocketAddr) -> Self where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, { @@ -172,8 +191,8 @@ impl HttpConnResource { self.scheme } - fn addr(&self) -> SocketAddr { - self.addr + fn addr(&self) -> &HttpSocketAddr { + &self.addr } } @@ -188,16 +207,17 @@ impl Resource for HttpConnResource { } /// Creates a new HttpConn resource which uses `io` as its transport. -pub fn http_create_conn_resource( +pub fn http_create_conn_resource( state: &mut OpState, io: S, - addr: SocketAddr, + addr: A, scheme: &'static str, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, + A: Into, { - let conn = HttpConnResource::new(io, scheme, addr); + let conn = HttpConnResource::new(io, scheme, addr.into()); let rid = state.resource_table.add(conn); Ok(rid) } @@ -375,30 +395,49 @@ async fn op_http_accept( fn req_url( req: &hyper::Request, scheme: &'static str, - addr: SocketAddr, + addr: &HttpSocketAddr, ) -> String { - let host: Cow = if let Some(auth) = req.uri().authority() { - match addr.port() { - 443 if scheme == "https" => Cow::Borrowed(auth.host()), - 80 if scheme == "http" => Cow::Borrowed(auth.host()), - _ => Cow::Borrowed(auth.as_str()), // Includes port number. + let host: Cow = match addr { + HttpSocketAddr::IpSocket(addr) => { + if let Some(auth) = req.uri().authority() { + match addr.port() { + 443 if scheme == "https" => Cow::Borrowed(auth.host()), + 80 if scheme == "http" => Cow::Borrowed(auth.host()), + _ => Cow::Borrowed(auth.as_str()), // Includes port number. + } + } else if let Some(host) = req.uri().host() { + Cow::Borrowed(host) + } else if let Some(host) = req.headers().get("HOST") { + match host.to_str() { + Ok(host) => Cow::Borrowed(host), + Err(_) => Cow::Owned( + host + .as_bytes() + .iter() + .cloned() + .map(char::from) + .collect::(), + ), + } + } else { + Cow::Owned(addr.to_string()) + } } - } else if let Some(host) = req.uri().host() { - Cow::Borrowed(host) - } else if let Some(host) = req.headers().get("HOST") { - match host.to_str() { - Ok(host) => Cow::Borrowed(host), - Err(_) => Cow::Owned( - host - .as_bytes() - .iter() - .cloned() - .map(char::from) - .collect::(), - ), - } - } else { - Cow::Owned(addr.to_string()) + // There is no standard way for unix domain socket URLs + // nginx and nodejs request use http://unix:[socket_path]:/ but it is not a valid URL + // httpie uses http+unix://[percent_encoding_of_path]/ which we follow + #[cfg(unix)] + HttpSocketAddr::UnixSocket(addr) => Cow::Owned( + percent_encode( + addr + .as_pathname() + .and_then(|x| x.to_str()) + .unwrap_or_default() + .as_bytes(), + percent_encoding::NON_ALPHANUMERIC, + ) + .to_string(), + ), }; let path = req.uri().path_and_query().map_or("/", |p| p.as_str()); [scheme, "://", &host, path].concat() diff --git a/runtime/ops/http.rs b/runtime/ops/http.rs index fddac92612..53a99bd47d 100644 --- a/runtime/ops/http.rs +++ b/runtime/ops/http.rs @@ -8,6 +8,7 @@ use deno_core::OpState; use deno_core::ResourceId; use deno_http::http_create_conn_resource; use deno_net::io::TcpStreamResource; +use deno_net::io::UnixStreamResource; use deno_net::ops_tls::TlsStreamResource; pub fn init() -> Extension { @@ -45,5 +46,20 @@ fn op_http_start( return http_create_conn_resource(state, tls_stream, addr, "https"); } + #[cfg(unix)] + if let Ok(resource_rc) = state + .resource_table + .take::(tcp_stream_rid) + { + super::check_unstable(state, "Deno.serveHttp"); + + let resource = Rc::try_unwrap(resource_rc) + .expect("Only a single use of this resource should happen"); + let (read_half, write_half) = resource.into_inner(); + let unix_stream = read_half.reunite(write_half)?; + let addr = unix_stream.local_addr()?; + return http_create_conn_resource(state, unix_stream, addr, "http+unix"); + } + Err(bad_resource_id()) }