From 9bca80c1a205679b793763bd3634b94fdb9852ba Mon Sep 17 00:00:00 2001 From: Kai Tamkun <13513421+heimskr@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:53:03 -0800 Subject: [PATCH] DNS fixes (#15864) Co-authored-by: Jarred Sumner --- packages/bun-types/bun.d.ts | 4 + scripts/runner.node.mjs | 9 +- src/bun.js/api/Timer.zig | 9 + src/bun.js/api/bun/dns_resolver.zig | 965 +++++++++++++----- src/bun.js/bindings/BunObject.cpp | 85 +- src/bun.js/bindings/ErrorCode.cpp | 9 + src/bun.js/bindings/ErrorCode.ts | 4 + src/bun.js/bindings/bindings.cpp | 6 + src/bun.js/bindings/bindings.zig | 40 +- .../bindings/generated_classes_list.zig | 1 + src/bun.js/bindings/headers-handwritten.h | 1 + src/bun.js/javascript.zig | 4 + src/bun.js/node/node.classes.ts | 74 ++ src/bun.js/rare_data.zig | 1 + src/bun_js.zig | 2 + src/cli.zig | 8 +- src/deps/c_ares.zig | 583 +++++++++-- src/dns.zig | 9 + src/js/node/dgram.ts | 2 +- src/js/node/dns.ts | 910 ++++++++++++----- test/js/node/dns/node-dns.test.js | 8 +- test/js/node/test/common/dns.js | 1 + .../test-dns-cancel-reverse-lookup.js | 28 + .../test-dns-channel-cancel-promise.js | 59 ++ .../test/parallel/test-dns-channel-cancel.js | 46 + .../test/parallel/test-dns-channel-timeout.js | 61 ++ .../parallel/test-dns-default-order-ipv4.js | 51 + .../parallel/test-dns-default-order-ipv6.js | 51 + .../test-dns-default-order-verbatim.js | 55 + .../node/test/parallel/test-dns-get-server.js | 11 + ...-dns-lookup-promises-options-deprecated.js | 44 + test/js/node/test/parallel/test-dns-lookup.js | 223 ++++ .../test-dns-lookupService-promises.js | 20 + .../test/parallel/test-dns-lookupService.js | 28 + .../test/parallel/test-dns-multi-channel.js | 52 + .../test/parallel/test-dns-promises-exists.js | 33 + .../parallel/test-dns-resolve-promises.js | 15 + .../test-dns-resolveany-bad-ancount.js | 55 + .../node/test/parallel/test-dns-resolveany.js | 69 ++ .../parallel/test-dns-resolvens-typeerror.js | 55 + .../parallel/test-dns-set-default-order.js | 108 ++ .../test/parallel/test-dns-setlocaladdress.js | 40 + .../test-dns-setserver-when-querying.js | 29 + .../test-dns-setservers-type-check.js | 117 +++ test/js/node/test/parallel/test-dns.js | 461 +++++++++ 45 files changed, 3806 insertions(+), 640 deletions(-) create mode 100644 test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js create mode 100644 test/js/node/test/parallel/test-dns-channel-cancel-promise.js create mode 100644 test/js/node/test/parallel/test-dns-channel-cancel.js create mode 100644 test/js/node/test/parallel/test-dns-channel-timeout.js create mode 100644 test/js/node/test/parallel/test-dns-default-order-ipv4.js create mode 100644 test/js/node/test/parallel/test-dns-default-order-ipv6.js create mode 100644 test/js/node/test/parallel/test-dns-default-order-verbatim.js create mode 100644 test/js/node/test/parallel/test-dns-get-server.js create mode 100644 test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js create mode 100644 test/js/node/test/parallel/test-dns-lookup.js create mode 100644 test/js/node/test/parallel/test-dns-lookupService-promises.js create mode 100644 test/js/node/test/parallel/test-dns-lookupService.js create mode 100644 test/js/node/test/parallel/test-dns-multi-channel.js create mode 100644 test/js/node/test/parallel/test-dns-promises-exists.js create mode 100644 test/js/node/test/parallel/test-dns-resolve-promises.js create mode 100644 test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js create mode 100644 test/js/node/test/parallel/test-dns-resolveany.js create mode 100644 test/js/node/test/parallel/test-dns-resolvens-typeerror.js create mode 100644 test/js/node/test/parallel/test-dns-set-default-order.js create mode 100644 test/js/node/test/parallel/test-dns-setlocaladdress.js create mode 100644 test/js/node/test/parallel/test-dns-setserver-when-querying.js create mode 100644 test/js/node/test/parallel/test-dns-setservers-type-check.js create mode 100644 test/js/node/test/parallel/test-dns.js diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index e4f66b59f9..e7ef78f9a9 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -1035,6 +1035,10 @@ declare module "bun" { errors: number; totalCount: number; }; + + ADDRCONFIG: number; + ALL: number; + V4MAPPED: number; }; interface DNSLookup { diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index c20fe83dd9..b530ba9483 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -49,6 +49,13 @@ const spawnTimeout = 5_000; const testTimeout = 3 * 60_000; const integrationTimeout = 5 * 60_000; +function getNodeParallelTestTimeout(testPath) { + if (testPath.includes("test-dns")) { + return 90_000; + } + return 10_000; +} + const { values: options, positionals: filters } = parseArgs({ allowPositionals: true, options: { @@ -251,7 +258,7 @@ async function runTests() { const { ok, error, stdout } = await spawnBun(execPath, { cwd: cwd, args: [title], - timeout: 10_000, + timeout: getNodeParallelTestTimeout(title), env: { FORCE_COLOR: "0", }, diff --git a/src/bun.js/api/Timer.zig b/src/bun.js/api/Timer.zig index ab532acabb..309cfdea2d 100644 --- a/src/bun.js/api/Timer.zig +++ b/src/bun.js/api/Timer.zig @@ -10,6 +10,7 @@ const Async = @import("async"); const uv = bun.windows.libuv; const StatWatcherScheduler = @import("../node/node_fs_stat_watcher.zig").StatWatcherScheduler; const Timer = @This(); +const DNSResolver = @import("./bun/dns_resolver.zig").DNSResolver; /// TimeoutMap is map of i32 to nullable Timeout structs /// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc. @@ -730,6 +731,7 @@ pub const EventLoopTimer = struct { TestRunner, StatWatcherScheduler, UpgradedDuplex, + DNSResolver, WindowsNamedPipe, PostgresSQLConnectionTimeout, PostgresSQLConnectionMaxLifetime, @@ -741,6 +743,7 @@ pub const EventLoopTimer = struct { .TestRunner => JSC.Jest.TestRunner, .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, + .DNSResolver => DNSResolver, .WindowsNamedPipe => uws.WindowsNamedPipe, .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, @@ -752,6 +755,7 @@ pub const EventLoopTimer = struct { TestRunner, StatWatcherScheduler, UpgradedDuplex, + DNSResolver, PostgresSQLConnectionTimeout, PostgresSQLConnectionMaxLifetime, @@ -762,6 +766,7 @@ pub const EventLoopTimer = struct { .TestRunner => JSC.Jest.TestRunner, .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, + .DNSResolver => DNSResolver, .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, }; @@ -841,6 +846,10 @@ pub const EventLoopTimer = struct { return .disarm; } + if (comptime t.Type() == DNSResolver) { + return container.checkTimeouts(now, vm); + } + return container.callback(container); }, } diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index 4151f720e6..1ae5ccd926 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -18,6 +18,8 @@ const Async = bun.Async; const GetAddrInfoAsyncCallback = fn (i32, ?*std.c.addrinfo, ?*anyopaque) callconv(.C) void; const INET6_ADDRSTRLEN = if (bun.Environment.isWindows) 65 else 46; const IANA_DNS_PORT = 53; +const EventLoopTimer = JSC.BunTimer.EventLoopTimer; +const timespec = bun.timespec; const LibInfo = struct { // static int32_t (*getaddrinfo_async_start)(mach_port_t*, @@ -79,7 +81,7 @@ const LibInfo = struct { var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native); if (cache == .inflight) { - var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch bun.outOfMemory(); + var dns_lookup = DNSLookup.init(this, globalThis, globalThis.allocator()) catch bun.outOfMemory(); cache.inflight.append(dns_lookup); @@ -99,7 +101,7 @@ const LibInfo = struct { query, globalThis, "pending_host_cache_native", - ) catch unreachable; + ) catch bun.outOfMemory(); const promise_value = request.head.promise.value(); const hints = query.options.toLibC(); @@ -132,6 +134,7 @@ const LibInfo = struct { bun.assert(rc == .result); poll.enableKeepingProcessAlive(this.vm.eventLoop()); + this.requestSent(globalThis.bunVM()); return promise_value; } @@ -146,7 +149,7 @@ const LibC = struct { var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native); if (cache == .inflight) { - var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch unreachable; + var dns_lookup = DNSLookup.init(this, globalThis, globalThis.allocator()) catch bun.outOfMemory(); cache.inflight.append(dns_lookup); @@ -157,21 +160,18 @@ const LibC = struct { var request = GetAddrInfoRequest.init( cache, - .{ - .libc = .{ - .query = query, - }, - }, + .{ .libc = .{ .query = query } }, this, query, globalThis, "pending_host_cache_native", - ) catch unreachable; + ) catch bun.outOfMemory(); const promise_value = request.head.promise.value(); - var io = GetAddrInfoRequest.Task.createOnJSThread(this.vm.allocator, globalThis, request) catch unreachable; + var io = GetAddrInfoRequest.Task.createOnJSThread(this.vm.allocator, globalThis, request) catch bun.outOfMemory(); io.schedule(); + this.requestSent(globalThis.bunVM()); return promise_value; } @@ -196,7 +196,7 @@ const LibUVBackend = struct { } }; - var holder = bun.default_allocator.create(Holder) catch unreachable; + var holder = bun.default_allocator.create(Holder) catch bun.outOfMemory(); holder.* = .{ .uv_info = uv_info, .task = undefined, @@ -211,7 +211,7 @@ const LibUVBackend = struct { var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native); if (cache == .inflight) { - var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch bun.outOfMemory(); + var dns_lookup = DNSLookup.init(this, globalThis, globalThis.allocator()) catch bun.outOfMemory(); cache.inflight.append(dns_lookup); @@ -305,10 +305,18 @@ pub fn ResolveInfoRequest(comptime cares_type: type, comptime type_name: []const const hash = hasher.final(); var poll_ref = Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); + if (resolver) |resolver_| resolver_.ref(); request.* = .{ .resolver_for_caching = resolver, .hash = hash, - .head = .{ .poll_ref = poll_ref, .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .allocated = false, .name = name }, + .head = .{ + .resolver = resolver, + .poll_ref = poll_ref, + .globalThis = globalThis, + .promise = JSC.JSPromise.Strong.init(globalThis), + .allocated = false, + .name = name, + }, }; request.tail = &request.head; if (cache == .new) { @@ -356,6 +364,7 @@ pub fn ResolveInfoRequest(comptime cares_type: type, comptime type_name: []const pub fn onCaresComplete(this: *@This(), err_: ?c_ares.Error, timeout: i32, result: ?*cares_type) void { if (this.resolver_for_caching) |resolver| { + defer resolver.requestCompleted(); if (this.cache.pending_cache) { resolver.drainPendingCares( this.cache.pos_in_pending, @@ -402,10 +411,18 @@ pub const GetHostByAddrInfoRequest = struct { const hash = hasher.final(); var poll_ref = Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); + if (resolver) |resolver_| resolver_.ref(); request.* = .{ .resolver_for_caching = resolver, .hash = hash, - .head = .{ .poll_ref = poll_ref, .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .allocated = false, .name = name }, + .head = .{ + .resolver = resolver, + .poll_ref = poll_ref, + .globalThis = globalThis, + .promise = JSC.JSPromise.Strong.init(globalThis), + .allocated = false, + .name = name, + }, }; request.tail = &request.head; if (cache == .new) { @@ -474,7 +491,7 @@ pub const GetHostByAddrInfoRequest = struct { pub const CAresNameInfo = struct { const log = Output.scoped(.CAresNameInfo, true); - globalThis: *JSC.JSGlobalObject = undefined, + globalThis: *JSC.JSGlobalObject, promise: JSC.JSPromise.Strong, poll_ref: bun.Async.KeepAlive, allocated: bool = false, @@ -485,22 +502,24 @@ pub const CAresNameInfo = struct { const this = try allocator.create(@This()); var poll_ref = bun.Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); - this.* = .{ .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .poll_ref = poll_ref, .allocated = true, .name = name }; + 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_nameinfo) void { if (err_) |err| { - var promise = this.promise; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, err.toJS(globalThis)); + err.toDeferred("getnameinfo", this.name, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } if (result == null) { - var promise = this.promise; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, c_ares.Error.ENOTFOUND.toJS(globalThis)); + c_ares.Error.ENOTFOUND.toDeferred("getnameinfo", this.name, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } @@ -523,8 +542,9 @@ pub const CAresNameInfo = struct { // freed bun.default_allocator.free(this.name); - if (this.allocated) + if (this.allocated) { this.globalThis.allocator().destroy(this); + } } }; @@ -603,6 +623,7 @@ pub const GetNameInfoRequest = struct { pub fn onCaresComplete(this: *@This(), err_: ?c_ares.Error, timeout: i32, result: ?c_ares.struct_nameinfo) void { if (this.resolver_for_caching) |resolver| { + defer resolver.requestCompleted(); if (this.cache.pending_cache) { resolver.drainPendingNameInfoCares( this.cache.pos_in_pending, @@ -644,11 +665,13 @@ pub const GetAddrInfoRequest = struct { var request = try globalThis.allocator().create(GetAddrInfoRequest); var poll_ref = Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); + if (resolver) |resolver_| resolver_.ref(); request.* = .{ .backend = backend, .resolver_for_caching = resolver, .hash = query.hash(), .head = .{ + .resolver = resolver, .globalThis = globalThis, .poll_ref = poll_ref, .promise = JSC.JSPromise.Strong.init(globalThis), @@ -774,7 +797,7 @@ pub const GetAddrInfoRequest = struct { // https://github.com/ziglang/zig/pull/14242 defer std.c.freeaddrinfo(addrinfo.?); - this.* = .{ .success = GetAddrInfo.Result.toList(default_allocator, addrinfo.?) catch unreachable }; + this.* = .{ .success = GetAddrInfo.Result.toList(default_allocator, addrinfo.?) catch bun.outOfMemory() }; } }, @@ -878,33 +901,41 @@ pub const GetAddrInfoRequest = struct { pub const CAresReverse = struct { const log = Output.scoped(.CAresReverse, false); - globalThis: *JSC.JSGlobalObject = undefined, + resolver: ?*DNSResolver, + globalThis: *JSC.JSGlobalObject, promise: JSC.JSPromise.Strong, poll_ref: Async.KeepAlive, allocated: bool = false, next: ?*@This() = null, name: []const u8, - pub fn init(globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator, name: []const u8) !*@This() { + pub fn init(resolver: ?*DNSResolver, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator, name: []const u8) !*@This() { + if (resolver) |resolver_| { + resolver_.ref(); + } + const this = try allocator.create(@This()); var poll_ref = Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); - this.* = .{ .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .poll_ref = poll_ref, .allocated = true, .name = name }; + this.* = .{ + .resolver = resolver, + .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; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, err.toJS(globalThis)); + err.toDeferred("getHostByAddr", this.name, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } if (result == null) { - var promise = this.promise; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, c_ares.Error.ENOTFOUND.toJS(globalThis)); + c_ares.Error.ENOTFOUND.toDeferred("getHostByAddr", this.name, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } @@ -919,6 +950,9 @@ pub const CAresReverse = struct { const globalThis = this.globalThis; this.promise = .{}; promise.resolveTask(globalThis, result); + if (this.resolver) |resolver| { + resolver.requestCompleted(); + } this.deinit(); } @@ -926,8 +960,13 @@ pub const CAresReverse = struct { this.poll_ref.unref(this.globalThis.bunVM()); bun.default_allocator.free(this.name); - if (this.allocated) + if (this.resolver) |resolver| { + resolver.deref(); + } + + if (this.allocated) { this.globalThis.allocator().destroy(this); + } } }; @@ -935,7 +974,8 @@ pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) ty return struct { const log = Output.scoped(.CAresLookup, true); - globalThis: *JSC.JSGlobalObject = undefined, + resolver: ?*DNSResolver, + globalThis: *JSC.JSGlobalObject, promise: JSC.JSPromise.Strong, poll_ref: Async.KeepAlive, allocated: bool = false, @@ -944,11 +984,16 @@ pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) ty pub usingnamespace bun.New(@This()); - pub fn init(globalThis: *JSC.JSGlobalObject, _: std.mem.Allocator, name: []const u8) !*@This() { + pub fn init(resolver: ?*DNSResolver, globalThis: *JSC.JSGlobalObject, _: std.mem.Allocator, name: []const u8) !*@This() { + if (resolver) |resolver_| { + resolver_.ref(); + } + var poll_ref = Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); return @This().new( .{ + .resolver = resolver, .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .poll_ref = poll_ref, @@ -959,17 +1004,15 @@ pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) ty } pub fn processResolve(this: *@This(), err_: ?c_ares.Error, _: i32, result: ?*cares_type) void { + const syscall = comptime "query" ++ &[_]u8{std.ascii.toUpper(type_name[0])} ++ type_name[1..]; + if (err_) |err| { - var promise = this.promise; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, err.toJS(globalThis)); + err.toDeferred(syscall, this.name, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } if (result == null) { - var promise = this.promise; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, c_ares.Error.ENOTFOUND.toJS(globalThis)); + c_ares.Error.ENOTFOUND.toDeferred(syscall, this.name, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } @@ -985,6 +1028,9 @@ pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) ty const globalThis = this.globalThis; this.promise = .{}; promise.resolveTask(globalThis, result); + if (this.resolver) |resolver| { + resolver.requestCompleted(); + } this.deinit(); } @@ -992,8 +1038,13 @@ pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) ty this.poll_ref.unref(this.globalThis.bunVM()); bun.default_allocator.free(this.name); - if (this.allocated) + if (this.resolver) |resolver| { + resolver.deref(); + } + + if (this.allocated) { this.destroy(); + } } }; } @@ -1001,20 +1052,23 @@ pub fn CAresLookup(comptime cares_type: type, comptime type_name: []const u8) ty pub const DNSLookup = struct { const log = Output.scoped(.DNSLookup, false); + resolver: ?*DNSResolver, globalThis: *JSC.JSGlobalObject = undefined, promise: JSC.JSPromise.Strong, allocated: bool = false, next: ?*DNSLookup = null, poll_ref: Async.KeepAlive, - pub fn init(globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) !*DNSLookup { + pub fn init(resolver: *DNSResolver, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) !*DNSLookup { log("init", .{}); + resolver.ref(); const this = try allocator.create(DNSLookup); var poll_ref = Async.KeepAlive.init(); poll_ref.ref(globalThis.bunVM()); this.* = .{ + .resolver = resolver, .globalThis = globalThis, .poll_ref = poll_ref, .promise = JSC.JSPromise.Strong.init(globalThis), @@ -1032,19 +1086,8 @@ pub const DNSLookup = struct { pub fn processGetAddrInfoNative(this: *DNSLookup, status: i32, result: ?*std.c.addrinfo) void { log("processGetAddrInfoNative: status={d}", .{status}); if (c_ares.Error.initEAI(status)) |err| { - var promise = this.promise; - const globalThis = this.globalThis; - - const error_value = brk: { - if (err == .ESERVFAIL) { - break :brk bun.sys.Error.fromCode(bun.C.getErrno(@as(c_int, -1)), .getaddrinfo).toJSC(globalThis); - } - - break :brk err.toJS(globalThis); - }; - + err.toDeferred("getaddrinfo", null, &this.promise).rejectLater(this.globalThis); this.deinit(); - promise.rejectTask(globalThis, error_value); return; } onCompleteNative(this, .{ .addrinfo = result }); @@ -1053,19 +1096,13 @@ pub const DNSLookup = struct { pub fn processGetAddrInfo(this: *DNSLookup, err_: ?c_ares.Error, _: i32, result: ?*c_ares.AddrInfo) void { log("processGetAddrInfo", .{}); if (err_) |err| { - var promise = this.promise; - const globalThis = this.globalThis; - promise.rejectTask(globalThis, err.toJS(globalThis)); + err.toDeferred("getaddrinfo", null, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } if (result == null or result.?.node == null) { - var promise = this.promise; - const globalThis = this.globalThis; - - const error_value = c_ares.Error.ENOTFOUND.toJS(globalThis); - promise.rejectTask(globalThis, error_value); + c_ares.Error.ENOTFOUND.toDeferred("getaddrinfo", null, &this.promise).rejectLater(this.globalThis); this.deinit(); return; } @@ -1086,6 +1123,9 @@ pub const DNSLookup = struct { this.promise = .{}; const globalThis = this.globalThis; promise.resolveTask(globalThis, result); + if (this.resolver) |resolver| { + resolver.requestCompleted(); + } this.deinit(); } @@ -1093,8 +1133,13 @@ pub const DNSLookup = struct { log("deinit", .{}); this.poll_ref.unref(this.globalThis.bunVM()); - if (this.allocated) + if (this.resolver) |resolver| { + resolver.deref(); + } + + if (this.allocated) { this.globalThis.allocator().destroy(this); + } } }; @@ -1102,7 +1147,7 @@ pub const GlobalData = struct { resolver: DNSResolver, pub fn init(allocator: std.mem.Allocator, vm: *JSC.VirtualMachine) *GlobalData { - const global = allocator.create(GlobalData) catch unreachable; + const global = allocator.create(GlobalData) catch bun.outOfMemory(); global.* = .{ .resolver = .{ .vm = vm, @@ -1730,7 +1775,14 @@ pub const DNSResolver = struct { channel: ?*c_ares.Channel = null, vm: *JSC.VirtualMachine, - polls: PollsMap = undefined, + polls: PollsMap, + options: c_ares.ChannelOptions = .{}, + + ref_count: u32 = 1, + event_loop_timer: EventLoopTimer = .{ + .next = .{}, + .tag = .DNSResolver, + }, pending_host_cache_cares: PendingCache = PendingCache.init(), pending_host_cache_native: PendingCache = PendingCache.init(), @@ -1743,9 +1795,15 @@ 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(), + pending_a_cache_cares: APendingCache = APendingCache.init(), + pending_aaaa_cache_cares: AAAAPendingCache = AAAAPendingCache.init(), + pending_any_cache_cares: AnyPendingCache = AnyPendingCache.init(), + pending_addr_cache_cares: AddrPendingCache = AddrPendingCache.init(), pending_nameinfo_cache_cares: NameInfoPendingCache = NameInfoPendingCache.init(), + pub usingnamespace JSC.Codegen.JSDNSResolver; + pub usingnamespace bun.NewRefCounted(@This(), deinit); + const PollsMap = std.AutoArrayHashMap(c_ares.ares_socket_t, *PollType); const PollType = if (Environment.isWindows) @@ -1759,14 +1817,64 @@ pub const DNSResolver = struct { poll: bun.windows.libuv.uv_poll_t, pub fn fromPoll(poll: *bun.windows.libuv.uv_poll_t) *UvDnsPoll { - const poll_bytes: [*]u8 = @ptrCast(poll); - const result: [*]u8 = poll_bytes - @offsetOf(UvDnsPoll, "poll"); - return @alignCast(@ptrCast(result)); + return @fieldParentPtr("poll", poll); } pub usingnamespace bun.New(@This()); }; + pub fn init(allocator: std.mem.Allocator, vm: *JSC.VirtualMachine) *DNSResolver { + log("init", .{}); + return DNSResolver.new(.{ + .vm = vm, + .polls = DNSResolver.PollsMap.init(allocator), + }); + } + + pub fn finalize(this: *DNSResolver) void { + this.deref(); + } + + pub fn deinit(this: *DNSResolver) void { + if (this.channel) |channel| { + channel.deinit(); + } + + this.destroy(); + } + + pub const Order = enum(u8) { + verbatim = 0, + ipv4first = 4, + ipv6first = 6, + + pub const default = .verbatim; + + pub const map = bun.ComptimeStringMap(Order, .{ + .{ "verbatim", .verbatim }, + .{ "ipv4first", .ipv4first }, + .{ "ipv6first", .ipv6first }, + .{ "0", .verbatim }, + .{ "4", .ipv4first }, + .{ "6", .ipv6first }, + }); + + pub fn toJS(this: Order, globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { + return JSC.ZigString.init(@tagName(this)).toJS(globalThis); + } + + pub fn fromString(order: []const u8) ?Order { + return Order.map.get(order); + } + + pub fn fromStringOrDie(order: []const u8) Order { + return fromString(order) orelse { + Output.prettyErrorln("error: Invalid DNS result order.", .{}); + Global.exit(1); + }; + } + }; + const PendingCache = bun.HiveArray(GetAddrInfoRequest.PendingCacheKey, 32); const SrvPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_ares_srv_reply, "srv").PendingCacheKey, 32); const SoaPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_ares_soa_reply, "soa").PendingCacheKey, 32); @@ -1777,9 +1885,82 @@ 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 APendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.hostent_with_ttls, "a").PendingCacheKey, 32); + const AAAAPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.hostent_with_ttls, "aaaa").PendingCacheKey, 32); + const AnyPendingCache = bun.HiveArray(ResolveInfoRequest(c_ares.struct_any_reply, "any").PendingCacheKey, 32); const AddrPendingCache = bun.HiveArray(GetHostByAddrInfoRequest.PendingCacheKey, 32); const NameInfoPendingCache = bun.HiveArray(GetNameInfoRequest.PendingCacheKey, 32); + pub fn checkTimeouts(this: *DNSResolver, now: *const timespec, vm: *JSC.VirtualMachine) EventLoopTimer.Arm { + defer { + vm.timer.incrementTimerRef(-1); + this.deref(); + } + + this.event_loop_timer.state = .PENDING; + + if (this.getChannelOrError(vm.global)) |channel| { + if (this.anyRequestsPending()) { + c_ares.ares_process_fd(channel, c_ares.ARES_SOCKET_BAD, c_ares.ARES_SOCKET_BAD); + if (this.addTimer(now)) { + return .{ .rearm = this.event_loop_timer.next }; + } + } + } else |_| {} + + return .disarm; + } + + fn anyRequestsPending(this: *DNSResolver) bool { + inline for (@typeInfo(DNSResolver).Struct.fields) |field| { + if (comptime std.mem.startsWith(u8, field.name, "pending_")) { + const set = &@field(this, field.name).available; + if (set.count() < set.capacity()) { + return true; + } + } + } + return false; + } + + fn requestSent(this: *DNSResolver, _: *JSC.VirtualMachine) void { + _ = this.addTimer(null); + } + + fn requestCompleted(this: *DNSResolver) void { + if (this.anyRequestsPending()) { + _ = this.addTimer(null); + } else { + this.removeTimer(); + } + } + + fn addTimer(this: *DNSResolver, now: ?*const timespec) bool { + if (this.event_loop_timer.state == .ACTIVE) { + return false; + } + + this.ref(); + this.event_loop_timer.next = (now orelse ×pec.now()).addMs(1000); + this.vm.timer.incrementTimerRef(1); + this.vm.timer.insert(&this.event_loop_timer); + return true; + } + + fn removeTimer(this: *DNSResolver) void { + if (this.event_loop_timer.state != .ACTIVE) { + return; + } + + // Normally checkTimeouts does this, so we have to be sure to do it ourself if we cancel the timer + defer { + this.vm.timer.incrementTimerRef(-1); + this.deref(); + } + + this.vm.timer.remove(&this.event_loop_timer); + } + fn getKey(this: *DNSResolver, index: u8, comptime cache_name: []const u8, comptime request_type: type) request_type.PendingCacheKey { var cache = &@field(this, cache_name); bun.assert(!cache.available.isSet(index)); @@ -1796,6 +1977,9 @@ pub const DNSResolver = struct { pub fn drainPendingCares(this: *DNSResolver, index: u8, err: ?c_ares.Error, timeout: i32, comptime request_type: type, comptime cares_type: type, comptime lookup_name: []const u8, result: ?*cares_type) void { const cache_name = comptime std.fmt.comptimePrint("pending_{s}_cache_cares", .{lookup_name}); + this.ref(); + defer this.deref(); + const key = this.getKey(index, cache_name, request_type); var addr = result orelse { @@ -1839,6 +2023,9 @@ pub const DNSResolver = struct { pub fn drainPendingHostCares(this: *DNSResolver, index: u8, err: ?c_ares.Error, timeout: i32, result: ?*c_ares.AddrInfo) void { const key = this.getKey(index, "pending_host_cache_cares", GetAddrInfoRequest); + this.ref(); + defer this.deref(); + var addr = result orelse { var pending: ?*DNSLookup = key.lookup.head.next; key.lookup.head.processGetAddrInfo(err, timeout, null); @@ -1882,6 +2069,9 @@ pub const DNSResolver = struct { log("drainPendingHostNative", .{}); const key = this.getKey(index, "pending_host_cache_native", GetAddrInfoRequest); + this.ref(); + defer this.deref(); + var array = result.toJS(globalObject) orelse { var pending: ?*DNSLookup = key.lookup.head.next; var head = key.lookup.head; @@ -1924,7 +2114,10 @@ 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); + const key = this.getKey(index, "pending_addr_cache_cares", GetHostByAddrInfoRequest); + + this.ref(); + defer this.deref(); var addr = result orelse { var pending: ?*CAresReverse = key.lookup.head.next; @@ -1969,6 +2162,9 @@ pub const DNSResolver = struct { pub fn drainPendingNameInfoCares(this: *DNSResolver, index: u8, err: ?c_ares.Error, timeout: i32, result: ?c_ares.struct_nameinfo) void { const key = this.getKey(index, "pending_nameinfo_cache_cares", GetNameInfoRequest); + this.ref(); + defer this.deref(); + var name_info = result orelse { var pending: ?*CAresNameInfo = key.lookup.head.next; key.lookup.head.processResolve(err, timeout, null); @@ -2084,7 +2280,7 @@ pub const DNSResolver = struct { }; pub fn getChannel(this: *DNSResolver) ChannelResult { if (this.channel == null) { - if (c_ares.Channel.init(DNSResolver, this)) |err| { + if (c_ares.Channel.init(DNSResolver, this, this.options)) |err| { return .{ .err = err }; } } @@ -2092,11 +2288,34 @@ pub const DNSResolver = struct { return .{ .result = this.channel.? }; } + fn getChannelFromVM(globalThis: *JSC.JSGlobalObject) bun.JSError!*c_ares.Channel { + var vm = globalThis.bunVM(); + var resolver = vm.rareData().globalDNSResolver(vm); + return resolver.getChannelOrError(globalThis); + } + + pub fn getChannelOrError(this: *DNSResolver, globalThis: *JSC.JSGlobalObject) bun.JSError!*c_ares.Channel { + switch (this.getChannel()) { + .result => |result| return result, + .err => |err| { + const system_error = JSC.SystemError{ + .errno = -1, + .code = bun.String.static(err.code()), + .message = bun.String.static(err.label()), + }; + + return globalThis.throwValue(system_error.toErrorInstance(globalThis)); + }, + } + } + pub fn onDNSPollUv(watcher: [*c]bun.windows.libuv.uv_poll_t, status: c_int, events: c_int) callconv(.C) void { const poll = UvDnsPoll.fromPoll(watcher); const vm = poll.parent.vm; vm.eventLoop().enter(); defer vm.eventLoop().exit(); + poll.parent.ref(); + defer poll.parent.deref(); // channel must be non-null here as c_ares must have been initialized if we're receiving callbacks const channel = poll.parent.channel.?; if (status < 0) { @@ -2130,6 +2349,9 @@ pub const DNSResolver = struct { return; }; + this.ref(); + defer this.deref(); + channel.process( poll.fd.int(), poll.isReadable(), @@ -2212,9 +2434,10 @@ pub const DNSResolver = struct { ttl: i32 = 0, }; - pub const RecordType = enum(u8) { + pub const RecordType = enum(c_int) { A = 1, AAAA = 28, + CAA = 257, CNAME = 5, MX = 15, NS = 2, @@ -2222,12 +2445,15 @@ pub const DNSResolver = struct { SOA = 6, SRV = 33, TXT = 16, + ANY = 255, pub const default = RecordType.A; pub const map = bun.ComptimeStringMap(RecordType, .{ .{ "A", .A }, .{ "AAAA", .AAAA }, + .{ "ANY", .ANY }, + .{ "CAA", .CAA }, .{ "CNAME", .CNAME }, .{ "MX", .MX }, .{ "NS", .NS }, @@ -2237,6 +2463,8 @@ pub const DNSResolver = struct { .{ "TXT", .TXT }, .{ "a", .A }, .{ "aaaa", .AAAA }, + .{ "any", .ANY }, + .{ "caa", .CAA }, .{ "cname", .CNAME }, .{ "mx", .MX }, .{ "ns", .NS }, @@ -2247,10 +2475,16 @@ pub const DNSResolver = struct { }); }; - pub fn resolve(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolve(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolve(globalThis, callframe); + } + + pub fn resolve(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(3); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolve", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolve", 3, arguments.len); } const record_type: RecordType = if (arguments.len == 1) @@ -2270,7 +2504,7 @@ pub const DNSResolver = struct { } break :brk RecordType.map.getWithEql(record_type_str.getZigString(globalThis), JSC.ZigString.eqlComptime) orelse { - return globalThis.throwInvalidArgumentType("resolve", "record", "one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT"); + return globalThis.throwInvalidArgumentType("resolve", "record", "one of: A, AAAA, CAA, CNAME, MX, NS, PTR, SOA, SRV, TXT"); }; }; @@ -2290,48 +2524,53 @@ pub const DNSResolver = struct { const name = name_str.toSliceClone(globalThis, bun.default_allocator); - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - //TODO: ANY CASE switch (record_type) { RecordType.A => { - defer name.deinit(); - const options = GetAddrInfo.Options{ .family = GetAddrInfo.Family.inet }; - return resolver.doLookup(name.slice(), 0, options, globalThis); + return this.doResolveCAres(c_ares.hostent_with_ttls, "a", name.slice(), globalThis); }, RecordType.AAAA => { - defer name.deinit(); - const options = GetAddrInfo.Options{ .family = GetAddrInfo.Family.inet6 }; - return resolver.doLookup(name.slice(), 0, options, globalThis); + return this.doResolveCAres(c_ares.hostent_with_ttls, "aaaa", name.slice(), globalThis); + }, + RecordType.ANY => { + return this.doResolveCAres(c_ares.struct_any_reply, "any", name.slice(), globalThis); + }, + RecordType.CAA => { + return this.doResolveCAres(c_ares.struct_ares_caa_reply, "caa", name.slice(), globalThis); }, RecordType.CNAME => { - return resolver.doResolveCAres(c_ares.struct_hostent, "cname", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_hostent, "cname", name.slice(), globalThis); }, RecordType.MX => { - return resolver.doResolveCAres(c_ares.struct_ares_mx_reply, "mx", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_mx_reply, "mx", name.slice(), globalThis); }, RecordType.NS => { - return resolver.doResolveCAres(c_ares.struct_hostent, "ns", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_hostent, "ns", name.slice(), globalThis); }, RecordType.PTR => { - return resolver.doResolveCAres(c_ares.struct_hostent, "ptr", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_hostent, "ptr", name.slice(), globalThis); }, RecordType.SOA => { - return resolver.doResolveCAres(c_ares.struct_ares_soa_reply, "soa", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_soa_reply, "soa", name.slice(), globalThis); }, RecordType.SRV => { - return resolver.doResolveCAres(c_ares.struct_ares_srv_reply, "srv", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_srv_reply, "srv", name.slice(), globalThis); }, RecordType.TXT => { - return resolver.doResolveCAres(c_ares.struct_ares_txt_reply, "txt", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_txt_reply, "txt", name.slice(), globalThis); }, } } - pub fn reverse(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalReverse(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.reverse(globalThis, callframe); + } + + pub fn reverse(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("reverse", 2, arguments.len); + return globalThis.throwNotEnoughArguments("reverse", 1, arguments.len); } const ip_value = arguments.ptr[0]; @@ -2348,35 +2587,32 @@ pub const DNSResolver = struct { const ip_slice = ip_str.toSliceClone(globalThis, bun.default_allocator); const ip = ip_slice.slice(); - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - var channel: *c_ares.Channel = switch (resolver.getChannel()) { + const channel: *c_ares.Channel = switch (this.getChannel()) { .result => |res| res, .err => |err| { - defer ip_slice.deinit(); - return globalThis.throwValue(err.toJS(globalThis)); + return globalThis.throwValue(err.toJSWithSyscallAndHostname(globalThis, "getHostByAddr", ip)); }, }; const key = GetHostByAddrInfoRequest.PendingCacheKey.init(ip); - var cache = resolver.getOrPutIntoResolvePendingCache( + var cache = this.getOrPutIntoResolvePendingCache( GetHostByAddrInfoRequest, key, - "pending_addr_cache_crares", + "pending_addr_cache_cares", ); if (cache == .inflight) { - var cares_reverse = CAresReverse.init(globalThis, globalThis.allocator(), ip) catch unreachable; + var cares_reverse = CAresReverse.init(this, globalThis, globalThis.allocator(), ip) catch bun.outOfMemory(); cache.inflight.append(cares_reverse); return cares_reverse.promise.value(); } var request = GetHostByAddrInfoRequest.init( cache, - resolver, + this, ip, globalThis, - "pending_addr_cache_crares", - ) catch unreachable; + "pending_addr_cache_cares", + ) catch bun.outOfMemory(); const promise = request.tail.promise.value(); channel.getHostByAddr( @@ -2386,10 +2622,11 @@ pub const DNSResolver = struct { GetHostByAddrInfoRequest.onCaresComplete, ); + this.requestSent(globalThis.bunVM()); return promise; } - pub fn lookup(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalLookup(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { return globalThis.throwNotEnoughArguments("lookup", 2, arguments.len); @@ -2413,14 +2650,17 @@ pub const DNSResolver = struct { var port: u16 = 0; if (arguments.len > 1 and arguments.ptr[1].isCell()) { - if (try arguments.ptr[1].get(globalThis, "port")) |port_value| { - if (port_value.isNumber()) { - port = port_value.to(u16); - } + const optionsObject = arguments.ptr[1]; + + if (try optionsObject.getTruthy(globalThis, "port")) |port_value| { + port = try port_value.toPortNumber(globalThis); } - options = GetAddrInfo.Options.fromJS(arguments.ptr[1], globalThis) catch |err| { - return globalThis.throw("Invalid options passed to lookup(): {s}", .{@errorName(err)}); + options = GetAddrInfo.Options.fromJS(optionsObject, globalThis) catch |err| { + return switch (err) { + error.InvalidFlags => globalThis.throwInvalidArgumentValue("flags", try optionsObject.getTruthy(globalThis, "flags") orelse .undefined), + else => globalThis.throw("Invalid options passed to lookup(): {s}", .{@errorName(err)}), + }; }; } @@ -2455,10 +2695,16 @@ pub const DNSResolver = struct { }; } - pub fn resolveSrv(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveSrv(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveSrv(globalThis, callframe); + } + + pub fn resolveSrv(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveSrv", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveSrv", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2476,17 +2722,19 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_ares_srv_reply, "srv", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_srv_reply, "srv", name.slice(), globalThis); } - pub fn resolveSoa(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveSoa(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveSoa(globalThis, callframe); + } + + pub fn resolveSoa(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveSoa", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveSoa", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2500,17 +2748,19 @@ pub const DNSResolver = struct { }; const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_ares_soa_reply, "soa", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_soa_reply, "soa", name.slice(), globalThis); } - pub fn resolveCaa(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveCaa(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveCaa(globalThis, callframe); + } + + pub fn resolveCaa(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveCaa", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveCaa", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2528,17 +2778,19 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_ares_caa_reply, "caa", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_caa_reply, "caa", name.slice(), globalThis); } - pub fn resolveNs(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveNs(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveNs(globalThis, callframe); + } + + pub fn resolveNs(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveNs", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveNs", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2552,17 +2804,19 @@ pub const DNSResolver = struct { }; const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_hostent, "ns", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_hostent, "ns", name.slice(), globalThis); } - pub fn resolvePtr(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolvePtr(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolvePtr(globalThis, callframe); + } + + pub fn resolvePtr(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolvePtr", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolvePtr", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2580,17 +2834,19 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_hostent, "ptr", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_hostent, "ptr", name.slice(), globalThis); } - pub fn resolveCname(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveCname(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveCname(globalThis, callframe); + } + + pub fn resolveCname(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveCname", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveCname", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2608,17 +2864,19 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_hostent, "cname", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_hostent, "cname", name.slice(), globalThis); } - pub fn resolveMx(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveMx(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveMx(globalThis, callframe); + } + + pub fn resolveMx(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveMx", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveMx", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2636,17 +2894,19 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_ares_mx_reply, "mx", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_mx_reply, "mx", name.slice(), globalThis); } - pub fn resolveNaptr(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + pub fn globalResolveNaptr(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveNaptr(globalThis, callframe); + } + + pub fn resolveNaptr(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const arguments = callframe.arguments_old(2); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveNaptr", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveNaptr", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2664,17 +2924,19 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - - return resolver.doResolveCAres(c_ares.struct_ares_naptr_reply, "naptr", name.slice(), globalThis); + return this.doResolveCAres(c_ares.struct_ares_naptr_reply, "naptr", name.slice(), globalThis); } - pub fn resolveTxt(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(2); + pub fn globalResolveTxt(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveTxt(globalThis, callframe); + } + + pub fn resolveTxt(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(1); if (arguments.len < 1) { - return globalThis.throwNotEnoughArguments("resolveTxt", 2, arguments.len); + return globalThis.throwNotEnoughArguments("resolveTxt", 1, arguments.len); } const name_value = arguments.ptr[0]; @@ -2692,18 +2954,44 @@ pub const DNSResolver = struct { } const name = name_str.toSliceClone(globalThis, bun.default_allocator); + return this.doResolveCAres(c_ares.struct_ares_txt_reply, "txt", name.slice(), globalThis); + } - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); + pub fn globalResolveAny(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const vm = globalThis.bunVM(); + const resolver = vm.rareData().globalDNSResolver(vm); + return resolver.resolveAny(globalThis, callframe); + } - return resolver.doResolveCAres(c_ares.struct_ares_txt_reply, "txt", name.slice(), globalThis); + pub fn resolveAny(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(1); + if (arguments.len < 1) { + return globalThis.throwNotEnoughArguments("resolveAny", 1, arguments.len); + } + + const name_value = arguments.ptr[0]; + + if (name_value.isEmptyOrUndefinedOrNull() or !name_value.isString()) { + return globalThis.throwInvalidArgumentType("resolveAny", "hostname", "string"); + } + + const name_str = name_value.toStringOrNull(globalThis) orelse { + return .zero; + }; + + if (name_str.length() == 0) { + return globalThis.throwInvalidArgumentType("resolveAny", "hostname", "non-empty string"); + } + + const name = name_str.toSliceClone(globalThis, bun.default_allocator); + return this.doResolveCAres(c_ares.struct_any_reply, "any", name.slice(), globalThis); } pub fn doResolveCAres(this: *DNSResolver, comptime cares_type: type, comptime type_name: []const u8, name: []const u8, globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { var channel: *c_ares.Channel = switch (this.getChannel()) { .result => |res| res, .err => |err| { - return globalThis.throwValue(err.toJS(globalThis)); + return globalThis.throwValue(err.toJSWithSyscall(globalThis, "query" ++ &[_]u8{std.ascii.toUpper(type_name[0])} ++ type_name[1..])); }, }; @@ -2714,7 +3002,7 @@ pub const DNSResolver = struct { var cache = this.getOrPutIntoResolvePendingCache(ResolveInfoRequest(cares_type, type_name), key, cache_name); if (cache == .inflight) { // CAresLookup will have the name ownership - var cares_lookup = CAresLookup(cares_type, type_name).init(globalThis, globalThis.allocator(), name) catch unreachable; + var cares_lookup = CAresLookup(cares_type, type_name).init(this, globalThis, globalThis.allocator(), name) catch bun.outOfMemory(); cache.inflight.append(cares_lookup); return cares_lookup.promise.value(); } @@ -2725,7 +3013,7 @@ pub const DNSResolver = struct { name, // CAresLookup will have the ownership globalThis, cache_name, - ) catch unreachable; + ) catch bun.outOfMemory(); const promise = request.tail.promise.value(); channel.resolve( @@ -2737,19 +3025,24 @@ pub const DNSResolver = struct { ResolveInfoRequest(cares_type, type_name).onCaresComplete, ); + this.requestSent(globalThis.bunVM()); return promise; } pub fn c_aresLookupWithNormalizedName(this: *DNSResolver, query: GetAddrInfo, globalThis: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { var channel: *c_ares.Channel = switch (this.getChannel()) { .result => |res| res, .err => |err| { + const syscall = bun.String.createAtomASCII(query.name); + defer syscall.deref(); + const system_error = JSC.SystemError{ .errno = -1, .code = bun.String.static(err.code()), .message = bun.String.static(err.label()), + .syscall = syscall, }; - return globalThis.throwValue(system_error.toErrorInstance(globalThis)) catch .zero; + return globalThis.throwValue(system_error.toErrorInstance(globalThis)); }, }; @@ -2757,7 +3050,7 @@ pub const DNSResolver = struct { var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_cares); if (cache == .inflight) { - var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch unreachable; + var dns_lookup = DNSLookup.init(this, globalThis, globalThis.allocator()) catch bun.outOfMemory(); cache.inflight.append(dns_lookup); return dns_lookup.promise.value(); } @@ -2765,14 +3058,12 @@ pub const DNSResolver = struct { const hints_buf = &[_]c_ares.AddrInfo_hints{query.toCAres()}; var request = GetAddrInfoRequest.init( cache, - .{ - .c_ares = {}, - }, + .{ .c_ares = {} }, this, query, globalThis, "pending_host_cache_cares", - ) catch unreachable; + ) catch bun.outOfMemory(); const promise = request.tail.promise.value(); channel.getAddrInfo( @@ -2784,27 +3075,12 @@ pub const DNSResolver = struct { GetAddrInfoRequest.onCaresComplete, ); + this.requestSent(globalThis.bunVM()); return promise; } - pub fn getServers(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + fn getChannelServers(channel: *c_ares.Channel, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { _ = callframe; - - var vm = globalThis.bunVM(); - var resolver = vm.rareData().globalDNSResolver(vm); - const 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()), - }; - - return globalThis.throwValue(system_error.toErrorInstance(globalThis)); - }, - }; - var servers: ?*c_ares.struct_ares_addr_port_node = null; const r = c_ares.ares_get_servers_ports(channel, &servers); if (r != c_ares.ARES_SUCCESS) { @@ -2864,17 +3140,197 @@ pub const DNSResolver = struct { return values; } + pub fn getGlobalServers(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return getChannelServers(try getChannelFromVM(globalThis), globalThis, callframe); + } + + pub fn getServers(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return getChannelServers(try this.getChannelOrError(globalThis), globalThis, callframe); + } + + pub fn setLocalAddress(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return setChannelLocalAddresses(try this.getChannelOrError(globalThis), globalThis, callframe); + } + + fn setChannelLocalAddresses(channel: *c_ares.Channel, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments(); + if (arguments.len == 0) { + return globalThis.throwNotEnoughArguments("setLocalAddress", 1, 0); + } + + const first_af = try setChannelLocalAddress(channel, globalThis, arguments[0]); + + if (arguments.len < 2 or arguments[1].isUndefined()) { + return .undefined; + } + + const second_af = try setChannelLocalAddress(channel, globalThis, arguments[1]); + + if (first_af != second_af) { + return .undefined; + } + + switch (first_af) { + c_ares.AF.INET => return globalThis.throwInvalidArguments("Cannot specify two IPv4 addresses.", .{}), + c_ares.AF.INET6 => return globalThis.throwInvalidArguments("Cannot specify two IPv6 addresses.", .{}), + else => unreachable, + } + } + + fn setChannelLocalAddress(channel: *c_ares.Channel, globalThis: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!c_int { + const str = try value.toBunString2(globalThis); + defer str.deref(); + + const slice = str.toSlice(bun.default_allocator).slice(); + var buffer = bun.default_allocator.alloc(u8, slice.len + 1) catch bun.outOfMemory(); + defer bun.default_allocator.free(buffer); + _ = strings.copy(buffer[0..], slice); + buffer[slice.len] = 0; + + var addr: [16]u8 = undefined; + + if (c_ares.ares_inet_pton(c_ares.AF.INET, buffer.ptr, &addr) == 1) { + const ip = std.mem.readInt(u32, addr[0..4], .big); + c_ares.ares_set_local_ip4(channel, ip); + return c_ares.AF.INET; + } + + if (c_ares.ares_inet_pton(c_ares.AF.INET6, buffer.ptr, &addr) == 1) { + c_ares.ares_set_local_ip6(channel, &addr); + return c_ares.AF.INET6; + } + + return JSC.Error.ERR_INVALID_IP_ADDRESS.throw(globalThis, "Invalid IP address: \"{s}\"", .{slice}); + } + + fn setChannelServers(channel: *c_ares.Channel, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + // It's okay to call dns.setServers with active queries, but not dns.Resolver.setServers + if (channel != try getChannelFromVM(globalThis) and c_ares.ares_queue_active_queries(channel) != 0) { + return globalThis.ERR_DNS_SET_SERVERS_FAILED("Failed to set servers: there are pending queries", .{}).throw(); + } + + const arguments = callframe.arguments(); + if (arguments.len == 0) { + return globalThis.throwNotEnoughArguments("setServers", 1, 0); + } + + const argument = arguments[0]; + if (!argument.isArray()) { + return globalThis.throwInvalidArgumentType("setServers", "servers", "array"); + } + + var triplesIterator = argument.arrayIterator(globalThis); + + if (triplesIterator.len == 0) { + const r = c_ares.ares_set_servers_ports(channel, null); + if (r != c_ares.ARES_SUCCESS) { + const err = c_ares.Error.get(r).?; + return globalThis.throwValue(globalThis.createErrorInstance("ares_set_servers_ports error: {s}", .{err.label()})); + } + return .undefined; + } + + const allocator = bun.default_allocator; + + const entries = allocator.alloc(c_ares.struct_ares_addr_port_node, triplesIterator.len) catch bun.outOfMemory(); + defer allocator.free(entries); + + var i: u32 = 0; + + while (triplesIterator.next()) |triple| : (i += 1) { + if (!triple.isArray()) { + return globalThis.throwInvalidArgumentType("setServers", "triple", "array"); + } + + const family = JSValue.getIndex(triple, globalThis, 0).toInt32(); + const port = JSValue.getIndex(triple, globalThis, 2).toInt32(); + + if (family != 4 and family != 6) { + return globalThis.throwInvalidArguments("Invalid address family", .{}); + } + + const addressString = try JSValue.getIndex(triple, globalThis, 1).toBunString2(globalThis); + defer addressString.deref(); + + const addressSlice = try addressString.toOwnedSlice(allocator); + defer allocator.free(addressSlice); + + var addressBuffer = allocator.alloc(u8, addressSlice.len + 1) catch bun.outOfMemory(); + defer allocator.free(addressBuffer); + + _ = strings.copy(addressBuffer[0..], addressSlice); + addressBuffer[addressSlice.len] = 0; + + const af: c_int = if (family == 4) std.posix.AF.INET else std.posix.AF.INET6; + + entries[i] = .{ + .next = null, + .family = af, + .addr = undefined, + .udp_port = port, + .tcp_port = port, + }; + + if (c_ares.ares_inet_pton(af, addressBuffer.ptr, &entries[i].addr) != 1) { + return JSC.Error.ERR_INVALID_IP_ADDRESS.throw(globalThis, "Invalid IP address: \"{s}\"", .{addressSlice}); + } + + if (i > 0) { + entries[i - 1].next = &entries[i]; + } + } + + const r = c_ares.ares_set_servers_ports(channel, entries.ptr); + if (r != c_ares.ARES_SUCCESS) { + const err = c_ares.Error.get(r).?; + return globalThis.throwValue(globalThis.createErrorInstance("ares_set_servers_ports error: {s}", .{err.label()})); + } + + return .undefined; + } + + pub fn setGlobalServers(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return setChannelServers(try getChannelFromVM(globalThis), globalThis, callframe); + } + + pub fn setServers(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return setChannelServers(try this.getChannelOrError(globalThis), globalThis, callframe); + } + + pub fn newResolver(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const resolver = DNSResolver.init(globalThis.allocator(), globalThis.bunVM()); + + const options = callframe.argument(0); + if (options.isObject()) { + if (try options.getTruthy(globalThis, "timeout")) |timeout| { + resolver.options.timeout = timeout.coerceToInt32(globalThis); + } + + if (try options.getTruthy(globalThis, "tries")) |tries| { + resolver.options.tries = tries.coerceToInt32(globalThis); + } + } + + return resolver.toJS(globalThis); + } + + pub fn cancel(this: *DNSResolver, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + _ = callframe; + const channel = try this.getChannelOrError(globalThis); + c_ares.ares_cancel(channel); + return .undefined; + } + // Resolves the given address and port into a host name and service using the operating system's underlying getnameinfo implementation. // If address is not a valid IP address, a TypeError will be thrown. The port will be coerced to a number. // If it is not a legal port, a TypeError will be thrown. - pub fn lookupService(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(3); + pub fn globalLookupService(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const arguments = callframe.arguments_old(2); if (arguments.len < 2) { - return globalThis.throwNotEnoughArguments("lookupService", 3, arguments.len); + return globalThis.throwNotEnoughArguments("lookupService", 2, arguments.len); } const addr_value = arguments.ptr[0]; - const port_value = arguments.ptr[1]; if (addr_value.isEmptyOrUndefinedOrNull() or !addr_value.isString()) { return globalThis.throwInvalidArgumentType("lookupService", "address", "string"); } @@ -2884,36 +3340,22 @@ pub const DNSResolver = struct { if (addr_str.length() == 0) { return globalThis.throwInvalidArgumentType("lookupService", "address", "non-empty string"); } - const addr_s = addr_str.getZigString(globalThis).slice(); - const port: u16 = if (port_value.isNumber()) blk: { - break :blk port_value.to(u16); - } else { - return globalThis.throwInvalidArgumentType("lookupService", "port", "invalid port"); - }; + + const port_value = arguments.ptr[1]; + const port: u16 = try port_value.toPortNumber(globalThis); var sa: std.posix.sockaddr.storage = std.mem.zeroes(std.posix.sockaddr.storage); if (c_ares.getSockaddr(addr_s, port, @as(*std.posix.sockaddr, @ptrCast(&sa))) != 0) { - return globalThis.throwInvalidArgumentType("lookupService", "address", "invalid address"); + return globalThis.throwInvalidArgumentValue("address", addr_value); } 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()), - }; - - return globalThis.throwValue(system_error.toErrorInstance(globalThis)); - }, - }; + var channel = try resolver.getChannelOrError(globalThis); // This string will be freed in `CAresNameInfo.deinit` - const cache_name = std.fmt.allocPrint(bun.default_allocator, "{s}|{d}", .{ addr_s, port }) catch unreachable; + const cache_name = std.fmt.allocPrint(bun.default_allocator, "{s}|{d}", .{ addr_s, port }) catch bun.outOfMemory(); const key = GetNameInfoRequest.PendingCacheKey.init(cache_name); var cache = resolver.getOrPutIntoResolvePendingCache( @@ -2923,7 +3365,7 @@ pub const DNSResolver = struct { ); if (cache == .inflight) { - var info = CAresNameInfo.init(globalThis, globalThis.allocator(), cache_name) catch unreachable; + var info = CAresNameInfo.init(globalThis, globalThis.allocator(), cache_name) catch bun.outOfMemory(); cache.inflight.append(info); return info.promise.value(); } @@ -2934,7 +3376,7 @@ pub const DNSResolver = struct { cache_name, // transfer ownership here globalThis, "pending_nameinfo_cache_cares", - ) catch unreachable; + ) catch bun.outOfMemory(); const promise = request.tail.promise.value(); channel.getNameInfo( @@ -2944,41 +3386,50 @@ pub const DNSResolver = struct { GetNameInfoRequest.onCaresComplete, ); + resolver.requestSent(globalThis.bunVM()); return promise; } + pub fn getRuntimeDefaultResultOrderOption(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return globalThis.bunVM().dns_result_order.toJS(globalThis); + } + comptime { - const js_resolve = JSC.toJSHostFunction(resolve); - @export(js_resolve, .{ .name = "Bun__DNSResolver__resolve" }); - const js_lookup = JSC.toJSHostFunction(lookup); - @export(js_lookup, .{ .name = "Bun__DNSResolver__lookup" }); - const js_resolveTxt = JSC.toJSHostFunction(resolveTxt); - @export(js_resolveTxt, .{ .name = "Bun__DNSResolver__resolveTxt" }); - const js_resolveSoa = JSC.toJSHostFunction(resolveSoa); - @export(js_resolveSoa, .{ .name = "Bun__DNSResolver__resolveSoa" }); - const js_resolveMx = JSC.toJSHostFunction(resolveMx); - @export(js_resolveMx, .{ .name = "Bun__DNSResolver__resolveMx" }); - const js_resolveNaptr = JSC.toJSHostFunction(resolveNaptr); - @export(js_resolveNaptr, .{ .name = "Bun__DNSResolver__resolveNaptr" }); - const js_resolveSrv = JSC.toJSHostFunction(resolveSrv); - @export(js_resolveSrv, .{ .name = "Bun__DNSResolver__resolveSrv" }); - const js_resolveCaa = JSC.toJSHostFunction(resolveCaa); - @export(js_resolveCaa, .{ .name = "Bun__DNSResolver__resolveCaa" }); - const js_resolveNs = JSC.toJSHostFunction(resolveNs); - @export(js_resolveNs, .{ .name = "Bun__DNSResolver__resolveNs" }); - const js_resolvePtr = JSC.toJSHostFunction(resolvePtr); - @export(js_resolvePtr, .{ .name = "Bun__DNSResolver__resolvePtr" }); - const js_resolveCname = JSC.toJSHostFunction(resolveCname); - @export(js_resolveCname, .{ .name = "Bun__DNSResolver__resolveCname" }); - const js_getServers = JSC.toJSHostFunction(getServers); - @export(js_getServers, .{ .name = "Bun__DNSResolver__getServers" }); - const js_reverse = JSC.toJSHostFunction(reverse); - @export(js_reverse, .{ .name = "Bun__DNSResolver__reverse" }); - const js_lookupService = JSC.toJSHostFunction(lookupService); - @export(js_lookupService, .{ .name = "Bun__DNSResolver__lookupService" }); + const js_resolve = JSC.toJSHostFunction(globalResolve); + @export(js_resolve, .{ .name = "Bun__DNS__resolve" }); + const js_lookup = JSC.toJSHostFunction(globalLookup); + @export(js_lookup, .{ .name = "Bun__DNS__lookup" }); + const js_resolveTxt = JSC.toJSHostFunction(globalResolveTxt); + @export(js_resolveTxt, .{ .name = "Bun__DNS__resolveTxt" }); + const js_resolveSoa = JSC.toJSHostFunction(globalResolveSoa); + @export(js_resolveSoa, .{ .name = "Bun__DNS__resolveSoa" }); + const js_resolveMx = JSC.toJSHostFunction(globalResolveMx); + @export(js_resolveMx, .{ .name = "Bun__DNS__resolveMx" }); + const js_resolveNaptr = JSC.toJSHostFunction(globalResolveNaptr); + @export(js_resolveNaptr, .{ .name = "Bun__DNS__resolveNaptr" }); + const js_resolveSrv = JSC.toJSHostFunction(globalResolveSrv); + @export(js_resolveSrv, .{ .name = "Bun__DNS__resolveSrv" }); + const js_resolveCaa = JSC.toJSHostFunction(globalResolveCaa); + @export(js_resolveCaa, .{ .name = "Bun__DNS__resolveCaa" }); + const js_resolveNs = JSC.toJSHostFunction(globalResolveNs); + @export(js_resolveNs, .{ .name = "Bun__DNS__resolveNs" }); + const js_resolvePtr = JSC.toJSHostFunction(globalResolvePtr); + @export(js_resolvePtr, .{ .name = "Bun__DNS__resolvePtr" }); + const js_resolveCname = JSC.toJSHostFunction(globalResolveCname); + @export(js_resolveCname, .{ .name = "Bun__DNS__resolveCname" }); + const js_resolveAny = JSC.toJSHostFunction(globalResolveAny); + @export(js_resolveAny, .{ .name = "Bun__DNS__resolveAny" }); + const js_getGlobalServers = JSC.toJSHostFunction(getGlobalServers); + @export(js_getGlobalServers, .{ .name = "Bun__DNS__getServers" }); + const js_setGlobalServers = JSC.toJSHostFunction(setGlobalServers); + @export(js_setGlobalServers, .{ .name = "Bun__DNS__setServers" }); + const js_reverse = JSC.toJSHostFunction(globalReverse); + @export(js_reverse, .{ .name = "Bun__DNS__reverse" }); + const js_lookupService = JSC.toJSHostFunction(globalLookupService); + @export(js_lookupService, .{ .name = "Bun__DNS__lookupService" }); const js_prefetchFromJS = JSC.toJSHostFunction(InternalDNS.prefetchFromJS); - @export(js_prefetchFromJS, .{ .name = "Bun__DNSResolver__prefetch" }); + @export(js_prefetchFromJS, .{ .name = "Bun__DNS__prefetch" }); const js_getDNSCacheStats = JSC.toJSHostFunction(InternalDNS.getDNSCacheStats); - @export(js_getDNSCacheStats, .{ .name = "Bun__DNSResolver__getCacheStats" }); + @export(js_getDNSCacheStats, .{ .name = "Bun__DNS__getCacheStats" }); } }; diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index 6a812c8355..2dc702a59a 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -36,25 +36,34 @@ #include "BunObject+exports.h" #include "ErrorCode.h" #include "GeneratedBunObject.h" - #include "JavaScriptCore/BunV8HeapSnapshotBuilder.h" -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__lookup); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolve); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveSrv); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveTxt); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveSoa); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveNaptr); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveMx); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveCaa); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveNs); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolvePtr); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__resolveCname); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__getServers); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__reverse); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__lookupService); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__prefetch); -BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__getCacheStats); +#ifdef WIN32 +#include +#else +#include +#endif + +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__lookup); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolve); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveSrv); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveTxt); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveSoa); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveNaptr); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveMx); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveCaa); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveNs); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolvePtr); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveCname); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__resolveAny); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__getServers); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__setServers); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__reverse); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__lookupService); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__prefetch); +BUN_DECLARE_HOST_FUNCTION(Bun__DNS__getCacheStats); +BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__new); +BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__cancel); BUN_DECLARE_HOST_FUNCTION(Bun__fetch); BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect); BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7); @@ -340,37 +349,47 @@ static JSValue constructDNSObject(VM& vm, JSObject* bunObject) { JSGlobalObject* globalObject = bunObject->globalObject(); JSC::JSObject* dnsObject = JSC::constructEmptyObject(globalObject); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "lookup"_s), 2, Bun__DNSResolver__lookup, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "lookup"_s), 2, Bun__DNS__lookup, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, vm.propertyNames->resolve, 2, Bun__DNSResolver__resolve, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, vm.propertyNames->resolve, 2, Bun__DNS__resolve, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveSrv"_s), 2, Bun__DNSResolver__resolveSrv, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveSrv"_s), 2, Bun__DNS__resolveSrv, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveTxt"_s), 2, Bun__DNSResolver__resolveTxt, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveTxt"_s), 2, Bun__DNS__resolveTxt, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveSoa"_s), 2, Bun__DNSResolver__resolveSoa, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveSoa"_s), 2, Bun__DNS__resolveSoa, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveNaptr"_s), 2, Bun__DNSResolver__resolveNaptr, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveNaptr"_s), 2, Bun__DNS__resolveNaptr, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveMx"_s), 2, Bun__DNSResolver__resolveMx, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveMx"_s), 2, Bun__DNS__resolveMx, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveCaa"_s), 2, Bun__DNSResolver__resolveCaa, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveCaa"_s), 2, Bun__DNS__resolveCaa, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveNs"_s), 2, Bun__DNSResolver__resolveNs, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveNs"_s), 2, Bun__DNS__resolveNs, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolvePtr"_s), 2, Bun__DNSResolver__resolvePtr, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolvePtr"_s), 2, Bun__DNS__resolvePtr, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveCname"_s), 2, Bun__DNSResolver__resolveCname, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveCname"_s), 2, Bun__DNS__resolveCname, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getServers"_s), 2, Bun__DNSResolver__getServers, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "resolveAny"_s), 2, Bun__DNS__resolveAny, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "reverse"_s), 2, Bun__DNSResolver__reverse, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getServers"_s), 2, Bun__DNS__getServers, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "lookupService"_s), 2, Bun__DNSResolver__lookupService, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "setServers"_s), 2, Bun__DNS__setServers, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "prefetch"_s), 2, Bun__DNSResolver__prefetch, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "reverse"_s), 2, Bun__DNS__reverse, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | 0); - dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getCacheStats"_s), 0, Bun__DNSResolver__getCacheStats, ImplementationVisibility::Public, NoIntrinsic, + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "lookupService"_s), 2, Bun__DNS__lookupService, ImplementationVisibility::Public, NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "prefetch"_s), 2, Bun__DNS__prefetch, ImplementationVisibility::Public, NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + dnsObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getCacheStats"_s), 0, Bun__DNS__getCacheStats, ImplementationVisibility::Public, NoIntrinsic, + JSC::PropertyAttribute::DontDelete | 0); + dnsObject->putDirect(vm, JSC::Identifier::fromString(vm, "ADDRCONFIG"_s), jsNumber(AI_ADDRCONFIG), + JSC::PropertyAttribute::DontDelete | 0); + dnsObject->putDirect(vm, JSC::Identifier::fromString(vm, "ALL"_s), jsNumber(AI_ALL), + JSC::PropertyAttribute::DontDelete | 0); + dnsObject->putDirect(vm, JSC::Identifier::fromString(vm, "V4MAPPED"_s), jsNumber(AI_V4MAPPED), JSC::PropertyAttribute::DontDelete | 0); return dnsObject; } diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 1e5d8e720c..8fd7698a68 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -823,6 +823,15 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSValue::encode(ERR_INVALID_ARG_TYPE(scope, globalObject, arg0, arg1, arg2)); } + case Bun::ErrorCode::ERR_INVALID_IP_ADDRESS: { + JSValue arg0 = callFrame->argument(1); + + auto param = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + return JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_IP_ADDRESS, makeString("Invalid IP address: "_s, param))); + } + case Bun::ErrorCode::ERR_INVALID_ARG_VALUE: { JSValue arg0 = callFrame->argument(1); JSValue arg1 = callFrame->argument(2); diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 3c7b465a24..0a859c2417 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -49,6 +49,7 @@ export default [ ["ERR_STREAM_RELEASE_LOCK", Error, "AbortError"], ["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError, "TypeError"], ["ERR_INVALID_URI", URIError, "URIError"], + ["ERR_INVALID_IP_ADDRESS", TypeError, "TypeError"], ["ERR_SCRIPT_EXECUTION_TIMEOUT", Error, "Error"], ["ERR_SCRIPT_EXECUTION_INTERRUPTED", Error, "Error"], ["ERR_UNHANDLED_ERROR", Error], @@ -73,6 +74,9 @@ export default [ // Console ["ERR_CONSOLE_WRITABLE_STREAM", TypeError, "TypeError"], + // DNS + ["ERR_DNS_SET_SERVERS_FAILED", Error], + // NET ["ERR_SOCKET_CLOSED_BEFORE_CONNECTION", Error], ["ERR_SOCKET_CLOSED", Error], diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 7638c302ef..bce7203c73 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1978,6 +1978,12 @@ JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC::PropertyAttribute::DontDelete | 0); } + if (err.hostname.tag != BunStringTag::Empty) { + JSC::JSValue hostname = Bun::toJS(globalObject, err.hostname); + result->putDirect(vm, names.hostnamePublicName(), hostname, + JSC::PropertyAttribute::DontDelete | 0); + } + result->putDirect(vm, names.errnoPublicName(), JSC::JSValue(err.errno_), JSC::PropertyAttribute::DontDelete | 0); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index c27c7432e8..a2ed61e0f9 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1731,6 +1731,7 @@ pub const SystemError = extern struct { message: String = String.empty, path: String = String.empty, syscall: String = String.empty, + hostname: String = String.empty, fd: bun.FileDescriptor = bun.toFD(-1), dest: String = String.empty, @@ -1756,6 +1757,7 @@ pub const SystemError = extern struct { this.code.deref(); this.message.deref(); this.syscall.deref(); + this.hostname.deref(); this.dest.deref(); } @@ -1764,17 +1766,11 @@ pub const SystemError = extern struct { this.code.ref(); this.message.ref(); this.syscall.ref(); - this.dest.ref(); + this.hostname.ref(); } pub fn toErrorInstance(this: *const SystemError, global: *JSGlobalObject) JSValue { - defer { - this.path.deref(); - this.code.deref(); - this.message.deref(); - this.syscall.deref(); - this.dest.deref(); - } + defer this.deref(); return shim.cppFn("toErrorInstance", .{ this, global }); } @@ -2965,6 +2961,15 @@ pub const JSGlobalObject = opaque { return this.throwValue(this.createInvalidArgumentType(name_, field, typename)); } + pub fn throwInvalidArgumentValue( + this: *JSGlobalObject, + argname: []const u8, + value: JSValue, + ) bun.JSError { + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = this }; + return this.ERR_INVALID_ARG_VALUE("The \"{s}\" argument is invalid. Received {}", .{ argname, value.toFmt(&formatter) }).throw(); + } + pub fn throwInvalidArgumentTypeValue( this: *JSGlobalObject, argname: []const u8, @@ -4052,6 +4057,25 @@ pub const JSValue = enum(i64) { }; } + pub fn toPortNumber(this: JSValue, global: *JSGlobalObject) bun.JSError!u16 { + if (this.isNumber()) { + // const double = try this.toNumber(global); + const double = this.coerceToDouble(global); + if (std.math.isNan(double)) { + return JSC.Error.ERR_SOCKET_BAD_PORT.throw(global, "Invalid port number", .{}); + } + + const port = this.to(i64); + if (0 <= port and port <= 65535) { + return @as(u16, @truncate(@max(0, port))); + } else { + return JSC.Error.ERR_SOCKET_BAD_PORT.throw(global, "Port number out of range: {d}", .{port}); + } + } + + return JSC.Error.ERR_SOCKET_BAD_PORT.throw(global, "Invalid port number", .{}); + } + pub fn isInstanceOf(this: JSValue, global: *JSGlobalObject, constructor: JSValue) bool { if (!this.isCell()) return false; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 995ddb4466..2f6acfc87e 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -77,6 +77,7 @@ pub const Classes = struct { pub const NativeZlib = JSC.API.NativeZlib; pub const NativeBrotli = JSC.API.NativeBrotli; pub const FrameworkFileSystemRouter = bun.bake.FrameworkRouter.JSFrameworkRouter; + pub const DNSResolver = JSC.DNS.DNSResolver; pub const S3Client = JSC.WebCore.S3Client; pub const S3Stat = JSC.WebCore.S3Stat; diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 97080b4802..01c5c1a978 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -124,6 +124,7 @@ typedef struct SystemError { BunString message; BunString path; BunString syscall; + BunString hostname; int fd; BunString dest; } SystemError; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 13b08f72bd..41d5c1ba2f 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -83,6 +83,7 @@ const PendingResolution = @import("../resolver/resolver.zig").PendingResolution; const ThreadSafeFunction = JSC.napi.ThreadSafeFunction; const PackageManager = @import("../install/install.zig").PackageManager; const IPC = @import("ipc.zig"); +const DNSResolver = @import("api/bun/dns_resolver.zig").DNSResolver; pub const GenericWatcher = @import("../watcher.zig"); const ModuleLoader = JSC.ModuleLoader; @@ -790,6 +791,7 @@ pub const VirtualMachine = struct { unhandled_pending_rejection_to_capture: ?*JSC.JSValue = null, standalone_module_graph: ?*bun.StandaloneModuleGraph = null, smol: bool = false, + dns_result_order: DNSResolver.Order = .verbatim, hot_reload: bun.CLI.Command.HotReload = .none, jsc: *JSC.VM = undefined, @@ -1977,6 +1979,7 @@ pub const VirtualMachine = struct { env_loader: ?*DotEnv.Loader = null, store_fd: bool = false, smol: bool = false, + dns_result_order: DNSResolver.Order = .verbatim, // --print needs the result from evaluating the main module eval: bool = false, @@ -2070,6 +2073,7 @@ pub const VirtualMachine = struct { vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); vm.smol = opts.smol; + vm.dns_result_order = opts.dns_result_order; if (opts.smol) is_smol_mode = opts.smol; diff --git a/src/bun.js/node/node.classes.ts b/src/bun.js/node/node.classes.ts index c252f72954..3a8d426c6b 100644 --- a/src/bun.js/node/node.classes.ts +++ b/src/bun.js/node/node.classes.ts @@ -1,6 +1,80 @@ import { define } from "../../codegen/class-definitions"; export default [ + define({ + name: "DNSResolver", + construct: false, + noConstructor: true, + finalize: true, + configurable: false, + klass: {}, + proto: { + setServers: { + fn: "setServers", + length: 1, + }, + getServers: { + fn: "getServers", + length: 0, + }, + resolve: { + fn: "resolve", + length: 3, + }, + resolveSrv: { + fn: "resolveSrv", + length: 1, + }, + resolveTxt: { + fn: "resolveTxt", + length: 1, + }, + resolveSoa: { + fn: "resolveSoa", + length: 1, + }, + resolveNaptr: { + fn: "resolveNaptr", + length: 1, + }, + resolveMx: { + fn: "resolveMx", + length: 1, + }, + resolveCaa: { + fn: "resolveCaa", + length: 1, + }, + resolveNs: { + fn: "resolveNs", + length: 1, + }, + resolvePtr: { + fn: "resolvePtr", + length: 1, + }, + resolveCname: { + fn: "resolveCname", + length: 1, + }, + resolveAny: { + fn: "resolveAny", + length: 1, + }, + setLocalAddress: { + fn: "setLocalAddress", + length: 1, + }, + cancel: { + fn: "cancel", + length: 0, + }, + reverse: { + fn: "reverse", + length: 1, + }, + }, + }), define({ name: "FSWatcher", construct: false, diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 3cc1880157..e49925e148 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -422,6 +422,7 @@ pub fn spawnIPCContext(rare: *RareData, vm: *JSC.VirtualMachine) *uws.SocketCont pub fn globalDNSResolver(rare: *RareData, vm: *JSC.VirtualMachine) *JSC.DNS.DNSResolver { if (rare.global_dns_data == null) { rare.global_dns_data = JSC.DNS.GlobalData.init(vm.allocator, vm); + rare.global_dns_data.?.resolver.ref(); // live forever } return &rare.global_dns_data.?.resolver; diff --git a/src/bun_js.zig b/src/bun_js.zig index dae0636554..3c25404f89 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -30,6 +30,7 @@ const which = @import("which.zig").which; const JSC = bun.JSC; const AsyncHTTP = bun.http.AsyncHTTP; const Arena = @import("./allocators/mimalloc_arena.zig").Arena; +const DNSResolver = @import("bun.js/api/bun/dns_resolver.zig").DNSResolver; const OpaqueWrap = JSC.OpaqueWrap; const VirtualMachine = JSC.VirtualMachine; @@ -199,6 +200,7 @@ pub const Run = struct { .smol = ctx.runtime_options.smol, .eval = ctx.runtime_options.eval.eval_and_print, .debugger = ctx.runtime_options.debugger, + .dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order), .is_main_thread = true, }, ), diff --git a/src/cli.zig b/src/cli.zig index ba9d1561fa..49bbc29484 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -236,7 +236,8 @@ pub const Arguments = struct { clap.parseParam("--conditions ... Pass custom conditions to resolve") catch unreachable, clap.parseParam("--fetch-preconnect ... Preconnect to a URL while code is loading") catch unreachable, clap.parseParam("--max-http-header-size Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable, - clap.parseParam("--expose-internals Expose internals used for testing Bun itself. Usage of these APIs are completely unsupported.") catch unreachable, + clap.parseParam("--expose-internals Expose internals used for testing Bun itself. Usage of these APIs is completely unsupported.") catch unreachable, + clap.parseParam("--dns-result-order Set the default order of DNS lookup results. Valid orders: verbatim (default), ipv4first, ipv6first") catch unreachable, clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable, clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable, clap.parseParam("--throw-deprecation Determine whether or not deprecation warnings result in errors.") catch unreachable, @@ -765,6 +766,10 @@ pub const Arguments = struct { ctx.runtime_options.preconnect = args.options("--fetch-preconnect"); ctx.runtime_options.expose_gc = args.flag("--expose-gc"); + if (args.option("--dns-result-order")) |order| { + ctx.runtime_options.dns_result_order = order; + } + if (args.option("--inspect")) |inspect_flag| { ctx.runtime_options.debugger = if (inspect_flag.len == 0) Command.Debugger{ .enable = .{} } @@ -1498,6 +1503,7 @@ pub const Command = struct { eval_and_print: bool = false, } = .{}, preconnect: []const []const u8 = &[_][]const u8{}, + dns_result_order: []const u8 = "verbatim", /// `--expose-gc` makes `globalThis.gc()` available. Added for Node /// compatibility. expose_gc: bool = false, diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index 4ea5141427..09c365a34c 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -192,47 +192,46 @@ pub const Options = extern struct { evsys: ares_evsys_t = 0, server_failover_opts: struct_ares_server_failover_options = @import("std").mem.zeroes(struct_ares_server_failover_options), }; + pub const struct_hostent = extern struct { - h_name: [*c]u8, - h_aliases: [*c][*c]u8, - h_addrtype: c_int, - h_length: c_int, - h_addr_list: [*c][*c]u8, + h_name: ?[*:0]u8, + h_aliases: ?[*:null]?[*:0]u8, + h_addrtype: hostent_int, + h_length: hostent_int, + h_addr_list: ?[*:null]?[*:0]u8, + + // hostent in glibc uses int for h_addrtype and h_length, whereas hostent in winsock2.h uses short. + const hostent_int = if (bun.Environment.isWindows) c_short else c_int; pub fn toJSResponse(this: *struct_hostent, _: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime lookup_name: []const u8) JSC.JSValue { - - // A cname lookup always returns a single record but we follow the common API here. if (comptime strings.eqlComptime(lookup_name, "cname")) { - if (this.h_name != null) { - const array = JSC.JSValue.createEmptyArray(globalThis, 1); - const h_name_len = bun.len(this.h_name); - const h_name_slice = this.h_name[0..h_name_len]; - array.putIndex(globalThis, 0, JSC.ZigString.fromUTF8(h_name_slice).toJS(globalThis)); - return array; - } - return JSC.JSValue.createEmptyArray(globalThis, 0); - } else { - if (this.h_aliases == null) { + // A cname lookup always returns a single record but we follow the common API here. + if (this.h_name == null) { return JSC.JSValue.createEmptyArray(globalThis, 0); } - - var count: u32 = 0; - while (this.h_aliases[count] != null) { - count += 1; - } - - const array = JSC.JSValue.createEmptyArray(globalThis, count); - count = 0; - - while (this.h_aliases[count]) |alias| { - const alias_len = bun.len(alias); - const alias_slice = alias[0..alias_len]; - array.putIndex(globalThis, count, JSC.ZigString.fromUTF8(alias_slice).toJS(globalThis)); - count += 1; - } - - return array; + return bun.String.toJSArray(globalThis, &[_]bun.String{bun.String.fromUTF8(this.h_name.?[0..bun.len(this.h_name.?)])}); } + + if (this.h_aliases == null) { + return JSC.JSValue.createEmptyArray(globalThis, 0); + } + + var count: u32 = 0; + while (this.h_aliases.?[count] != null) { + count += 1; + } + + const array = JSC.JSValue.createEmptyArray(globalThis, count); + count = 0; + + while (this.h_aliases.?[count]) |alias| { + const alias_len = bun.len(alias); + const alias_slice = alias[0..alias_len]; + array.putIndex(globalThis, count, JSC.ZigString.fromUTF8(alias_slice).toJS(globalThis)); + count += 1; + } + + return array; } pub fn Callback(comptime Type: type) type { @@ -269,7 +268,17 @@ pub const struct_hostent = extern struct { } var start: [*c]struct_hostent = undefined; - if (comptime strings.eqlComptime(lookup_name, "ns")) { + if (comptime strings.eqlComptime(lookup_name, "cname")) { + var addrttls: [256]struct_ares_addrttl = undefined; + var naddrttls: i32 = 256; + + const result = ares_parse_a_reply(buffer, buffer_length, &start, &addrttls, &naddrttls); + if (result != ARES_SUCCESS) { + function(this, Error.get(result), timeouts, null); + return; + } + function(this, null, timeouts, start); + } else if (comptime strings.eqlComptime(lookup_name, "ns")) { const result = ares_parse_ns_reply(buffer, buffer_length, &start); if (result != ARES_SUCCESS) { function(this, Error.get(result), timeouts, null); @@ -283,16 +292,8 @@ pub const struct_hostent = extern struct { return; } function(this, null, timeouts, start); - } else if (comptime strings.eqlComptime(lookup_name, "cname")) { - var addrttls: [256]struct_ares_addrttl = undefined; - var naddrttls: i32 = 256; - - const result = ares_parse_a_reply(buffer, buffer_length, &start, &addrttls, &naddrttls); - if (result != ARES_SUCCESS) { - function(this, Error.get(result), timeouts, null); - return; - } - function(this, null, timeouts, start); + } else { + @compileError(std.fmt.comptimePrint("Unsupported struct_hostent record type: {s}", .{lookup_name})); } } }.handle; @@ -303,6 +304,129 @@ pub const struct_hostent = extern struct { } }; +pub const hostent_with_ttls = struct { + hostent: *struct_hostent, + ttls: [256]c_int = [_]c_int{-1} ** 256, + + pub fn toJSResponse(this: *hostent_with_ttls, _: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime lookup_name: []const u8) JSC.JSValue { + if (comptime strings.eqlComptime(lookup_name, "a") or strings.eqlComptime(lookup_name, "aaaa")) { + if (this.hostent.h_addr_list == null) { + return JSC.JSValue.createEmptyArray(globalThis, 0); + } + + var count: u32 = 0; + while (this.hostent.h_addr_list.?[count] != null) { + count += 1; + } + + const array = JSC.JSValue.createEmptyArray(globalThis, count); + count = 0; + + const addressKey = JSC.ZigString.static("address").withEncoding(); + const ttlKey = JSC.ZigString.static("ttl").withEncoding(); + + while (this.hostent.h_addr_list.?[count]) |addr| : (count += 1) { + const addrString = (if (this.hostent.h_addrtype == AF.INET6) + bun.dns.addressToJS(&std.net.Address.initIp6(addr[0..16].*, 0, 0, 0), globalThis) + else + bun.dns.addressToJS(&std.net.Address.initIp4(addr[0..4].*, 0), globalThis)) catch return globalThis.throwOutOfMemoryValue(); + + const ttl: ?c_int = if (count < this.ttls.len) this.ttls[count] else null; + const resultObject = JSC.JSValue.createObject2(globalThis, &addressKey, &ttlKey, addrString, if (ttl) |val| JSC.jsNumber(val) else .undefined); + array.putIndex(globalThis, count, resultObject); + } + + return array; + } else { + @compileError(std.fmt.comptimePrint("Unsupported hostent_with_ttls record type: {s}", .{lookup_name})); + } + } + + pub fn Callback(comptime Type: type) type { + return fn (*Type, status: ?Error, timeouts: i32, results: ?*hostent_with_ttls) 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: ?*hostent_with_ttls) callconv(.C) void { + const 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, + comptime function: Callback(Type), + ) ares_callback { + return &struct { + pub fn handle(ctx: ?*anyopaque, status: c_int, timeouts: c_int, buffer: [*c]u8, buffer_length: c_int) callconv(.C) void { + const this = bun.cast(*Type, ctx.?); + if (status != ARES_SUCCESS) { + function(this, Error.get(status), timeouts, null); + return; + } + + switch (parse(lookup_name, buffer, buffer_length)) { + .result => |result| function(this, null, timeouts, result), + .err => |err| function(this, err, timeouts, null), + } + } + }.handle; + } + + pub fn parse(comptime lookup_name: []const u8, buffer: [*c]u8, buffer_length: c_int) JSC.Node.Maybe(*hostent_with_ttls, Error) { + var start: ?*struct_hostent = null; + + if (comptime strings.eqlComptime(lookup_name, "a")) { + var addrttls: [256]struct_ares_addrttl = undefined; + var naddrttls: c_int = 256; + + const result = ares_parse_a_reply(buffer, buffer_length, &start, &addrttls, &naddrttls); + if (result != ARES_SUCCESS) { + return .{ .err = Error.get(result).? }; + } + var with_ttls = bun.default_allocator.create(hostent_with_ttls) catch bun.outOfMemory(); + with_ttls.hostent = start.?; + for (addrttls[0..@intCast(naddrttls)], 0..) |ttl, i| { + with_ttls.ttls[i] = ttl.ttl; + } + return .{ .result = with_ttls }; + } + + if (comptime strings.eqlComptime(lookup_name, "aaaa")) { + var addr6ttls: [256]struct_ares_addr6ttl = undefined; + var naddr6ttls: c_int = 256; + + const result = ares_parse_aaaa_reply(buffer, buffer_length, &start, &addr6ttls, &naddr6ttls); + if (result != ARES_SUCCESS) { + return .{ .err = Error.get(result).? }; + } + var with_ttls = bun.default_allocator.create(hostent_with_ttls) catch bun.outOfMemory(); + with_ttls.hostent = start.?; + for (addr6ttls[0..@intCast(naddr6ttls)], 0..) |ttl, i| { + with_ttls.ttls[i] = ttl.ttl; + } + return .{ .result = with_ttls }; + } + + @compileError(std.fmt.comptimePrint("Unsupported hostent_with_ttls record type: {s}", .{lookup_name})); + } + + pub fn deinit(this: *hostent_with_ttls) void { + this.hostent.deinit(); + bun.default_allocator.destroy(this); + } +}; + pub const struct_nameinfo = extern struct { node: [*c]u8, service: [*c]u8, @@ -351,7 +475,7 @@ pub const struct_nameinfo = extern struct { } }; -pub const struct_timeval = opaque {}; +pub const struct_timeval = std.posix.timeval; pub const struct_Channeldata = opaque {}; pub const AddrInfo_cname = extern struct { ttl: c_int, @@ -389,10 +513,7 @@ pub const AddrInfo = extern struct { globalThis: *JSC.JSGlobalObject, ) JSC.JSValue { var node = addr_info.node orelse return JSC.JSValue.createEmptyArray(globalThis, 0); - const array = JSC.JSValue.createEmptyArray( - globalThis, - node.count(), - ); + const array = JSC.JSValue.createEmptyArray(globalThis, node.count()); { var j: u32 = 0; @@ -462,8 +583,13 @@ pub const AddrInfo_hints = extern struct { } }; +pub const ChannelOptions = struct { + timeout: ?i32 = null, + tries: ?i32 = null, +}; + pub const Channel = opaque { - pub fn init(comptime Container: type, this: *Container) ?Error { + pub fn init(comptime Container: type, this: *Container, options: ChannelOptions) ?Error { var channel: *Channel = undefined; libraryInit(); @@ -483,8 +609,8 @@ pub const Channel = opaque { opts.flags = ARES_FLAG_NOCHECKRESP; opts.sock_state_cb = &SockStateWrap.onSockState; opts.sock_state_cb_data = @as(*anyopaque, @ptrCast(this)); - opts.timeout = -1; - opts.tries = 4; + opts.timeout = options.timeout orelse -1; + opts.tries = options.tries orelse 4; const optmask: c_int = ARES_OPT_FLAGS | ARES_OPT_TIMEOUTMS | @@ -499,6 +625,10 @@ pub const Channel = opaque { return null; } + pub fn deinit(this: *Channel) void { + ares_destroy(this); + } + /// ///The ares_getaddrinfo function initiates a host query by name on the name service channel identified by channel. The name and service parameters give the hostname and service as NULL-terminated C strings. The hints parameter is an ares_addrinfo_hints structure: /// @@ -719,6 +849,7 @@ pub extern fn ares_create_query(name: [*c]const u8, dnsclass: c_int, @"type": c_ pub extern fn ares_mkquery(name: [*c]const u8, dnsclass: c_int, @"type": c_int, id: c_ushort, rd: c_int, buf: [*c][*c]u8, buflen: [*c]c_int) c_int; pub extern fn ares_expand_name(encoded: [*c]const u8, abuf: [*c]const u8, alen: c_int, s: [*c][*c]u8, enclen: [*c]c_long) c_int; pub extern fn ares_expand_string(encoded: [*c]const u8, abuf: [*c]const u8, alen: c_int, s: [*c][*c]u8, enclen: [*c]c_long) c_int; +pub extern fn ares_queue_active_queries(channel: *const Channel) usize; const union_unnamed_2 = extern union { _S6_u8: [16]u8, }; @@ -726,7 +857,7 @@ pub const struct_ares_in6_addr = extern struct { _S6_un: union_unnamed_2, }; pub const struct_ares_addrttl = extern struct { - ipaddr: struct_in_addr, + ipaddr: u32, ttl: c_int, }; pub const struct_ares_addr6ttl = extern struct { @@ -1009,6 +1140,28 @@ pub const struct_ares_txt_reply = extern struct { return array; } + pub fn toJSForAny(this: *struct_ares_txt_reply, _: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime _: []const u8) JSC.JSValue { + var count: usize = 0; + var txt: ?*struct_ares_txt_reply = this; + while (txt != null) : (txt = txt.?.next) { + count += 1; + } + + const array = JSC.JSValue.createEmptyArray(globalThis, count); + + txt = this; + var i: u32 = 0; + while (txt != null) : (txt = txt.?.next) { + var node = txt.?; + array.putIndex(globalThis, i, JSC.ZigString.fromUTF8(node.txt[0..node.length]).toJS(globalThis)); + i += 1; + } + + return JSC.JSObject.create(.{ + .entries = array, + }, globalThis).toJS(); + } + pub fn Callback(comptime Type: type) type { return fn (*Type, status: ?Error, timeouts: i32, results: ?*struct_ares_txt_reply) void; } @@ -1218,6 +1371,207 @@ pub const struct_ares_uri_reply = extern struct { uri: [*c]u8, ttl: c_int, }; + +pub const struct_any_reply = struct { + a_reply: ?*hostent_with_ttls = null, + aaaa_reply: ?*hostent_with_ttls = null, + mx_reply: ?*struct_ares_mx_reply = null, + ns_reply: ?*struct_hostent = null, + txt_reply: ?*struct_ares_txt_reply = null, + srv_reply: ?*struct_ares_srv_reply = null, + ptr_reply: ?*struct_hostent = null, + naptr_reply: ?*struct_ares_naptr_reply = null, + soa_reply: ?*struct_ares_soa_reply = null, + caa_reply: ?*struct_ares_caa_reply = null, + + pub fn toJSResponse(this: *struct_any_reply, parent_allocator: std.mem.Allocator, globalThis: *JSC.JSGlobalObject, comptime _: []const u8) JSC.JSValue { + var stack = std.heap.stackFallback(2048, parent_allocator); + var arena = bun.ArenaAllocator.init(stack.get()); + defer arena.deinit(); + + const allocator = arena.allocator(); + + return this.toJS(globalThis, allocator); + } + + fn append(globalThis: *JSC.JSGlobalObject, array: JSC.JSValue, i: *u32, response: JSC.JSValue, comptime lookup_name: []const u8) void { + const transformed = if (response.isString()) + JSC.JSObject.create(.{ + .value = response, + }, globalThis).toJS() + else blk: { + bun.assert(response.isObject()); + break :blk response; + }; + + var upper = comptime lookup_name[0..lookup_name.len].*; + inline for (&upper) |*char| { + char.* = std.ascii.toUpper(char.*); + } + + transformed.put(globalThis, "type", bun.String.ascii(&upper).toJS(globalThis)); + array.putIndex(globalThis, i.*, transformed); + i.* += 1; + } + + fn appendAll(globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator, array: JSC.JSValue, i: *u32, reply: anytype, comptime lookup_name: []const u8) void { + const response: JSC.JSValue = if (comptime @hasDecl(@TypeOf(reply.*), "toJSForAny")) + reply.toJSForAny(allocator, globalThis, lookup_name) + else + reply.toJSResponse(allocator, globalThis, lookup_name); + + if (response.isArray()) { + var iterator = response.arrayIterator(globalThis); + while (iterator.next()) |item| { + append(globalThis, array, i, item, lookup_name); + } + } else { + append(globalThis, array, i, response, lookup_name); + } + } + + pub fn toJS(this: *struct_any_reply, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) JSC.JSValue { + const array = JSC.JSValue.createEmptyArray(globalThis, blk: { + var len: usize = 0; + inline for (comptime @typeInfo(struct_any_reply).Struct.fields) |field| { + if (comptime std.mem.endsWith(u8, field.name, "_reply")) { + len += @intFromBool(@field(this, field.name) != null); + } + } + break :blk len; + }); + + var i: u32 = 0; + + inline for (comptime @typeInfo(struct_any_reply).Struct.fields) |field| { + if (comptime std.mem.endsWith(u8, field.name, "_reply")) { + if (@field(this, field.name)) |reply| { + const lookup_name = comptime field.name[0 .. field.name.len - "_reply".len]; + appendAll(globalThis, allocator, array, &i, reply, lookup_name); + } + } + } + + return array; + } + + pub fn Callback(comptime Type: type) type { + return fn (*Type, status: ?Error, timeouts: i32, results: ?*struct_any_reply) void; + } + + pub fn callbackWrapper( + comptime _: []const u8, + comptime Type: type, + comptime function: Callback(Type), + ) ares_callback { + return &struct { + pub fn handleAny(ctx: ?*anyopaque, status: c_int, timeouts: c_int, buffer: [*c]u8, buffer_length: c_int) callconv(.C) void { + const this = bun.cast(*Type, ctx.?); + if (status != ARES_SUCCESS) { + function(this, Error.get(status), timeouts, null); + return; + } + + var any_success = false; + var last_error: ?c_int = null; + var reply = bun.default_allocator.create(struct_any_reply) catch bun.outOfMemory(); + reply.* = .{}; + + switch (hostent_with_ttls.parse("a", buffer, buffer_length)) { + .result => |result| { + reply.a_reply = result; + any_success = true; + }, + .err => |err| last_error = @intFromEnum(err), + } + + switch (hostent_with_ttls.parse("aaaa", buffer, buffer_length)) { + .result => |result| { + reply.aaaa_reply = result; + any_success = true; + }, + .err => |err| last_error = @intFromEnum(err), + } + + var result = ares_parse_mx_reply(buffer, buffer_length, &reply.mx_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_ns_reply(buffer, buffer_length, &reply.ns_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_txt_reply(buffer, buffer_length, &reply.txt_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_srv_reply(buffer, buffer_length, &reply.srv_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_ptr_reply(buffer, buffer_length, null, 0, AF.INET, &reply.ptr_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_naptr_reply(buffer, buffer_length, &reply.naptr_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_soa_reply(buffer, buffer_length, &reply.soa_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + result = ares_parse_caa_reply(buffer, buffer_length, &reply.caa_reply); + if (result == ARES_SUCCESS) { + any_success = true; + } else { + last_error = result; + } + + if (!any_success) { + reply.deinit(); + function(this, Error.get(last_error.?), timeouts, null); + return; + } + + function(this, null, timeouts, reply); + } + }.handleAny; + } + + pub fn deinit(this: *struct_any_reply) void { + inline for (@typeInfo(struct_any_reply).Struct.fields) |field| { + if (comptime std.mem.endsWith(u8, field.name, "_reply")) { + if (@field(this, field.name)) |reply| { + reply.deinit(); + } + } + } + + bun.default_allocator.destroy(this); + } +}; pub extern fn ares_parse_a_reply(abuf: [*c]const u8, alen: c_int, host: [*c]?*struct_hostent, addrttls: [*c]struct_ares_addrttl, naddrttls: [*c]c_int) c_int; pub extern fn ares_parse_aaaa_reply(abuf: [*c]const u8, alen: c_int, host: [*c]?*struct_hostent, addrttls: [*c]struct_ares_addr6ttl, naddrttls: [*c]c_int) c_int; pub extern fn ares_parse_caa_reply(abuf: [*c]const u8, alen: c_int, caa_out: [*c][*c]struct_ares_caa_reply) c_int; @@ -1288,6 +1642,7 @@ pub const ARES_ELOADIPHLPAPI = 22; pub const ARES_EADDRGETNETWORKPARAMS = 23; pub const ARES_ECANCELLED = 24; pub const ARES_ESERVICE = 25; +pub const ARES_ENOSERVER = 26; pub const Error = enum(i32) { ENODATA = ARES_ENODATA, @@ -1315,25 +1670,105 @@ pub const Error = enum(i32) { EADDRGETNETWORKPARAMS = ARES_EADDRGETNETWORKPARAMS, ECANCELLED = ARES_ECANCELLED, ESERVICE = ARES_ESERVICE, + ENOSERVER = ARES_ENOSERVER, + + const Deferred = struct { + errno: Error, + syscall: []const u8, + hostname: ?bun.String, + promise: JSC.JSPromise.Strong, + + pub usingnamespace bun.New(@This()); + + pub fn init(errno: Error, syscall: []const u8, hostname: ?bun.String, promise: JSC.JSPromise.Strong) *Deferred { + return Deferred.new(.{ + .errno = errno, + .syscall = syscall, + .hostname = hostname, + .promise = promise, + }); + } + + pub fn reject(this: *Deferred, globalThis: *JSC.JSGlobalObject) void { + const system_error = JSC.SystemError{ + .errno = @intFromEnum(this.errno), + .code = bun.String.static(this.errno.code()), + .message = if (this.hostname) |hostname| bun.String.createFormat("{s} {s} {s}", .{ this.syscall, this.errno.code()[4..], hostname }) catch bun.outOfMemory() else bun.String.empty, + .syscall = bun.String.createUTF8(this.syscall), + .hostname = this.hostname orelse bun.String.empty, + }; + + const instance = system_error.toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + + this.promise.reject(globalThis, instance); + this.hostname = null; + this.deinit(); + } + + pub fn rejectLater(this: *Deferred, globalThis: *JSC.JSGlobalObject) void { + const Context = struct { + deferred: *Deferred, + globalThis: *JSC.JSGlobalObject, + pub fn callback(context: *@This()) void { + context.deferred.reject(context.globalThis); + } + }; + + const context = bun.default_allocator.create(Context) catch bun.outOfMemory(); + context.deferred = this; + context.globalThis = globalThis; + // TODO(@heimskr): new custom Task type + globalThis.bunVM().enqueueTask(JSC.ManagedTask.New(Context, Context.callback).init(context)); + } + + pub fn deinit(this: *@This()) void { + if (this.hostname) |hostname| { + hostname.deref(); + } + this.promise.deinit(); + this.destroy(); + } + }; + + pub fn toDeferred(this: Error, syscall: []const u8, hostname: ?[]const u8, promise: *JSC.JSPromise.Strong) *Deferred { + const host_string: ?bun.String = if (hostname) |host| + bun.String.createUTF8(host) + else + null; + defer promise.* = .{}; + return Deferred.init(this, syscall, host_string, promise.*); + } pub fn toJS(this: Error, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - const error_value = globalThis.createErrorInstance("{s}", .{this.label()}); - error_value.put( - globalThis, - JSC.ZigString.static("name"), - bun.String.static("DNSException").toJS(globalThis), - ); - error_value.put( - globalThis, - JSC.ZigString.static("code"), - JSC.ZigString.init(this.code()).toJS(globalThis), - ); - error_value.put( - globalThis, - JSC.ZigString.static("errno"), - JSC.jsNumber(@intFromEnum(this)), - ); - return error_value; + const instance = (JSC.SystemError{ + .errno = @intFromEnum(this), + .code = bun.String.static(this.code()), + }).toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + return instance; + } + + pub fn toJSWithSyscall(this: Error, globalThis: *JSC.JSGlobalObject, comptime syscall: []const u8) JSC.JSValue { + const instance = (JSC.SystemError{ + .errno = @intFromEnum(this), + .code = bun.String.static(this.code()), + .syscall = bun.String.static((syscall ++ "\x00")[0..syscall.len :0]), + }).toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + return instance; + } + + pub fn toJSWithSyscallAndHostname(this: Error, globalThis: *JSC.JSGlobalObject, comptime syscall: []const u8, hostname: []const u8) JSC.JSValue { + const instance = (JSC.SystemError{ + .errno = @intFromEnum(this), + .code = bun.String.static(this.code()), + .message = bun.String.createFormat("{s} {s} {s}", .{ syscall, this.code()[4..], hostname }) catch bun.outOfMemory(), + .syscall = bun.String.static((syscall ++ "\x00")[0..syscall.len :0]), + .hostname = bun.String.createUTF8(hostname), + }).toErrorInstance(globalThis); + instance.put(globalThis, "name", bun.String.static("DNSException").toJS(globalThis)); + return instance; } pub fn initEAI(rc: i32) ?Error { @@ -1422,6 +1857,7 @@ pub const Error = enum(i32) { .{ .EADDRGETNETWORKPARAMS, "DNS_EADDRGETNETWORKPARAMS" }, .{ .ECANCELLED, "DNS_ECANCELLED" }, .{ .ESERVICE, "DNS_ESERVICE" }, + .{ .ENOSERVER, "DNS_ENOSERVER" }, }); pub const label = bun.enumMap(Error, .{ @@ -1450,6 +1886,7 @@ pub const Error = enum(i32) { .{ .EADDRGETNETWORKPARAMS, "EADDRGETNETWORKPARAMS" }, .{ .ECANCELLED, "DNS query cancelled" }, .{ .ESERVICE, "Service not available" }, + .{ .ENOSERVER, "No DNS servers were configured" }, }); pub fn get(rc: i32) ?Error { @@ -1460,8 +1897,8 @@ pub const Error = enum(i32) { return switch (rc) { 0 => null, - 1...ARES_ESERVICE => @as(Error, @enumFromInt(rc)), - -ARES_ESERVICE...-1 => @as(Error, @enumFromInt(-rc)), + 1...ARES_ENOSERVER => @as(Error, @enumFromInt(rc)), + -ARES_ENOSERVER...-1 => @as(Error, @enumFromInt(-rc)), else => unreachable, }; } diff --git a/src/dns.zig b/src/dns.zig index 6601ee8eb3..95d9d74635 100644 --- a/src/dns.zig +++ b/src/dns.zig @@ -3,6 +3,12 @@ const std = @import("std"); const JSC = bun.JSC; const JSValue = JSC.JSValue; +const netdb = if (bun.Environment.isWindows) .{ + .AI_V4MAPPED = @as(c_int, 2048), + .AI_ADDRCONFIG = @as(c_int, 1024), + .AI_ALL = @as(c_int, 256), +} else @cImport(@cInclude("netdb.h")); + pub const GetAddrInfo = struct { name: []const u8 = "", port: u16 = 0, @@ -95,6 +101,9 @@ pub const GetAddrInfo = struct { return error.InvalidFlags; options.flags = flags.coerce(i32, globalObject); + + if (options.flags & ~(netdb.AI_ALL | netdb.AI_ADDRCONFIG | netdb.AI_V4MAPPED) != 0) + return error.InvalidFlags; } return options; diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index ed652132e5..4e7edeed48 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -283,7 +283,7 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { family, }); }, - error: (_socket, error) => { + error: error => { this.emit("error", error); }, }, diff --git a/src/js/node/dns.ts b/src/js/node/dns.ts index cfde60acd3..0900318df4 100644 --- a/src/js/node/dns.ts +++ b/src/js/node/dns.ts @@ -1,7 +1,45 @@ // Hardcoded module "node:dns" -// only resolve4, resolve, lookup, resolve6, resolveSrv, and reverse are implemented. const dns = Bun.dns; const utilPromisifyCustomSymbol = Symbol.for("nodejs.util.promisify.custom"); +const { isIP } = require("./net"); +const { + validateFunction, + validateArray, + validateString, + validateBoolean, + validateNumber, +} = require("internal/validators"); + +const errorCodes = { + NODATA: "ENODATA", + FORMERR: "EFORMERR", + SERVFAIL: "ESERVFAIL", + NOTFOUND: "ENOTFOUND", + NOTIMP: "ENOTIMP", + REFUSED: "EREFUSED", + BADQUERY: "EBADQUERY", + BADNAME: "EBADNAME", + BADFAMILY: "EBADFAMILY", + BADRESP: "EBADRESP", + CONNREFUSED: "ECONNREFUSED", + TIMEOUT: "ETIMEOUT", + EOF: "EOF", + FILE: "EFILE", + NOMEM: "ENOMEM", + DESTRUCTION: "EDESTRUCTION", + BADSTR: "EBADSTR", + BADFLAGS: "EBADFLAGS", + NONAME: "ENONAME", + BADHINTS: "EBADHINTS", + NOTINITIALIZED: "ENOTINITIALIZED", + LOADIPHLPAPI: "ELOADIPHLPAPI", + ADDRGETNETWORKPARAMS: "EADDRGETNETWORKPARAMS", + CANCELLED: "ECANCELLED", +}; + +const IANA_DNS_PORT = 53; +const IPv6RE = /^\[([^[\]]*)\]/; +const addrSplitRE = /(^.+?)(?::(\d+))?$/; function translateErrorCode(promise: Promise) { return promise.catch(error => { @@ -23,32 +61,255 @@ function getServers() { return dns.getServers(); } -function lookup(domain, options, callback) { - if (typeof options == "function") { - callback = options; +function setServers(servers) { + return setServersOn(servers, dns); +} + +const getRuntimeDefaultResultOrderOption = $newZigFunction( + "dns_resolver.zig", + "DNSResolver.getRuntimeDefaultResultOrderOption", + 0, +); + +function newResolver(options) { + if (!newResolver.zig) { + newResolver.zig = $newZigFunction("dns_resolver.zig", "DNSResolver.newResolver", 1); + } + return newResolver.zig(options); +} + +function defaultResultOrder() { + if (typeof defaultResultOrder.value === "undefined") { + defaultResultOrder.value = getRuntimeDefaultResultOrderOption(); } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + return defaultResultOrder.value; +} - if (typeof options == "number") { - options = { family: options }; - } +function setDefaultResultOrder(order) { + validateOrder(order); + defaultResultOrder.value = order; +} - if (domain !== domain || (typeof domain !== "number" && !domain)) { - console.warn( - `DeprecationWarning: The provided hostname "${String( - domain, - )}" is not a valid hostname, and is supported in the dns module solely for compatibility.`, - ); - callback(null, null, 4); +function getDefaultResultOrder() { + return defaultResultOrder; +} + +function setServersOn(servers, object) { + validateArray(servers, "servers"); + + const triples = []; + + servers.forEach((server, i) => { + validateString(server, `servers[${i}]`); + let ipVersion = isIP(server); + + if (ipVersion !== 0) { + triples.push([ipVersion, server, IANA_DNS_PORT]); + return; + } + + const match = IPv6RE.exec(server); + + // Check for an IPv6 in brackets. + if (match) { + ipVersion = isIP(match[1]); + if (ipVersion !== 0) { + const port = parseInt(addrSplitRE[Symbol.replace](server, "$2")) || IANA_DNS_PORT; + triples.push([ipVersion, match[1], port]); + return; + } + } + + // addr:port + const addrSplitMatch = addrSplitRE.exec(server); + + if (addrSplitMatch) { + const hostIP = addrSplitMatch[1]; + const port = addrSplitMatch[2] || IANA_DNS_PORT; + + ipVersion = isIP(hostIP); + + if (ipVersion !== 0) { + triples.push([ipVersion, hostIP, parseInt(port)]); + return; + } + } + + throw $ERR_INVALID_IP_ADDRESS(server); + }); + + object.setServers(triples); +} + +function validateFlagsOption(options) { + if (options.flags === undefined) { return; } - dns.lookup(domain, options).then(res => { + validateNumber(options.flags); + + if ((options.flags & ~(dns.ALL | dns.ADDRCONFIG | dns.V4MAPPED)) != 0) { + throw $ERR_INVALID_ARG_VALUE("hints", options.flags, "is invalid"); + } +} + +function validateFamily(family) { + if (family !== 6 && family !== 4 && family !== 0) { + throw $ERR_INVALID_ARG_VALUE("family", family, "must be one of 0, 4 or 6"); + } +} + +function validateFamilyOption(options) { + if (options.family != null) { + switch (options.family) { + case "IPv4": + options.family = 4; + break; + case "IPv6": + options.family = 6; + break; + default: + validateFamily(options.family); + break; + } + } +} + +function validateAllOption(options) { + if (options.all !== undefined) { + validateBoolean(options.all); + } +} + +function validateVerbatimOption(options) { + if (options.verbatim !== undefined) { + validateBoolean(options.verbatim); + } +} + +function validateOrder(order) { + if (!["ipv4first", "ipv6first", "verbatim"].includes(order)) { + throw $ERR_INVALID_ARG_VALUE("order", order, "is invalid"); + } +} + +function validateOrderOption(options) { + if (options.order !== undefined) { + validateOrder(options.order); + } +} + +function validateResolve(hostname, callback) { + if (typeof hostname !== "string") { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); + } + + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); + } +} + +function validateLocalAddresses(first, second) { + validateString(first); + if (typeof second !== "undefined") { + validateString(second); + } +} + +function invalidHostname(hostname) { + if (invalidHostname.warned) { + return; + } + + invalidHostname.warned = true; + process.emitWarning( + `The provided hostname "${String(hostname)}" is not a valid hostname, and is supported in the dns module solely for compatibility.`, + "DeprecationWarning", + "DEP0118", + ); +} + +function translateLookupOptions(options) { + if (!options || typeof options !== "object") { + options = { family: options }; + } + + let { family, order, verbatim, hints: flags, all } = options; + + if (order === undefined && typeof verbatim === "boolean") { + order = verbatim ? "verbatim" : "ipv4first"; + } + + order ??= defaultResultOrder(); + + return { + family, + flags, + all, + order, + verbatim, + }; +} + +function validateLookupOptions(options) { + validateFlagsOption(options); + validateFamilyOption(options); + validateAllOption(options); + validateVerbatimOption(options); + validateOrderOption(options); +} + +function lookup(hostname, options, callback) { + if (typeof hostname !== "string" && hostname) { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); + } + + if (typeof options === "function") { + callback = options; + options = { family: 0 }; + } else if (typeof options === "number") { + validateFunction(callback, "callback"); + validateFamily(options); + options = { family: options }; + } else if (options !== undefined && typeof options !== "object") { + validateFunction(arguments.length === 2 ? options : callback, "callback"); + throw $ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options); + } + + validateFunction(callback, "callback"); + + options = translateLookupOptions(options); + validateLookupOptions(options); + + if (!hostname) { + invalidHostname(hostname); + if (options.all) { + callback(null, []); + } else { + callback(null, null, 4); + } + return; + } + + const family = isIP(hostname); + if (family) { + if (options.all) { + process.nextTick(callback, null, [{ address: hostname, family }]); + } else { + process.nextTick(callback, null, hostname, family); + } + return; + } + + dns.lookup(hostname, options).then(res => { throwIfEmpty(res); - res.sort((a, b) => a.family - b.family); + + if (options.order == "ipv4first") { + res.sort((a, b) => a.family - b.family); + } else if (options.order == "ipv6first") { + res.sort((a, b) => b.family - a.family); + } if (options?.all) { callback(null, res.map(mapLookupAll)); @@ -60,11 +321,17 @@ function lookup(domain, options, callback) { } function lookupService(address, port, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (arguments.length < 3) { + throw $ERR_MISSING_ARGS('The "address", "port", and "callback" arguments must be specified'); } - dns.lookupService(address, port, callback).then( + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); + } + + validateString(address); + + dns.lookupService(address, port).then( results => { callback(null, ...results); }, @@ -74,41 +341,77 @@ function lookupService(address, port, callback) { ); } -var InternalResolver = class Resolver { - constructor(options) {} +function validateResolverOptions(options) { + if (options === undefined) { + return; + } - cancel() {} + for (const key of ["timeout", "tries"]) { + if (key in options) { + if (typeof options[key] !== "number") { + throw $ERR_INVALID_ARG_TYPE(key, "number", options[key]); + } + } + } + + if ("timeout" in options) { + const timeout = options.timeout; + if ((timeout < 0 && timeout != -1) || Math.floor(timeout) != timeout || timeout >= 2 ** 31) { + throw $ERR_OUT_OF_RANGE("Invalid timeout", timeout); + } + } +} + +var InternalResolver = class Resolver { + #resolver; + + constructor(options) { + validateResolverOptions(options); + this.#resolver = this._handle = newResolver(options); + } + + cancel() { + this.#resolver.cancel(); + } + + static #getResolver(object) { + return typeof object !== "undefined" && #resolver in object ? object.#resolver : dns; + } getServers() { - return []; + return Resolver.#getResolver(this).getServers() || []; } resolve(hostname, rrtype, callback) { - if (typeof rrtype == "function") { + if (typeof rrtype === "function") { callback = rrtype; - rrtype = null; + rrtype = "A"; + } else if (typeof rrtype === "undefined") { + rrtype = "A"; + } else if (typeof rrtype !== "string") { + throw $ERR_INVALID_ARG_TYPE("rrtype", "string", rrtype); } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolve(hostname).then( - results => { - switch (rrtype?.toLowerCase()) { - case "a": - case "aaaa": - callback(null, hostname, results.map(mapResolveX)); - break; - default: - callback(null, results); - break; - } - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolve(hostname) + .then( + results => { + switch (rrtype?.toLowerCase()) { + case "a": + case "aaaa": + callback(null, hostname, results.map(mapResolveX)); + break; + default: + callback(null, results); + break; + } + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolve4(hostname, options, callback) { @@ -117,18 +420,18 @@ var InternalResolver = class Resolver { options = null; } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.lookup(hostname, { family: 4 }).then( - addresses => { - callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolve(hostname, "A") + .then( + addresses => { + callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolve6(hostname, options, callback) { @@ -137,174 +440,200 @@ var InternalResolver = class Resolver { options = null; } - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.lookup(hostname, { family: 6 }).then( - addresses => { - callback(null, options?.ttl ? addresses : addresses.map(({ address }) => address)); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolve(hostname, "AAAA") + .then( + addresses => { + callback(null, options?.ttl ? addresses : addresses.map(mapResolveX)); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveAny(hostname, callback) { - callback(null, []); + validateResolve(hostname, callback); + + Resolver.#getResolver(this) + .resolveAny(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveCname(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveCname(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveCname(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveMx(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveMx(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveMx(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveNaptr(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveNaptr(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveNaptr(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveNs(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveNs(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveNs(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolvePtr(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolvePtr(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolvePtr(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveSrv(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); - } + validateResolve(hostname, callback); - dns.resolveSrv(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveSrv(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveCaa(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.resolveCaa(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveCaa(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveTxt(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.resolveTxt(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveTxt(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } resolveSoa(hostname, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.resolveSoa(hostname, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .resolveSoa(hostname) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } reverse(ip, callback) { - if (typeof callback != "function") { - throw new TypeError("callback must be a function"); + if (typeof callback !== "function") { + throw $ERR_INVALID_ARG_TYPE("callback", "function", callback); } - dns.reverse(ip, callback).then( - results => { - callback(null, results); - }, - error => { - callback(withTranslatedError(error)); - }, - ); + Resolver.#getResolver(this) + .reverse(ip) + .then( + results => { + callback(null, results); + }, + error => { + callback(withTranslatedError(error)); + }, + ); } - setServers(servers) {} + setLocalAddress(first, second) { + validateLocalAddresses(first, second); + Resolver.#getResolver(this).setLocalAddress(first, second); + } + + setServers(servers) { + return setServersOn(servers, Resolver.#getResolver(this)); + } }; function Resolver(options) { @@ -331,9 +660,6 @@ var { resolveTxt, } = InternalResolver.prototype; -function setDefaultResultOrder() {} -function setServers() {} - const mapLookupAll = res => { const { address, family } = res; return { address, family }; @@ -352,65 +678,133 @@ function throwIfEmpty(res) { } Object.defineProperty(throwIfEmpty, "name", { value: "::bunternal::" }); -const promisifyLookup = res => { +const promisifyLookup = order => res => { throwIfEmpty(res); - res.sort((a, b) => a.family - b.family); + if (order == "ipv4first") { + res.sort((a, b) => a.family - b.family); + } else if (order == "ipv6first") { + res.sort((a, b) => b.family - a.family); + } const [{ address, family }] = res; return { address, family }; }; -const promisifyLookupAll = res => { +const promisifyLookupAll = order => res => { throwIfEmpty(res); - res.sort((a, b) => a.family - b.family); + if (order == "ipv4first") { + res.sort((a, b) => a.family - b.family); + } else if (order == "ipv6first") { + res.sort((a, b) => b.family - a.family); + } return res.map(mapLookupAll); }; const mapResolveX = a => a.address; -const promisifyResolveX = res => { - return res?.map(mapResolveX); +const promisifyResolveX = ttl => { + if (ttl) { + return res => res; + } else { + return res => { + return res?.map(mapResolveX); + }; + } }; // promisified versions const promises = { - lookup(domain, options) { - if (options?.all) { - return translateErrorCode(dns.lookup(domain, options).then(promisifyLookupAll)); + ...errorCodes, + + lookup(hostname, options) { + if (typeof hostname !== "string" && hostname) { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); } - return translateErrorCode(dns.lookup(domain, options).then(promisifyLookup)); + + if (typeof options === "number") { + validateFamily(options); + options = { family: options }; + } else if (options !== undefined && typeof options !== "object") { + throw $ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options); + } + + options = translateLookupOptions(options); + validateLookupOptions(options); + + if (!hostname) { + invalidHostname(hostname); + return Promise.resolve( + options.all + ? [] + : { + address: null, + family: 4, + }, + ); + } + + const family = isIP(hostname); + if (family) { + const obj = { address: hostname, family }; + return Promise.resolve(options.all ? [obj] : obj); + } + + if (options.all) { + return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookupAll(options.order))); + } + return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookup(options.order))); }, lookupService(address, port) { - return translateErrorCode(dns.lookupService(address, port)); + if (arguments.length !== 2) { + throw $ERR_MISSING_ARGS('The "address" and "port" arguments must be specified'); + } + + validateString(address); + + try { + return translateErrorCode(dns.lookupService(address, port)).then(([hostname, service]) => ({ + hostname, + service, + })); + } catch (err) { + if (err.name === "TypeError" || err.name === "RangeError") { + throw err; + } + return Promise.reject(withTranslatedError(err)); + } }, resolve(hostname, rrtype) { - if (typeof rrtype !== "string") { - rrtype = null; + if (typeof hostname !== "string") { + throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname); } + + if (typeof rrtype === "undefined") { + rrtype = "A"; + } else if (typeof rrtype !== "string") { + throw $ERR_INVALID_ARG_TYPE("rrtype", "string", rrtype); + } + switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup)); + return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup(defaultResultOrder()))); default: return translateErrorCode(dns.resolve(hostname, rrtype)); } }, resolve4(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 4 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 4 }).then(promisifyResolveX)); + return translateErrorCode(dns.resolve(hostname, "A").then(promisifyResolveX(options?.ttl))); }, resolve6(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 6 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 6 }).then(promisifyResolveX)); + return translateErrorCode(dns.resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl))); }, + resolveAny(hostname) { + return translateErrorCode(dns.resolveAny(hostname)); + }, resolveSrv(hostname) { return translateErrorCode(dns.resolveSrv(hostname)); }, @@ -423,7 +817,6 @@ const promises = { resolveNaptr(hostname) { return translateErrorCode(dns.resolveNaptr(hostname)); }, - resolveMx(hostname) { return translateErrorCode(dns.resolveMx(hostname)); }, @@ -444,91 +837,111 @@ const promises = { }, Resolver: class Resolver { - constructor(options) {} + #resolver; - cancel() {} + constructor(options) { + validateResolverOptions(options); + this.#resolver = this._handle = newResolver(options); + } + + cancel() { + this.#resolver.cancel(); + } + + static #getResolver(object) { + return typeof object !== "undefined" && #resolver in object ? object.#resolver : dns; + } getServers() { - return []; + return Resolver.#getResolver(this).getServers() || []; } resolve(hostname, rrtype) { - if (typeof rrtype !== "string") { + if (typeof rrtype === "undefined") { + rrtype = "A"; + } else if (typeof rrtype !== "string") { rrtype = null; } switch (rrtype?.toLowerCase()) { case "a": case "aaaa": - return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup)); + return translateErrorCode( + Resolver.#getResolver(this).resolve(hostname, rrtype).then(promisifyLookup(defaultResultOrder())), + ); default: - return translateErrorCode(dns.resolve(hostname, rrtype)); + return translateErrorCode(Resolver.#getResolver(this).resolve(hostname, rrtype)); } } resolve4(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 4 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 4 }).then(promisifyResolveX)); + return translateErrorCode( + Resolver.#getResolver(this).resolve(hostname, "A").then(promisifyResolveX(options?.ttl)), + ); } resolve6(hostname, options) { - if (options?.ttl) { - return translateErrorCode(dns.lookup(hostname, { family: 6 })); - } - return translateErrorCode(dns.lookup(hostname, { family: 6 }).then(promisifyResolveX)); + return translateErrorCode( + Resolver.#getResolver(this).resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl)), + ); } resolveAny(hostname) { - return Promise.resolve([]); + return translateErrorCode(Resolver.#getResolver(this).resolveAny(hostname)); } resolveCname(hostname) { - return translateErrorCode(dns.resolveCname(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveCname(hostname)); } resolveMx(hostname) { - return translateErrorCode(dns.resolveMx(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveMx(hostname)); } resolveNaptr(hostname) { - return translateErrorCode(dns.resolveNaptr(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveNaptr(hostname)); } resolveNs(hostname) { - return translateErrorCode(dns.resolveNs(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveNs(hostname)); } resolvePtr(hostname) { - return translateErrorCode(dns.resolvePtr(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolvePtr(hostname)); } resolveSoa(hostname) { - return translateErrorCode(dns.resolveSoa(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveSoa(hostname)); } resolveSrv(hostname) { - return translateErrorCode(dns.resolveSrv(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveSrv(hostname)); } resolveCaa(hostname) { - return translateErrorCode(dns.resolveCaa(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveCaa(hostname)); } resolveTxt(hostname) { - return translateErrorCode(dns.resolveTxt(hostname)); + return translateErrorCode(Resolver.#getResolver(this).resolveTxt(hostname)); } reverse(ip) { - return translateErrorCode(dns.reverse(ip)); + return translateErrorCode(Resolver.#getResolver(this).reverse(ip)); } - setServers(servers) {} + setLocalAddress(first, second) { + validateLocalAddresses(first, second); + Resolver.#getResolver(this).setLocalAddress(first, second); + } + + setServers(servers) { + return setServersOn(servers, Resolver.#getResolver(this)); + } }, + + setDefaultResultOrder, + setServers, }; -for (const key of ["resolveAny"]) { - promises[key] = () => Promise.resolve(undefined); -} // Compatibility with util.promisify(dns[method]) for (const [method, pMethod] of [ @@ -553,42 +966,19 @@ for (const [method, pMethod] of [ } export default { - // these are wrong - ADDRCONFIG: 0, - ALL: 1, - V4MAPPED: 2, + ADDRCONFIG: dns.ADDRCONFIG, + ALL: dns.ALL, + V4MAPPED: dns.V4MAPPED, // ERROR CODES - NODATA: "DNS_ENODATA", - FORMERR: "DNS_EFORMERR", - SERVFAIL: "DNS_ESERVFAIL", - NOTFOUND: "DNS_ENOTFOUND", - NOTIMP: "DNS_ENOTIMP", - REFUSED: "DNS_EREFUSED", - BADQUERY: "DNS_EBADQUERY", - BADNAME: "DNS_EBADNAME", - BADFAMILY: "DNS_EBADFAMILY", - BADRESP: "DNS_EBADRESP", - CONNREFUSED: "DNS_ECONNREFUSED", - TIMEOUT: "DNS_ETIMEOUT", - EOF: "DNS_EOF", - FILE: "DNS_EFILE", - NOMEM: "DNS_ENOMEM", - DESTRUCTION: "DNS_EDESTRUCTION", - BADSTR: "DNS_EBADSTR", - BADFLAGS: "DNS_EBADFLAGS", - NONAME: "DNS_ENONAME", - BADHINTS: "DNS_EBADHINTS", - NOTINITIALIZED: "DNS_ENOTINITIALIZED", - LOADIPHLPAPI: "DNS_ELOADIPHLPAPI", - ADDRGETNETWORKPARAMS: "DNS_EADDRGETNETWORKPARAMS", - CANCELLED: "DNS_ECANCELLED", + ...errorCodes, lookup, lookupService, Resolver, setServers, setDefaultResultOrder, + getDefaultResultOrder, resolve, reverse, resolve4, diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index be19e9436e..48977c9ef3 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -220,8 +220,6 @@ test("dns.resolveNs (empty string) ", () => { dns.resolveNs("", (err, results) => { try { expect(err).toBeNull(); - console.log("resolveNs:", results); - expect(results instanceof Array).toBe(true); // root servers expect(results.sort()).toStrictEqual( @@ -254,7 +252,6 @@ test("dns.resolvePtr (ptr.socketify.dev)", () => { dns.resolvePtr("ptr.socketify.dev", (err, results) => { try { expect(err).toBeNull(); - console.log("resolvePtr:", results); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); @@ -270,7 +267,6 @@ test("dns.resolveCname (cname.socketify.dev)", () => { dns.resolveCname("cname.socketify.dev", (err, results) => { try { expect(err).toBeNull(); - console.log("resolveCname:", results); expect(results instanceof Array).toBe(true); expect(results[0]).toBe("bun.sh"); resolve(); @@ -427,7 +423,7 @@ describe("test invalid arguments", () => { }).toThrow("Expected address to be a non-empty string for 'lookupService'."); expect(() => { dns.lookupService("google.com", 443, (err, hostname, service) => {}); - }).toThrow("Expected address to be a invalid address for 'lookupService'."); + }).toThrow('The "address" argument is invalid. Received google.com'); }); }); @@ -486,7 +482,7 @@ describe("dns.lookupService", () => { ["1.1.1.1", 80, ["one.one.one.one", "http"]], ["1.1.1.1", 443, ["one.one.one.one", "https"]], ])("promises.lookupService(%s, %d)", async (address, port, expected) => { - const [hostname, service] = await dns.promises.lookupService(address, port); + const { hostname, service } = await dns.promises.lookupService(address, port); expect(hostname).toStrictEqual(expected[0]); expect(service).toStrictEqual(expected[1]); }); diff --git a/test/js/node/test/common/dns.js b/test/js/node/test/common/dns.js index d854c73629..8fa264dc2c 100644 --- a/test/js/node/test/common/dns.js +++ b/test/js/node/test/common/dns.js @@ -15,6 +15,7 @@ const types = { TXT: 16, ANY: 255, CAA: 257, + SRV: 33, }; const classes = { diff --git a/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js b/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js new file mode 100644 index 0000000000..e0cb4d1854 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-cancel-reverse-lookup.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new Resolver(); + +server.bind(0, common.mustCall(() => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.reverse('123.45.67.89', common.mustCall((err, res) => { + assert.strictEqual(err.code, 'ECANCELLED'); + assert.strictEqual(err.syscall, 'getHostByAddr'); + assert.strictEqual(err.hostname, '123.45.67.89'); + server.close(); + })); +})); + +server.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, '89.67.45.123.in-addr.arpa'); + + // Do not send a reply. + resolver.cancel(); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-cancel-promise.js b/test/js/node/test/parallel/test-dns-channel-cancel-promise.js new file mode 100644 index 0000000000..6dee3e6a77 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-cancel-promise.js @@ -0,0 +1,59 @@ +'use strict'; +const common = require('../common'); +const { promises: dnsPromises } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new dnsPromises.Resolver(); + +server.bind(0, common.mustCall(async () => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + + // Single promise + { + server.once('message', () => { + resolver.cancel(); + }); + + const hostname = 'example0.org'; + + await assert.rejects( + resolver.resolve4(hostname), + { + code: 'ECANCELLED', + syscall: 'queryA', + hostname + } + ); + } + + // Multiple promises + { + server.once('message', () => { + resolver.cancel(); + }); + + const assertions = []; + const assertionCount = 10; + + for (let i = 1; i <= assertionCount; i++) { + const hostname = `example${i}.org`; + + assertions.push( + assert.rejects( + resolver.resolve4(hostname), + { + code: 'ECANCELLED', + syscall: 'queryA', + hostname: hostname + } + ) + ); + } + + await Promise.all(assertions); + } + + server.close(); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-cancel.js b/test/js/node/test/parallel/test-dns-channel-cancel.js new file mode 100644 index 0000000000..405b31e4cc --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-cancel.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const server = dgram.createSocket('udp4'); +const resolver = new Resolver(); + +const desiredQueries = 11; +let finishedQueries = 0; + +server.bind(0, common.mustCall(async () => { + resolver.setServers([`127.0.0.1:${server.address().port}`]); + + const callback = common.mustCall((err, res) => { + assert.strictEqual(err.code, 'ECANCELLED'); + assert.strictEqual(err.syscall, 'queryA'); + assert.strictEqual(err.hostname, `example${finishedQueries}.org`); + + finishedQueries++; + if (finishedQueries === desiredQueries) { + server.close(); + } + }, desiredQueries); + + const next = (...args) => { + callback(...args); + + server.once('message', () => { + resolver.cancel(); + }); + + // Multiple queries + for (let i = 1; i < desiredQueries; i++) { + resolver.resolve4(`example${i}.org`, callback); + } + }; + + server.once('message', () => { + resolver.cancel(); + }); + + // Single query + resolver.resolve4('example0.org', next); +})); diff --git a/test/js/node/test/parallel/test-dns-channel-timeout.js b/test/js/node/test/parallel/test-dns-channel-timeout.js new file mode 100644 index 0000000000..153d7ad907 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-channel-timeout.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dgram = require('dgram'); +const dns = require('dns'); + +if (typeof Bun !== 'undefined') { + if (process.platform === 'win32' && require('harness').isCI) { + // TODO(@heimskr): This test mysteriously takes forever in Windows in CI + // possibly due to UDP keeping the event loop alive longer than it should. + process.exit(0); + } +} + +for (const ctor of [dns.Resolver, dns.promises.Resolver]) { + for (const timeout of [null, true, false, '', '2']) { + assert.throws(() => new ctor({ timeout }), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + } + + for (const timeout of [-2, 4.2, 2 ** 31]) { + assert.throws(() => new ctor({ timeout }), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); + } + + for (const timeout of [-1, 0, 1]) new ctor({ timeout }); // OK +} + +for (const timeout of [0, 1, 2]) { + const server = dgram.createSocket('udp4'); + server.bind(0, '127.0.0.1', common.mustCall(() => { + const resolver = new dns.Resolver({ timeout }); + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.resolve4('nodejs.org', common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ETIMEOUT', + name: /^(DNSException|Error)$/, + }); + server.close(); + })); + })); +} + +for (const timeout of [0, 1, 2]) { + const server = dgram.createSocket('udp4'); + server.bind(0, '127.0.0.1', common.mustCall(() => { + const resolver = new dns.promises.Resolver({ timeout }); + resolver.setServers([`127.0.0.1:${server.address().port}`]); + resolver.resolve4('nodejs.org').catch(common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ETIMEOUT', + name: /^(DNSException|Error)$/, + }); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-dns-default-order-ipv4.js b/test/js/node/test/parallel/test-dns-default-order-ipv4.js new file mode 100644 index 0000000000..ea3deec4f7 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-ipv4.js @@ -0,0 +1,51 @@ +// Flags: --dns-result-order=ipv4first +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that --dns-result-order=ipv4first works as expected. + +if (!process.execArgv.includes("--dns-result-order=ipv4first")) { + process.exit(0); +} + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of ipv4first only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-default-order-ipv6.js b/test/js/node/test/parallel/test-dns-default-order-ipv6.js new file mode 100644 index 0000000000..aeb2dc2b2a --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-ipv6.js @@ -0,0 +1,51 @@ +// Flags: --dns-result-order=ipv6first +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that --dns-result-order=ipv6first works as expected. + +if (!process.execArgv.includes("--dns-result-order=ipv6first")) { + process.exit(0); +} + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of ipv6first only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-default-order-verbatim.js b/test/js/node/test/parallel/test-dns-default-order-verbatim.js new file mode 100644 index 0000000000..562250ca48 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-default-order-verbatim.js @@ -0,0 +1,55 @@ +// Flags: --dns-result-order=verbatim +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of verbatim only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter("verbatim"); + + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter("verbatim"); + + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter("verbatim"); + + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter("verbatim"); + + await allowFailed( + promisify(dns.lookup)('example.org', { order: 'ipv4first' }) + ); + checkParameter("ipv4first"); + + await allowFailed( + promisify(dns.lookup)('example.org', { order: 'ipv6first' }) + ); + checkParameter("ipv6first"); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-get-server.js b/test/js/node/test/parallel/test-dns-get-server.js new file mode 100644 index 0000000000..3ce6a45ac7 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-get-server.js @@ -0,0 +1,11 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const { Resolver } = require('dns'); + +const resolver = new Resolver(); +assert(resolver.getServers().length > 0); + +resolver._handle.getServers = common.mustCall(); +assert.strictEqual(resolver.getServers().length, 0); diff --git a/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js b/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js new file mode 100644 index 0000000000..934796198b --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookup-promises-options-deprecated.js @@ -0,0 +1,44 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +Bun.dns.lookup = hostname => { + throw Object.assign(new Error('Out of memory'), { + name: 'DNSException', + code: 'ENOMEM', + syscall: 'getaddrinfo', + hostname, + }); +}; + +// This test ensures that dns.lookup issues a DeprecationWarning +// when invalid options type is given + +const dnsPromises = require('dns/promises'); + +common.expectWarning({ + // 'internal/test/binding': [ + // 'These APIs are for internal testing only. Do not use them.', + // ], +}); + +assert.throws(() => { + dnsPromises.lookup('127.0.0.1', { hints: '-1' }); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { hints: -1 }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { family: '6' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { all: 'true' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { verbatim: 'true' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', { order: 'true' }), + { code: 'ERR_INVALID_ARG_VALUE' }); +assert.throws(() => dnsPromises.lookup('127.0.0.1', '6'), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => dnsPromises.lookup('localhost'), + { code: 'ENOMEM' }); diff --git a/test/js/node/test/parallel/test-dns-lookup.js b/test/js/node/test/parallel/test-dns-lookup.js new file mode 100644 index 0000000000..bef563df60 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookup.js @@ -0,0 +1,223 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Stub `getaddrinfo` to *always* error. This has to be done before we load the +// `dns` module to guarantee that the `dns` module uses the stub. +if (typeof Bun === "undefined") { + const { internalBinding } = require('internal/test/binding'); + const cares = internalBinding('cares_wrap'); + cares.getaddrinfo = () => internalBinding('uv').UV_ENOMEM; +} else { + Bun.dns.lookup = (hostname) => Promise.reject(Object.assign(new Error('Out of memory'), { code: 'ENOMEM', hostname })); +} + +const dns = require('dns'); +const dnsPromises = dns.promises; + +{ + const err = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "hostname" argument must be of type string\. Received( type number|: "number")/ + }; + + assert.throws(() => dns.lookup(1, {}), err); + assert.throws(() => dnsPromises.lookup(1, {}), err); +} + +// This also verifies different expectWarning notations. +common.expectWarning({ + // For 'internal/test/binding' module. + ...(typeof Bun === "undefined"? { + 'internal/test/binding': [ + 'These APIs are for internal testing only. Do not use them.', + ] + } : {}), + // For calling `dns.lookup` with falsy `hostname`. + 'DeprecationWarning': { + DEP0118: 'The provided hostname "false" is not a valid ' + + 'hostname, and is supported in the dns module solely for compatibility.' + } +}); + +assert.throws(() => { + dns.lookup(false, 'cb'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => { + dns.lookup(false, 'options', 'cb'); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +{ + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The argument 'hints' is invalid\. Received:? 100/ + }; + const options = { + hints: 100, + family: 0, + all: false + }; + + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +} + +{ + const family = 20; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /^The (property 'options.family' must be one of: 0, 4, 6|argument 'family' must be one of 0, 4 or 6)\. Received:? 20$/ + }; + const options = { + hints: 0, + family, + all: false + }; + + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +} + +[1, 0n, 1n, '', '0', Symbol(), true, false, {}, [], () => {}] + .forEach((family) => { + const err = { code: 'ERR_INVALID_ARG_VALUE' }; + const options = { family }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); + }); +[0n, 1n, '', '0', Symbol(), true, false].forEach((family) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + assert.throws(() => { dnsPromises.lookup(false, family); }, err); + assert.throws(() => { + dns.lookup(false, family, common.mustNotCall()); + }, err); +}); +assert.throws(() => dnsPromises.lookup(false, () => {}), + { code: 'ERR_INVALID_ARG_TYPE' }); + +[0n, 1n, '', '0', Symbol(), true, false, {}, [], () => {}].forEach((hints) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { hints }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((all) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { all }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((verbatim) => { + const err = { code: 'ERR_INVALID_ARG_TYPE' }; + const options = { verbatim }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +[0, 1, 0n, 1n, '', '0', Symbol(), {}, [], () => {}].forEach((order) => { + const err = { code: 'ERR_INVALID_ARG_VALUE' }; + const options = { order }; + assert.throws(() => { dnsPromises.lookup(false, options); }, err); + assert.throws(() => { + dns.lookup(false, options, common.mustNotCall()); + }, err); +}); + +(async function() { + let res; + + res = await dnsPromises.lookup(false, { + hints: 0, + family: 0, + all: true + }); + assert.deepStrictEqual(res, []); + + res = await dnsPromises.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: true + }); + assert.deepStrictEqual(res, [{ address: '127.0.0.1', family: 4 }]); + + res = await dnsPromises.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: false + }); + assert.deepStrictEqual(res, { address: '127.0.0.1', family: 4 }); +})().then(common.mustCall()); + +dns.lookup(false, { + hints: 0, + family: 0, + all: true +}, common.mustSucceed((result, addressType) => { + assert.deepStrictEqual(result, []); + assert.strictEqual(addressType, undefined); +})); + +dns.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: true +}, common.mustSucceed((result, addressType) => { + assert.deepStrictEqual(result, [{ + address: '127.0.0.1', + family: 4 + }]); + assert.strictEqual(addressType, undefined); +})); + +dns.lookup('127.0.0.1', { + hints: 0, + family: 4, + all: false +}, common.mustSucceed((result, addressType) => { + assert.strictEqual(result, '127.0.0.1'); + assert.strictEqual(addressType, 4); +})); + +let tickValue = 0; + +// Should fail due to stub. +dns.lookup('example.com', common.mustCall((error, result, addressType) => { + assert(error); + assert.strictEqual(tickValue, 1); + assert.strictEqual(error.code, 'ENOMEM'); + const descriptor = Object.getOwnPropertyDescriptor(error, 'message'); + // The error message should be non-enumerable. + assert.strictEqual(descriptor.enumerable, false); +})); + +// Make sure that the error callback is called on next tick. +tickValue = 1; + +// Should fail due to stub. +assert.rejects(dnsPromises.lookup('example.com'), + { code: 'ENOMEM', hostname: 'example.com' }).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-lookupService-promises.js b/test/js/node/test/parallel/test-dns-lookupService-promises.js new file mode 100644 index 0000000000..f4053d484d --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookupService-promises.js @@ -0,0 +1,20 @@ +'use strict'; + +const common = require('../common'); +if (common.isWindows) return; // TODO: BUN + +const assert = require('assert'); +const dnsPromises = require('dns').promises; + +dnsPromises.lookupService('127.0.0.1', 22).then(common.mustCall((result) => { + assert(['ssh', '22'].includes(result.service)); + assert.strictEqual(typeof result.hostname, 'string'); + assert.notStrictEqual(result.hostname.length, 0); +})); + +// Use an IP from the RFC 5737 test range to cause an error. +// Refs: https://tools.ietf.org/html/rfc5737 +assert.rejects( + () => dnsPromises.lookupService('192.0.2.1', 22), + { code: /^(?:ENOTFOUND|EAI_AGAIN)$/ } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-lookupService.js b/test/js/node/test/parallel/test-dns-lookupService.js new file mode 100644 index 0000000000..e4e48de8bb --- /dev/null +++ b/test/js/node/test/parallel/test-dns-lookupService.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +// Stub `getnameinfo` to *always* error. +Bun.dns.lookupService = (addr, port) => { + throw Object.assign(new Error(`getnameinfo ENOENT ${addr}`), {code: 'ENOENT', syscall: 'getnameinfo'}); +}; + +const dns = require('dns'); + +assert.throws( + () => dns.lookupService('127.0.0.1', 80, common.mustNotCall()), + { + code: 'ENOENT', + message: 'getnameinfo ENOENT 127.0.0.1', + syscall: 'getnameinfo' + } +); + +assert.rejects( + dns.promises.lookupService('127.0.0.1', 80), + { + code: 'ENOENT', + message: 'getnameinfo ENOENT 127.0.0.1', + syscall: 'getnameinfo' + } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-multi-channel.js b/test/js/node/test/parallel/test-dns-multi-channel.js new file mode 100644 index 0000000000..026ef44e33 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-multi-channel.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const { Resolver } = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); + +const servers = [ + { + socket: dgram.createSocket('udp4'), + reply: { type: 'A', address: '1.2.3.4', ttl: 123, domain: 'example.org' } + }, + { + socket: dgram.createSocket('udp4'), + reply: { type: 'A', address: '5.6.7.8', ttl: 123, domain: 'example.org' } + }, +]; + +let waiting = servers.length; +for (const { socket, reply } of servers) { + socket.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + socket.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: [reply], + }), port, address); + })); + + socket.bind(0, common.mustCall(() => { + if (--waiting === 0) ready(); + })); +} + + +function ready() { + const resolvers = servers.map((server) => ({ + server, + resolver: new Resolver() + })); + + for (const { server: { socket, reply }, resolver } of resolvers) { + resolver.setServers([`127.0.0.1:${socket.address().port}`]); + resolver.resolve4('example.org', common.mustSucceed((res) => { + assert.deepStrictEqual(res, [reply.address]); + socket.close(); + })); + } +} diff --git a/test/js/node/test/parallel/test-dns-promises-exists.js b/test/js/node/test/parallel/test-dns-promises-exists.js new file mode 100644 index 0000000000..d88ecefaa9 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-promises-exists.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const dnsPromises = require('dns/promises'); +const dns = require('dns'); + +assert.strictEqual(dnsPromises, dns.promises); + +assert.strictEqual(dnsPromises.NODATA, dns.NODATA); +assert.strictEqual(dnsPromises.FORMERR, dns.FORMERR); +assert.strictEqual(dnsPromises.SERVFAIL, dns.SERVFAIL); +assert.strictEqual(dnsPromises.NOTFOUND, dns.NOTFOUND); +assert.strictEqual(dnsPromises.NOTIMP, dns.NOTIMP); +assert.strictEqual(dnsPromises.REFUSED, dns.REFUSED); +assert.strictEqual(dnsPromises.BADQUERY, dns.BADQUERY); +assert.strictEqual(dnsPromises.BADNAME, dns.BADNAME); +assert.strictEqual(dnsPromises.BADFAMILY, dns.BADFAMILY); +assert.strictEqual(dnsPromises.BADRESP, dns.BADRESP); +assert.strictEqual(dnsPromises.CONNREFUSED, dns.CONNREFUSED); +assert.strictEqual(dnsPromises.TIMEOUT, dns.TIMEOUT); +assert.strictEqual(dnsPromises.EOF, dns.EOF); +assert.strictEqual(dnsPromises.FILE, dns.FILE); +assert.strictEqual(dnsPromises.NOMEM, dns.NOMEM); +assert.strictEqual(dnsPromises.DESTRUCTION, dns.DESTRUCTION); +assert.strictEqual(dnsPromises.BADSTR, dns.BADSTR); +assert.strictEqual(dnsPromises.BADFLAGS, dns.BADFLAGS); +assert.strictEqual(dnsPromises.NONAME, dns.NONAME); +assert.strictEqual(dnsPromises.BADHINTS, dns.BADHINTS); +assert.strictEqual(dnsPromises.NOTINITIALIZED, dns.NOTINITIALIZED); +assert.strictEqual(dnsPromises.LOADIPHLPAPI, dns.LOADIPHLPAPI); +assert.strictEqual(dnsPromises.ADDRGETNETWORKPARAMS, dns.ADDRGETNETWORKPARAMS); +assert.strictEqual(dnsPromises.CANCELLED, dns.CANCELLED); diff --git a/test/js/node/test/parallel/test-dns-resolve-promises.js b/test/js/node/test/parallel/test-dns-resolve-promises.js new file mode 100644 index 0000000000..b9965614ac --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolve-promises.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dnsPromises = require('dns').promises; + +Bun.dns.resolve = (hostname, rrtype) => Promise.reject({code: 'EPERM', syscall: 'query' + rrtype[0].toUpperCase() + rrtype.substr(1), hostname}); + +assert.rejects( + dnsPromises.resolve('example.org'), + { + code: 'EPERM', + syscall: 'queryA', + hostname: 'example.org' + } +).then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js b/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js new file mode 100644 index 0000000000..88369a87f8 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolveany-bad-ancount.js @@ -0,0 +1,55 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const dns = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); +const dnsPromises = dns.promises; + +const server = dgram.createSocket('udp4'); +const resolver = new dns.Resolver({ timeout: 100, tries: 1 }); +const resolverPromises = new dnsPromises.Resolver({ timeout: 100, tries: 1 }); + +server.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + + assert.strictEqual(domain, 'example.org'); + + const buf = dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: { type: 'A', address: '1.2.3.4', ttl: 123, domain }, + }); + // Overwrite the # of answers with 2, which is incorrect. The response is + // discarded in c-ares >= 1.21.0. This is the reason why a small timeout is + // used in the `Resolver` constructor. See + // https://github.com/nodejs/node/pull/50743#issue-1994909204 + buf.writeUInt16LE(2, 6); + server.send(buf, port, address); +}, 2)); + +server.bind(0, common.mustCall(async () => { + const address = server.address(); + resolver.setServers([`127.0.0.1:${address.port}`]); + resolverPromises.setServers([`127.0.0.1:${address.port}`]); + + resolverPromises.resolveAny('example.org') + .then(common.mustNotCall()) + .catch(common.expectsError({ + // May return EBADRESP or ETIMEOUT + code: /^(?:EBADRESP|ETIMEOUT)$/, + syscall: 'queryAny', + hostname: 'example.org' + })); + + resolver.resolveAny('example.org', common.mustCall((err) => { + assert.notStrictEqual(err.code, 'SUCCESS'); + assert.strictEqual(err.syscall, 'queryAny'); + assert.strictEqual(err.hostname, 'example.org'); + const descriptor = Object.getOwnPropertyDescriptor(err, 'message'); + // The error message should be non-enumerable. + assert.strictEqual(descriptor.enumerable, false); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-dns-resolveany.js b/test/js/node/test/parallel/test-dns-resolveany.js new file mode 100644 index 0000000000..f64dbfc93e --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolveany.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const dns = require('dns'); +const assert = require('assert'); +const dgram = require('dgram'); +const dnsPromises = dns.promises; + +const answers = [ + { type: 'A', address: '1.2.3.4', ttl: 123 }, + { type: 'AAAA', address: '::42', ttl: 123 }, + { type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 124 }, + { type: 'NS', value: 'foobar.org', ttl: 457 }, + { type: 'TXT', entries: [ 'v=spf1 ~all xyz\0foo' ] }, + { type: 'PTR', value: 'baz.org', ttl: 987 }, + { + type: 'SOA', + nsname: 'ns1.example.com', + hostmaster: 'admin.example.com', + serial: 156696742, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 60 + }, + { + type: 'CAA', + critical: 128, + issue: 'platynum.ch' + }, +]; + +const server = dgram.createSocket('udp4'); + +server.on('message', common.mustCall((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + server.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: answers.map((answer) => Object.assign({ domain }, answer)), + }), port, address); +}, 2)); + +server.bind(0, common.mustCall(async () => { + const address = server.address(); + dns.setServers([`127.0.0.1:${address.port}`]); + + validateResults(await dnsPromises.resolveAny('example.org')); + + dns.resolveAny('example.org', common.mustSucceed((res) => { + validateResults(res); + server.close(); + })); +})); + +function validateResults(res) { + // TTL values are only provided for A and AAAA entries. + assert.deepStrictEqual(res.map(maybeRedactTTL), answers.map(maybeRedactTTL)); +} + +function maybeRedactTTL(r) { + const ret = { ...r }; + if (!['A', 'AAAA'].includes(r.type)) + delete ret.ttl; + return ret; +} diff --git a/test/js/node/test/parallel/test-dns-resolvens-typeerror.js b/test/js/node/test/parallel/test-dns-resolvens-typeerror.js new file mode 100644 index 0000000000..c1eea2ddeb --- /dev/null +++ b/test/js/node/test/parallel/test-dns-resolvens-typeerror.js @@ -0,0 +1,55 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); + +// This test ensures `dns.resolveNs()` does not raise a C++-land assertion error +// and throw a JavaScript TypeError instead. +// Issue https://github.com/nodejs/node-v0.x-archive/issues/7070 + +const assert = require('assert'); +const dns = require('dns'); +const dnsPromises = dns.promises; + +assert.throws( + () => dnsPromises.resolveNs([]), // bad name + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^(The "(host)?name" argument must be of type string|Expected hostname to be a string)/ + } +); +assert.throws( + () => dns.resolveNs([]), // bad name + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^(The "(host)?name" argument must be of type string|Expected hostname to be a string)/ + } +); +assert.throws( + () => dns.resolveNs(''), // bad callback + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + } +); diff --git a/test/js/node/test/parallel/test-dns-set-default-order.js b/test/js/node/test/parallel/test-dns-set-default-order.js new file mode 100644 index 0000000000..47bc08e6b1 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-set-default-order.js @@ -0,0 +1,108 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { promisify } = require('util'); + +// Test that `dns.setDefaultResultOrder()` and +// `dns.promises.setDefaultResultOrder()` work as expected. + +const originalLookup = Bun.dns.lookup; +const calls = []; +Bun.dns.lookup = common.mustCallAtLeast((...args) => { + calls.push(args); + return originalLookup(...args); +}, 1); + +const dns = require('dns'); +const dnsPromises = dns.promises; + +// We want to test the parameter of order only so that we +// ignore possible errors here. +function allowFailed(fn) { + return fn.catch((_err) => { + // + }); +} + +assert.throws(() => dns.setDefaultResultOrder('my_order'), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.promises.setDefaultResultOrder('my_order'), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.setDefaultResultOrder(4), { + code: 'ERR_INVALID_ARG_VALUE', +}); +assert.throws(() => dns.promises.setDefaultResultOrder(4), { + code: 'ERR_INVALID_ARG_VALUE', +}); + +(async () => { + let callsLength = 0; + const checkParameter = (expected) => { + assert.strictEqual(calls.length, callsLength + 1); + const { order } = calls[callsLength][1]; + assert.strictEqual(order, expected); + callsLength += 1; + }; + + dns.setDefaultResultOrder('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('verbatim'); + + dns.setDefaultResultOrder('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); + + dns.setDefaultResultOrder('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); + + dns.promises.setDefaultResultOrder('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('verbatim'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('verbatim'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('verbatim'); + + dns.promises.setDefaultResultOrder('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv4first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv4first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv4first'); + + dns.promises.setDefaultResultOrder('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org')); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org')); + checkParameter('ipv6first'); + await allowFailed(promisify(dns.lookup)('example.org', {})); + checkParameter('ipv6first'); + await allowFailed(dnsPromises.lookup('example.org', {})); + checkParameter('ipv6first'); +})().then(common.mustCall()); diff --git a/test/js/node/test/parallel/test-dns-setlocaladdress.js b/test/js/node/test/parallel/test-dns-setlocaladdress.js new file mode 100644 index 0000000000..25bece328f --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setlocaladdress.js @@ -0,0 +1,40 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const dns = require('dns'); +const resolver = new dns.Resolver(); +const promiseResolver = new dns.promises.Resolver(); + +// Verifies that setLocalAddress succeeds with IPv4 and IPv6 addresses +{ + resolver.setLocalAddress('127.0.0.1'); + resolver.setLocalAddress('::1'); + resolver.setLocalAddress('127.0.0.1', '::1'); + promiseResolver.setLocalAddress('127.0.0.1', '::1'); +} + +// Verify that setLocalAddress throws if called with an invalid address +{ + assert.throws(() => { + resolver.setLocalAddress('127.0.0.1', '127.0.0.1'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress('::1', '::1'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress('bad'); + }, Error); + assert.throws(() => { + resolver.setLocalAddress(123); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => { + resolver.setLocalAddress('127.0.0.1', 42); + }, { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => { + resolver.setLocalAddress(); + }, Error); + assert.throws(() => { + promiseResolver.setLocalAddress(); + }, Error); +} diff --git a/test/js/node/test/parallel/test-dns-setserver-when-querying.js b/test/js/node/test/parallel/test-dns-setserver-when-querying.js new file mode 100644 index 0000000000..1a002df498 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setserver-when-querying.js @@ -0,0 +1,29 @@ +'use strict'; + +const common = require('../common'); + +const assert = require('assert'); +const dns = require('dns'); + +const localhost = [ '127.0.0.1' ]; + +{ + // Fix https://github.com/nodejs/node/issues/14734 + + { + const resolver = new dns.Resolver(); + resolver.resolve('localhost', common.mustCall()); + + assert.throws(resolver.setServers.bind(resolver, localhost), { + code: 'ERR_DNS_SET_SERVERS_FAILED', + message: /[Tt]here are pending queries/ + }); + } + + { + dns.resolve('localhost', common.mustCall()); + + // should not throw + dns.setServers(localhost); + } +} diff --git a/test/js/node/test/parallel/test-dns-setservers-type-check.js b/test/js/node/test/parallel/test-dns-setservers-type-check.js new file mode 100644 index 0000000000..7a19dc5eb0 --- /dev/null +++ b/test/js/node/test/parallel/test-dns-setservers-type-check.js @@ -0,0 +1,117 @@ +'use strict'; +const common = require('../common'); +const { addresses } = require('../common/internet'); +const assert = require('assert'); +const dns = require('dns'); +const resolver = new dns.promises.Resolver(); +const dnsPromises = dns.promises; +const promiseResolver = new dns.promises.Resolver(); + +{ + [ + null, + undefined, + Number(addresses.DNS4_SERVER), + addresses.DNS4_SERVER, + { + address: addresses.DNS4_SERVER + }, + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers" argument must be an instance of Array\./ + }; + assert.throws( + () => { + dns.setServers(val); + }, errObj + ); + assert.throws( + () => { + resolver.setServers(val); + }, errObj + ); + assert.throws( + () => { + dnsPromises.setServers(val); + }, errObj + ); + assert.throws( + () => { + promiseResolver.setServers(val); + }, errObj + ); + }); +} + +{ + [ + [null], + [undefined], + [Number(addresses.DNS4_SERVER)], + [ + { + address: addresses.DNS4_SERVER + }, + ], + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers\[0\]" argument must be of type string\./ + }; + assert.throws( + () => { + dns.setServers(val); + }, errObj + ); + assert.throws( + () => { + resolver.setServers(val); + }, errObj + ); + assert.throws( + () => { + dnsPromises.setServers(val); + }, errObj + ); + assert.throws( + () => { + promiseResolver.setServers(val); + }, errObj + ); + }); +} + +// This test for 'dns/promises' +{ + const { + setServers + } = require('dns/promises'); + + // This should not throw any error. + (async () => { + setServers([ '127.0.0.1' ]); + })().then(common.mustCall()); + + [ + [null], + [undefined], + [Number(addresses.DNS4_SERVER)], + [ + { + address: addresses.DNS4_SERVER + }, + ], + ].forEach((val) => { + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "servers\[0\]" argument must be of type string\./ + }; + assert.throws(() => { + setServers(val); + }, errObj); + }); +} diff --git a/test/js/node/test/parallel/test-dns.js b/test/js/node/test/parallel/test-dns.js new file mode 100644 index 0000000000..8c2b0f8e48 --- /dev/null +++ b/test/js/node/test/parallel/test-dns.js @@ -0,0 +1,461 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const dnstools = require('../common/dns'); +const assert = require('assert'); + +const dns = require('dns'); +const dnsPromises = dns.promises; +const dgram = require('dgram'); + +const existing = dns.getServers(); +assert(existing.length > 0); + +// Verify that setServers() handles arrays with holes and other oddities +{ + const servers = []; + + servers[0] = '127.0.0.1'; + servers[2] = '0.0.0.0'; + dns.setServers(servers); + + assert.deepStrictEqual(dns.getServers(), ['127.0.0.1', '0.0.0.0']); +} + +{ + const servers = ['127.0.0.1', '192.168.1.1']; + + servers[3] = '127.1.0.1'; + servers[4] = '127.1.0.1'; + servers[5] = '127.1.1.1'; + + Object.defineProperty(servers, 2, { + enumerable: true, + get: () => { + servers.length = 3; + return '0.0.0.0'; + } + }); + + dns.setServers(servers); + assert.deepStrictEqual(dns.getServers(), [ + '127.0.0.1', + '192.168.1.1', + '0.0.0.0', + ]); +} + +{ + // Various invalidities, all of which should throw a clean error. + const invalidServers = [ + ' ', + '\n', + '\0', + '1'.repeat(3 * 4), + // Check for REDOS issues. + ':'.repeat(100000), + '['.repeat(100000), + '['.repeat(100000) + ']'.repeat(100000) + 'a', + ]; + invalidServers.forEach((serv) => { + assert.throws( + () => { + dns.setServers([serv]); + }, + { + name: 'TypeError', + code: 'ERR_INVALID_IP_ADDRESS' + } + ); + }); +} + +const goog = [ + '8.8.8.8', + '8.8.4.4', +]; +dns.setServers(goog); +assert.deepStrictEqual(dns.getServers(), goog); +assert.throws(() => dns.setServers(['foobar']), { + code: 'ERR_INVALID_IP_ADDRESS', + name: 'TypeError', + message: 'Invalid IP address: foobar' +}); +assert.throws(() => dns.setServers(['127.0.0.1:va']), { + code: 'ERR_INVALID_IP_ADDRESS', + name: 'TypeError', + message: 'Invalid IP address: 127.0.0.1:va' +}); +assert.deepStrictEqual(dns.getServers(), goog); + +const goog6 = [ + '2001:4860:4860::8888', + '2001:4860:4860::8844', +]; +dns.setServers(goog6); +assert.deepStrictEqual(dns.getServers(), goog6); + +goog6.push('4.4.4.4'); +dns.setServers(goog6); +assert.deepStrictEqual(dns.getServers(), goog6); + +const ports = [ + '4.4.4.4:53', + '[2001:4860:4860::8888]:53', + '103.238.225.181:666', + '[fe80::483a:5aff:fee6:1f04]:666', + '[fe80::483a:5aff:fee6:1f04]', +]; +const portsExpected = [ + '4.4.4.4', + '2001:4860:4860::8888', + '103.238.225.181:666', + '[fe80::483a:5aff:fee6:1f04]:666', + 'fe80::483a:5aff:fee6:1f04', +]; +dns.setServers(ports); +assert.deepStrictEqual(dns.getServers(), portsExpected); + +dns.setServers([]); +assert.deepStrictEqual(dns.getServers(), []); + +{ + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "rrtype" argument must be of type string\. Received( an instance of Array|: \[\]|: "object")$/ + }; + assert.throws(() => { + dns.resolve('example.com', [], common.mustNotCall()); + }, errObj); + assert.throws(() => { + dnsPromises.resolve('example.com', []); + }, errObj); +} +{ + const errObj = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /The "(host)?name" argument must be of type string\. Received:? undefined$/ + }; + assert.throws(() => { + dnsPromises.resolve(); + }, errObj); +} + +// dns.lookup should accept only falsey and string values +{ + const errorReg = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: /^The "hostname" argument must be of type string\. Received:? .*|^Expected hostname to be a string/ + }; + + assert.throws(() => dns.lookup({}, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup([], common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(true, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(1, common.mustNotCall()), errorReg); + + assert.throws(() => dns.lookup(common.mustNotCall(), common.mustNotCall()), + errorReg); + + assert.throws(() => dnsPromises.lookup({}), errorReg); + assert.throws(() => dnsPromises.lookup([]), errorReg); + assert.throws(() => dnsPromises.lookup(true), errorReg); + assert.throws(() => dnsPromises.lookup(1), errorReg); + assert.throws(() => dnsPromises.lookup(common.mustNotCall()), errorReg); +} + +// dns.lookup should accept falsey values +{ + const checkCallback = (err, address, family) => { + assert.ifError(err); + assert.strictEqual(address, null); + assert.strictEqual(family, 4); + }; + + ['', null, undefined, 0, NaN].forEach(async (value) => { + const res = await dnsPromises.lookup(value); + assert.deepStrictEqual(res, { address: null, family: 4 }); + dns.lookup(value, common.mustCall(checkCallback)); + }); +} + +{ + // Make sure that dns.lookup throws if hints does not represent a valid flag. + // (dns.V4MAPPED | dns.ADDRCONFIG | dns.ALL) + 1 is invalid because: + // - it's different from dns.V4MAPPED and dns.ADDRCONFIG and dns.ALL. + // - it's different from any subset of them bitwise ored. + // - it's different from 0. + // - it's an odd number different than 1, and thus is invalid, because + // flags are either === 1 or even. + const hints = (dns.V4MAPPED | dns.ADDRCONFIG | dns.ALL) + 1; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The (argument 'hints'|"hints" option) is invalid\. Received:? \d+/ + }; + + assert.throws(() => { + dnsPromises.lookup('nodejs.org', { hints }); + }, err); + assert.throws(() => { + dns.lookup('nodejs.org', { hints }, common.mustNotCall()); + }, err); +} + +assert.throws(() => dns.lookup('nodejs.org'), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => dns.lookup('nodejs.org', 4), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +assert.throws(() => dns.lookup('', { + family: 'nodejs.org', + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, +}), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +dns.lookup('', { family: 4, hints: 0 }, common.mustCall()); + +dns.lookup('', { + family: 6, + hints: dns.ADDRCONFIG +}, common.mustCall()); + +dns.lookup('', { hints: dns.V4MAPPED }, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.V4MAPPED | dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, + family: 'IPv4' +}, common.mustCall()); + +dns.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL, + family: 'IPv6' +}, common.mustCall()); + +(async function() { + await dnsPromises.lookup('', { family: 4, hints: 0 }); + await dnsPromises.lookup('', { family: 6, hints: dns.ADDRCONFIG }); + await dnsPromises.lookup('', { hints: dns.V4MAPPED }); + await dnsPromises.lookup('', { hints: dns.ADDRCONFIG | dns.V4MAPPED }); + await dnsPromises.lookup('', { hints: dns.ALL }); + await dnsPromises.lookup('', { hints: dns.V4MAPPED | dns.ALL }); + await dnsPromises.lookup('', { + hints: dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL + }); + await dnsPromises.lookup('', { order: 'verbatim' }); +})().then(common.mustCall()); + +{ + const err = { + code: 'ERR_MISSING_ARGS', + name: 'TypeError', + message: 'The "address", "port", and "callback" arguments must be ' + + 'specified' + }; + + assert.throws(() => dns.lookupService('0.0.0.0'), err); + err.message = 'The "address" and "port" arguments must be specified'; + assert.throws(() => dnsPromises.lookupService('0.0.0.0'), err); +} + +{ + const invalidAddress = 'fasdfdsaf'; + const err = { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The (argument 'address'|"address" argument) is invalid\. Received/ + }; + + assert.throws(() => { + dnsPromises.lookupService(invalidAddress, 0); + }, err); + + assert.throws(() => { + dns.lookupService(invalidAddress, 0, common.mustNotCall()); + }, err); +} + +const portErr = (port) => { + const err = { + code: 'ERR_SOCKET_BAD_PORT', + name: 'RangeError' + }; + + assert.throws(() => { + dnsPromises.lookupService('0.0.0.0', port); + }, err); + + assert.throws(() => { + dns.lookupService('0.0.0.0', port, common.mustNotCall()); + }, err); +}; +[null, undefined, 65538, 'test', NaN, Infinity, Symbol(), 0n, true, false, '', () => {}, {}].forEach(portErr); + +assert.throws(() => { + dns.lookupService('0.0.0.0', 80, null); +}, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' +}); + +{ + dns.resolveMx('foo.onion', function(err) { + assert.strictEqual(err.code, 'ENOTFOUND'); + assert.strictEqual(err.syscall, 'queryMx'); + assert.strictEqual(err.hostname, 'foo.onion'); + assert.strictEqual(err.message, 'queryMx ENOTFOUND foo.onion'); + }); +} + +{ + const cases = [ + // { method: 'resolveAny', + // answers: [ + // { type: 'A', address: '1.2.3.4', ttl: 0 }, + // { type: 'AAAA', address: '::42', ttl: 0 }, + // { type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 0 }, + // { type: 'NS', value: 'foobar.org', ttl: 0 }, + // { type: 'PTR', value: 'baz.org', ttl: 0 }, + // { + // type: 'SOA', + // nsname: 'ns1.example.com', + // hostmaster: 'admin.example.com', + // serial: 3210987654, + // refresh: 900, + // retry: 900, + // expire: 1800, + // minttl: 3333333333 + // }, + // ] }, + + { method: 'resolve4', + options: { ttl: true }, + answers: [ { type: 'A', address: '1.2.3.4', ttl: 0 } ] }, + + { method: 'resolve6', + options: { ttl: true }, + answers: [ { type: 'AAAA', address: '::42', ttl: 0 } ] }, + + { method: 'resolveSoa', + answers: [ + { + type: 'SOA', + nsname: 'ns1.example.com', + hostmaster: 'admin.example.com', + serial: 3210987654, + refresh: 900, + retry: 900, + expire: 1800, + minttl: 3333333333 + }, + ] }, + ]; + + const server = dgram.createSocket('udp4'); + + server.on('message', common.mustCallAtLeast((msg, { address, port }) => { + const parsed = dnstools.parseDNSPacket(msg); + const domain = parsed.questions[0].domain; + assert.strictEqual(domain, 'example.org'); + + server.send(dnstools.writeDNSPacket({ + id: parsed.id, + questions: parsed.questions, + answers: cases[0].answers.map( + (answer) => Object.assign({ domain }, answer) + ), + }), port, address); + }, cases.length * 2 - 1)); + + server.bind(0, common.mustCall(() => { + const address = server.address(); + dns.setServers([`127.0.0.1:${address.port}`]); + + function validateResults(res) { + if (!Array.isArray(res)) + res = [res]; + + assert.deepStrictEqual(res.map(tweakEntry), + cases[0].answers.map(tweakEntry)); + } + + function tweakEntry(r) { + const ret = { ...r }; + + const { method } = cases[0]; + + // TTL values are only provided for A and AAAA entries. + if (!['A', 'AAAA'].includes(ret.type) && !/^resolve(4|6)?$/.test(method)) + delete ret.ttl; + + if (method !== 'resolveAny') + delete ret.type; + + return ret; + } + + (async function nextCase() { + if (cases.length === 0) + return server.close(); + + const { method, options } = cases[0]; + + validateResults(await dnsPromises[method]('example.org', options)); + + dns[method]('example.org', ...(options? [options] : []), common.mustSucceed((res) => { + validateResults(res); + cases.shift(); + nextCase(); + })); + })().then(common.mustCall()); + + })); +}