Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ sha2.workspace = true
sha3 = { workspace = true, features = ["oid"] }
signature.workspace = true
sm3.workspace = true
socket2.workspace = true
spki.workspace = true
sys_traits = { workspace = true, features = ["real", "winapi", "libc"] }
thiserror.workspace = true
Expand Down
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ deno_core::extension!(deno_node,
ops::crypto::x509::op_node_x509_key_usage,
ops::crypto::x509::op_node_x509_public_key,
ops::dns::op_node_getaddrinfo<P>,
ops::dns::op_node_getnameinfo<P>,
ops::fs::op_node_fs_exists_sync<P>,
ops::fs::op_node_fs_exists<P>,
ops::fs::op_node_lchmod_sync<P>,
Expand Down
16 changes: 16 additions & 0 deletions ext/node/ops/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ pub const UV_FS_COPYFILE_EXCL: i32 = 1;
pub const UV_FS_COPYFILE_FICLONE: i32 = 2;
pub const UV_FS_COPYFILE_FICLONE_FORCE: i32 = 4;

// https://github.com/nodejs/node/blob/591ba692bfe30408e6a67397e7d18bfa1b9c3561/deps/uv/include/uv/errno.h#L35-L48
pub const UV_EAI_ADDRFAMILY: i32 = -3000;
pub const UV_EAI_AGAIN: i32 = -3001;
pub const UV_EAI_BADFLAGS: i32 = -3002;
pub const UV_EAI_CANCELED: i32 = -3003;
pub const UV_EAI_FAIL: i32 = -3004;
pub const UV_EAI_FAMILY: i32 = -3005;
pub const UV_EAI_MEMORY: i32 = -3006;
pub const UV_EAI_NODATA: i32 = -3007;
pub const UV_EAI_NONAME: i32 = -3008;
pub const UV_EAI_OVERFLOW: i32 = -3009;
pub const UV_EAI_SERVICE: i32 = -3010;
pub const UV_EAI_SOCKTYPE: i32 = -3011;
pub const UV_EAI_BADHINTS: i32 = -3013;
pub const UV_EAI_PROTOCOL: i32 = -3014;

#[cfg(unix)]
/// https://github.com/nodejs/node/blob/9d2368f64329bf194c4e82b349e76fdad879d32a/deps/uv/include/uv/unix.h#L506
pub const UV_FS_O_FILEMAP: i32 = 0;
Expand Down
200 changes: 196 additions & 4 deletions ext/node/ops/dns.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,71 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use std::cell::RefCell;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::rc::Rc;
use std::str::FromStr;
#[cfg(target_family = "windows")]
use std::sync::OnceLock;

use deno_core::OpState;
use deno_core::op2;
use deno_core::unsync::spawn_blocking;
use deno_error::JsError;
use deno_net::ops::NetPermToken;
use deno_permissions::PermissionCheckError;
use hyper_util::client::legacy::connect::dns::GaiResolver;
use hyper_util::client::legacy::connect::dns::Name;
use socket2::SockAddr;
use tower_service::Service;

#[derive(Debug, thiserror::Error, JsError)]
pub enum GetAddrInfoError {
pub enum DnsError {
#[class(type)]
#[error(transparent)]
AddrParseError(#[from] std::net::AddrParseError),
#[class(inherit)]
#[error(transparent)]
Permission(#[from] PermissionCheckError),
#[class(type)]
#[error("Could not resolve the hostname \"{0}\"")]
Resolution(String),
#[class(inherit)]
#[error("{0}")]
Io(
#[from]
#[inherit]
std::io::Error,
),
#[class(generic)]
#[error("{0}")]
#[property("uv_errcode" = self.code())]
RawUvErr(i32),
#[cfg(not(any(unix, windows)))]
#[class(generic)]
#[error("Unsupported platform.")]
UnsupportedPlatform,
}

impl DnsError {
fn code(&self) -> i32 {
match self {
Self::RawUvErr(code) => *code,
_ => 0,
}
}
}

#[cfg(target_family = "windows")]
static WINSOCKET_INIT: OnceLock<i32> = OnceLock::new();

#[op2(async, stack_trace)]
#[cppgc]
pub async fn op_node_getaddrinfo<P>(
state: Rc<RefCell<OpState>>,
#[string] hostname: String,
port: Option<u16>,
) -> Result<NetPermToken, GetAddrInfoError>
) -> Result<NetPermToken, DnsError>
where
P: crate::NodePermissions + 'static,
{
Expand All @@ -41,11 +77,11 @@ where

let mut resolver = GaiResolver::new();
let name = Name::from_str(&hostname)
.map_err(|_| GetAddrInfoError::Resolution(hostname.clone()))?;
.map_err(|_| DnsError::Resolution(hostname.clone()))?;
let resolved_ips = resolver
.call(name)
.await
.map_err(|_| GetAddrInfoError::Resolution(hostname.clone()))?
.map_err(|_| DnsError::Resolution(hostname.clone()))?
.map(|addr| addr.ip().to_string())
.collect::<Vec<_>>();
Ok(NetPermToken {
Expand All @@ -54,3 +90,159 @@ where
resolved_ips,
})
}

#[op2(async, stack_trace)]
#[serde]
pub async fn op_node_getnameinfo<P>(
state: Rc<RefCell<OpState>>,
#[string] ip: String,
#[smi] port: u16,
) -> Result<(String, String), DnsError>
where
P: crate::NodePermissions + 'static,
{
{
let mut state_ = state.borrow_mut();
let permissions = state_.borrow_mut::<P>();
permissions
.check_net((ip.as_str(), Some(port)), "node:dns.lookupService()")?;
}

let ip_addr: IpAddr = ip.parse()?;
let socket_addr = SocketAddr::new(ip_addr, port);

match spawn_blocking(move || getnameinfo(socket_addr)).await {
Ok(result) => result,
Err(err) => Err(DnsError::Io(err.into())),
}
}

fn getnameinfo(socket_addr: SocketAddr) -> Result<(String, String), DnsError> {
let sock: SockAddr = socket_addr.into();
let c_sockaddr = sock.as_ptr();
let c_sockaddr_len = sock.len();
#[cfg(unix)]
{
const NI_MAXSERV: u32 = 32;

let mut c_host = [0_u8; libc::NI_MAXHOST as usize];
let mut c_service = [0_u8; NI_MAXSERV as usize];

// SAFETY: Calling getnameinfo
let code = unsafe {
libc::getnameinfo(
c_sockaddr,
c_sockaddr_len,
c_host.as_mut_ptr() as _,
c_host.len() as _,
c_service.as_mut_ptr() as _,
c_service.len() as _,
libc::NI_NAMEREQD as _,
)
};
assert_success(code)?;

// SAFETY: c_host is initialized by getnameinfo on success.
let host_cstr = unsafe { std::ffi::CStr::from_ptr(c_host.as_ptr() as _) };
// SAFETY: c_service is initialized by getnameinfo on success.
let service_cstr =
unsafe { std::ffi::CStr::from_ptr(c_service.as_ptr() as _) };

Ok((
host_cstr.to_string_lossy().into_owned(),
service_cstr.to_string_lossy().into_owned(),
))
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStringExt;

use winapi::shared::minwindef::MAKEWORD;
use windows_sys::Win32::Networking::WinSock;

// SAFETY: winapi call
let wsa_startup_code = *WINSOCKET_INIT.get_or_init(|| unsafe {
let mut wsa_data: WinSock::WSADATA = std::mem::zeroed();
WinSock::WSAStartup(MAKEWORD(2, 2), &mut wsa_data)
});
assert_success(wsa_startup_code)?;

let mut c_host = [0_u16; WinSock::NI_MAXHOST as usize];
let mut c_service = [0_u16; WinSock::NI_MAXSERV as usize];

// SAFETY: Calling getnameinfo
let code = unsafe {
WinSock::GetNameInfoW(
c_sockaddr as _,
c_sockaddr_len,
c_host.as_mut_ptr() as _,
c_host.len() as _,
c_service.as_mut_ptr() as _,
c_service.len() as _,
WinSock::NI_NAMEREQD as _,
)
};
assert_success(code)?;

let host_str_len = c_host.iter().take_while(|&&c| c != 0).count();
let host_str = std::ffi::OsString::from_wide(&c_host[..host_str_len])
.to_string_lossy()
.into_owned();

let service_str_len = c_service.iter().take_while(|&&c| c != 0).count();
let service_str =
std::ffi::OsString::from_wide(&c_service[..service_str_len])
.to_string_lossy()
.into_owned();

Ok((host_str, service_str))
}
#[cfg(not(any(unix, windows)))]
{
Err(DnsError::UnsupportedPlatform)
}
}

#[cfg(any(unix, windows))]
fn assert_success(code: i32) -> Result<(), DnsError> {
#[cfg(windows)]
use windows_sys::Win32::Networking::WinSock;

use crate::ops::constant;

if code == 0 {
return Ok(());
}

#[cfg(unix)]
let err = match code {
libc::EAI_AGAIN => DnsError::RawUvErr(constant::UV_EAI_AGAIN),
libc::EAI_BADFLAGS => DnsError::RawUvErr(constant::UV_EAI_BADFLAGS),
libc::EAI_FAIL => DnsError::RawUvErr(constant::UV_EAI_FAIL),
libc::EAI_FAMILY => DnsError::RawUvErr(constant::UV_EAI_FAMILY),
libc::EAI_MEMORY => DnsError::RawUvErr(constant::UV_EAI_MEMORY),
libc::EAI_NONAME => DnsError::RawUvErr(constant::UV_EAI_NONAME),
libc::EAI_OVERFLOW => DnsError::RawUvErr(constant::UV_EAI_OVERFLOW),
libc::EAI_SYSTEM => DnsError::Io(std::io::Error::last_os_error()),
_ => DnsError::Io(std::io::Error::from_raw_os_error(code)),
};

#[cfg(windows)]
let err = match code {
WinSock::WSATRY_AGAIN => DnsError::RawUvErr(constant::UV_EAI_AGAIN),
WinSock::WSAEINVAL => DnsError::RawUvErr(constant::UV_EAI_BADFLAGS),
WinSock::WSANO_RECOVERY => DnsError::RawUvErr(constant::UV_EAI_FAIL),
WinSock::WSAEAFNOSUPPORT => DnsError::RawUvErr(constant::UV_EAI_FAMILY),
WinSock::WSA_NOT_ENOUGH_MEMORY => {
DnsError::RawUvErr(constant::UV_EAI_MEMORY)
}
WinSock::WSAHOST_NOT_FOUND => DnsError::RawUvErr(constant::UV_EAI_NONAME),
WinSock::WSATYPE_NOT_FOUND => DnsError::RawUvErr(constant::UV_EAI_SERVICE),
WinSock::WSAESOCKTNOSUPPORT => {
DnsError::RawUvErr(constant::UV_EAI_SOCKTYPE)
}
_ => DnsError::Io(std::io::Error::from_raw_os_error(code)),
};

Err(err)
}
62 changes: 62 additions & 0 deletions ext/node/polyfills/dns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
validateFunction,
validateNumber,
validateOneOf,
validatePort,
validateString,
} from "ext:deno_node/internal/validators.mjs";
import { isIP } from "ext:deno_node/internal/net.ts";
Expand Down Expand Up @@ -80,6 +81,8 @@ import {
dnsException,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_MISSING_ARGS,
handleDnsError,
} from "ext:deno_node/internal/errors.ts";
import {
AI_ADDRCONFIG as ADDRCONFIG,
Expand All @@ -89,9 +92,13 @@ import {
import cares, {
type ChannelWrapQuery,
GetAddrInfoReqWrap,
GetNameInfoReqWrap,
QueryReqWrap,
} from "ext:deno_node/internal_binding/cares_wrap.ts";
import { domainToASCII } from "ext:deno_node/internal/idna.ts";
import { primordials } from "ext:core/mod.js";

const { ObjectDefineProperty } = primordials;

function onlookup(
this: GetAddrInfoReqWrap,
Expand Down Expand Up @@ -305,6 +312,60 @@ Object.defineProperty(lookup, customPromisifyArgs, {
enumerable: false,
});

function onlookupservice(
this: GetNameInfoReqWrap,
err: Error | null,
hostname?: string,
service?: string,
) {
if (err) {
return this.callback!(handleDnsError(err, "getnameinfo", this.address));
}

this.callback!(err, hostname, service);
}

export function lookupService(
address: string,
port: number,
callback: (
err: ErrnoException | null,
hostname?: string,
service?: string,
) => void,
): GetNameInfoReqWrap {
if (arguments.length !== 3) {
throw new ERR_MISSING_ARGS("address", "port", "callback");
}

if (isIP(address) === 0) {
throw new ERR_INVALID_ARG_VALUE("address", address);
}

port = validatePort(port);

validateFunction(callback, "callback");

const req = new GetNameInfoReqWrap();
req.callback = callback;
req.address = address;
req.port = port;
req.oncomplete = onlookupservice;

const errCode = cares.getnameinfo(req, address, port);
if (errCode) {
throw dnsException(errCode, "getnameinfo", address);
}

return req;
}

ObjectDefineProperty(lookupService, customPromisifyArgs, {
__proto__: null,
value: ["hostname", "service"],
enumerable: false,
});

function onresolve(
this: QueryReqWrap,
err: number,
Expand Down Expand Up @@ -986,6 +1047,7 @@ export default {
ALL,
V4MAPPED,
lookup,
lookupService,
getServers,
resolveAny,
resolve4,
Expand Down
Loading
Loading