mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user