feat(node:dns): implement dns.reverse. (#4332)

* feat(node:dns): implement `dns.reverse`.
Close: #4299

* fix dns reverse for ipv6

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
Ai Hoshino
2023-08-29 11:10:33 +08:00
committed by GitHub
parent d1c2d6b25c
commit c53372c9f3
6 changed files with 412 additions and 9 deletions

View File

@@ -719,6 +719,99 @@ pub fn ResolveInfoRequest(comptime cares_type: type, comptime type_name: []const
};
}
pub const GetHostByAddrInfoRequest = struct {
const request_type = @This();
const log = Output.scoped(@This(), false);
resolver_for_caching: ?*DNSResolver = null,
hash: u64 = 0,
cache: @This().CacheConfig = @This().CacheConfig{},
head: CAresReverse,
tail: *CAresReverse = undefined,
pub fn init(
cache: DNSResolver.LookupCacheHit(@This()),
resolver: ?*DNSResolver,
name: []const u8,
globalThis: *JSC.JSGlobalObject,
comptime cache_field: []const u8,
) !*@This() {
var request = try globalThis.allocator().create(@This());
var hasher = std.hash.Wyhash.init(0);
hasher.update(name);
const hash = hasher.final();
var poll_ref = JSC.PollRef.init();
poll_ref.ref(globalThis.bunVM());
request.* = .{
.resolver_for_caching = resolver,
.hash = hash,
.head = .{ .poll_ref = poll_ref, .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .allocated = false, .name = name },
};
request.tail = &request.head;
if (cache == .new) {
request.resolver_for_caching = resolver;
request.cache = @This().CacheConfig{
.pending_cache = true,
.entry_cache = false,
.pos_in_pending = @as(u5, @truncate(@field(resolver.?, cache_field).indexOf(cache.new).?)),
.name_len = @as(u9, @truncate(name.len)),
};
cache.new.lookup = request;
}
return request;
}
pub const CacheConfig = packed struct(u16) {
pending_cache: bool = false,
entry_cache: bool = false,
pos_in_pending: u5 = 0,
name_len: u9 = 0,
};
pub const PendingCacheKey = struct {
hash: u64,
len: u16,
lookup: *request_type = undefined,
pub fn append(this: *PendingCacheKey, cares_lookup: *CAresReverse) void {
var tail = this.lookup.tail;
tail.next = cares_lookup;
this.lookup.tail = cares_lookup;
}
pub fn init(name: []const u8) PendingCacheKey {
var hasher = std.hash.Wyhash.init(0);
hasher.update(name);
const hash = hasher.final();
return PendingCacheKey{
.hash = hash,
.len = @as(u16, @truncate(name.len)),
.lookup = undefined,
};
}
};
pub fn onCaresComplete(this: *@This(), err_: ?c_ares.Error, timeout: i32, result: ?*c_ares.struct_hostent) void {
if (this.resolver_for_caching) |resolver| {
if (this.cache.pending_cache) {
resolver.drainPendingAddrCares(
this.cache.pos_in_pending,
err_,
timeout,
result,
);
return;
}
}
var head = this.head;
bun.default_allocator.destroy(this);
head.processResolve(err_, timeout, result);
}
};
pub const GetAddrInfoRequest = struct {
const log = Output.scoped(.GetAddrInfoRequest, false);
@@ -945,6 +1038,76 @@ pub const GetAddrInfoRequest = struct {
}
};
pub const CAresReverse = struct {
const log = Output.scoped(@This(), true);
globalThis: *JSC.JSGlobalObject = undefined,
promise: JSC.JSPromise.Strong,
poll_ref: JSC.PollRef,
allocated: bool = false,
next: ?*@This() = null,
name: []const u8,
pub fn init(globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator, name: []const u8) !*@This() {
var this = try allocator.create(@This());
var poll_ref = JSC.PollRef.init();
poll_ref.ref(globalThis.bunVM());
this.* = .{ .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .poll_ref = poll_ref, .allocated = true, .name = name };
return this;
}
pub fn processResolve(this: *@This(), err_: ?c_ares.Error, _: i32, result: ?*c_ares.struct_hostent) void {
if (err_) |err| {
var promise = this.promise;
var globalThis = this.globalThis;
const error_value = globalThis.createErrorInstance("reverse failed: {s}", .{err.label()});
error_value.put(
globalThis,
JSC.ZigString.static("code"),
JSC.ZigString.init(err.code()).toValueGC(globalThis),
);
promise.reject(globalThis, error_value);
this.deinit();
return;
}
if (result == null) {
var promise = this.promise;
var globalThis = this.globalThis;
const error_value = globalThis.createErrorInstance("reverse failed: No results", .{});
error_value.put(
globalThis,
JSC.ZigString.static("code"),
JSC.ZigString.init("EUNREACHABLE").toValueGC(globalThis),
);
promise.reject(globalThis, error_value);
this.deinit();
return;
}
var node = result.?;
const array = node.toJSReponse(this.globalThis.allocator(), this.globalThis, "");
this.onComplete(array);
return;
}
pub fn onComplete(this: *@This(), result: JSC.JSValue) void {
var promise = this.promise;
var globalThis = this.globalThis;
this.promise = .{};
promise.resolve(globalThis, result);
this.deinit();
}
pub fn deinit(this: *@This()) void {
this.poll_ref.unrefOnNextTick(this.globalThis.bunVM());
bun.default_allocator.free(this.name);
if (this.allocated)
this.globalThis.allocator().destroy(this);
}
};
pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) type {
return struct {
const log = Output.scoped(@This(), true);
@@ -1161,6 +1324,7 @@ pub const DNSResolver = struct {
pending_ns_cache_cares: NSPendingCache = NSPendingCache.init(),
pending_ptr_cache_cares: PtrPendingCache = PtrPendingCache.init(),
pending_cname_cache_cares: CnamePendingCache = CnamePendingCache.init(),
pending_addr_cache_crares: AddrPendingCache = AddrPendingCache.init(),
const PendingCache = bun.HiveArray(GetAddrInfoRequest.PendingCacheKey, 32);
const SrvPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_ares_srv_reply, "srv").PendingCacheKey, 32);
@@ -1172,6 +1336,7 @@ pub const DNSResolver = struct {
const NSPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_hostent, "ns").PendingCacheKey, 32);
const PtrPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_hostent, "ptr").PendingCacheKey, 32);
const CnamePendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_hostent, "cname").PendingCacheKey, 32);
const AddrPendingCache = bun.HiveArray(GetHostByAddrInfoRequest.PendingCacheKey, 32);
fn getKey(this: *DNSResolver, index: u8, comptime cache_name: []const u8, comptime request_type: type) request_type.PendingCacheKey {
var cache = &@field(this, cache_name);
@@ -1315,6 +1480,47 @@ pub const DNSResolver = struct {
}
}
pub fn drainPendingAddrCares(this: *DNSResolver, index: u8, err: ?c_ares.Error, timeout: i32, result: ?*c_ares.struct_hostent) void {
const key = this.getKey(index, "pending_addr_cache_crares", GetHostByAddrInfoRequest);
var addr = result orelse {
var pending: ?*CAresReverse = key.lookup.head.next;
key.lookup.head.processResolve(err, timeout, null);
bun.default_allocator.destroy(key.lookup);
while (pending) |value| {
pending = value.next;
value.processResolve(err, timeout, null);
}
return;
};
var pending: ?*CAresReverse = key.lookup.head.next;
var prev_global = key.lookup.head.globalThis;
var array = addr.toJSReponse(this.vm.allocator, prev_global, "");
defer addr.deinit();
array.ensureStillAlive();
key.lookup.head.onComplete(array);
bun.default_allocator.destroy(key.lookup);
array.ensureStillAlive();
while (pending) |value| {
var new_global = value.globalThis;
if (prev_global != new_global) {
array = addr.toJSReponse(this.vm.allocator, new_global, "");
prev_global = new_global;
}
pending = value.next;
{
array.ensureStillAlive();
value.onComplete(array);
array.ensureStillAlive();
}
}
}
pub const CacheHit = union(enum) {
inflight: *GetAddrInfoRequest.PendingCacheKey,
new: *GetAddrInfoRequest.PendingCacheKey,
@@ -1582,10 +1788,76 @@ pub const DNSResolver = struct {
},
}
}
// pub fn reverse(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
// const arguments = callframe.arguments(3);
// }
pub fn reverse(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
const arguments = callframe.arguments(2);
if (arguments.len < 1) {
globalThis.throwNotEnoughArguments("reverse", 2, arguments.len);
return .zero;
}
const ip_value = arguments.ptr[0];
if (ip_value.isEmptyOrUndefinedOrNull() or !ip_value.isString()) {
globalThis.throwInvalidArgumentType("reverse", "ip", "string");
return .zero;
}
const ip_str = ip_value.toStringOrNull(globalThis) orelse {
return .zero;
};
if (ip_str.length() == 0) {
globalThis.throwInvalidArgumentType("reverse", "ip", "non-empty string");
return .zero;
}
const ip = ip_str.toSliceClone(globalThis, bun.default_allocator).slice();
var vm = globalThis.bunVM();
var resolver = vm.rareData().globalDNSResolver(vm);
var channel: *c_ares.Channel = switch (resolver.getChannel()) {
.result => |res| res,
.err => |err| {
const system_error = JSC.SystemError{
.errno = -1,
.code = bun.String.static(err.code()),
.message = bun.String.static(err.label()),
};
globalThis.throwValue(system_error.toErrorInstance(globalThis));
return .zero;
},
};
const key = GetHostByAddrInfoRequest.PendingCacheKey.init(ip);
var cache = resolver.getOrPutIntoResolvePendingCache(
GetHostByAddrInfoRequest,
key,
"pending_addr_cache_crares",
);
if (cache == .inflight) {
var cares_reverse = CAresReverse.init(globalThis, globalThis.allocator(), ip) catch unreachable;
cache.inflight.append(cares_reverse);
return cares_reverse.promise.value();
}
var request = GetHostByAddrInfoRequest.init(
cache,
resolver,
ip,
globalThis,
"pending_addr_cache_crares",
) catch unreachable;
const promise = request.tail.promise.value();
channel.getHostByAddr(
ip,
GetHostByAddrInfoRequest,
request,
GetHostByAddrInfoRequest.onCaresComplete,
);
return promise;
}
pub fn lookup(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
const arguments = callframe.arguments(2);
@@ -2185,6 +2457,12 @@ pub const DNSResolver = struct {
.name = "Bun__DNSResolver__getServers",
},
);
@export(
reverse,
.{
.name = "Bun__DNSResolver__reverse",
},
);
}
// pub fn lookupService(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
// const arguments = callframe.arguments(3);

