Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 9 additions & 2 deletions cli/tsc/dts/lib.deno.ns.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6345,10 +6345,11 @@ declare namespace Deno {
* {@linkcode Deno.CreateHttpClientOptions}.
*
* Supported proxies:
* - HTTP/HTTPS proxy: this uses the HTTP CONNECT method to tunnel HTTP
* requests through a different server.
* - HTTP/HTTPS proxy: this uses passthrough to tunnel HTTP requests, or HTTP
* CONNECT to tunnel HTTPS requests through a different server.
* - SOCKS5 proxy: this uses the SOCKS5 protocol to tunnel TCP connections
* through a different server.
* - TCP socket: this sends all requests to a specified TCP socket.
* - Unix domain socket: this sends all requests to a local Unix domain
* socket rather than a TCP socket. *Not supported on Windows.*
* - Vsock socket: this sends all requests to a local vsock socket.
Expand All @@ -6370,6 +6371,12 @@ declare namespace Deno {
url: string;
/** The basic auth credentials to be used against the proxy server. */
basicAuth?: BasicAuth;
} | {
transport: "tcp";
/** The hostname of the TCP server to connect to. */
hostname: string;
/** The port of the TCP server to connect to. */
port: number;
} | {
transport: "unix";
/** The path to the unix domain socket to use. */
Expand Down
5 changes: 2 additions & 3 deletions ext/fetch/22_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@ function createHttpClient(options) {
options.proxy.transport = "http";
break;
}
case "unix": {
break;
}
case "tcp":
case "unix":
case "vsock": {
break;
}
Expand Down
30 changes: 30 additions & 0 deletions ext/fetch/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@ impl Drop for ResourceToBodyAdapter {
}

pub trait FetchPermissions {
fn check_net(
&mut self,
host: &str,
port: u16,
api_name: &str,
) -> Result<(), PermissionCheckError>;
fn check_net_url(
&mut self,
url: &Url,
Expand All @@ -425,6 +431,20 @@ pub trait FetchPermissions {
}

impl FetchPermissions for deno_permissions::PermissionsContainer {
#[inline(always)]
fn check_net(
&mut self,
host: &str,
port: u16,
api_name: &str,
) -> Result<(), PermissionCheckError> {
deno_permissions::PermissionsContainer::check_net(
self,
&(host, Some(port)),
api_name,
)
}

#[inline(always)]
fn check_net_url(
&mut self,
Expand Down Expand Up @@ -929,6 +949,9 @@ where
let url = Url::parse(url)?;
permissions.check_net_url(&url, "Deno.createHttpClient()")?;
}
Proxy::Tcp { hostname, port } => {
permissions.check_net(hostname, *port, "Deno.createHttpClient()")?;
}
Proxy::Unix {
path: original_path,
} => {
Expand Down Expand Up @@ -1120,6 +1143,13 @@ pub fn create_http_client(
}
intercept
}
Proxy::Tcp {
hostname: host,
port,
} => {
let target = proxy::Target::new_tcp(host, port);
proxy::Intercept::all(target)
}
#[cfg(not(windows))]
Proxy::Unix { path } => {
let target = proxy::Target::new_unix(PathBuf::from(path));
Expand Down
50 changes: 38 additions & 12 deletions ext/fetch/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ pub(crate) enum Target {
dst: Uri,
auth: Option<(String, String)>,
},
Tcp {
hostname: String,
port: u16,
},
#[cfg(not(windows))]
Unix {
path: PathBuf,
Expand Down Expand Up @@ -223,6 +227,9 @@ impl Intercept {
Target::Socks { ref mut auth, .. } => {
*auth = Some((user.into(), pass.into()));
}
Target::Tcp { .. } => {
// Auth not supported for Tcp sockets
}
#[cfg(not(windows))]
Target::Unix { .. } => {
// Auth not supported for Unix sockets
Expand Down Expand Up @@ -333,6 +340,10 @@ impl Target {
Some(target)
}

pub(crate) fn new_tcp(hostname: String, port: u16) -> Self {
Target::Tcp { hostname, port }
}

#[cfg(not(windows))]
pub(crate) fn new_unix(path: PathBuf) -> Self {
Target::Unix { path }
Expand Down Expand Up @@ -535,8 +546,8 @@ type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub enum Proxied<T> {
/// Not proxied
PassThrough(T),
/// An HTTP forwarding proxy needed absolute-form
HttpForward(T),
/// Forwarded via TCP socket
Tcp(T),
/// Tunneled through HTTP CONNECT
HttpTunneled(Box<TokioIo<TlsStream<TokioIo<T>>>>),
/// Tunneled through SOCKS
Expand Down Expand Up @@ -601,7 +612,7 @@ where
.await?;
Ok(Proxied::HttpTunneled(Box::new(TokioIo::new(io))))
} else {
Ok(Proxied::HttpForward(io))
Ok(Proxied::Tcp(io))
}
})
}
Expand Down Expand Up @@ -645,6 +656,23 @@ where
}
})
}
Target::Tcp {
hostname: host,
port,
} => {
let mut connector =
HttpsConnector::from((self.http.clone(), self.tls_proxy.clone()));
let Ok(uri) = format!("http://{}:{}", host, port).parse() else {
return Box::pin(async {
Err("failed to parse tcp proxy uri".into())
});
};
let connecting = connector.call(uri);
Box::pin(async move {
let io = connecting.await?;
Ok(Proxied::Tcp(io))
})
}
#[cfg(not(windows))]
Target::Unix { path } => {
let path = path.clone();
Expand Down Expand Up @@ -768,7 +796,7 @@ where
) -> Poll<Result<(), std::io::Error>> {
match *self {
Proxied::PassThrough(ref mut p) => Pin::new(p).poll_read(cx, buf),
Proxied::HttpForward(ref mut p) => Pin::new(p).poll_read(cx, buf),
Proxied::Tcp(ref mut p) => Pin::new(p).poll_read(cx, buf),
Proxied::HttpTunneled(ref mut p) => Pin::new(p).poll_read(cx, buf),
Proxied::Socks(ref mut p) => Pin::new(p).poll_read(cx, buf),
Proxied::SocksTls(ref mut p) => Pin::new(p).poll_read(cx, buf),
Expand All @@ -795,7 +823,7 @@ where
) -> Poll<Result<usize, std::io::Error>> {
match *self {
Proxied::PassThrough(ref mut p) => Pin::new(p).poll_write(cx, buf),
Proxied::HttpForward(ref mut p) => Pin::new(p).poll_write(cx, buf),
Proxied::Tcp(ref mut p) => Pin::new(p).poll_write(cx, buf),
Proxied::HttpTunneled(ref mut p) => Pin::new(p).poll_write(cx, buf),
Proxied::Socks(ref mut p) => Pin::new(p).poll_write(cx, buf),
Proxied::SocksTls(ref mut p) => Pin::new(p).poll_write(cx, buf),
Expand All @@ -816,7 +844,7 @@ where
) -> Poll<Result<(), std::io::Error>> {
match *self {
Proxied::PassThrough(ref mut p) => Pin::new(p).poll_flush(cx),
Proxied::HttpForward(ref mut p) => Pin::new(p).poll_flush(cx),
Proxied::Tcp(ref mut p) => Pin::new(p).poll_flush(cx),
Proxied::HttpTunneled(ref mut p) => Pin::new(p).poll_flush(cx),
Proxied::Socks(ref mut p) => Pin::new(p).poll_flush(cx),
Proxied::SocksTls(ref mut p) => Pin::new(p).poll_flush(cx),
Expand All @@ -837,7 +865,7 @@ where
) -> Poll<Result<(), std::io::Error>> {
match *self {
Proxied::PassThrough(ref mut p) => Pin::new(p).poll_shutdown(cx),
Proxied::HttpForward(ref mut p) => Pin::new(p).poll_shutdown(cx),
Proxied::Tcp(ref mut p) => Pin::new(p).poll_shutdown(cx),
Proxied::HttpTunneled(ref mut p) => Pin::new(p).poll_shutdown(cx),
Proxied::Socks(ref mut p) => Pin::new(p).poll_shutdown(cx),
Proxied::SocksTls(ref mut p) => Pin::new(p).poll_shutdown(cx),
Expand All @@ -855,7 +883,7 @@ where
fn is_write_vectored(&self) -> bool {
match *self {
Proxied::PassThrough(ref p) => p.is_write_vectored(),
Proxied::HttpForward(ref p) => p.is_write_vectored(),
Proxied::Tcp(ref p) => p.is_write_vectored(),
Proxied::HttpTunneled(ref p) => p.is_write_vectored(),
Proxied::Socks(ref p) => p.is_write_vectored(),
Proxied::SocksTls(ref p) => p.is_write_vectored(),
Expand All @@ -879,9 +907,7 @@ where
Proxied::PassThrough(ref mut p) => {
Pin::new(p).poll_write_vectored(cx, bufs)
}
Proxied::HttpForward(ref mut p) => {
Pin::new(p).poll_write_vectored(cx, bufs)
}
Proxied::Tcp(ref mut p) => Pin::new(p).poll_write_vectored(cx, bufs),
Proxied::HttpTunneled(ref mut p) => {
Pin::new(p).poll_write_vectored(cx, bufs)
}
Expand All @@ -906,7 +932,7 @@ where
fn connected(&self) -> Connected {
match self {
Proxied::PassThrough(p) => p.connected(),
Proxied::HttpForward(p) => p.connected().proxy(true),
Proxied::Tcp(p) => p.connected().proxy(true),
Proxied::HttpTunneled(p) => {
let tunneled_tls = p.inner().get_ref();
if tunneled_tls.1.alpn_protocol() == Some(b"h2") {
Expand Down
4 changes: 4 additions & 0 deletions ext/tls/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ pub enum Proxy {
url: String,
basic_auth: Option<BasicAuth>,
},
Tcp {
hostname: String,
port: u16,
},
Unix {
path: String,
},
Expand Down
9 changes: 9 additions & 0 deletions runtime/snapshot_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ impl deno_web::TimersPermission for Permissions {
}

impl deno_fetch::FetchPermissions for Permissions {
fn check_net(
&mut self,
_host: &str,
_port: u16,
_api_name: &str,
) -> Result<(), PermissionCheckError> {
unreachable!("snapshotting!")
}

fn check_net_url(
&mut self,
_url: &deno_core::url::Url,
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/fetch_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2328,3 +2328,52 @@ Deno.test(
assert(client instanceof Deno.HttpClient);
},
);

Deno.test(
{
permissions: { net: true, read: true, write: true },
ignore: Deno.build.os === "windows",
},
async function fetchTcpProxy() {
const started = Promise.withResolvers<number>();
await using _server = Deno.serve({
transport: "tcp",
port: 0,
onListen: ({ port }) => started.resolve(port),
}, (req) => {
const url = new URL(req.url);
if (url.pathname === "/ping") {
return new Response(url.href, {
headers: { "content-type": "text/plain" },
});
} else {
return new Response("Not found", { status: 404 });
}
});

const port = await started.promise;

using client = Deno.createHttpClient({
proxy: {
transport: "tcp",
hostname: "localhost",
port,
},
});

const resp1 = await fetch("https://example.com/ping", { client });
assertEquals(resp1.status, 200);
assertEquals(resp1.headers.get("content-type"), "text/plain");
// TODO(@lucacasonato): should be https://example.com/ping. bug in https://github.com/hyperium/hyper-util/blob/00035bac2da1cfa820eda4db7bf7ddcbd30be3c1/src/client/legacy/client.rs#L915-L917
assertEquals(await resp1.text(), "http://example.com/ping");

const resp2 = await fetch("http://localhost:42424/ping", { client });
assertEquals(resp2.status, 200);
assertEquals(resp2.headers.get("content-type"), "text/plain");
assertEquals(await resp2.text(), "http://localhost:42424/ping");

const resp3 = await fetch("http://localhost:42424/not-found", { client });
assertEquals(resp3.status, 404);
assertEquals(await resp3.text(), "Not found");
},
);
Loading