const Bun = @This(); const default_allocator = @import("bun").default_allocator; const bun = @import("bun"); const Environment = bun.Environment; const NetworkThread = @import("bun").HTTP.NetworkThread; const Global = bun.Global; const strings = bun.strings; const string = bun.string; const Output = @import("bun").Output; const MutableString = @import("bun").MutableString; const std = @import("std"); const Allocator = std.mem.Allocator; const JSC = @import("bun").JSC; const JSValue = JSC.JSValue; const JSGlobalObject = JSC.JSGlobalObject; const c_ares = bun.c_ares; const GetAddrInfoAsyncCallback = fn (i32, ?*std.c.addrinfo, ?*anyopaque) callconv(.C) void; const LibInfo = struct { // static int32_t (*getaddrinfo_async_start)(mach_port_t*, // const char*, // const char*, // const struct addrinfo*, // getaddrinfo_async_callback, // void*); // static int32_t (*getaddrinfo_async_handle_reply)(void*); // static void (*getaddrinfo_async_cancel)(mach_port_t); // typedef void getaddrinfo_async_callback(int32_t, struct addrinfo*, void*) const GetaddrinfoAsyncStart = fn (*?*anyopaque, noalias node: ?[*:0]const u8, noalias service: ?[*:0]const u8, noalias hints: ?*const std.c.addrinfo, callback: *const GetAddrInfoAsyncCallback, noalias context: ?*anyopaque) callconv(.C) i32; const GetaddrinfoAsyncHandleReply = fn (?**anyopaque) callconv(.C) i32; const GetaddrinfoAsyncCancel = fn (?**anyopaque) callconv(.C) void; var handle: ?*anyopaque = null; var loaded = false; pub fn getHandle() ?*anyopaque { if (loaded) return handle; loaded = true; const RTLD_LAZY = 1; const RTLD_LOCAL = 4; handle = std.c.dlopen("libinfo.dylib", RTLD_LAZY | RTLD_LOCAL); if (handle == null) Output.debug("libinfo.dylib not found", .{}); return handle; } pub const getaddrinfo_async_start = struct { pub fn get() ?*const GetaddrinfoAsyncStart { bun.Environment.onlyMac(); return bun.C.dlsymWithHandle(*const GetaddrinfoAsyncStart, "getaddrinfo_async_start", getHandle); } }.get; pub const getaddrinfo_async_handle_reply = struct { pub fn get() ?*const GetaddrinfoAsyncHandleReply { bun.Environment.onlyMac(); return bun.C.dlsymWithHandle(*const GetaddrinfoAsyncHandleReply, "getaddrinfo_async_handle_reply", getHandle); } }.get; pub fn get() ?*const GetaddrinfoAsyncCancel { bun.Environment.onlyMac(); return bun.C.dlsymWithHandle(*const GetaddrinfoAsyncCancel, "getaddrinfo_async_cancel", getHandle); } pub fn lookup(this: *DNSResolver, query: GetAddrInfo, globalThis: *JSC.JSGlobalObject) JSC.JSValue { bun.Environment.onlyMac(); const getaddrinfo_async_start_ = LibInfo.getaddrinfo_async_start() orelse return LibC.lookup(this, query, globalThis); var key = GetAddrInfoRequest.PendingCacheKey.init(query); var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native); if (cache == .inflight) { var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch unreachable; cache.inflight.append(dns_lookup); return dns_lookup.promise.value(); } var name_buf: [1024]u8 = undefined; _ = strings.copy(name_buf[0..], query.name); name_buf[query.name.len] = 0; var name_z = name_buf[0..query.name.len :0]; var request = GetAddrInfoRequest.init( cache, .{ .libinfo = undefined }, this, query, globalThis, "pending_host_cache_native", ) catch unreachable; const promise_value = request.head.promise.value(); const errno = getaddrinfo_async_start_( &request.backend.libinfo.machport, name_z.ptr, null, null, GetAddrInfoRequest.getAddrInfoAsyncCallback, request, ); if (errno != 0) { request.head.promise.reject(globalThis, globalThis.createErrorInstance("getaddrinfo_async_start error: {s}", .{@tagName(std.c.getErrno(errno))})); if (request.cache.pending_cache) this.pending_host_cache_native.available.set(request.cache.pos_in_pending); this.vm.allocator.destroy(request); return promise_value; } std.debug.assert(request.backend.libinfo.machport != null); request.backend.libinfo.file_poll = bun.JSC.FilePoll.init(this.vm, std.math.maxInt(i32) - 1, .{}, GetAddrInfoRequest, request); std.debug.assert( request.backend.libinfo.file_poll.?.registerWithFd( this.vm.uws_event_loop.?, .machport, true, @ptrToInt(request.backend.libinfo.machport), ) == .result, ); return promise_value; } }; const LibC = struct { pub fn lookup(this: *DNSResolver, query_init: GetAddrInfo, globalThis: *JSC.JSGlobalObject) JSC.JSValue { const key = GetAddrInfoRequest.PendingCacheKey.init(query_init); var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_native); if (cache == .inflight) { var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch unreachable; cache.inflight.append(dns_lookup); return dns_lookup.promise.value(); } var query = query_init.clone(); var request = GetAddrInfoRequest.init( cache, .{ .libc = .{ .query = query, }, }, this, query, globalThis, "pending_host_cache_native", ) catch unreachable; const promise_value = request.head.promise.value(); var io = GetAddrInfoRequest.Task.createOnJSThread(this.vm.allocator, globalThis, request) catch unreachable; io.schedule(); return promise_value; } }; pub fn addressToString( allocator: std.mem.Allocator, address: std.net.Address, ) JSC.ZigString { const str: []const u8 = brk: { switch (address.any.family) { std.os.AF.INET => { var self = address.in; const bytes = @ptrCast(*const [4]u8, &self.sa.addr); break :brk std.fmt.allocPrint(allocator, "{}.{}.{}.{}", .{ bytes[0], bytes[1], bytes[2], bytes[3], }) catch unreachable; }, std.os.AF.INET6 => { var out = std.fmt.allocPrint(allocator, "{any}", .{address}) catch unreachable; // TODO: this is a hack, fix it // This removes [.*]:port // ^ ^^^^^^ break :brk out[1 .. out.len - 1 - std.fmt.count("{d}", .{address.in6.getPort()}) - 1]; }, std.os.AF.UNIX => { break :brk std.mem.sliceTo(&address.un.path, 0); }, else => break :brk "", } }; return JSC.ZigString.init(str); } pub fn normalizeDNSName(name: []const u8, backend: *GetAddrInfo.Backend) []const u8 { if (backend.* == .c_ares) { // https://github.com/c-ares/c-ares/issues/477 if (strings.endsWithComptime(name, ".localhost")) { backend.* = .system; return "localhost"; } else if (strings.endsWithComptime(name, ".local")) { backend.* = .system; // https://github.com/c-ares/c-ares/pull/463 } else if (strings.isIPV6Address(name)) { backend.* = .system; } } return name; } pub fn addressToJS( allocator: std.mem.Allocator, address: std.net.Address, globalThis: *JSC.JSGlobalObject, ) JSC.JSValue { return addressToString(allocator, address).toValueGC(globalThis); } fn addrInfoCount(addrinfo: *std.c.addrinfo) u32 { var count: u32 = 1; var current: ?*std.c.addrinfo = addrinfo.next; while (current != null) : (current = current.?.next) { count += @boolToInt(current.?.addr != null); } return count; } pub fn addrInfoToJSArray( parent_allocator: std.mem.Allocator, addr_info: *std.c.addrinfo, globalThis: *JSC.JSGlobalObject, ) JSC.JSValue { var stack = std.heap.stackFallback(2048, parent_allocator); var arena = std.heap.ArenaAllocator.init(stack.get()); const array = JSC.JSValue.createEmptyArray( globalThis, addrInfoCount(addr_info), ); { defer arena.deinit(); var allocator = arena.allocator(); var j: u32 = 0; var current: ?*std.c.addrinfo = addr_info; while (current) |this_node| : (current = current.?.next) { array.putIndex( globalThis, j, bun.JSC.DNS.GetAddrInfo.Result.toJS( &(bun.JSC.DNS.GetAddrInfo.Result.fromAddrInfo(this_node) orelse continue), globalThis, allocator, ), ); j += 1; } } return array; } pub const GetAddrInfo = struct { name: []const u8 = "", port: u16 = 0, options: Options = Options{}, pub fn clone(this: GetAddrInfo) GetAddrInfo { return GetAddrInfo{ .name = bun.default_allocator.dupe(u8, this.name) catch unreachable, .port = this.port, .options = this.options, }; } pub fn toCAres(this: GetAddrInfo) bun.c_ares.AddrInfo_hints { var hints: bun.c_ares.AddrInfo_hints = undefined; @memset(std.mem.asBytes(&hints), 0, @sizeOf(bun.c_ares.AddrInfo_hints)); hints.ai_family = this.options.family.toLibC(); hints.ai_socktype = this.options.socktype.toLibC(); hints.ai_protocol = this.options.protocol.toLibC(); hints.ai_flags = this.options.flags; return hints; } pub fn hash(self: GetAddrInfo) u64 { var hasher = std.hash.Wyhash.init(0); const bytes = std.mem.asBytes(&self.port) ++ std.mem.asBytes(&self.options); hasher.update(bytes); hasher.update(self.name); return hasher.final(); } pub const Options = packed struct { family: Family = .unspecified, socktype: SocketType = .unspecified, protocol: Protocol = .unspecified, backend: Backend = Backend.default, flags: i32 = 0, pub fn toLibC(this: Options) ?std.c.addrinfo { if (this.family == .unspecified and this.socktype == .unspecified and this.protocol == .unspecified and this.flags == 0) { return null; } var hints: std.c.addrinfo = undefined; @memset(std.mem.asBytes(&hints), 0, @sizeOf(std.c.addrinfo)); hints.family = this.family.toLibC(); hints.socktype = this.socktype.toLibC(); hints.protocol = this.protocol.toLibC(); hints.flags = this.flags; return hints; } pub fn fromJS(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject) !Options { if (value.isEmptyOrUndefinedOrNull()) return Options{}; if (value.isObject()) { var options = Options{}; if (value.get(globalObject, "family")) |family| { options.family = try Family.fromJS(family, globalObject); } if (value.get(globalObject, "socketType") orelse value.get(globalObject, "socktype")) |socktype| { options.socktype = try SocketType.fromJS(socktype, globalObject); } if (value.get(globalObject, "protocol")) |protocol| { options.protocol = try Protocol.fromJS(protocol, globalObject); } if (value.get(globalObject, "backend")) |backend| { options.backend = try Backend.fromJS(backend, globalObject); } if (value.get(globalObject, "flags")) |flags| { if (!flags.isNumber()) return error.InvalidFlags; options.flags = flags.coerce(i32, globalObject); } return options; } return error.InvalidOptions; } }; pub const Family = enum(u2) { unspecified, inet, inet6, unix, pub const map = bun.ComptimeStringMap(Family, .{ .{ "IPv4", Family.inet }, .{ "IPv6", Family.inet6 }, .{ "ipv4", Family.inet }, .{ "ipv6", Family.inet6 }, .{ "any", Family.unspecified }, }); pub fn fromJS(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject) !Family { if (value.isEmptyOrUndefinedOrNull()) return .unspecified; if (value.isNumber()) { return switch (value.to(i32)) { 0 => .unspecified, 4 => .inet, 6 => .inet6, else => return error.InvalidFamily, }; } if (value.isString()) { const str = value.getZigString(globalObject); if (str.len == 0) return .unspecified; return map.getWithEql(str, JSC.ZigString.eqlComptime) orelse return error.InvalidFamily; } return error.InvalidFamily; } pub fn toLibC(this: Family) i32 { return switch (this) { .unspecified => 0, .inet => std.os.AF.INET, .inet6 => std.os.AF.INET6, .unix => std.os.AF.UNIX, }; } }; pub const SocketType = enum(u2) { unspecified, stream, dgram, const map = bun.ComptimeStringMap(SocketType, .{ .{ "stream", SocketType.stream }, .{ "dgram", SocketType.dgram }, .{ "tcp", SocketType.stream }, .{ "udp", SocketType.dgram }, }); pub fn toLibC(this: SocketType) i32 { switch (this) { .unspecified => return 0, .stream => return std.os.SOCK.STREAM, .dgram => return std.os.SOCK.DGRAM, } } pub fn fromJS(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject) !SocketType { if (value.isEmptyOrUndefinedOrNull()) return .unspecified; if (value.isNumber()) { return switch (value.to(i32)) { 0 => .unspecified, 1 => .stream, 2 => .dgram, else => return error.InvalidSocketType, }; } if (value.isString()) { const str = value.getZigString(globalObject); if (str.len == 0) return .unspecified; return map.getWithEql(str, JSC.ZigString.eqlComptime) orelse return error.InvalidSocketType; } return error.InvalidSocketType; } }; pub const Protocol = enum(u2) { unspecified, tcp, udp, const map = bun.ComptimeStringMap(Protocol, .{ .{ "tcp", Protocol.tcp }, .{ "udp", Protocol.udp }, }); pub fn fromJS(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject) !Protocol { if (value.isEmptyOrUndefinedOrNull()) return .unspecified; if (value.isNumber()) { return switch (value.to(i32)) { 0 => .unspecified, 6 => .tcp, 17 => .udp, else => return error.InvalidProtocol, }; } if (value.isString()) { const str = value.getZigString(globalObject); if (str.len == 0) return .unspecified; return map.getWithEql(str, JSC.ZigString.eqlComptime) orelse return error.InvalidProtocol; } return error.InvalidProtocol; } pub fn toLibC(this: Protocol) i32 { switch (this) { .unspecified => return 0, .tcp => return std.os.IPPROTO.TCP, .udp => return std.os.IPPROTO.UDP, } } }; pub const Backend = enum(u2) { c_ares, system, libc, pub const label = bun.ComptimeStringMap(GetAddrInfo.Backend, .{ .{ "c-ares", .c_ares }, .{ "c_ares", .c_ares }, .{ "cares", .c_ares }, .{ "async", .c_ares }, .{ "libc", .libc }, .{ "system", .system }, .{ "getaddrinfo", .libc }, }); pub const default: GetAddrInfo.Backend = if (Environment.isMac) GetAddrInfo.Backend.system else GetAddrInfo.Backend.c_ares; pub fn fromJS(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject) !Backend { if (value.isEmptyOrUndefinedOrNull()) return default; if (value.isString()) { const str = value.getZigString(globalObject); if (str.len == 0) return default; return label.getWithEql(str, JSC.ZigString.eqlComptime) orelse return error.InvalidBackend; } return error.InvalidBackend; } }; pub const Result = struct { address: std.net.Address, ttl: i32 = 0, pub const List = std.ArrayList(Result); pub const Any = union(enum) { addrinfo: ?*std.c.addrinfo, list: List, pub fn toJS(this: Any, globalThis: *JSC.JSGlobalObject) ?JSC.JSValue { return switch (this) { .addrinfo => |addrinfo| addrInfoToJSArray(globalThis.allocator(), addrinfo orelse return null, globalThis), .list => |list| brk: { var stack = std.heap.stackFallback(2048, globalThis.allocator()); var arena = std.heap.ArenaAllocator.init(stack.get()); const array = JSC.JSValue.createEmptyArray(globalThis, @truncate(u32, list.items.len)); var i: u32 = 0; const items: []const Result = list.items; for (items) |item| { array.putIndex(globalThis, i, item.toJS(globalThis, arena.allocator())); i += 1; } break :brk array; }, }; } pub fn deinit(this: Any) void { switch (this) { .addrinfo => |addrinfo| { if (addrinfo) |a| { std.c.freeaddrinfo(a); } }, .list => |list| { var list_ = list; list_.deinit(); }, } } }; pub fn toList(allocator: std.mem.Allocator, addrinfo: *std.c.addrinfo) !List { var list = try List.initCapacity(allocator, addrInfoCount(addrinfo)); var addr: ?*std.c.addrinfo = addrinfo; while (addr) |a| : (addr = a.next) { list.appendAssumeCapacity(fromAddrInfo(a) orelse continue); } return list; } pub fn fromAddrInfo(addrinfo: *std.c.addrinfo) ?Result { return Result{ .address = std.net.Address.initPosix(@alignCast(4, addrinfo.addr orelse return null)), // no TTL in POSIX getaddrinfo() .ttl = 0, }; } pub fn toJS(this: *const Result, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) JSValue { const obj = JSC.JSValue.createEmptyObject(globalThis, 3); obj.put(globalThis, JSC.ZigString.static("address"), addressToJS(allocator, this.address, globalThis)); obj.put(globalThis, JSC.ZigString.static("family"), switch (this.address.any.family) { std.os.AF.INET => JSValue.jsNumber(4), std.os.AF.INET6 => JSValue.jsNumber(6), else => JSValue.jsNumber(0), }); obj.put(globalThis, JSC.ZigString.static("ttl"), JSValue.jsNumber(this.ttl)); return obj; } }; }; pub const GetAddrInfoRequest = struct { const log = Output.scoped(.GetAddrInfoRequest, false); backend: Backend = undefined, resolver_for_caching: ?*DNSResolver = null, hash: u64 = 0, cache: CacheConfig = CacheConfig{}, head: DNSLookup, tail: *DNSLookup = undefined, task: bun.ThreadPool.Task = undefined, pub fn init( cache: DNSResolver.CacheHit, backend: Backend, resolver: ?*DNSResolver, query: GetAddrInfo, globalThis: *JSC.JSGlobalObject, comptime cache_field: []const u8, ) !*GetAddrInfoRequest { var request = try globalThis.allocator().create(GetAddrInfoRequest); request.* = .{ .backend = backend, .resolver_for_caching = resolver, .hash = query.hash(), .head = .{ .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .allocated = false, }, }; request.tail = &request.head; if (cache == .new) { request.resolver_for_caching = resolver; request.cache = CacheConfig{ .pending_cache = true, .entry_cache = false, .pos_in_pending = @truncate(u5, @field(resolver.?, cache_field).indexOf(cache.new).?), .name_len = @truncate(u9, query.name.len), }; cache.new.lookup = request; } return request; } pub const Task = bun.JSC.WorkTask(GetAddrInfoRequest, false); pub const CacheConfig = packed struct(u16) { pending_cache: bool = false, entry_cache: bool = false, pos_in_pending: u5 = 0, name_len: u9 = 0, }; pub const PendingCacheKey = struct { hash: u64, len: u16, lookup: *GetAddrInfoRequest = undefined, pub fn append(this: *PendingCacheKey, dns_lookup: *DNSLookup) void { var tail = this.lookup.tail; tail.next = dns_lookup; this.lookup.tail = dns_lookup; } pub fn init(query: GetAddrInfo) PendingCacheKey { return PendingCacheKey{ .hash = query.hash(), .len = @truncate(u16, query.name.len), .lookup = undefined, }; } }; pub fn getAddrInfoAsyncCallback( status: i32, addr_info: ?*std.c.addrinfo, arg: ?*anyopaque, ) callconv(.C) void { const this = @intToPtr(*GetAddrInfoRequest, @ptrToInt(arg)); log("getAddrInfoAsyncCallback: status={d}", .{status}); if (this.backend == .libinfo) { if (this.backend.libinfo.file_poll) |poll| poll.deinit(); } if (this.resolver_for_caching) |resolver| { if (this.cache.pending_cache) { resolver.drainPendingHostNative(this.cache.pos_in_pending, this.head.globalThis, status, .{ .addrinfo = addr_info }); return; } } var head = this.head; bun.default_allocator.destroy(this); head.processGetAddrInfoNative(status, addr_info); } pub const Backend = union(enum) { c_ares: void, libinfo: GetAddrInfoRequest.Backend.LibInfo, libc: union(enum) { success: GetAddrInfo.Result.List, err: i32, query: GetAddrInfo, pub fn run(this: *@This()) void { const query = this.query; defer bun.default_allocator.free(bun.constStrToU8(query.name)); var hints = query.options.toLibC(); var port_buf: [128]u8 = undefined; var port = std.fmt.bufPrintIntToSlice(&port_buf, query.port, 10, .lower, .{}); port_buf[port.len] = 0; var portZ = port_buf[0..port.len :0]; var hostname: [bun.MAX_PATH_BYTES]u8 = undefined; _ = strings.copy(hostname[0..], query.name); hostname[query.name.len] = 0; var addrinfo: *std.c.addrinfo = undefined; var host = hostname[0..query.name.len :0]; const debug_timer = bun.Output.DebugTimer.start(); const err = std.c.getaddrinfo( host.ptr, if (port.len > 0) portZ.ptr else null, if (hints) |*hint| hint else null, &addrinfo, ); JSC.Node.Syscall.syslog("getaddrinfo({s}, {d}) = {d} ({any})", .{ query.name, port, err, debug_timer, }); if (@enumToInt(err) != 0) { this.* = .{ .err = @enumToInt(err) }; return; } // do not free addrinfo when err != 0 // https://github.com/ziglang/zig/pull/14242 defer std.c.freeaddrinfo(addrinfo); this.* = .{ .success = GetAddrInfo.Result.toList(default_allocator, addrinfo) catch unreachable }; } }, pub const LibInfo = struct { file_poll: ?*bun.JSC.FilePoll = null, machport: ?*anyopaque = null, extern fn getaddrinfo_send_reply(*anyopaque, *const JSC.DNS.LibInfo.GetaddrinfoAsyncHandleReply) bool; pub fn onMachportChange(this: *GetAddrInfoRequest) void { if (comptime !Environment.isMac) unreachable; if (!getaddrinfo_send_reply(this.backend.libinfo.machport.?, JSC.DNS.LibInfo.getaddrinfo_async_handle_reply().?)) { log("onMachportChange: getaddrinfo_send_reply failed", .{}); getAddrInfoAsyncCallback(-1, null, this); } } }; }; pub const onMachportChange = Backend.LibInfo.onMachportChange; pub fn run(this: *GetAddrInfoRequest, task: *Task) void { this.backend.libc.run(); task.onFinish(); } pub fn then(this: *GetAddrInfoRequest, _: *JSC.JSGlobalObject) void { switch (this.backend.libc) { .success => |result| { const any = GetAddrInfo.Result.Any{ .list = result }; defer any.deinit(); if (this.resolver_for_caching) |resolver| { // if (this.cache.entry_cache and result != null and result.?.node != null) { // resolver.putEntryInCache(this.hash, this.cache.name_len, result.?); // } if (this.cache.pending_cache) { resolver.drainPendingHostNative(this.cache.pos_in_pending, this.head.globalThis, 0, any); return; } } var head = this.head; bun.default_allocator.destroy(this); head.onCompleteNative(any); }, .err => |err| { getAddrInfoAsyncCallback(err, null, this); }, else => unreachable, } } pub fn onCaresComplete(this: *GetAddrInfoRequest, err_: ?c_ares.Error, timeout: i32, result: ?*c_ares.AddrInfo) void { if (this.resolver_for_caching) |resolver| { // if (this.cache.entry_cache and result != null and result.?.node != null) { // resolver.putEntryInCache(this.hash, this.cache.name_len, result.?); // } if (this.cache.pending_cache) { resolver.drainPendingHostCares( this.cache.pos_in_pending, err_, timeout, result, ); return; } } var head = this.head; bun.default_allocator.destroy(this); head.processGetAddrInfo(err_, timeout, result); } }; pub const DNSLookup = struct { const log = Output.scoped(.DNSLookup, true); globalThis: *JSC.JSGlobalObject = undefined, promise: JSC.JSPromise.Strong, allocated: bool = false, next: ?*DNSLookup = null, pub fn init(globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) !*DNSLookup { var this = try allocator.create(DNSLookup); this.* = .{ .globalThis = globalThis, .promise = JSC.JSPromise.Strong.init(globalThis), .allocated = true, }; return this; } pub fn onCompleteNative(this: *DNSLookup, result: GetAddrInfo.Result.Any) void { const array = result.toJS(this.globalThis).?; this.onCompleteWithArray(array); } pub fn processGetAddrInfoNative(this: *DNSLookup, status: i32, result: ?*std.c.addrinfo) void { if (c_ares.Error.initEAI(status)) |err| { var promise = this.promise; var globalThis = this.globalThis; const error_value = brk: { if (err == .ESERVFAIL) { break :brk JSC.Node.Syscall.Error.fromCode(std.c.getErrno(-1), .getaddrinfo).toJSC(globalThis); } const error_value = globalThis.createErrorInstance("DNS lookup failed: {s}", .{err.label()}); error_value.put( globalThis, JSC.ZigString.static("code"), JSC.ZigString.init(err.code()).toValueGC(globalThis), ); break :brk error_value; }; this.deinit(); promise.reject(globalThis, error_value); return; } onCompleteNative(this, .{ .addrinfo = result }); } pub fn processGetAddrInfo(this: *DNSLookup, err_: ?c_ares.Error, _: i32, result: ?*c_ares.AddrInfo) void { if (err_) |err| { var promise = this.promise; var globalThis = this.globalThis; const error_value = globalThis.createErrorInstance("DNS lookup failed: {s}", .{err.label()}); error_value.put( globalThis, JSC.ZigString.static("code"), JSC.ZigString.init(err.code()).toValueGC(globalThis), ); this.deinit(); promise.reject(globalThis, error_value); return; } if (result == null or result.?.node == null) { var promise = this.promise; var globalThis = this.globalThis; const error_value = globalThis.createErrorInstance("DNS lookup failed: {s}", .{"No results"}); error_value.put( globalThis, JSC.ZigString.static("code"), JSC.ZigString.init("EUNREACHABLE").toValueGC(globalThis), ); this.deinit(); promise.reject(globalThis, error_value); return; } this.onComplete(result.?); } pub fn onComplete(this: *DNSLookup, result: *c_ares.AddrInfo) void { const array = result.toJSArray(this.globalThis.allocator(), this.globalThis); this.onCompleteWithArray(array); } pub fn onCompleteWithArray(this: *DNSLookup, result: JSC.JSValue) void { var promise = this.promise; var globalThis = this.globalThis; this.promise = .{}; this.deinit(); promise.resolveOnNextTick(globalThis, result); } pub fn deinit(this: *DNSLookup) void { if (this.allocated) this.globalThis.allocator().destroy(this); } }; pub const GlobalData = struct { resolver: DNSResolver, pub fn init(allocator: std.mem.Allocator, vm: *JSC.VirtualMachine) *GlobalData { var global = allocator.create(GlobalData) catch unreachable; global.* = .{ .resolver = .{ .vm = vm, .polls = std.AutoArrayHashMap(i32, ?*JSC.FilePoll).init(allocator), }, }; return global; } }; pub const DNSResolver = struct { const log = Output.scoped(.DNSResolver, true); channel: ?*c_ares.Channel = null, vm: *JSC.VirtualMachine, polls: std.AutoArrayHashMap(i32, ?*JSC.FilePoll) = undefined, pending_host_cache_cares: PendingCache = PendingCache.init(), pending_host_cache_native: PendingCache = PendingCache.init(), // entry_host_cache: std.BoundedArray(128) const PendingCache = bun.HiveArray(GetAddrInfoRequest.PendingCacheKey, 32); fn getKey(this: *DNSResolver, index: u8, comptime cache_name: []const u8) GetAddrInfoRequest.PendingCacheKey { var cache: *PendingCache = &@field(this, cache_name); std.debug.assert(!cache.available.isSet(index)); const entry = cache.buffer[index]; cache.buffer[index] = undefined; var available = cache.available; available.set(index); cache.available = available; return entry; } 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"); var addr = result orelse { var pending: ?*DNSLookup = key.lookup.head.next; key.lookup.head.processGetAddrInfo(err, timeout, null); bun.default_allocator.destroy(key.lookup); while (pending) |value| { pending = value.next; value.processGetAddrInfo(err, timeout, null); } return; }; var pending: ?*DNSLookup = key.lookup.head.next; var prev_global = key.lookup.head.globalThis; var array = addr.toJSArray(this.vm.allocator, prev_global); defer addr.deinit(); array.ensureStillAlive(); key.lookup.head.onCompleteWithArray(array); bun.default_allocator.destroy(key.lookup); array.ensureStillAlive(); // std.c.addrinfo while (pending) |value| { var new_global = value.globalThis; if (prev_global != new_global) { array = addr.toJSArray(this.vm.allocator, new_global); prev_global = new_global; } pending = value.next; { array.ensureStillAlive(); value.onCompleteWithArray(array); array.ensureStillAlive(); } } } pub fn drainPendingHostNative(this: *DNSResolver, index: u8, globalObject: *JSC.JSGlobalObject, err: i32, result: GetAddrInfo.Result.Any) void { const key = this.getKey(index, "pending_host_cache_native"); var array = result.toJS(globalObject) orelse { var pending: ?*DNSLookup = key.lookup.head.next; var head = key.lookup.head; head.processGetAddrInfoNative(err, null); bun.default_allocator.destroy(key.lookup); while (pending) |value| { pending = value.next; value.processGetAddrInfoNative(err, null); } return; }; var pending: ?*DNSLookup = key.lookup.head.next; var prev_global = key.lookup.head.globalThis; { array.ensureStillAlive(); key.lookup.head.onCompleteWithArray(array); bun.default_allocator.destroy(key.lookup); array.ensureStillAlive(); } // std.c.addrinfo while (pending) |value| { var new_global = value.globalThis; pending = value.next; if (prev_global != new_global) { array = result.toJS(new_global).?; prev_global = new_global; } { array.ensureStillAlive(); value.onCompleteWithArray(array); array.ensureStillAlive(); } } } pub const CacheHit = union(enum) { inflight: *GetAddrInfoRequest.PendingCacheKey, new: *GetAddrInfoRequest.PendingCacheKey, disabled: void, }; pub fn getOrPutIntoPendingCache( this: *DNSResolver, key: GetAddrInfoRequest.PendingCacheKey, comptime field: std.meta.FieldEnum(DNSResolver), ) CacheHit { var cache: *PendingCache = &@field(this, @tagName(field)); var inflight_iter = cache.available.iterator( .{ .kind = .unset, }, ); while (inflight_iter.next()) |index| { var entry: *GetAddrInfoRequest.PendingCacheKey = &cache.buffer[index]; if (entry.hash == key.hash and entry.len == key.len) { return .{ .inflight = entry }; } } if (cache.get()) |new| { new.hash = key.hash; new.len = key.len; return .{ .new = new }; } return .{ .disabled = {} }; } pub const ChannelResult = union(enum) { err: c_ares.Error, result: *c_ares.Channel, }; pub fn getChannel(this: *DNSResolver) ChannelResult { if (this.channel == null) { if (c_ares.Channel.init(DNSResolver, this)) |err| { return .{ .err = err }; } } return .{ .result = this.channel.? }; } pub fn onDNSPoll( this: *DNSResolver, poll: *JSC.FilePoll, ) void { var channel = this.channel orelse { _ = this.polls.orderedRemove(@intCast(i32, poll.fd)); poll.deinit(); return; }; channel.process( @intCast(i32, poll.fd), poll.isReadable(), poll.isWritable(), ); } pub fn onDNSSocketState( this: *DNSResolver, fd: i32, readable: bool, writable: bool, ) void { var vm = this.vm; if (!readable and !writable) { // read == 0 and write == 0 this is c-ares's way of notifying us that // the socket is now closed. We must free the data associated with // socket. if (this.polls.fetchOrderedRemove(fd)) |entry| { if (entry.value) |val| { val.deinitWithVM(vm); } } return; } var poll_entry = this.polls.getOrPut(fd) catch unreachable; if (!poll_entry.found_existing) { poll_entry.value_ptr.* = JSC.FilePoll.init(vm, fd, .{}, DNSResolver, this); } var poll = poll_entry.value_ptr.*.?; if (readable and !poll.flags.contains(.poll_readable)) _ = poll.register(vm.uws_event_loop.?, .readable, false); if (writable and !poll.flags.contains(.poll_writable)) _ = poll.register(vm.uws_event_loop.?, .writable, false); } const DNSQuery = struct { name: JSC.ZigString.Slice, record_type: RecordType, ttl: i32 = 0, }; pub const RecordType = enum(u8) { A = 1, AAAA = 28, CNAME = 5, MX = 15, NS = 2, PTR = 12, SOA = 6, SRV = 33, TXT = 16, pub const default = RecordType.A; pub const map = bun.ComptimeStringMap(RecordType, .{ .{ "A", .A }, .{ "AAAA", .AAAA }, .{ "CNAME", .CNAME }, .{ "MX", .MX }, .{ "NS", .NS }, .{ "PTR", .PTR }, .{ "SOA", .SOA }, .{ "SRV", .SRV }, .{ "TXT", .TXT }, .{ "a", .A }, .{ "aaaa", .AAAA }, .{ "cname", .CNAME }, .{ "mx", .MX }, .{ "ns", .NS }, .{ "ptr", .PTR }, .{ "soa", .SOA }, .{ "srv", .SRV }, .{ "txt", .TXT }, }); }; pub fn resolve(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { const arguments = callframe.arguments(3); if (arguments.len < 1) { globalThis.throwNotEnoughArguments("resolve", 2, arguments.len); return .zero; } const record_type: RecordType = if (arguments.len == 1) RecordType.default else brk: { const record_type_value = arguments.ptr[1]; if (record_type_value.isEmptyOrUndefinedOrNull() or !record_type_value.isString()) { break :brk RecordType.default; } const record_type_str = record_type_value.toStringOrNull(globalThis) orelse { return .zero; }; if (record_type_str.length() == 0) { break :brk RecordType.default; } break :brk RecordType.map.getWithEql(record_type_str.getZigString(globalThis), JSC.ZigString.eqlComptime) orelse { globalThis.throwInvalidArgumentType("resolve", "record", "one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT"); return .zero; }; }; _ = record_type; const name_value = arguments.ptr[0]; if (name_value.isEmptyOrUndefinedOrNull() or !name_value.isString()) { globalThis.throwInvalidArgumentType("resolve", "name", "string"); return .zero; } const name_str = name_value.toStringOrNull(globalThis) orelse { return .zero; }; if (name_str.length() == 0) { globalThis.throwInvalidArgumentType("resolve", "name", "non-empty string"); return .zero; } // const name = name_str.toSliceZ(globalThis).cloneZ(bun.default_allocator) catch unreachable; // TODO: return JSC.JSValue.jsUndefined(); } // pub fn reverse(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { // const arguments = callframe.arguments(3); // } pub fn lookup(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { const arguments = callframe.arguments(2); if (arguments.len < 1) { globalThis.throwNotEnoughArguments("lookup", 2, arguments.len); return .zero; } const name_value = arguments.ptr[0]; if (name_value.isEmptyOrUndefinedOrNull() or !name_value.isString()) { globalThis.throwInvalidArgumentType("lookup", "hostname", "string"); return .zero; } const name_str = name_value.toStringOrNull(globalThis) orelse { return .zero; }; if (name_str.length() == 0) { globalThis.throwInvalidArgumentType("lookup", "hostname", "non-empty string"); return .zero; } var options = GetAddrInfo.Options{}; var port: u16 = 0; if (arguments.len > 1 and arguments.ptr[1].isCell()) { if (arguments.ptr[1].get(globalThis, "port")) |port_value| { if (port_value.isNumber()) { port = port_value.to(u16); } } options = GetAddrInfo.Options.fromJS(arguments.ptr[1], globalThis) catch |err| { globalThis.throw("Invalid options passed to lookup(): {s}", .{@errorName(err)}); return .zero; }; } const name = name_str.toSlice(globalThis, bun.default_allocator); defer name.deinit(); var vm = globalThis.bunVM(); var resolver = vm.rareData().globalDNSResolver(vm); return resolver.doLookup(name.slice(), port, options, globalThis); } pub fn doLookup(this: *DNSResolver, name: []const u8, port: u16, options: GetAddrInfo.Options, globalThis: *JSC.JSGlobalObject) JSC.JSValue { var opts = options; var backend = opts.backend; const normalized = normalizeDNSName(name, &backend); opts.backend = backend; const query = GetAddrInfo{ .options = opts, .port = port, .name = normalized, }; return switch (opts.backend) { .c_ares => this.c_aresLookupWithNormalizedName(query, globalThis), .libc => LibC.lookup(this, query, globalThis), .system => if (comptime Environment.isMac) LibInfo.lookup(this, query, globalThis) else LibC.lookup(this, query, globalThis), }; } pub fn c_aresLookupWithNormalizedName(this: *DNSResolver, query: GetAddrInfo, globalThis: *JSC.JSGlobalObject) JSC.JSValue { var channel: *c_ares.Channel = switch (this.getChannel()) { .result => |res| res, .err => |err| { const system_error = JSC.SystemError{ .errno = -1, .code = JSC.ZigString.init(err.code()), .message = JSC.ZigString.init(err.label()), }; globalThis.throwValue(system_error.toErrorInstance(globalThis)); return .zero; }, }; const key = GetAddrInfoRequest.PendingCacheKey.init(query); var cache = this.getOrPutIntoPendingCache(key, .pending_host_cache_cares); if (cache == .inflight) { var dns_lookup = DNSLookup.init(globalThis, globalThis.allocator()) catch unreachable; cache.inflight.append(dns_lookup); return dns_lookup.promise.value(); } // var hints_buf = &[_]c_ares.AddrInfo_hints{query.toCAres()}; var request = GetAddrInfoRequest.init( cache, .{ .c_ares = {}, }, this, query, globalThis, "pending_host_cache_cares", ) catch unreachable; const promise = request.tail.promise.value(); channel.getAddrInfo( query.name, query.port, &.{}, GetAddrInfoRequest, request, GetAddrInfoRequest.onCaresComplete, ); return promise; } comptime { @export( lookup, .{ .name = "Bun__DNSResolver__lookup", }, ); } // pub fn lookupService(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { // const arguments = callframe.arguments(3); // } // pub fn cancel(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { // const arguments = callframe.arguments(3); // } };