View File

@@ -214,6 +214,7 @@ extern "C" EncodedJSValue Bun__DNSResolver__resolveNs(JSGlobalObject*, JSC::Call
extern "C" EncodedJSValue Bun__DNSResolver__resolvePtr(JSGlobalObject*, JSC::CallFrame*);
extern "C" EncodedJSValue Bun__DNSResolver__resolveCname(JSGlobalObject*, JSC::CallFrame*);
extern "C" EncodedJSValue Bun__DNSResolver__getServers(JSGlobalObject*, JSC::CallFrame*);
extern "C" EncodedJSValue Bun__DNSResolver__reverse(JSGlobalObject*, JSC::CallFrame*);
static JSValue constructDNSObject(VM& vm, JSObject* bunObject)
{
@@ -243,6 +244,8 @@ static JSValue constructDNSObject(VM& vm, JSObject* bunObject)
JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0);
dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getServers"_s), 2, Bun__DNSResolver__getServers, ImplementationVisibility::Public, NoIntrinsic,
JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0);
dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "reverse"_s), 2, Bun__DNSResolver__reverse, ImplementationVisibility::Public, NoIntrinsic,
JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0);
return dnsObject;
}

View File

@@ -221,6 +221,22 @@ pub const struct_hostent = extern struct {
return fn (*Type, status: ?Error, timeouts: i32, results: ?*struct_hostent) void;
}
pub fn hostCallbackWrapper(
comptime Type: type,
comptime function: Callback(Type),
) ares_host_callback {
return &struct {
pub fn handle(ctx: ?*anyopaque, status: c_int, timeouts: c_int, hostent: ?*struct_hostent) callconv(.C) void {
var this = bun.cast(*Type, ctx.?);
if (status != ARES_SUCCESS) {
function(this, Error.get(status), timeouts, null);
return;
}
function(this, null, timeouts, hostent);
}
}.handle;
}
pub fn callbackWrapper(
comptime lookup_name: []const u8,
comptime Type: type,
@@ -524,6 +540,37 @@ pub const Channel = opaque {
ares_query(this, name_ptr, NSClass.ns_c_in, @field(NSType, field_name), cares_type.callbackWrapper(lookup_name, Type, callback), ctx);
}
pub fn getHostByAddr(this: *Channel, ip_addr: []const u8, comptime Type: type, ctx: *Type, comptime callback: struct_hostent.Callback(Type)) void {
// "0000:0000:0000:0000:0000:ffff:192.168.100.228".length = 45
const buf_size = 46;
var addr_buf: [buf_size]u8 = undefined;
const addr_ptr: ?[*:0]const u8 = brk: {
if (ip_addr.len == 0 or ip_addr.len >= buf_size) {
break :brk null;
}
const len = @min(ip_addr.len, addr_buf.len - 1);
@memcpy(addr_buf[0..len], ip_addr[0..len]);
addr_buf[len] = 0;
break :brk addr_buf[0..len :0];
};
// https://c-ares.org/ares_inet_pton.html
// https://github.com/c-ares/c-ares/blob/7f3262312f246556d8c1bdd8ccc1844847f42787/src/lib/ares_gethostbyaddr.c#L71-L72
// `ares_inet_pton` allows passing raw bytes as `dst`,
// which can avoid the use of `struct_in_addr` to reduce extra bytes.
var addr: [16]u8 = undefined;
if (addr_ptr != null) {
if (ares_inet_pton(std.os.AF.INET, addr_ptr, &addr) == 1) {
ares_gethostbyaddr(this, &addr, 4, std.os.AF.INET, struct_hostent.hostCallbackWrapper(Type, callback), ctx);
return;
} else if (ares_inet_pton(std.os.AF.INET6, addr_ptr, &addr) == 1) {
return ares_gethostbyaddr(this, &addr, 16, std.os.AF.INET6, struct_hostent.hostCallbackWrapper(Type, callback), ctx);
}
}
struct_hostent.hostCallbackWrapper(Type, callback).?(ctx, ARES_ENOTIMP, 0, null);
}
pub inline fn process(this: *Channel, fd: i32, readable: bool, writable: bool) void {
ares_process_fd(
this,
@@ -1091,6 +1138,7 @@ pub const struct_ares_soa_reply = extern struct {
ares_free_data(this);
}
};
pub const struct_ares_uri_reply = extern struct {
next: [*c]struct_ares_uri_reply,
priority: c_ushort,

View File

@@ -179,6 +179,21 @@ function lookupService(address, port, callback) {
callback(null, address, port);
}
function reverse(ip, callback) {
if (typeof callback != "function") {
throw new TypeError("callback must be a function");
}
dns.reverse(ip, callback).then(
results => {
callback(null, results);
},
error => {
callback(error);
},
);
}
var InternalResolver = class Resolver {
constructor(options) {}
@@ -395,7 +410,18 @@ var InternalResolver = class Resolver {
}
reverse(ip, callback) {
callback(null, []);
if (typeof callback != "function") {
throw new TypeError("callback must be a function");
}
dns.reverse(ip, callback).then(
results => {
callback(null, results);
},
error => {
callback(error);
},
);
}
setServers(servers) {}
@@ -549,6 +575,9 @@ const promises = {
resolveCname(hostname) {
return dns.resolveCname(hostname);
},
reverse(ip) {
return dns.reverse(ip);
},
Resolver: class Resolver {
constructor(options) {}
@@ -627,13 +656,13 @@ const promises = {
}
reverse(ip) {
return Promise.resolve([]);
return dns.reverse(ip);
}
setServers(servers) {}
},
};
for (const key of ["resolveAny", "reverse"]) {
for (const key of ["resolveAny"]) {
promises[key] = () => Promise.resolve(undefined);
}

File diff suppressed because one or more lines are too long

View File

@@ -223,3 +223,48 @@ test("dns.getServers", done => {
}
done();
});
test("dns.reverse", done => {
dns.reverse("8.8.8.8", (err, hostnames) => {
try {
expect(err).toBeNull();
expect(hostnames).toContain("dns.google");
done();
} catch (err) {
done(err);
}
});
dns.reverse("1.1.1.1", (err, hostnames) => {
try {
expect(err).toBeNull();
expect(hostnames).toContain("one.one.one.one");
done();
} catch (err) {
done(err);
}
});
dns.reverse("2606:4700:4700::1111", (err, hostnames) => {
try {
expect(err).toBeNull();
expect(hostnames).toContain("one.one.one.one");
done();
} catch (err) {
done(err);
}
});
});
test("dns.promises.reverse", async () => {
{
let hostnames = await dns.promises.reverse("8.8.8.8");
expect(hostnames).toContain("dns.google");
}
{
let hostnames = await dns.promises.reverse("1.1.1.1");
expect(hostnames).toContain("one.one.one.one");
}
{
let hostnames = await dns.promises.reverse("2606:4700:4700::1111");
expect(hostnames).toContain("one.one.one.one");
}
});