feat(ext/fetch): add support for fetch on unix sockets (#29154)

This commit adds support for using Unix socket proxies in `fetch` API.

This is facilitated by passing an appropriate `Deno.HttpClient` instance
to the `fetch` API:
```
const client = Deno.createHttpClient({
  proxy: {
    transport: "unix",
    path: "/path/to/unix.sock",
  },
});

await fetch("http://localhost/ping", { client });
```

Closes https://github.com/denoland/deno/issues/8821

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Luca Casonato 2025-05-13 18:06:17 +02:00 committed by GitHub
parent 9b2b1c41f5
commit 3b6c70e5b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 366 additions and 35 deletions

View file

@ -14,6 +14,8 @@ use std::future;
use std::future::Future;
use std::net::IpAddr;
use std::path::Path;
#[cfg(not(windows))]
use std::path::PathBuf;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
@ -47,10 +49,13 @@ use deno_core::RcRef;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_error::JsErrorBox;
use deno_fs::open_options_with_access_check;
use deno_fs::CheckedPath;
pub use deno_fs::FsError;
use deno_fs::OpenOptions;
use deno_path_util::PathToUrlError;
use deno_permissions::PermissionCheckError;
use deno_permissions::PermissionsContainer;
use deno_tls::rustls::RootCertStore;
use deno_tls::Proxy;
use deno_tls::RootCertStoreProvider;
@ -407,6 +412,12 @@ pub trait FetchPermissions {
api_name: &str,
get_path: &'a dyn deno_fs::GetPath,
) -> Result<deno_fs::CheckedPath<'a>, FsError>;
fn check_write<'a>(
&mut self,
path: Cow<'a, Path>,
api_name: &str,
get_path: &'a dyn deno_fs::GetPath,
) -> Result<deno_fs::CheckedPath<'a>, FsError>;
}
impl FetchPermissions for deno_permissions::PermissionsContainer {
@ -456,6 +467,42 @@ impl FetchPermissions for deno_permissions::PermissionsContainer {
Ok(CheckedPath::Unresolved(path))
}
}
fn check_write<'a>(
&mut self,
path: Cow<'a, Path>,
api_name: &str,
get_path: &'a dyn deno_fs::GetPath,
) -> Result<deno_fs::CheckedPath<'a>, FsError> {
if self.allows_all() {
return Ok(deno_fs::CheckedPath::Unresolved(path));
}
let (needs_canonicalize, normalized_path) = get_path.normalized(path)?;
let path = deno_permissions::PermissionsContainer::check_write_path(
self,
normalized_path,
api_name,
)
.map_err(|_| FsError::NotCapable("write"))?;
let path = if needs_canonicalize {
let path = get_path.resolved(&path)?;
Cow::Owned(path)
} else {
path
};
self
.check_special_file(&path, api_name)
.map_err(FsError::NotCapable)?;
if needs_canonicalize {
Ok(CheckedPath::Resolved(path))
} else {
Ok(CheckedPath::Unresolved(path))
}
}
}
#[op2(stack_trace)]
@ -894,8 +941,6 @@ pub struct CreateHttpClientArgs {
proxy: Option<Proxy>,
pool_max_idle_per_host: Option<usize>,
pool_idle_timeout: Option<serde_json::Value>,
#[serde(default)]
use_hickory_resolver: bool,
#[serde(default = "default_true")]
http1: bool,
#[serde(default = "default_true")]
@ -909,6 +954,27 @@ fn default_true() -> bool {
true
}
fn sync_permission_check<'a, P: FetchPermissions + 'static>(
permissions: &'a mut P,
api_name: &'static str,
) -> impl deno_fs::AccessCheckFn + 'a {
move |path, _options, _resolve| {
let read_path = permissions.check_read(path.clone(), api_name, _resolve)?;
let write_path = permissions.check_write(path, api_name, _resolve)?;
match (&read_path, &write_path) {
(CheckedPath::Resolved(a), CheckedPath::Resolved(b))
| (CheckedPath::Unresolved(a), CheckedPath::Resolved(b))
| (CheckedPath::Resolved(a), CheckedPath::Unresolved(b))
| (CheckedPath::Unresolved(a), CheckedPath::Unresolved(b))
if a == b =>
{
Ok(write_path)
}
_ => Err(FsError::NotCapable("write")),
}
}
}
#[op2(stack_trace)]
#[smi]
pub fn op_fetch_custom_client<FP>(
@ -919,10 +985,33 @@ pub fn op_fetch_custom_client<FP>(
where
FP: FetchPermissions + 'static,
{
if let Some(proxy) = args.proxy.clone() {
if let Some(proxy) = &args.proxy {
let permissions = state.borrow_mut::<FP>();
let url = Url::parse(&proxy.url)?;
permissions.check_net_url(&url, "Deno.createHttpClient()")?;
match proxy {
Proxy::Http { url, .. } => {
let url = Url::parse(url)?;
permissions.check_net_url(&url, "Deno.createHttpClient()")?;
}
Proxy::Unix { path } => {
let path = Path::new(path);
let mut access_check = sync_permission_check::<PermissionsContainer>(
state.borrow_mut(),
"Deno.createHttpClient()",
);
let (resolved_path, _) = open_options_with_access_check(
OpenOptions {
read: true,
write: true,
..Default::default()
},
path,
Some(&mut access_check),
)?;
if path != resolved_path {
return Err(FetchError::NotCapable("write"));
}
}
}
}
let options = state.borrow::<Options>();
@ -940,11 +1029,7 @@ where
.map_err(HttpClientCreateError::RootCertStore)?,
ca_certs,
proxy: args.proxy,
dns_resolver: if args.use_hickory_resolver {
dns::Resolver::hickory().map_err(FetchError::Dns)?
} else {
dns::Resolver::default()
},
dns_resolver: dns::Resolver::default(),
unsafely_ignore_certificate_errors: options
.unsafely_ignore_certificate_errors
.clone(),
@ -1024,6 +1109,8 @@ pub enum HttpClientCreateError {
#[class(inherit)]
#[error(transparent)]
RootCertStore(JsErrorBox),
#[error("Unix proxy is not supported on Windows")]
UnixProxyNotSupportedOnWindows,
}
/// Create new instance of async Client. This client supports
@ -1079,11 +1166,26 @@ pub fn create_http_client(
let mut proxies = proxy::from_env();
if let Some(proxy) = options.proxy {
let mut intercept = proxy::Intercept::all(&proxy.url)
.ok_or_else(|| HttpClientCreateError::InvalidProxyUrl)?;
if let Some(basic_auth) = &proxy.basic_auth {
intercept.set_auth(&basic_auth.username, &basic_auth.password);
}
let intercept = match proxy {
Proxy::Http { url, basic_auth } => {
let target = proxy::Target::parse(&url)
.ok_or_else(|| HttpClientCreateError::InvalidProxyUrl)?;
let mut intercept = proxy::Intercept::all(target);
if let Some(basic_auth) = &basic_auth {
intercept.set_auth(&basic_auth.username, &basic_auth.password);
}
intercept
}
#[cfg(not(windows))]
Proxy::Unix { path } => {
let target = proxy::Target::new_unix(PathBuf::from(path));
proxy::Intercept::all(target)
}
#[cfg(windows)]
Proxy::Unix { .. } => {
return Err(HttpClientCreateError::UnixProxyNotSupportedOnWindows);
}
};
proxies.prepend(intercept);
}
let proxies = Arc::new(proxies);