From 61edc583620e6d07c761ce6fb35527a73e178b32 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Mon, 24 Feb 2025 11:18:16 -0800 Subject: [PATCH] feat(node/net): add `SocketAddress` (#17154) Co-authored-by: DonIsaac <22823424+DonIsaac@users.noreply.github.com> --- src/bun.js/api/bun/socket.zig | 38 +- src/bun.js/api/bun/socket/SocketAddress.zig | 655 ++++++++++++++++++ src/bun.js/api/bun/udp_socket.zig | 29 +- src/bun.js/api/server.zig | 25 +- src/bun.js/api/sockets.classes.ts | 58 ++ src/bun.js/bindings/BunCommonStrings.h | 4 + src/bun.js/bindings/BunString.cpp | 3 +- src/bun.js/bindings/JSSocketAddress.zig | 11 - ...cketAddress.cpp => JSSocketAddressDTO.cpp} | 29 +- ...JSSocketAddress.h => JSSocketAddressDTO.h} | 5 +- src/bun.js/bindings/ZigGlobalObject.cpp | 29 +- src/bun.js/bindings/ZigGlobalObject.h | 5 +- src/bun.js/bindings/bindings.zig | 130 +++- .../bindings/generated_classes_list.zig | 1 + src/bun.js/node/node_net_binding.zig | 10 + src/bun.js/node/nodejs_error_code.zig | 3 + src/codegen/class-definitions.ts | 69 +- src/deps/c_ares.zig | 7 + src/deps/uws.zig | 36 +- src/js/internal/net.ts | 3 +- src/js/node/net.ts | 9 +- src/jsc.zig | 8 + src/string.zig | 13 +- src/string/WTFStringImpl.zig | 9 +- test/js/bun/http/serve.test.ts | 118 ++-- test/js/bun/udp/udp_socket.test.ts | 2 +- test/js/node/net/socketaddress.spec.ts | 331 +++++++++ .../parallel/needs-test/test-socketaddress.js | 179 +++++ 28 files changed, 1617 insertions(+), 202 deletions(-) create mode 100644 src/bun.js/api/bun/socket/SocketAddress.zig delete mode 100644 src/bun.js/bindings/JSSocketAddress.zig rename src/bun.js/bindings/{JSSocketAddress.cpp => JSSocketAddressDTO.cpp} (57%) rename src/bun.js/bindings/{JSSocketAddress.h => JSSocketAddressDTO.h} (61%) create mode 100644 test/js/node/net/socketaddress.spec.ts create mode 100644 test/js/node/test/parallel/needs-test/test-socketaddress.js diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index ddc4b10a2b..95f9655b18 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -21,6 +21,9 @@ const Async = bun.Async; const uv = bun.windows.libuv; const H2FrameParser = @import("./h2_frame_parser.zig").H2FrameParser; const NodePath = @import("../../node/path.zig"); + +pub const SocketAddress = @import("./socket/SocketAddress.zig"); + noinline fn getSSLException(globalThis: *JSC.JSGlobalObject, defaultMessage: []const u8) JSValue { var zig_str: ZigString = ZigString.init(""); var output_buf: [4096]u8 = undefined; @@ -313,6 +316,24 @@ pub const SocketConfig = struct { reusePort: bool = false, ipv6Only: bool = false, + pub fn socketFlags(this: *const SocketConfig) i32 { + var flags: i32 = if (this.exclusive) + uws.LIBUS_LISTEN_EXCLUSIVE_PORT + else if (this.reusePort) + uws.LIBUS_LISTEN_REUSE_PORT | uws.LIBUS_LISTEN_REUSE_ADDR + else + uws.LIBUS_LISTEN_DEFAULT; + + if (this.allowHalfOpen) { + flags |= uws.LIBUS_SOCKET_ALLOW_HALF_OPEN; + } + if (this.ipv6Only) { + flags |= uws.LIBUS_SOCKET_IPV6_ONLY; + } + + return flags; + } + pub fn fromJS(vm: *JSC.VirtualMachine, opts: JSC.JSValue, globalObject: *JSC.JSGlobalObject) bun.JSError!SocketConfig { var hostname_or_unix: JSC.ZigString.Slice = JSC.ZigString.Slice.empty; errdefer hostname_or_unix.deinit(); @@ -609,25 +630,12 @@ pub const Listener = struct { var ssl = socket_config.ssl; var handlers = socket_config.handlers; var protos: ?[]const u8 = null; - const exclusive = socket_config.exclusive; handlers.is_server = true; const ssl_enabled = ssl != null; - var socket_flags: i32 = if (exclusive) - uws.LIBUS_LISTEN_EXCLUSIVE_PORT - else if (socket_config.reusePort) - uws.LIBUS_LISTEN_REUSE_PORT | uws.LIBUS_LISTEN_REUSE_ADDR - else - uws.LIBUS_LISTEN_DEFAULT; - - if (socket_config.allowHalfOpen) { - socket_flags |= uws.LIBUS_SOCKET_ALLOW_HALF_OPEN; - } - if (socket_config.ipv6Only) { - socket_flags |= uws.LIBUS_SOCKET_IPV6_ONLY; - } - defer if (ssl != null) ssl.?.deinit(); + const socket_flags = socket_config.socketFlags(); + defer if (ssl) |*_ssl| _ssl.deinit(); if (Environment.isWindows) { if (port == null) { diff --git a/src/bun.js/api/bun/socket/SocketAddress.zig b/src/bun.js/api/bun/socket/SocketAddress.zig new file mode 100644 index 0000000000..37d7a223aa --- /dev/null +++ b/src/bun.js/api/bun/socket/SocketAddress.zig @@ -0,0 +1,655 @@ +//! An IP socket address meant to be used by both native and JS code. +//! +//! JS getters are named `getFoo`, while native getters are named `foo`. +//! +//! TODO: add a inspect method (under `Symbol.for("nodejs.util.inspect.custom")`). +//! Requires updating bindgen. +const SocketAddress = @This(); + +// NOTE: not std.net.Address b/c .un is huge and we don't use it. +// NOTE: not C.sockaddr_storage b/c it's _huge_. we need >= 28 bytes for sockaddr_in6, +// but sockaddr_storage is 128 bytes. +/// @internal +_addr: sockaddr, +/// Cached address in presentation format. Prevents repeated conversion between +/// strings and bytes. +/// +/// - `.Dead` is used as an alternative to `null` +/// - `.Empty` is used for default ipv4 and ipv6 addresses (`127.0.0.1` and `::`, respectively). +/// +/// @internal +_presentation: bun.String = .dead, + +pub const Options = struct { + family: AF = AF.INET, + /// When `null`, default is determined by address family. + /// - `127.0.0.1` for IPv4 + /// - `::1` for IPv6 + address: ?bun.String = null, + port: u16 = 0, + /// IPv6 flow label. JS getters for v4 addresses always return `0`. + flowlabel: ?u32 = null, + + /// NOTE: assumes options object has been normalized and validated by JS code. + pub fn fromJS(global: *JSC.JSGlobalObject, obj: JSValue) bun.JSError!Options { + if (!obj.isObject()) return global.throwInvalidArgumentTypeValue("options", "object", obj); + + const address_str: ?bun.String = if (try obj.get(global, "address")) |a| addr: { + if (!a.isString()) return global.throwInvalidArgumentTypeValue("options.address", "string", a); + break :addr try bun.String.fromJS2(a, global); + } else null; + + const _family: AF = if (try obj.get(global, "family")) |fam| blk: { + // "ipv4" or "ipv6", ignoring case + if (fam.isString()) { + const fam_str = try bun.String.fromJS2(fam, global); + defer fam_str.deref(); + if (fam_str.length() != 4) + return throwBadFamilyIP(global, fam); + + if (fam_str.is8Bit()) { + const slice = fam_str.latin1(); + if (std.ascii.eqlIgnoreCase(slice[0..4], "ipv4")) { + break :blk AF.INET; + } else if (std.ascii.eqlIgnoreCase(slice[0..4], "ipv6")) { + break :blk AF.INET6; + } else return throwBadFamilyIP(global, fam); + } else { + // not full ignore-case since that would require converting + // utf16 -> latin1 and the allocation isn't worth it. + if (fam_str.eqlComptime("ipv4") or fam_str.eqlComptime("IPv4")) { + break :blk AF.INET; + } else if (fam_str.eqlComptime("ipv6") or fam_str.eqlComptime("IPv6")) { + break :blk AF.INET6; + } else { + return throwBadFamilyIP(global, fam); + } + } + } else if (fam.isUInt32AsAnyInt()) { + break :blk switch (fam.toU32()) { + AF.INET.int() => AF.INET, + AF.INET6.int() => AF.INET6, + else => return global.throwInvalidArgumentPropertyValue("options.family", "AF_INET or AF_INET6", fam), + }; + } else { + return global.throwInvalidArgumentPropertyValue("options.family", "a string or number", fam); + } + } else AF.INET; + + // required. Validated by `validatePort`. + const _port: u16 = if (try obj.get(global, "port")) |p| blk: { + if (!p.isFinite()) return throwBadPort(global, p); + const port32 = p.toInt32(); + if (port32 < 0 or port32 > std.math.maxInt(u16)) { + return throwBadPort(global, p); + } + break :blk @intCast(port32); + } else 0; + + const _flowlabel = if (try obj.get(global, "flowlabel")) |fl| blk: { + if (!fl.isNumber()) return global.throwInvalidArgumentTypeValue("options.flowlabel", "number", fl); + if (!fl.isUInt32AsAnyInt()) return global.throwRangeError(fl.asNumber(), .{ + .field_name = "options.flowlabel", + .min = 0, + .max = std.math.maxInt(u32), + }); + break :blk fl.toU32(); + } else null; + + return .{ + .family = _family, + .address = address_str, + .port = _port, + .flowlabel = _flowlabel, + }; + } + + inline fn throwBadFamilyIP(global: *JSC.JSGlobalObject, family_: JSC.JSValue) bun.JSError { + return global.throwInvalidArgumentPropertyValue("options.family", "'ipv4' or 'ipv6'", family_); + } + inline fn throwBadPort(global: *JSC.JSGlobalObject, port_: JSC.JSValue) bun.JSError { + const ty = global.determineSpecificType(port_) catch { + return global.ERR_SOCKET_BAD_PORT("The \"options.port\" argument must be a valid IP port number.", .{}).throw(); + }; + return global.ERR_SOCKET_BAD_PORT("The \"options.port\" argument must be a valid IP port number. Got {s}.", .{ty}).throw(); + } +}; + +pub usingnamespace JSC.Codegen.JSSocketAddress; +pub usingnamespace bun.New(SocketAddress); + +// ============================================================================= +// ============================== STATIC METHODS =============================== +// ============================================================================= + +/// ### `SocketAddress.parse(input: string): SocketAddress | undefined` +/// Parse an address string (with an optional `:port`) into a `SocketAddress`. +/// Returns `undefined` if the input is invalid. +pub fn parse(global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const input = blk: { + const input_arg = callframe.argument(0); + if (!input_arg.isString()) return global.throwInvalidArgumentTypeValue("input", "string", input_arg); + break :blk try bun.String.fromJS2(input_arg, global); + }; + var stackfb = std.heap.stackFallback(256, bun.default_allocator); + const alloc = stackfb.get(); + + const url_str = bun.String.createFromConcat( + alloc, + &[_]bun.String{ bun.String.static("http://"), input }, + ) catch return global.throwOutOfMemory(); + defer url_str.deref(); + + const url = JSC.URL.fromString(url_str) orelse return JSValue.jsUndefined(); + defer url.deinit(); + const host = url.host(); + const port_: u16 = blk: { + const port32 = url.port(); + break :blk if (port32 > std.math.maxInt(u16)) 0 else @intCast(port32); + }; + bun.assert(host.tag != .Dead); + bun.debugAssert(host.length() >= 2); + + // NOTE: parsed host cannot be used as presentation string. e.g. + // - "[::1]" -> "::1" + // - "0x.0x.0" -> "0.0.0.0" + const paddr = host.latin1(); // presentation address + const addr = if (paddr[0] == '[' and paddr[paddr.len - 1] == ']') v6: { + const v6 = net.Ip6Address.parse(paddr[1 .. paddr.len - 1], port_) catch return JSValue.jsUndefined(); + break :v6 SocketAddress{ ._addr = .{ .sin6 = v6.sa } }; + } else v4: { + const v4 = net.Ip4Address.parse(paddr, port_) catch return JSValue.jsUndefined(); + break :v4 SocketAddress{ ._addr = .{ .sin = v4.sa } }; + }; + + return SocketAddress.new(addr).toJS(global); +} + +/// ### `SocketAddress.isSocketAddress(value: unknown): value is SocketAddress` +/// Returns `true` if `value` is a `SocketAddress`. Subclasses and similarly-shaped +/// objects are not considered `SocketAddress`s. +pub fn isSocketAddress(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const value = callframe.argument(0); + return JSValue.jsBoolean(value.isCell() and SocketAddress.fromJSDirect(value) != null); +} + +// ============================================================================= +// =============================== CONSTRUCTORS ================================ +// ============================================================================= + +/// `new SocketAddress([options])` +/// +/// ## Safety +/// Constructor assumes that options object has already been sanitized and validated +/// by JS wrapper. +/// +/// ## References +/// - [Node docs](https://nodejs.org/api/net.html#new-netsocketaddressoptions) +pub fn constructor(global: *JSC.JSGlobalObject, frame: *JSC.CallFrame) bun.JSError!*SocketAddress { + const options_obj = frame.argument(0); + if (options_obj.isUndefined()) return SocketAddress.new(.{ + ._addr = sockaddr.@"127.0.0.1", + ._presentation = .empty, + // ._presentation = WellKnownAddress.@"127.0.0.1"(), + // ._presentation = bun.String.fromJS2(global.commonStrings().@"127.0.0.1"()) catch unreachable, + }); + options_obj.ensureStillAlive(); + + const options = try Options.fromJS(global, options_obj); + + // fast path for { family: 'ipv6' } + if (options.family == AF.INET6 and options.address == null and options.flowlabel == null and options.port == 0) { + return SocketAddress.new(.{ + ._addr = sockaddr.@"::", + ._presentation = .empty, + // ._presentation = WellKnownAddress.@"::"(), + }); + } + + return SocketAddress.create(global, options); +} + +/// Semi-structured JS api for creating a `SocketAddress`. If you have raw +/// socket address data, prefer `SocketAddress.new`. +/// +/// ## Safety +/// - `options.address` gets moved, much like `adoptRef`. Do not `deref` it +/// after passing it in. +pub fn create(global: *JSC.JSGlobalObject, options: Options) bun.JSError!*SocketAddress { + var presentation: bun.String = .empty; + + // We need a zero-terminated cstring for `ares_inet_pton`, which forces us to + // copy the string. + var stackfb = std.heap.stackFallback(64, bun.default_allocator); + const alloc = stackfb.get(); + + // NOTE: `zig translate-c` creates semantically invalid code for `C.ntohs`. + // Switch back to `htons(options.port)` when this issue gets resolved: + // https://github.com/ziglang/zig/issues/22804 + const addr: sockaddr = switch (options.family) { + AF.INET => v4: { + var sin: inet.sockaddr_in = .{ + .family = options.family.int(), + .port = std.mem.nativeToBig(u16, options.port), + .addr = undefined, + }; + if (options.address) |address_str| { + presentation = address_str; + const slice = address_str.toOwnedSliceZ(alloc) catch bun.outOfMemory(); + defer alloc.free(slice); + try pton(global, inet.AF_INET, slice, &sin.addr); + } else { + sin.addr = sockaddr.@"127.0.0.1".sin.addr; + } + break :v4 .{ .sin = sin }; + }, + AF.INET6 => v6: { + var sin6: inet.sockaddr_in6 = .{ + .family = options.family.int(), + .port = std.mem.nativeToBig(u16, options.port), + .flowinfo = options.flowlabel orelse 0, + .addr = undefined, + .scope_id = 0, + }; + if (options.address) |address_str| { + presentation = address_str; + const slice = address_str.toOwnedSliceZ(alloc) catch bun.outOfMemory(); + defer alloc.free(slice); + try pton(global, inet.AF_INET6, slice, &sin6.addr); + } else { + sin6.addr = inet.IN6ADDR_ANY_INIT; + } + break :v6 .{ .sin6 = sin6 }; + }, + }; + + return SocketAddress.new(.{ + ._addr = addr, + ._presentation = presentation, + }); +} + +pub const AddressError = error{ + /// Too long or short to be an IPv4 or IPv6 address. + InvalidLength, +}; + +/// Create a new IP socket address. `addr` is assumed to be a valid ipv4 or ipv6 +/// address. Port is in host byte order. +/// +/// ## Errors +/// - If `addr` is not 4 or 16 bytes long. +pub fn init(addr: []const u8, port_: u16) AddressError!SocketAddress { + return switch (addr.len) { + 4 => initIPv4(addr[0..4].*, port_), + 16 => initIPv6(addr[0..16].*, port_, 0, 0), + else => AddressError.InvalidLength, + }; +} + +/// Create an IPv4 socket address. `addr` is assumed to be valid. Port is in host byte order. +pub fn initIPv4(addr: [4]u8, port_: u16) SocketAddress { + // TODO: make sure casting doesn't swap byte order on us. + return .{ ._addr = sockaddr.v4(std.mem.nativeToBig(u16, port_), @bitCast(addr)) }; +} + +/// Create an IPv6 socket address. `addr` is assumed to be valid. Port is in +/// host byte order. +/// +/// Use `0` for `flowinfo` and `scope_id` if you don't know or care about their +/// values. +pub fn initIPv6(addr: [16]u8, port_: u16, flowinfo: u32, scope_id: u32) SocketAddress { + return .{ ._addr = sockaddr.v6( + std.mem.nativeToBig(u16, port_), + addr, + flowinfo, + scope_id, + ) }; +} + +// ============================================================================= +// ================================ DESTRUCTORS ================================ +// ============================================================================= + +pub fn deinit(this: *SocketAddress) void { + // .deref() on dead strings is a no-op. + this._presentation.deref(); +} + +pub fn finalize(this: *SocketAddress) void { + JSC.markBinding(@src()); + this.deinit(); + this.destroy(); +} + +// ============================================================================= + +/// Turn this address into a DTO. `this` is consumed and undefined after this call. +/// +/// This is similar to `.toJS`, but differs in the following ways: +/// - `this` is consumed +/// - result object is not an instance of `SocketAddress`, so +/// `SocketAddress.isSocketAddress(dto) === false` +/// - address, port, etc. are put directly onto the object instead of being +/// accessed via getters on the prototype. +/// +/// This method is slightly faster if you are creating a lot of socket addresses +/// that will not be around for very long. `createDTO` is even faster, but +/// requires callers to already have a presentation-formatted address. +pub fn intoDTO(this: *SocketAddress, global: *JSC.JSGlobalObject) JSC.JSValue { + var addr_str = this.address(); + defer this._presentation = .dead; + defer this.* = undefined; // removed in release builds, so setting _presentation to dead is still needed. + return JSSocketAddressDTO__create(global, addr_str.transferToJS(global), this.port(), this.family() == AF.INET6); +} + +/// Directly create a socket address DTO. This is a POJO with address, port, and family properties. +/// Used for hot paths that provide existing, pre-formatted/validated address +/// data to JS. +/// +/// - The address string is assumed to be ASCII and a valid IP address (either v4 or v6). +/// - Port is a valid `in_port_t` (between 0 and 2^16) in host byte order. +pub fn createDTO(globalObject: *JSC.JSGlobalObject, addr_: []const u8, port_: i32, is_ipv6: bool) JSC.JSValue { + if (comptime bun.Environment.isDebug) { + bun.assertWithLocation(port_ >= 0 and port_ <= std.math.maxInt(i32), @src()); + bun.assertWithLocation(addr_.len > 0, @src()); + } + + return JSSocketAddressDTO__create( + globalObject, + bun.String.createUTF8ForJS(globalObject, addr_), + port_, + is_ipv6, + ); +} + +extern "c" fn JSSocketAddressDTO__create(globalObject: *JSC.JSGlobalObject, address_: JSC.JSValue, port_: c_int, is_ipv6: bool) JSC.JSValue; + +// ============================================================================= + +pub fn getAddress(this: *SocketAddress, global: *JSC.JSGlobalObject) JSC.JSValue { + // toJS increments ref count + const addr_ = this.address(); + return switch (addr_.tag) { + .Dead => unreachable, + .Empty => switch (this.family()) { + AF.INET => global.commonStrings().@"127.0.0.1"(), + AF.INET6 => global.commonStrings().@"::"(), + }, + else => addr_.toJS(global), + }; +} + +/// Get the address in presentation format. Does not include the port. +/// +/// Returns an `.Empty` string for default ipv4 and ipv6 addresses (`127.0.0.1` +/// and `::`, respectively). +/// +/// ### TODO +/// - replace `addressToString` in `dns.zig` w this +/// - use this impl in server.zig +pub fn address(this: *SocketAddress) bun.String { + if (this._presentation.tag != .Dead) return this._presentation; + + var buf: [inet.INET6_ADDRSTRLEN]u8 = undefined; + const addr_src: *const anyopaque = if (this.family() == AF.INET) + @ptrCast(&this.asV4().addr) + else + @ptrCast(&this.asV6().addr); + + const formatted = std.mem.span(ares.ares_inet_ntop(this.family().int(), addr_src, &buf, buf.len) orelse { + std.debug.panic("Invariant violation: SocketAddress created with invalid IPv6 address ({any})", .{this._addr}); + }); + if (comptime bun.Environment.isDebug) { + bun.assertWithLocation(bun.strings.isAllASCII(formatted), @src()); + } + const presentation = bun.JSC.WebCore.Encoder.toBunStringComptime(formatted, .latin1); + bun.debugAssert(presentation.tag != .Dead); + this._presentation = presentation; + return presentation; +} + +/// `sockaddr.family` +/// +/// Returns a string representation of this address' family. Use `getAddrFamily` +/// for the numeric value. +/// +/// NOTE: node's `net.SocketAddress` wants `"ipv4"` and `"ipv6"` while Bun's APIs +/// use `"IPv4"` and `"IPv6"`. This is annoying. +pub fn getFamily(this: *SocketAddress, global: *JSC.JSGlobalObject) JSValue { + // NOTE: cannot use global.commonStrings().IPv[4,6]() b/c this needs to be + // lower case. + return switch (this.family()) { + AF.INET => bun.String.static("ipv4").toJS(global), + AF.INET6 => bun.String.static("ipv6").toJS(global), + }; +} + +/// `sockaddr.addrfamily` +pub fn getAddrFamily(this: *SocketAddress, _: *JSC.JSGlobalObject) JSValue { + return JSValue.jsNumber(this.family().int()); +} + +/// NOTE: zig std uses posix values only, while this returns whatever the +/// system uses. Do not compare to `std.posix.AF`. +pub fn family(this: *const SocketAddress) AF { + // NOTE: sockaddr_in and sockaddr_in6 have the same layout for family. + return @enumFromInt(this._addr.sin.family); +} + +pub fn getPort(this: *SocketAddress, _: *JSC.JSGlobalObject) JSValue { + return JSValue.jsNumber(this.port()); +} + +/// Get the port number in host byte order. +pub fn port(this: *const SocketAddress) u16 { + // NOTE: sockaddr_in and sockaddr_in6 have the same layout for port. + // NOTE: `zig translate-c` creates semantically invalid code for `C.ntohs`. + // Switch back to `ntohs` when this issue gets resolved: https://github.com/ziglang/zig/issues/22804 + return std.mem.bigToNative(u16, this._addr.sin.port); +} + +pub fn getFlowLabel(this: *SocketAddress, _: *JSC.JSGlobalObject) JSValue { + return JSValue.jsNumber(this.flowLabel() orelse 0); +} + +/// Returns `null` for non-IPv6 addresses. +/// +/// ## References +/// - [RFC 6437](https://tools.ietf.org/html/rfc6437) +pub fn flowLabel(this: *const SocketAddress) ?u32 { + if (this.family() == AF.INET6) { + const in6: inet.sockaddr_in6 = @bitCast(this._addr); + return in6.flowinfo; + } else { + return null; + } +} + +pub fn socklen(this: *const SocketAddress) inet.socklen_t { + switch (this._addr.family) { + AF.INET => return @sizeOf(inet.sockaddr_in), + AF.INET6 => return @sizeOf(inet.sockaddr_in6), + } +} + +pub fn estimatedSize(this: *SocketAddress) usize { + return @sizeOf(SocketAddress) + this._presentation.estimatedSize(); +} + +pub fn toJSON(this: *SocketAddress, global: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return JSC.JSObject.create(.{ + .address = this.getAddress(global), + .family = this.getFamily(global), + .port = this.port(), + .flowlabel = this.flowLabel() orelse 0, + }, global).toJS(); +} + +fn pton(global: *JSC.JSGlobalObject, comptime af: c_int, addr: [:0]const u8, dst: *anyopaque) bun.JSError!void { + return switch (ares.ares_inet_pton(af, addr.ptr, dst)) { + 0 => global.throwSysError(.{ .code = .ERR_INVALID_IP_ADDRESS }, "Invalid socket address", .{}), + + // TODO: figure out proper wayto convert a c errno into a js exception + -1 => global.throwSysError( + .{ .code = .ERR_INVALID_IP_ADDRESS, .errno = std.c._errno().* }, + "Invalid socket address", + .{}, + ), + 1 => {}, + else => unreachable, + }; +} + +inline fn asV4(this: *const SocketAddress) *const inet.sockaddr_in { + bun.debugAssert(this.family() == AF.INET); + return &this._addr.sin; +} + +inline fn asV6(this: *const SocketAddress) *const inet.sockaddr_in6 { + bun.debugAssert(this.family() == AF.INET6); + return &this._addr.sin6; +} + +// ============================================================================= + +// WTF::StringImpl and WTF::StaticStringImpl have the same shape +// (StringImplShape) so this is fine. We should probably add StaticStringImpl +// bindings though. +const StaticStringImpl = bun.WTF.StringImpl; +extern "c" const IPv4: StaticStringImpl; +extern "c" const IPv6: StaticStringImpl; +const ipv4: bun.String = .{ .tag = .WTFStringImpl, .value = .{ .WTFStringImpl = IPv4 } }; +const ipv6: bun.String = .{ .tag = .WTFStringImpl, .value = .{ .WTFStringImpl = IPv6 } }; + +// FIXME: c-headers-for-zig casts AF_* and PF_* to `c_int` when it should be `comptime_int` +pub const AF = enum(inet.sa_family_t) { + INET = @intCast(inet.AF_INET), + INET6 = @intCast(inet.AF_INET6), + pub inline fn int(this: AF) inet.sa_family_t { + return @intFromEnum(this); + } +}; + +/// ## Notes +/// - Linux broke compat between `sockaddr_in` and `sockaddr_in6` in v2.4. +/// They're no longer the same size. +/// - This replaces `sockaddr_storage` because it's huge. This is 28 bytes, +/// while `sockaddr_storage` is 128 bytes. +const sockaddr = extern union { + sin: inet.sockaddr_in, + sin6: inet.sockaddr_in6, + + pub fn v4(port_: inet.in_port_t, addr: u32) sockaddr { + return .{ .sin = .{ + .family = AF.INET.int(), + .port = port_, + .addr = addr, + } }; + } + + pub fn v6( + port_: inet.in_port_t, + addr: [16]u8, + /// set to 0 if you don't care + flowinfo: u32, + /// set to 0 if you don't care + scope_id: u32, + ) sockaddr { + return .{ .sin6 = .{ + .family = AF.INET6.int(), + .port = port_, + .flowinfo = flowinfo, + .scope_id = scope_id, + .addr = addr, + } }; + } + + // I'd be money endianess is going to screw us here. + pub const @"127.0.0.1": sockaddr = sockaddr.v4(0, @bitCast([_]u8{ 127, 0, 0, 1 })); + // TODO: check that `::` is all zeroes on all platforms. Should correspond + // to `IN6ADDR_ANY_INIT`. + pub const @"::": sockaddr = sockaddr.v6(0, inet.IN6ADDR_ANY_INIT, 0, 0); +}; + +const WellKnownAddress = struct { + extern "c" const INET_LOOPBACK: StaticStringImpl; + extern "c" const INET6_ANY: StaticStringImpl; + inline fn @"127.0.0.1"() bun.String { + return .{ + .tag = .WTFStringImpl, + .value = .{ .WTFStringImpl = INET_LOOPBACK }, + }; + } + inline fn @"::"() bun.String { + return .{ + .tag = .WTFStringImpl, + .value = .{ .WTFStringImpl = INET6_ANY }, + }; + } +}; + +// ============================================================================= + +// The same types are defined in a bunch of different places. We should probably unify them. +comptime { + // Windows doesn't have c.socklen_t. because of course it doesn't. + const other_socklens = if (@hasDecl(bun.C.translated, "socklen_t")) + .{ std.posix.socklen_t, bun.C.translated.socklen_t } + else + .{std.posix.socklen_t}; + for (other_socklens) |other_socklen| { + if (@sizeOf(inet.socklen_t) != @sizeOf(other_socklen)) @compileError("socklen_t size mismatch"); + if (@alignOf(inet.socklen_t) != @alignOf(other_socklen)) @compileError("socklen_t alignment mismatch"); + } + std.debug.assert(AF.INET.int() == ares.AF.INET); + std.debug.assert(AF.INET6.int() == ares.AF.INET6); +} + +const std = @import("std"); +const bun = @import("root").bun; +const ares = bun.c_ares; +const net = std.net; +const Environment = bun.Environment; +const string = bun.string; +const Output = bun.Output; + +const JSC = bun.JSC; +const ZigString = JSC.ZigString; +const CallFrame = JSC.CallFrame; +const JSValue = JSC.JSValue; + +const isDebug = bun.Environment.isDebug; +const allow_assert = bun.Environment.allow_assert; + +const inet = if (bun.Environment.isWindows) +win: { + const ws2 = std.os.windows.ws2_32; + break :win struct { + pub const IN4ADDR_LOOPBACK: u32 = ws2.IN4ADDR_LOOPBACK; + pub const INET6_ADDRSTRLEN = ws2.INET6_ADDRSTRLEN; + pub const IN6ADDR_ANY_INIT: [16]u8 = .{0} ** 16; + pub const AF_INET = ws2.AF.INET; + pub const AF_INET6 = ws2.AF.INET6; + pub const sa_family_t = ws2.ADDRESS_FAMILY; + pub const in_port_t = std.os.windows.USHORT; + pub const socklen_t = ares.socklen_t; + pub const sockaddr_in = std.posix.sockaddr.in; + pub const sockaddr_in6 = std.posix.sockaddr.in6; + }; +} else posix: { + const C = bun.C.translated; + break :posix struct { + pub const IN4ADDR_LOOPBACK = C.IN4ADDR_LOOPBACK; + pub const INET6_ADDRSTRLEN = C.INET6_ADDRSTRLEN; + // Make sure this is in line with IN6ADDR_ANY_INIT in `netinet/in.h` on all platforms. + pub const IN6ADDR_ANY_INIT: [16]u8 = .{0} ** 16; + pub const AF_INET = C.AF_INET; + pub const AF_INET6 = C.AF_INET6; + pub const sa_family_t = C.sa_family_t; + pub const in_port_t = C.in_port_t; + pub const socklen_t = ares.socklen_t; + pub const sockaddr_in = std.posix.sockaddr.in; + pub const sockaddr_in6 = std.posix.sockaddr.in6; + }; +}; diff --git a/src/bun.js/api/bun/udp_socket.zig b/src/bun.js/api/bun/udp_socket.zig index 5729da9b9b..022155c4c8 100644 --- a/src/bun.js/api/bun/udp_socket.zig +++ b/src/bun.js/api/bun/udp_socket.zig @@ -10,6 +10,7 @@ const JSC = bun.JSC; const CallFrame = JSC.CallFrame; const JSGlobalObject = JSC.JSGlobalObject; const JSValue = JSC.JSValue; +const SocketAddress = JSC.API.SocketAddress; const log = Output.scoped(.UdpSocket, false); @@ -20,7 +21,6 @@ extern fn htonl(hlong: u32) u32; extern fn htons(hshort: u16) u16; extern fn inet_ntop(af: c_int, src: ?*const anyopaque, dst: [*c]u8, size: c_int) ?[*:0]const u8; extern fn inet_pton(af: c_int, src: [*c]const u8, dst: ?*anyopaque) c_int; -extern fn JSSocketAddress__create(global: *JSGlobalObject, address: JSValue, port: i32, v6: bool) JSValue; fn onClose(socket: *uws.udp.Socket) callconv(.C) void { JSC.markBinding(@src()); @@ -855,16 +855,9 @@ pub const UDPSocket = struct { return JSValue.jsNumber(this.socket.boundPort()); } - fn addressToString(globalThis: *JSGlobalObject, address_bytes: []const u8) JSValue { - var text_buf: [512]u8 = undefined; - const address: std.net.Address = switch (address_bytes.len) { - 4 => std.net.Address.initIp4(address_bytes[0..4].*, 0), - 16 => std.net.Address.initIp6(address_bytes[0..16].*, 0, 0, 0), - else => return .undefined, - }; - - const slice = bun.fmt.formatIp(address, &text_buf) catch unreachable; - return bun.String.createUTF8ForJS(globalThis, slice); + fn createSockAddr(globalThis: *JSGlobalObject, address_bytes: []const u8, port: u16) JSValue { + var sockaddr = SocketAddress.init(address_bytes, port) catch return .undefined; + return sockaddr.intoDTO(globalThis); } pub fn getAddress(this: *This, globalThis: *JSGlobalObject) JSValue { @@ -875,12 +868,7 @@ pub const UDPSocket = struct { const address_bytes = buf[0..@as(usize, @intCast(length))]; const port = this.socket.boundPort(); - return JSSocketAddress__create( - globalThis, - addressToString(globalThis, address_bytes), - @intCast(port), - length == 16, - ); + return createSockAddr(globalThis, address_bytes, @intCast(port)); } pub fn getRemoteAddress(this: *This, globalThis: *JSC.JSGlobalObject) JSC.JSValue { @@ -891,12 +879,7 @@ pub const UDPSocket = struct { this.socket.remoteIp(&buf, &length); const address_bytes = buf[0..@as(usize, @intCast(length))]; - return JSSocketAddress__create( - globalThis, - addressToString(globalThis, address_bytes), - connect_info.port, - length == 16, - ); + return createSockAddr(globalThis, address_bytes, connect_info.port); } pub fn getBinaryType( diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 5214d1fad0..c4769f95c6 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -89,6 +89,8 @@ const Async = bun.Async; const httplog = Output.scoped(.Server, false); const ctxLog = Output.scoped(.RequestContext, false); const S3 = bun.S3; +const SocketAddress = @import("bun/socket.zig").SocketAddress; + const BlobFileContentResult = struct { data: [:0]const u8, @@ -6467,10 +6469,9 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp } pub fn requestIP(this: *ThisServer, request: *JSC.WebCore.Request) JSC.JSValue { - if (this.config.address == .unix) { - return JSValue.jsNull(); - } - return request.getRemoteSocketInfo(this.globalThis) orelse .null; + if (this.config.address == .unix) return JSValue.jsNull(); + const info = request.request_context.getRemoteSocketInfo() orelse return JSValue.jsNull(); + return SocketAddress.createDTO(this.globalThis, info.ip, @intCast(info.port), info.is_ipv6); } pub fn memoryCost(this: *ThisServer) usize { @@ -7021,16 +7022,12 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp port = @intCast(listener.getLocalPort()); var buf: [64]u8 = [_]u8{0} ** 64; - var is_ipv6: bool = false; - - if (listener.socket().localAddressText(&buf, &is_ipv6)) |slice| { - return JSC.JSSocketAddress.create( - this.globalThis, - slice, - port, - is_ipv6, - ); - } + const address_bytes = listener.socket().localAddress(&buf) orelse return JSValue.jsNull(); + var addr = SocketAddress.init(address_bytes, port) catch { + @branchHint(.unlikely); + return JSValue.jsNull(); + }; + return addr.intoDTO(this.globalThis); } return JSValue.jsNull(); }, diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index 931740d6e1..d476519ee9 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -370,4 +370,62 @@ export default [ }, klass: {}, }), + define({ + name: "SocketAddress", + construct: true, + call: false, + finalize: true, + estimatedSize: true, + JSType: "0b11101110", + klass: { + parse: { + fn: "parse", + length: 1, + enumerable: false, + configurable: true, + }, + isSocketAddress: { + fn: "isSocketAddress", + length: 1, + enumerable: false, + configurable: true, + }, + }, + proto: { + address: { + getter: "getAddress", + enumerable: false, + configurable: true, + cache: true, + }, + port: { + getter: "getPort", + enumerable: false, + configurable: true, + }, + family: { + getter: "getFamily", + enumerable: false, + configurable: true, + cache: true, + }, + addrfamily: { + getter: "getAddrFamily", + enumerable: false, + configurable: false, + }, + flowlabel: { + getter: "getFlowLabel", + enumerable: false, + configurable: true, + }, + toJSON: { + fn: "toJSON", + length: 0, + this: true, + enumerable: false, + configurable: true, + }, + }, + }), ]; diff --git a/src/bun.js/bindings/BunCommonStrings.h b/src/bun.js/bindings/BunCommonStrings.h index 49e1377491..734caa02d3 100644 --- a/src/bun.js/bindings/BunCommonStrings.h +++ b/src/bun.js/bindings/BunCommonStrings.h @@ -27,6 +27,10 @@ macro(ec, "ec") \ macro(x25519, "x25519") \ macro(ed25519, "ed25519") \ + macro(IPv4, "IPv4") \ + macro(IPv6, "IPv6") \ + macro(IN4Loopback, "127.0.0.1") \ + macro(IN6Any, "::") \ macro(OperationWasAborted, "The operation was aborted.") \ macro(OperationTimedOut, "The operation timed out.") \ macro(ConnectionWasClosed, "The connection was closed.") \ diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index e9eb96955e..9afa7aca97 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -159,8 +159,7 @@ JSC::JSValue toJS(JSC::JSGlobalObject* globalObject, BunString bunString) } if (bunString.tag == BunStringTag::WTFStringImpl) { #if ASSERT_ENABLED - unsigned refCount = bunString.impl.wtf->refCount(); - ASSERT(refCount > 0 && !bunString.impl.wtf->isEmpty()); + ASSERT(bunString.impl.wtf->hasAtLeastOneRef() && !bunString.impl.wtf->isEmpty()); #endif return JSValue(jsString(globalObject->vm(), String(bunString.impl.wtf))); diff --git a/src/bun.js/bindings/JSSocketAddress.zig b/src/bun.js/bindings/JSSocketAddress.zig deleted file mode 100644 index aa40c3176b..0000000000 --- a/src/bun.js/bindings/JSSocketAddress.zig +++ /dev/null @@ -1,11 +0,0 @@ -pub const JSSocketAddress = opaque { - extern fn JSSocketAddress__create(global: *JSC.JSGlobalObject, ip: JSValue, port: i32, is_ipv6: bool) JSValue; - - pub fn create(global: *JSC.JSGlobalObject, ip: []const u8, port: i32, is_ipv6: bool) JSValue { - return JSSocketAddress__create(global, bun.String.createUTF8ForJS(global, ip), port, is_ipv6); - } -}; - -const bun = @import("root").bun; -const JSC = bun.JSC; -const JSValue = JSC.JSValue; diff --git a/src/bun.js/bindings/JSSocketAddress.cpp b/src/bun.js/bindings/JSSocketAddressDTO.cpp similarity index 57% rename from src/bun.js/bindings/JSSocketAddress.cpp rename to src/bun.js/bindings/JSSocketAddressDTO.cpp index 1815073397..95927e3812 100644 --- a/src/bun.js/bindings/JSSocketAddress.cpp +++ b/src/bun.js/bindings/JSSocketAddressDTO.cpp @@ -1,4 +1,4 @@ -#include "JSSocketAddress.h" +#include "JSSocketAddressDTO.h" #include "ZigGlobalObject.h" #include "JavaScriptCore/JSObjectInlines.h" #include "JavaScriptCore/ObjectConstructor.h" @@ -7,7 +7,11 @@ using namespace JSC; namespace Bun { -namespace JSSocketAddress { +namespace JSSocketAddressDTO { + +static constexpr PropertyOffset addressOffset = 0; +static constexpr PropertyOffset familyOffset = 1; +static constexpr PropertyOffset portOffset = 2; // Using a structure with inlined offsets should be more lightweight than a class. Structure* createStructure(VM& vm, JSGlobalObject* globalObject) @@ -24,6 +28,7 @@ Structure* createStructure(VM& vm, JSGlobalObject* globalObject) JSC::Identifier::fromString(vm, "address"_s), 0, offset); + ASSERT(offset == addressOffset); structure = structure->addPropertyTransition( vm, @@ -31,6 +36,7 @@ Structure* createStructure(VM& vm, JSGlobalObject* globalObject) JSC::Identifier::fromString(vm, "family"_s), 0, offset); + ASSERT(offset == familyOffset); structure = structure->addPropertyTransition( vm, @@ -38,6 +44,7 @@ Structure* createStructure(VM& vm, JSGlobalObject* globalObject) JSC::Identifier::fromString(vm, "port"_s), 0, offset); + ASSERT(offset == portOffset); return structure; } @@ -45,19 +52,19 @@ Structure* createStructure(VM& vm, JSGlobalObject* globalObject) } // namespace JSSocketAddress } // namespace Bun -extern "C" JSObject* JSSocketAddress__create(JSGlobalObject* globalObject, JSString* value, int32_t port, bool isIPv6) +extern "C" JSC__JSValue JSSocketAddressDTO__create(JSGlobalObject* globalObject, JSString* address, int32_t port, bool isIPv6) { - static const NeverDestroyed IPv4 = MAKE_STATIC_STRING_IMPL("IPv4"); - static const NeverDestroyed IPv6 = MAKE_STATIC_STRING_IMPL("IPv6"); + ASSERT(port < std::numeric_limits::max()); VM& vm = globalObject->vm(); - auto* global = jsCast(globalObject); - JSObject* thisObject = constructEmptyObject(vm, global->JSSocketAddressStructure()); - thisObject->putDirectOffset(vm, 0, value); - thisObject->putDirectOffset(vm, 1, isIPv6 ? jsString(vm, IPv6) : jsString(vm, IPv4)); - thisObject->putDirectOffset(vm, 2, jsNumber(port)); + auto* af = isIPv6 ? global->commonStrings().IPv6String(global) : global->commonStrings().IPv4String(global); - return thisObject; + JSObject* thisObject = constructEmptyObject(vm, global->JSSocketAddressDTOStructure()); + thisObject->putDirectOffset(vm, Bun::JSSocketAddressDTO::addressOffset, address); + thisObject->putDirectOffset(vm, Bun::JSSocketAddressDTO::familyOffset, af); + thisObject->putDirectOffset(vm, Bun::JSSocketAddressDTO::portOffset, jsNumber(port)); + + return JSValue::encode(thisObject); } diff --git a/src/bun.js/bindings/JSSocketAddress.h b/src/bun.js/bindings/JSSocketAddressDTO.h similarity index 61% rename from src/bun.js/bindings/JSSocketAddress.h rename to src/bun.js/bindings/JSSocketAddressDTO.h index 77bdca5d4f..d03f14cf34 100644 --- a/src/bun.js/bindings/JSSocketAddress.h +++ b/src/bun.js/bindings/JSSocketAddressDTO.h @@ -1,16 +1,17 @@ // The object returned by Bun.serve's .requestIP() #pragma once +#include "headers.h" #include "root.h" #include "JavaScriptCore/JSObjectInlines.h" using namespace JSC; namespace Bun { -namespace JSSocketAddress { +namespace JSSocketAddressDTO { Structure* createStructure(VM& vm, JSGlobalObject* globalObject); } // namespace JSSocketAddress } // namespace Bun -extern "C" JSObject* JSSocketAddress__create(JSGlobalObject* globalObject, JSString* value, int port, bool isIPv6); +extern "C" JSC__JSValue JSSocketAddressDTO__create(JSGlobalObject* globalObject, JSString* address, int32_t port, bool isIPv6); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 18fbceff7b..d10491e23b 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -114,7 +114,7 @@ #include "JSReadableStreamDefaultController.h" #include "JSReadableStreamDefaultReader.h" #include "JSSink.h" -#include "JSSocketAddress.h" +#include "JSSocketAddressDTO.h" #include "JSSQLStatement.h" #include "JSStringDecoder.h" #include "JSTextEncoder.h" @@ -2879,6 +2879,11 @@ void GlobalObject::finishCreation(VM& vm) init.set(Bun::createCommonJSModuleStructure(reinterpret_cast(init.owner))); }); + m_JSSocketAddressDTOStructure.initLater( + [](const Initializer& init) { + init.set(Bun::JSSocketAddressDTO::createStructure(init.vm, init.owner)); + }); + m_JSSQLStatementStructure.initLater( [](const Initializer& init) { init.set(WebCore::createJSSQLStatementStructure(init.owner)); @@ -2910,11 +2915,6 @@ void GlobalObject::finishCreation(VM& vm) init.vm, reinterpret_cast(init.owner))); }); - m_JSSocketAddressStructure.initLater( - [](const Initializer& init) { - init.set(JSSocketAddress::createStructure(init.vm, init.owner)); - }); - m_errorConstructorPrepareStackTraceInternalValue.initLater( [](const Initializer& init) { init.set(JSFunction::create(init.vm, init.owner, 2, "ErrorPrepareStackTrace"_s, jsFunctionDefaultErrorPrepareStackTrace, ImplementationVisibility::Public)); @@ -3856,6 +3856,21 @@ extern "C" EncodedJSValue JSC__JSGlobalObject__getHTTP2CommonString(Zig::GlobalO return JSValue::encode(JSValue::JSUndefined); } +#define IMPL_GET_COMMON_STRING(name) \ + extern "C" EncodedJSValue JSC__JSGlobalObject__commonStrings__get##name(Zig::GlobalObject* globalObject) \ + { \ + JSC::JSString* value = globalObject->commonStrings().name##String(globalObject); \ + ASSERT(value != nullptr); \ + return JSValue::encode(value); \ + } + +IMPL_GET_COMMON_STRING(IPv4) +IMPL_GET_COMMON_STRING(IPv6) +IMPL_GET_COMMON_STRING(IN4Loopback) +IMPL_GET_COMMON_STRING(IN6Any) + +#undef IMPL_GET_COMMON_STRING + template void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) { @@ -3905,6 +3920,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_cachedGlobalProxyStructure.visit(visitor); thisObject->m_callSiteStructure.visit(visitor); thisObject->m_commonJSModuleObjectStructure.visit(visitor); + thisObject->m_JSSocketAddressDTOStructure.visit(visitor); thisObject->m_cryptoObject.visit(visitor); thisObject->m_errorConstructorPrepareStackTraceInternalValue.visit(visitor); thisObject->m_esmRegistryMap.visit(visitor); @@ -3933,7 +3949,6 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSHTTPSResponseSinkClassStructure.visit(visitor); thisObject->m_JSNetworkSinkClassStructure.visit(visitor); thisObject->m_JSFetchTaskletChunkedRequestControllerPrototype.visit(visitor); - thisObject->m_JSSocketAddressStructure.visit(visitor); thisObject->m_JSSQLStatementStructure.visit(visitor); thisObject->m_V8GlobalInternals.visit(visitor); thisObject->m_JSStringDecoderClassStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index ce784d6724..a390c73771 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -260,11 +260,10 @@ public: JSObject* lazyTestModuleObject() const { return m_lazyTestModuleObject.getInitializedOnMainThread(this); } JSObject* lazyPreloadTestModuleObject() const { return m_lazyPreloadTestModuleObject.getInitializedOnMainThread(this); } Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); } + Structure* JSSocketAddressDTOStructure() const { return m_JSSocketAddressDTOStructure.getInitializedOnMainThread(this); } Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); } Structure* AsyncContextFrameStructure() const { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); } - Structure* JSSocketAddressStructure() const { return m_JSSocketAddressStructure.getInitializedOnMainThread(this); } - JSWeakMap* vmModuleContextMap() const { return m_vmModuleContextMap.getInitializedOnMainThread(this); } Structure* NapiExternalStructure() const { return m_NapiExternalStructure.getInitializedOnMainThread(this); } @@ -578,7 +577,7 @@ public: LazyProperty m_cachedNodeVMGlobalObjectStructure; LazyProperty m_cachedGlobalProxyStructure; LazyProperty m_commonJSModuleObjectStructure; - LazyProperty m_JSSocketAddressStructure; + LazyProperty m_JSSocketAddressDTOStructure; LazyProperty m_memoryFootprintStructure; LazyProperty m_requireFunctionUnbound; LazyProperty m_requireResolveFunctionUnbound; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 20ef151bee..d61c1f374d 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1927,7 +1927,7 @@ pub const JSString = extern struct { pub const view = getZigString; - // doesn't always allocate + /// doesn't always allocate pub fn toSlice( this: *JSString, global: *JSGlobalObject, @@ -2887,6 +2887,7 @@ pub const JSGlobalObject = opaque { }; } + /// "Expected {field} to be a {typename} for '{name}'." pub fn createInvalidArgumentType( this: *JSGlobalObject, comptime name_: []const u8, @@ -2900,6 +2901,7 @@ pub const JSGlobalObject = opaque { return JSC.toJS(this, @TypeOf(value), value, lifetime); } + /// "Expected {field} to be a {typename} for '{name}'." pub fn throwInvalidArgumentType( this: *JSGlobalObject, comptime name_: []const u8, @@ -2909,6 +2911,7 @@ pub const JSGlobalObject = opaque { return this.throwValue(this.createInvalidArgumentType(name_, field, typename)); } + /// "The {argname} argument is invalid. Received {value}" pub fn throwInvalidArgumentValue( this: *JSGlobalObject, argname: []const u8, @@ -2919,6 +2922,25 @@ pub const JSGlobalObject = opaque { return this.ERR_INVALID_ARG_VALUE("The \"{s}\" argument is invalid. Received {}", .{ argname, actual_string_value }).throw(); } + /// Throw an `ERR_INVALID_ARG_VALUE` when the invalid value is a property of an object. + /// Message depends on whether `expected` is present. + /// - "The property "{argname}" is invalid. Received {value}" + /// - "The property "{argname}" is invalid. Expected {expected}, received {value}" + pub fn throwInvalidArgumentPropertyValue( + this: *JSGlobalObject, + argname: []const u8, + comptime expected: ?[]const u8, + value: JSValue, + ) bun.JSError { + const actual_string_value = try determineSpecificType(this, value); + defer actual_string_value.deref(); + if (comptime expected) |_expected| { + return this.ERR_INVALID_ARG_VALUE("The property \"{s}\" is invalid. Expected {s}, received {}", .{ argname, _expected, actual_string_value }).throw(); + } else { + return this.ERR_INVALID_ARG_VALUE("The property \"{s}\" is invalid. Received {}", .{ argname, actual_string_value }).throw(); + } + } + extern "c" fn Bun__ErrorCode__determineSpecificType(*JSGlobalObject, JSValue) String; pub fn determineSpecificType(global: *JSGlobalObject, value: JSValue) JSError!String { @@ -2930,7 +2952,7 @@ pub const JSGlobalObject = opaque { return str; } - /// "The argument must be of type . Received " + /// "The {argname} argument must be of type {typename}. Received {value}" pub fn throwInvalidArgumentTypeValue( this: *JSGlobalObject, argname: []const u8, @@ -3093,17 +3115,22 @@ pub const JSGlobalObject = opaque { return JSC.Error.ERR_INVALID_ARG_TYPE.fmt(this, fmt, args); } - pub fn createError( - this: *JSGlobalObject, + pub const SysErrOptions = struct { code: JSC.Node.ErrorCode, - error_name: string, - comptime message: string, + errno: ?i32 = null, + name: ?string = null, + }; + pub fn throwSysError( + this: *JSGlobalObject, + opts: SysErrOptions, + comptime message: bun.stringZ, args: anytype, - ) JSValue { + ) JSError { const err = createErrorInstance(this, message, args); - err.put(this, ZigString.static("code"), ZigString.init(@tagName(code)).toJS(this)); - err.put(this, ZigString.static("name"), ZigString.init(error_name).toJS(this)); - return err; + err.put(this, ZigString.static("code"), ZigString.init(@tagName(opts.code)).toJS(this)); + if (opts.name) |name| err.put(this, ZigString.static("name"), ZigString.init(name).toJS(this)); + if (opts.errno) |errno| err.put(this, ZigString.static("errno"), JSC.toJS(this, i32, errno, .temporary)); + return this.throwValue(err); } pub fn throw(this: *JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) JSError { @@ -3418,6 +3445,12 @@ pub const JSGlobalObject = opaque { return NewRuntimeFunction(global, ZigString.static(display_name), argument_count, toJSHostFunction(function), false, false, null); } + /// Get a lazily-initialized `JSC::String` from `BunCommonStrings.h`. + pub inline fn commonStrings(this: *JSC.JSGlobalObject) CommonStrings { + JSC.markBinding(@src()); + return .{ .globalObject = this }; + } + pub usingnamespace @import("ErrorCode").JSGlobalObjectExtensions; extern fn JSC__JSGlobalObject__bunVM(*JSGlobalObject) *VM; @@ -3431,6 +3464,46 @@ pub const JSGlobalObject = opaque { extern fn JSGlobalObject__throwTerminationException(this: *JSGlobalObject) void; }; +/// Common strings from `BunCommonStrings.h`. +/// +/// All getters return a `JSC::JSString`; +pub const CommonStrings = struct { + globalObject: *JSC.JSGlobalObject, + + pub inline fn IPv4(this: CommonStrings) JSValue { + return this.getString("IPv4"); + } + pub inline fn IPv6(this: CommonStrings) JSValue { + return this.getString("IPv6"); + } + pub inline fn @"127.0.0.1"(this: CommonStrings) JSValue { + return this.getString("IN4Loopback"); + } + pub inline fn @"::"(this: CommonStrings) JSValue { + return this.getString("IN6Any"); + } + + inline fn getString(this: CommonStrings, comptime name: anytype) JSValue { + JSC.markMemberBinding("CommonStrings", @src()); + const str: JSC.JSValue = @call( + .auto, + @field(CommonStrings, "JSC__JSGlobalObject__commonStrings__get" ++ name), + .{this.globalObject}, + ); + bun.assert(str != .zero); + if (comptime bun.Environment.isDebug) { + bun.assertWithLocation(str != .zero, @src()); + bun.assertWithLocation(str.isStringLiteral(), @src()); + } + return str; + } + + extern "C" fn JSC__JSGlobalObject__commonStrings__getIPv4(global: *JSC.JSGlobalObject) JSC.JSValue; + extern "C" fn JSC__JSGlobalObject__commonStrings__getIPv6(global: *JSC.JSGlobalObject) JSC.JSValue; + extern "C" fn JSC__JSGlobalObject__commonStrings__getIN4Loopback(global: *JSC.JSGlobalObject) JSC.JSValue; + extern "C" fn JSC__JSGlobalObject__commonStrings__getIN6Any(global: *JSC.JSGlobalObject) JSC.JSValue; +}; + pub const JSNativeFn = JSHostZigFunction; pub const JSArrayIterator = struct { @@ -4303,11 +4376,25 @@ pub const JSValue = enum(i64) { return this.jsType() == .JSDate; } + /// Protects a JSValue from garbage collection. + /// + /// This is useful when you want to store a JSValue in a global or on the + /// heap, where the garbage collector will not be able to discover your + /// reference to it. + /// + /// A value may be protected multiple times and must be unprotected an + /// equal number of times before becoming eligible for garbage collection. pub fn protect(this: JSValue) void { if (!this.isCell()) return; JSC.C.JSValueProtect(JSC.VirtualMachine.get().global, this.asObjectRef()); } + /// Unprotects a JSValue from garbage collection. + /// + /// A value may be protected multiple times and must be unprotected an + /// equal number of times before becoming eligible for garbage collection. + /// + /// This is the inverse of `protect`. pub fn unprotect(this: JSValue) void { if (!this.isCell()) return; JSC.C.JSValueUnprotect(JSC.VirtualMachine.get().global, this.asObjectRef()); @@ -4711,6 +4798,14 @@ pub const JSValue = enum(i64) { return this.isNumber() and !this.isInt32(); } + /// [21.1.2.2 Number.isFinite](https://tc39.es/ecma262/#sec-number.isfinite) + /// + /// Returns `false` for non-numbers, `NaN`, `Infinity`, and `-Infinity` + pub fn isFinite(this: JSValue) bool { + if (!this.isNumber()) return false; + return std.math.isFinite(this.asNumber()); + } + pub fn isError(this: JSValue) bool { if (!this.isCell()) return false; @@ -5614,6 +5709,13 @@ pub const JSValue = enum(i64) { return .none; } + /// Static cast a value into a `JSC::JSString`. Casting a non-string results + /// in safety-protected undefined behavior. + /// + /// - `this` is re-interpreted, so runtime casting does not occur (e.g. `this.toString()`) + /// - Does not allocate + /// - Does not increment ref count + /// - Make sure `this` stays on the stack. If you're method chaining, you may need to call `this.ensureStillAlive()`. pub fn asString(this: JSValue) *JSString { return cppFn("asString", .{ this, @@ -6787,7 +6889,7 @@ pub const URL = opaque { extern fn URL__search(*URL) String; extern fn URL__host(*URL) String; extern fn URL__hostname(*URL) String; - extern fn URL__port(*URL) String; + extern fn URL__port(*URL) u32; extern fn URL__deinit(*URL) void; extern fn URL__pathname(*URL) String; extern fn URL__getHrefFromJS(JSValue, *JSC.JSGlobalObject) String; @@ -6873,7 +6975,9 @@ pub const URL = opaque { JSC.markBinding(@src()); return URL__hostname(url); } - pub fn port(url: *URL) String { + /// Returns `std.math.maxInt(u32)` if the port is not set. Otherwise, `port` + /// is guaranteed to be within the `u16` range. + pub fn port(url: *URL) u32 { JSC.markBinding(@src()); return URL__port(url); } @@ -7037,5 +7141,3 @@ pub const DeferredError = struct { return err; } }; - -pub const JSSocketAddress = @import("./JSSocketAddress.zig").JSSocketAddress; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index d179e7059b..96bae55029 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -51,6 +51,7 @@ pub const Classes = struct { pub const TCPSocket = JSC.API.TCPSocket; pub const TLSSocket = JSC.API.TLSSocket; pub const UDPSocket = JSC.API.UDPSocket; + pub const SocketAddress = JSC.API.SocketAddress; pub const TextDecoder = JSC.WebCore.TextDecoder; pub const Timeout = JSC.API.Bun.Timer.TimerObject; pub const BuildArtifact = JSC.API.BuildArtifact; diff --git a/src/bun.js/node/node_net_binding.zig b/src/bun.js/node/node_net_binding.zig index ade4c17c0c..3967d67503 100644 --- a/src/bun.js/node/node_net_binding.zig +++ b/src/bun.js/node/node_net_binding.zig @@ -1,5 +1,6 @@ const std = @import("std"); const bun = @import("root").bun; +const C = bun.C.translated; const Environment = bun.Environment; const JSC = bun.JSC; const string = bun.string; @@ -71,3 +72,12 @@ pub fn setDefaultAutoSelectFamilyAttemptTimeout(global: *JSC.JSGlobalObject) JSC } }).setter, 1, .{}); } + +pub fn createBinding(global: *JSC.JSGlobalObject) JSC.JSValue { + const SocketAddress = bun.JSC.GeneratedClassesList.SocketAddress; + const net = JSC.JSValue.createEmptyObjectWithNullPrototype(global); + + net.put(global, "SocketAddress", SocketAddress.getConstructor(global)); + + return net; +} diff --git a/src/bun.js/node/nodejs_error_code.zig b/src/bun.js/node/nodejs_error_code.zig index 893e806c4f..bdc59b71c2 100644 --- a/src/bun.js/node/nodejs_error_code.zig +++ b/src/bun.js/node/nodejs_error_code.zig @@ -483,6 +483,9 @@ pub const Code = enum { /// There was a bug in Node.js or incorrect usage of Node.js internals. To fix the error, open an issue at https://github.com/nodejs/node/issues. ERR_INTERNAL_ASSERTION, + /// The provided IP address was not valid for the given address family. + ERR_INVALID_ADDRESS, + /// The provided address family is not understood by the Node.js API. ERR_INVALID_ADDRESS_FAMILY, diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 3915686c47..4195efee3d 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -28,6 +28,11 @@ export type Field = } & PropertyAttribute) | ({ fn: string; + /** + * Number of parameters accepted by the function. + * + * Sets [`function.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length). + */ length?: number; passThis?: boolean; DOMJIT?: { @@ -44,19 +49,71 @@ export type Field = * function: `camelCase(fileName + functionName + "CodeGenerator"`) */ builtin: string; + /** + * Number of parameters accepted by the function. + * + * Sets [`function.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length). + */ length?: number; }; export class ClassDefinition { + /** + * Class name. + * + * Used to find the proper struct and as the `.name` of the JS constructor + * function. + */ name: string; + /** + * Class constructor is newable. + */ construct?: boolean; + /** + * Class constructor is callable. In JS, ES6 class constructors are not + * callable. + */ call?: boolean; + /** + * ## IMPORTANT + * You _must_ free the pointer to your native class! + * ```zig + * pub const NativeClass = struct { + * pub usingnamespace bun.New(NativeClass); + * + * fn constructor(global: *JSC.JSGlobalObject, frame: *JSC.CallFrame) bun.JSError!*SocketAddress { + * // do stuff + * return NativeClass.new(.{ + * // ... + * }); + * } + * + * fn finalize(this: *NativeClass) void { + * // free allocations owned by this class, then free the struct itself. + * this.destroy(); + * } + * }; + * ``` + * @todo remove this and require all classes to implement `finalize`. + */ finalize?: boolean; overridesToJS?: boolean; + /** + * Static properties and methods. + */ klass: Record; + /** + * properties and methods on the prototype. + */ proto: Record; + /** + * Properties and methods attached to the instance itself. + */ own: Record; values?: string[]; + /** + * Set this to `"0b11101110"`. + */ JSType?: string; noConstructor?: boolean; @@ -67,9 +124,15 @@ export class ClassDefinition { wantsThis?: never; /** + * Class has an `estimatedSize` function that reports external allocations to GC. * Called from any thread. * - * Used for GC. + * When `true`, classes should have a method with this signature: + * ```zig + * pub fn estimatedSize(this: *@This()) usize; + * ``` + * + * Report `@sizeOf(@this())` as well as any external allocations. */ estimatedSize?: boolean; /** @@ -121,6 +184,10 @@ export interface CustomField { type?: string; } +/** + * Define a native class written in ZIg. Bun's codegen step will create CPP wrappers + * for interacting with JSC. + */ export function define( { klass = {}, diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index 5f8b945fae..4c77b428bb 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -1614,7 +1614,14 @@ pub extern fn ares_set_servers_csv(channel: *Channel, servers: [*c]const u8) c_i pub extern fn ares_set_servers_ports_csv(channel: *Channel, servers: [*c]const u8) c_int; pub extern fn ares_get_servers(channel: *Channel, servers: *?*struct_ares_addr_port_node) c_int; pub extern fn ares_get_servers_ports(channel: *Channel, servers: *?*struct_ares_addr_port_node) c_int; +/// https://c-ares.org/docs/ares_inet_ntop.html pub extern fn ares_inet_ntop(af: c_int, src: ?*const anyopaque, dst: [*c]u8, size: ares_socklen_t) ?[*:0]const u8; +/// https://c-ares.org/docs/ares_inet_pton.html +/// +/// ## Returns +/// - `1` if `src` was valid for the specified address family +/// - `0` if `src` was not parseable in the specified address family +/// - `-1` if some system error occurred. `errno` will have been set. pub extern fn ares_inet_pton(af: c_int, src: [*c]const u8, dst: ?*anyopaque) c_int; pub const ARES_SUCCESS = 0; pub const ARES_ENODATA = 1; diff --git a/src/deps/uws.zig b/src/deps/uws.zig index abfd469759..7396ded7de 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -1769,7 +1769,7 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { /// /// # Returns /// This function returns a slice of the buffer on success, or null on failure. - pub fn localAddressBinary(this: ThisSocket, buf: []u8) ?[]const u8 { + pub fn localAddress(this: ThisSocket, buf: []u8) ?[]const u8 { switch (this.socket) { .connected => |socket| { var length: i32 = @intCast(buf.len); @@ -1789,39 +1789,6 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { } } - /// Get the local address of a socket in text format. - /// - /// # Arguments - /// - `buf`: A buffer to store the text address data. - /// - `is_ipv6`: A pointer to a boolean representing whether the address is IPv6. - /// - /// # Returns - /// This function returns a slice of the buffer on success, or null on failure. - pub fn localAddressText(this: ThisSocket, buf: []u8, is_ipv6: *bool) ?[]const u8 { - const addr_v4_len = @sizeOf(@FieldType(std.posix.sockaddr.in, "addr")); - const addr_v6_len = @sizeOf(@FieldType(std.posix.sockaddr.in6, "addr")); - - var sa_buf: [addr_v6_len + 1]u8 = undefined; - const binary = this.localAddressBinary(&sa_buf) orelse return null; - const addr_len: usize = binary.len; - sa_buf[addr_len] = 0; - - var ret: ?[*:0]const u8 = null; - if (addr_len == addr_v4_len) { - ret = bun.c_ares.ares_inet_ntop(std.posix.AF.INET, &sa_buf, buf.ptr, @as(u32, @intCast(buf.len))); - is_ipv6.* = false; - } else if (addr_len == addr_v6_len) { - ret = bun.c_ares.ares_inet_ntop(std.posix.AF.INET6, &sa_buf, buf.ptr, @as(u32, @intCast(buf.len))); - is_ipv6.* = true; - } - - if (ret) |_| { - const length: usize = @intCast(bun.len(bun.cast([*:0]u8, buf))); - return buf[0..length]; - } - return null; - } - pub fn connect( host: []const u8, port: i32, @@ -4527,6 +4494,7 @@ pub const udp = struct { return us_udp_socket_bind(this, hostname, port); } + /// Get the bound port in host byte order pub fn boundPort(this: *This) c_int { return us_udp_socket_bound_port(this); } diff --git a/src/js/internal/net.ts b/src/js/internal/net.ts index 9016f6072d..51d2f13c09 100644 --- a/src/js/internal/net.ts +++ b/src/js/internal/net.ts @@ -1,3 +1,4 @@ const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket] = $zig("socket.zig", "createNodeTLSBinding"); +const { SocketAddress } = $zig("node_net_binding.zig", "createBinding"); -export default { addServerName, upgradeDuplexToTLS, isNamedPipeSocket }; +export default { addServerName, upgradeDuplexToTLS, isNamedPipeSocket, SocketAddress }; diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 894c7bcb72..f4c5c6afc9 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -22,8 +22,10 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. const { Duplex } = require("node:stream"); const EventEmitter = require("node:events"); -const { addServerName, upgradeDuplexToTLS, isNamedPipeSocket } = require("../internal/net"); +const { SocketAddress, addServerName, upgradeDuplexToTLS, isNamedPipeSocket } = require("../internal/net"); +// const { SocketAddress } = require("internal/net/socket_address"); const { ExceptionWithHostPort } = require("internal/shared"); +import type { SocketListener } from "bun"; // IPv4 Segment const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"; @@ -1144,6 +1146,8 @@ function createConnection(port, host, connectListener) { const connect = createConnection; +type MaybeListener = SocketListener | null; + function Server(options, connectionListener): void { if (!(this instanceof Server)) { return new Server(options, connectionListener); @@ -1154,7 +1158,7 @@ function Server(options, connectionListener): void { this[bunSocketServerConnections] = 0; this[bunSocketServerOptions] = undefined; this.maxConnections = 0; - this._handle = null; + this._handle = null as MaybeListener; if (typeof options === "function") { connectionListener = options; @@ -1623,6 +1627,7 @@ export default { setDefaultAutoSelectFamilyAttemptTimeout: $zig("node_net_binding.zig", "setDefaultAutoSelectFamilyAttemptTimeout"), BlockList, + SocketAddress, // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/net.js#L2456 Stream: Socket, }; diff --git a/src/jsc.zig b/src/jsc.zig index 970a289fd8..ae8a3394e1 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -48,6 +48,7 @@ pub const API = struct { pub const TCPSocket = @import("./bun.js/api/bun/socket.zig").TCPSocket; pub const TLSSocket = @import("./bun.js/api/bun/socket.zig").TLSSocket; pub const UDPSocket = @import("./bun.js/api/bun/udp_socket.zig").UDPSocket; + pub const SocketAddress = @import("./bun.js/api/bun/socket.zig").SocketAddress; pub const Listener = @import("./bun.js/api/bun/socket.zig").Listener; pub const H2FrameParser = @import("./bun.js/api/bun/h2_frame_parser.zig").H2FrameParser; pub const NativeZlib = @import("./bun.js/node/node_zlib_binding.zig").SNativeZlib; @@ -83,6 +84,13 @@ const __jsc_log = Output.scoped(.JSC, true); pub inline fn markBinding(src: std.builtin.SourceLocation) void { __jsc_log("{s} ({s}:{d})", .{ src.fn_name, src.file, src.line }); } +pub inline fn markMemberBinding(comptime class: anytype, src: std.builtin.SourceLocation) void { + const classname = switch (@typeInfo(@TypeOf(class))) { + .pointer => class, // assumed to be a static string + else => @typeName(class), + }; + __jsc_log("{s}.{s} ({s}:{d})", .{ classname, src.fn_name, src.file, src.line }); +} pub const Subprocess = API.Bun.Subprocess; pub const ResourceUsage = API.Bun.ResourceUsage; diff --git a/src/string.zig b/src/string.zig index afe82286a0..ed7cfb0e4f 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1046,7 +1046,7 @@ pub const String = extern struct { extern fn JSC__createTypeError(*JSC.JSGlobalObject, str: *const String) JSC.JSValue; extern fn JSC__createRangeError(*JSC.JSGlobalObject, str: *const String) JSC.JSValue; - fn concat(comptime n: usize, allocator: std.mem.Allocator, strings: *const [n]String) !String { + fn concat(comptime n: usize, allocator: std.mem.Allocator, strings: *const [n]String) std.mem.Allocator.Error!String { var num_16bit: usize = 0; inline for (strings) |str| { if (!str.is8Bit()) num_16bit += 1; @@ -1083,7 +1083,7 @@ pub const String = extern struct { /// Creates a new String from a given tuple (of comptime-known size) of String. /// /// Note: the callee owns the resulting string and must call `.deref()` on it once done - pub inline fn createFromConcat(allocator: std.mem.Allocator, strings: anytype) !String { + pub inline fn createFromConcat(allocator: std.mem.Allocator, strings: anytype) std.mem.Allocator.Error!String { return try concat(strings.len, allocator, strings); } @@ -1105,6 +1105,15 @@ pub const String = extern struct { return JSC.jsNumber(width); } + /// Reports owned allocation size, not the actual size of the string. + pub fn estimatedSize(this: *const String) usize { + return switch (this.tag) { + .Dead, .Empty, .StaticZigString => 0, + .ZigString => this.value.ZigString.len, + .WTFStringImpl => this.value.WTFStringImpl.byteLength(), + }; + } + // TODO: move ZigString.Slice here /// A UTF-8 encoded slice tied to the lifetime of a `bun.String` /// Must call `.deinit` to release memory diff --git a/src/string/WTFStringImpl.zig b/src/string/WTFStringImpl.zig index cef71d7e3e..650d53d51f 100644 --- a/src/string/WTFStringImpl.zig +++ b/src/string/WTFStringImpl.zig @@ -96,7 +96,7 @@ pub const WTFStringImplStruct = extern struct { pub inline fn deref(self: WTFStringImpl) void { JSC.markBinding(@src()); const current_count = self.refCount(); - bun.assert(current_count > 0); + bun.assert(self.hasAtLeastOneRef()); // do not use current_count, it breaks for static strings Bun__WTFStringImpl__deref(self); if (comptime bun.Environment.allow_assert) { if (current_count > 1) { @@ -108,11 +108,16 @@ pub const WTFStringImplStruct = extern struct { pub inline fn ref(self: WTFStringImpl) void { JSC.markBinding(@src()); const current_count = self.refCount(); - bun.assert(current_count > 0); + bun.assert(self.hasAtLeastOneRef()); // do not use current_count, it breaks for static strings Bun__WTFStringImpl__ref(self); bun.assert(self.refCount() > current_count or self.isStatic()); } + pub inline fn hasAtLeastOneRef(self: WTFStringImpl) bool { + // WTF::StringImpl::hasAtLeastOneRef + return self.m_refCount > 0; + } + pub fn toLatin1Slice(this: WTFStringImpl) ZigString.Slice { this.ref(); return ZigString.Slice.init(this.refCountAllocator(), this.latin1Slice()); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 7b83ca75ff..b279311995 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1467,65 +1467,69 @@ it("#5859 arrayBuffer", async () => { expect(async () => await Bun.file(tmp).json()).toThrow(); }); -it.if(isIPv4())("server.requestIP (v4)", async () => { - using server = Bun.serve({ - port: 0, - fetch(req, server) { - return Response.json(server.requestIP(req)); - }, - hostname: "127.0.0.1", - }); - - const response = await fetch(server.url.origin).then(x => x.json()); - expect(response).toEqual({ - address: "127.0.0.1", - family: "IPv4", - port: expect.any(Number), - }); -}); - -it.if(isIPv6())("server.requestIP (v6)", async () => { - using server = Bun.serve({ - port: 0, - fetch(req, server) { - return Response.json(server.requestIP(req)); - }, - hostname: "::1", - }); - - const response = await fetch(`http://localhost:${server.port}`).then(x => x.json()); - expect(response).toEqual({ - address: "::1", - family: "IPv6", - port: expect.any(Number), - }); -}); - -it.if(isPosix)("server.requestIP (unix)", async () => { - const unix = join(tmpdirSync(), "serve.sock"); - using server = Bun.serve({ - unix, - fetch(req, server) { - return Response.json(server.requestIP(req)); - }, - }); - const requestText = `GET / HTTP/1.1\r\nHost: localhost\r\n\r\n`; - const received: Buffer[] = []; - const { resolve, promise } = Promise.withResolvers(); - const connection = await Bun.connect({ - unix, - socket: { - data(socket, data) { - received.push(data); - resolve(); +describe("server.requestIP", () => { + it.if(isIPv4())("v4", async () => { + using server = Bun.serve({ + port: 0, + fetch(req, server) { + const ip = server.requestIP(req); + console.log(ip); + return Response.json(ip); }, - }, + hostname: "127.0.0.1", + }); + + const response = await fetch(server.url.origin).then(x => x.json()); + expect(response).toMatchObject({ + address: "127.0.0.1", + family: "IPv4", + port: expect.any(Number), + }); + }); + + it.if(isIPv6())("v6", async () => { + using server = Bun.serve({ + port: 0, + fetch(req, server) { + return Response.json(server.requestIP(req)); + }, + hostname: "::1", + }); + + const response = await fetch(`http://localhost:${server.port}`).then(x => x.json()); + expect(response).toMatchObject({ + address: "::1", + family: "IPv6", + port: expect.any(Number), + }); + }); + + it.if(isPosix)("server.requestIP (unix)", async () => { + const unix = join(tmpdirSync(), "serve.sock"); + using server = Bun.serve({ + unix, + fetch(req, server) { + return Response.json(server.requestIP(req)); + }, + }); + const requestText = `GET / HTTP/1.1\r\nHost: localhost\r\n\r\n`; + const received: Buffer[] = []; + const { resolve, promise } = Promise.withResolvers(); + const connection = await Bun.connect({ + unix, + socket: { + data(socket, data) { + received.push(data); + resolve(); + }, + }, + }); + connection.write(requestText); + connection.flush(); + await promise; + expect(Buffer.concat(received).toString()).toEndWith("\r\n\r\nnull"); + connection.end(); }); - connection.write(requestText); - connection.flush(); - await promise; - expect(Buffer.concat(received).toString()).toEndWith("\r\n\r\nnull"); - connection.end(); }); it("should response with HTTP 413 when request body is larger than maxRequestBodySize, issue#6031", async () => { diff --git a/test/js/bun/udp/udp_socket.test.ts b/test/js/bun/udp/udp_socket.test.ts index 8c8fe1f2c8..1152a40c87 100644 --- a/test/js/bun/udp/udp_socket.test.ts +++ b/test/js/bun/udp/udp_socket.test.ts @@ -20,7 +20,7 @@ describe("udpSocket()", () => { expect(socket.port).toBe(socket.port); // test that property is cached expect(socket.hostname).toBeString(); expect(socket.hostname).toBe(socket.hostname); // test that property is cached - expect(socket.address).toEqual({ + expect(socket.address).toMatchObject({ address: socket.hostname, family: socket.hostname === "::" ? "IPv6" : "IPv4", port: socket.port, diff --git a/test/js/node/net/socketaddress.spec.ts b/test/js/node/net/socketaddress.spec.ts new file mode 100644 index 0000000000..f34b68eb73 --- /dev/null +++ b/test/js/node/net/socketaddress.spec.ts @@ -0,0 +1,331 @@ +/** + * @see https://nodejs.org/api/net.html#class-netsocketaddress + */ +import { SocketAddress, SocketAddressInitOptions } from "node:net"; + +let v4: SocketAddress; +let v6: SocketAddress; + +beforeEach(() => { + v4 = new SocketAddress({ family: "ipv4" }); + v6 = new SocketAddress({ family: "ipv6" }); +}); + +describe("SocketAddress constructor", () => { + it("is named SocketAddress", () => { + expect(SocketAddress.name).toBe("SocketAddress"); + }); + + it("is newable", () => { + // @ts-expect-error -- types are wrong. default is kEmptyObject. + expect(new SocketAddress()).toBeInstanceOf(SocketAddress); + }); + + // FIXME: setting `call: false` in codegen has no effect, but should make the + // constructor non-callable. + it.skip("is not callable", () => { + // @ts-expect-error -- types are wrong. + expect(() => SocketAddress()).toThrow(TypeError); + }); + + describe.each([ + new SocketAddress(), + new SocketAddress(undefined), + new SocketAddress({}), + new SocketAddress({ family: undefined }), + new SocketAddress({ family: "ipv4" }), + ])("new SocketAddress()", address => { + it("creates an ipv4 address", () => { + expect(address.family).toBe("ipv4"); + }); + + it("address is 127.0.0.1", () => { + expect(address.address).toBe("127.0.0.1"); + }); + + it("port is 0", () => { + expect(address.port).toBe(0); + }); + + it("flowlabel is 0", () => { + expect(address.flowlabel).toBe(0); + }); + }); // + + describe("new SocketAddress({ family: 'ipv6' })", () => { + it("creates a new ipv6 any address", () => { + expect(v6).toMatchObject({ + address: "::", + port: 0, + family: "ipv6", + flowlabel: 0, + }); + }); + }); // + + it.each([ + [ + { family: "ipv4", address: "1.2.3.4", port: 1234, flowlabel: 9 }, + { address: "1.2.3.4", port: 1234, family: "ipv4", flowlabel: 0 }, + ], + // family gets lowercased + [{ family: "IPv4" }, { address: "127.0.0.1", family: "ipv4", port: 0 }], + [{ family: "IPV6" }, { address: "::", family: "ipv6", port: 0 }], + ] as [SocketAddressInitOptions, Partial][])( + "new SocketAddress(%o) matches %o", + (options, expected) => { + const address = new SocketAddress(options); + expect(address).toMatchObject(expected); + }, + ); + + // =========================================================================== + // ============================ INVALID ARGUMENTS ============================ + // =========================================================================== + + it.each([Symbol.for("ipv4"), function ipv4() {}, { family: "ipv4" }, "ipv1", "ip"])( + "given an invalid family, throws ERR_INVALID_ARG_VALUE", + (family: any) => { + expect(() => new SocketAddress({ family })).toThrowWithCode(Error, "ERR_INVALID_ARG_VALUE"); + }, + ); + + // =========================================================================== + // ============================= LEAK DETECTION ============================== + // =========================================================================== + + it("does not leak memory", () => { + const growthFactor = 3.0; // allowed growth factor for memory usage + const warmup = 1_000; // # of warmup iterations + const iters = 100_000; // # of iterations + const debug = false; + + // we want to hit both cached and uncached code paths + const options = [ + undefined, + { family: "ipv6" }, + { family: "ipv4", address: "1.2.3.4", port: 3000 }, + { family: "ipv6", address: "::3", port: 9 }, + ] as SocketAddressInitOptions[]; + + // warmup + var sa; + for (let i = 0; i < warmup; i++) { + sa = new SocketAddress(options[i % options.length]); + } + sa = undefined; + Bun.gc(true); + + const before = process.memoryUsage(); + if (debug) console.log("before", before); + + // actual test + for (let i = 0; i < iters; i++) { + sa = new SocketAddress(options[i % 2]); + } + sa = undefined; + Bun.gc(true); + + const after = process.memoryUsage(); + if (debug) console.log("after", after); + + expect(after.rss).toBeLessThanOrEqual(before.rss * growthFactor); + }); +}); // + +describe("SocketAddress.isSocketAddress", () => { + it("is a function that takes 1 argument", () => { + expect(SocketAddress).toHaveProperty("isSocketAddress"); + expect(SocketAddress.isSocketAddress).toBeInstanceOf(Function); + expect(SocketAddress.isSocketAddress).toHaveLength(1); + }); + + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress, "isSocketAddress"); + expect(desc).toEqual({ + value: expect.any(Function), + writable: true, + enumerable: false, + configurable: true, + }); + }); + + it("returns true for a SocketAddress instance", () => { + expect(SocketAddress.isSocketAddress(v4)).toBeTrue(); + expect(SocketAddress.isSocketAddress(v6)).toBeTrue(); + }); + + it("returns false for POJOs that look like a SocketAddress", () => { + const notASocketAddress = { + address: "127.0.0.1", + port: 0, + family: "ipv4", + flowlabel: 0, + }; + expect(SocketAddress.isSocketAddress(notASocketAddress)).toBeFalse(); + }); + + it("returns false for faked SocketAddresses", () => { + const fake = Object.create(SocketAddress.prototype); + for (const key of Object.keys(v4)) { + fake[key] = v4[key]; + } + expect(fake instanceof SocketAddress).toBeTrue(); + expect(SocketAddress.isSocketAddress(fake)).toBeFalse(); + }); + + it("returns false for subclasses", () => { + class NotASocketAddress extends SocketAddress {} + expect(SocketAddress.isSocketAddress(new NotASocketAddress())).toBeFalse(); + }); +}); // + +describe("SocketAddress.parse", () => { + it("is a function that takes 1 argument", () => { + expect(SocketAddress).toHaveProperty("parse"); + expect(SocketAddress.parse).toBeInstanceOf(Function); + expect(SocketAddress.parse).toHaveLength(1); + }); + + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress, "parse"); + expect(desc).toEqual({ + value: expect.any(Function), + writable: true, + enumerable: false, + configurable: true, + }); + }); + + it.each([ + ["1.2.3.4", { address: "1.2.3.4", port: 0, family: "ipv4" }], + ["192.168.257:1", { address: "192.168.1.1", port: 1, family: "ipv4" }], + ["256", { address: "0.0.1.0", port: 0, family: "ipv4" }], + ["999999999:12", { address: "59.154.201.255", port: 12, family: "ipv4" }], + ["0xffffffff", { address: "255.255.255.255", port: 0, family: "ipv4" }], + ["0x.0x.0", { address: "0.0.0.0", port: 0, family: "ipv4" }], + ["[1:0::]", { address: "1::", port: 0, family: "ipv6" }], + ["[1::8]:123", { address: "1::8", port: 123, family: "ipv6" }], + ])("(%s) == %o", (input, expected) => { + const sa = SocketAddress.parse(input); + expect(sa).toBeDefined(); + expect(sa.toJSON()).toMatchObject(expected); + }); + + it.each([ + "", + "invalid", + "1.2.3.4.5.6", + "0.0.0.9999", + "1.2.3.4:-1", + "1.2.3.4:null", + "1.2.3.4:65536", + "[1:0:::::::]", // line break + ])("parse('%s') == undefined", invalidInput => { + expect(SocketAddress.parse(invalidInput)).toBeUndefined(); + }); +}); // + +describe("SocketAddress.prototype.address", () => { + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress.prototype, "address"); + expect(desc).toEqual({ + get: expect.any(Function), + set: undefined, + enumerable: false, + configurable: true, + }); + }); + + it("is read-only", () => { + const addr = new SocketAddress(); + // @ts-expect-error -- ofc it's read-only + expect(() => (addr.address = "1.2.3.4")).toThrow(); + }); +}); // + +describe("SocketAddress.prototype.port", () => { + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress.prototype, "port"); + expect(desc).toEqual({ + get: expect.any(Function), + set: undefined, + enumerable: false, + configurable: true, + }); + }); +}); // + +describe("SocketAddress.prototype.family", () => { + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress.prototype, "family"); + expect(desc).toEqual({ + get: expect.any(Function), + set: undefined, + enumerable: false, + configurable: true, + }); + }); +}); // + +describe("SocketAddress.prototype.flowlabel", () => { + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress.prototype, "flowlabel"); + expect(desc).toEqual({ + get: expect.any(Function), + set: undefined, + enumerable: false, + configurable: true, + }); + }); +}); // + +describe("SocketAddress.prototype.toJSON", () => { + it("is a function that takes 0 arguments", () => { + expect(SocketAddress.prototype).toHaveProperty("toJSON"); + expect(SocketAddress.prototype.toJSON).toBeInstanceOf(Function); + expect(SocketAddress.prototype.toJSON).toHaveLength(0); + }); + + it("has the correct property descriptor", () => { + const desc = Object.getOwnPropertyDescriptor(SocketAddress.prototype, "toJSON"); + expect(desc).toEqual({ + value: expect.any(Function), + writable: true, + enumerable: false, + configurable: true, + }); + }); + + it("returns an object with address, port, family, and flowlabel", () => { + expect(v4.toJSON()).toEqual({ + address: "127.0.0.1", + port: 0, + family: "ipv4", + flowlabel: 0, + }); + expect(v6.toJSON()).toEqual({ + address: "::", + port: 0, + family: "ipv6", + flowlabel: 0, + }); + }); + + describe("When called on a default SocketAddress", () => { + let address: Record; + + beforeEach(() => { + address = v4.toJSON(); + }); + + it("SocketAddress.isSocketAddress() returns false", () => { + expect(SocketAddress.isSocketAddress(address)).toBeFalse(); + }); + + it("does not have SocketAddress as its prototype", () => { + expect(Object.getPrototypeOf(address)).not.toBe(SocketAddress.prototype); + expect(address instanceof SocketAddress).toBeFalse(); + }); + }); // +}); // diff --git a/test/js/node/test/parallel/needs-test/test-socketaddress.js b/test/js/node/test/parallel/needs-test/test-socketaddress.js new file mode 100644 index 0000000000..2b59d46945 --- /dev/null +++ b/test/js/node/test/parallel/needs-test/test-socketaddress.js @@ -0,0 +1,179 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../../common'); +const { + ok, + strictEqual, + throws, +} = require('assert'); +const { + SocketAddress, +} = require('net'); + +// NOTE: we don't check node's internals. +// const { +// InternalSocketAddress, +// } = require('internal/socketaddress'); +// const { internalBinding } = require('internal/test/binding'); +// const { +// SocketAddress: _SocketAddress, +// AF_INET, +// } = internalBinding('block_list'); + +const { describe, it } = require('node:test'); + +describe('net.SocketAddress...', () => { + + it('is cloneable', () => { + const sa = new SocketAddress(); + strictEqual(sa.address, '127.0.0.1'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); + + // NOTE: bun does not support `kClone` yet. + // const mc = new MessageChannel(); + // mc.port1.onmessage = common.mustCall(({ data }) => { + // ok(SocketAddress.isSocketAddress(data)); + + // strictEqual(data.address, '127.0.0.1'); + // strictEqual(data.port, 0); + // strictEqual(data.family, 'ipv4'); + // strictEqual(data.flowlabel, 0); + + // mc.port1.close(); + // }); + // mc.port2.postMessage(sa); + }); + + it('has reasonable defaults', () => { + const sa = new SocketAddress({}); + strictEqual(sa.address, '127.0.0.1'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); + }); + + it('interprets simple ipv4 correctly', () => { + const sa = new SocketAddress({ + address: '123.123.123.123', + }); + strictEqual(sa.address, '123.123.123.123'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); + }); + + it('sets the port correctly', () => { + const sa = new SocketAddress({ + address: '123.123.123.123', + port: 80 + }); + strictEqual(sa.address, '123.123.123.123'); + strictEqual(sa.port, 80); + strictEqual(sa.family, 'ipv4'); + strictEqual(sa.flowlabel, 0); + }); + + it('interprets simple ipv6 correctly', () => { + const sa = new SocketAddress({ + family: 'ipv6' + }); + strictEqual(sa.address, '::'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv6'); + strictEqual(sa.flowlabel, 0); + }); + + it('uses the flowlabel correctly', () => { + const sa = new SocketAddress({ + family: 'ipv6', + flowlabel: 1, + }); + strictEqual(sa.address, '::'); + strictEqual(sa.port, 0); + strictEqual(sa.family, 'ipv6'); + strictEqual(sa.flowlabel, 1); + }); + + it('validates input correctly', () => { + [1, false, 'hello'].forEach((i) => { + throws(() => new SocketAddress(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, false, {}, [], 'test'].forEach((family) => { + throws(() => new SocketAddress({ family }), { + code: 'ERR_INVALID_ARG_VALUE' + }); + }); + + [1, false, {}, []].forEach((address) => { + throws(() => new SocketAddress({ address }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [-1, false, {}, []].forEach((port) => { + throws(() => new SocketAddress({ port }), { + code: 'ERR_SOCKET_BAD_PORT' + }); + }); + + throws(() => new SocketAddress({ flowlabel: -1 }), { + code: 'ERR_OUT_OF_RANGE' + }); + }); + + // NOTE: we don't check node's internals. + // it('InternalSocketAddress correctly inherits from SocketAddress', () => { + // // Test that the internal helper class InternalSocketAddress correctly + // // inherits from SocketAddress and that it does not throw when its properties + // // are accessed. + + // const address = '127.0.0.1'; + // const port = 8080; + // const flowlabel = 0; + // const handle = new _SocketAddress(address, port, AF_INET, flowlabel); + // const addr = new InternalSocketAddress(handle); + // ok(addr instanceof SocketAddress); + // strictEqual(addr.address, address); + // strictEqual(addr.port, port); + // strictEqual(addr.family, 'ipv4'); + // strictEqual(addr.flowlabel, flowlabel); + // }); + + it('SocketAddress.parse() works as expected', () => { + const good = [ + { input: '1.2.3.4', address: '1.2.3.4', port: 0, family: 'ipv4' }, + { input: '192.168.257:1', address: '192.168.1.1', port: 1, family: 'ipv4' }, + { input: '256', address: '0.0.1.0', port: 0, family: 'ipv4' }, + { input: '999999999:12', address: '59.154.201.255', port: 12, family: 'ipv4' }, + { input: '0xffffffff', address: '255.255.255.255', port: 0, family: 'ipv4' }, + { input: '0x.0x.0', address: '0.0.0.0', port: 0, family: 'ipv4' }, + { input: '[1:0::]', address: '1::', port: 0, family: 'ipv6' }, + { input: '[1::8]:123', address: '1::8', port: 123, family: 'ipv6' }, + ]; + + good.forEach((i) => { + const addr = SocketAddress.parse(i.input); + strictEqual(addr.address, i.address); + strictEqual(addr.port, i.port); + strictEqual(addr.family, i.family); + }); + + const bad = [ + 'not an ip', + 'abc.123', + '259.1.1.1', + '12:12:12', + ]; + + bad.forEach((i) => { + strictEqual(SocketAddress.parse(i), undefined); + }); + }); + +});