From 9e201eff9e0df60bec571d7df35bf7540f87bd51 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Thu, 1 May 2025 15:09:44 -0800 Subject: [PATCH] node:net: implement BlockList (#19277) --- src/bun.js/api.zig | 1 + src/bun.js/api/bun/socket/SocketAddress.zig | 143 +++++---- src/bun.js/api/sockets.classes.ts | 37 +++ src/bun.js/bindings/JSGlobalObject.zig | 11 + .../bindings/generated_classes_list.zig | 2 +- .../webcore/SerializedScriptValue.cpp | 1 + src/bun.js/node/net/BlockList.zig | 240 +++++++++++++++ src/bun.js/node/node_net_binding.zig | 18 +- src/codegen/class-definitions.ts | 5 +- src/codegen/generate-classes.ts | 27 ++ src/codegen/generate-node-errors.ts | 9 +- src/js/internal/net.ts | 14 - src/js/node/dgram.ts | 2 +- src/js/node/dns.ts | 2 +- src/js/node/net.ts | 39 +-- src/js/node/tls.ts | 2 +- src/ptr/CowSlice.zig | 2 +- src/string.zig | 8 + test/internal/ban-words.test.ts | 1 + test/js/node/cluster.test.ts | 36 ++- .../test/parallel/test-blocklist-clone.js | 31 ++ test/js/node/test/parallel/test-blocklist.js | 284 ++++++++++++++++++ test/js/web/workers/message-channel.test.ts | 27 +- test/js/web/workers/structured-clone.test.ts | 14 + 24 files changed, 840 insertions(+), 116 deletions(-) create mode 100644 src/bun.js/node/net/BlockList.zig delete mode 100644 src/js/internal/net.ts create mode 100644 test/js/node/test/parallel/test-blocklist-clone.js create mode 100644 test/js/node/test/parallel/test-blocklist.js diff --git a/src/bun.js/api.zig b/src/bun.js/api.zig index 9ca9b4f52d..fc932fd3ab 100644 --- a/src/bun.js/api.zig +++ b/src/bun.js/api.zig @@ -42,6 +42,7 @@ pub const TCPSocket = @import("api/bun/socket.zig").TCPSocket; pub const TLSSocket = @import("api/bun/socket.zig").TLSSocket; pub const UDPSocket = @import("api/bun/udp_socket.zig").UDPSocket; pub const Valkey = @import("../valkey/js_valkey.zig").JSValkeyClient; +pub const BlockList = @import("./node/net/BlockList.zig"); pub const napi = @import("../napi/napi.zig"); diff --git a/src/bun.js/api/bun/socket/SocketAddress.zig b/src/bun.js/api/bun/socket/SocketAddress.zig index 865ef39707..55438bfc71 100644 --- a/src/bun.js/api/bun/socket/SocketAddress.zig +++ b/src/bun.js/api/bun/socket/SocketAddress.zig @@ -5,6 +5,7 @@ //! TODO: add a inspect method (under `Symbol.for("nodejs.util.inspect.custom")`). //! Requires updating bindgen. const SocketAddress = @This(); +const validators = @import("./../../../node/util/validators.zig"); pub const js = JSC.Codegen.JSSocketAddress; pub const toJS = js.toJS; pub const fromJS = js.fromJS; @@ -47,39 +48,7 @@ pub const Options = struct { 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.fromJS(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); - } + break :blk try .fromJS(global, fam); } else AF.INET; // required. Validated by `validatePort`. @@ -110,9 +79,6 @@ pub const Options = struct { }; } - 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(); @@ -218,6 +184,16 @@ pub fn constructor(global: *JSC.JSGlobalObject, frame: *JSC.CallFrame) bun.JSErr return SocketAddress.create(global, options); } +pub fn initFromAddrFamily(global: *JSC.JSGlobalObject, address_js: JSValue, family_js: JSValue) bun.JSError!SocketAddress { + if (!address_js.isString()) return global.throwInvalidArgumentTypeValue("options.address", "string", address_js); + const address_: bun.String = try .fromJS(address_js, global); + const family_: AF = try .fromJS(global, family_js); + return .initJS(global, .{ + .address = address_, + .family = family_, + }); +} + /// Semi-structured JS api for creating a `SocketAddress`. If you have raw /// socket address data, prefer `SocketAddress.new`. /// @@ -225,6 +201,10 @@ pub fn constructor(global: *JSC.JSGlobalObject, frame: *JSC.CallFrame) bun.JSErr /// - `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 { + return .new(try .initJS(global, options)); +} + +pub fn initJS(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 @@ -272,10 +252,10 @@ pub fn create(global: *JSC.JSGlobalObject, options: Options) bun.JSError!*Socket }, }; - return SocketAddress.new(.{ + return .{ ._addr = addr, ._presentation = presentation, - }); + }; } pub const AddressError = error{ @@ -399,20 +379,9 @@ pub fn getAddress(this: *SocketAddress, global: *JSC.JSGlobalObject) JSC.JSValue /// - 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.webcore.encoding.toBunStringComptime(formatted, .latin1); + const formatted = this._addr.fmt(&buf); + const presentation = JSC.WebCore.encoding.toBunStringComptime(formatted, .latin1); bun.debugAssert(presentation.tag != .Dead); this._presentation = presentation; return presentation; @@ -535,9 +504,46 @@ const ipv6: bun.String = .{ .tag = .WTFStringImpl, .value = .{ .WTFStringImpl = 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); } + + pub fn fromJS(global: *JSC.JSGlobalObject, value: JSValue) !AF { + if (value.isString()) { + const fam_str = try bun.String.fromJS(value, global); + defer fam_str.deref(); + if (fam_str.length() != 4) return global.throwInvalidArgumentPropertyValue("options.family", "'ipv4' or 'ipv6'", value); + + if (fam_str.is8Bit()) { + const slice = fam_str.latin1(); + if (std.ascii.eqlIgnoreCase(slice[0..4], "ipv4")) return AF.INET; + if (std.ascii.eqlIgnoreCase(slice[0..4], "ipv6")) return AF.INET6; + return global.throwInvalidArgumentPropertyValue("options.family", "'ipv4' or 'ipv6'", value); + } 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")) return AF.INET; + if (fam_str.eqlComptime("ipv6") or fam_str.eqlComptime("IPv6")) return AF.INET6; + return global.throwInvalidArgumentPropertyValue("options.family", "'ipv4' or 'ipv6'", value); + } + } else if (value.isUInt32AsAnyInt()) { + return switch (value.toU32()) { + AF.INET.int() => AF.INET, + AF.INET6.int() => AF.INET6, + else => return global.throwInvalidArgumentPropertyValue("options.family", "AF_INET or AF_INET6", value), + }; + } else { + return global.throwInvalidArgumentPropertyValue("options.family", "a string or number", value); + } + } + + pub fn upper(this: AF) [:0]const u8 { + return switch (this) { + .INET => "IPv4", + .INET6 => "IPv6", + }; + } }; /// ## Notes @@ -545,7 +551,7 @@ pub const AF = enum(inet.sa_family_t) { /// 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 { +pub const sockaddr = extern union { sin: inet.sockaddr_in, sin6: inet.sockaddr_in6, @@ -574,11 +580,42 @@ const sockaddr = extern union { } }; } + pub fn as_v4(self: *const sockaddr) ?u32 { + if (self.sin.family == std.posix.AF.INET) return self.sin.addr; + if (self.sin.family == std.posix.AF.INET6) { + if (!std.mem.allEqual(u8, self.sin6.addr[0..10], 0)) return null; + if (self.sin6.addr[10] != 255) return null; + if (self.sin6.addr[11] != 255) return null; + return @bitCast(self.sin6.addr[12..16].*); + } + return null; + } + + pub fn family(self: *const sockaddr) AF { + return switch (self.sin.family) { + std.posix.AF.INET => .INET, + std.posix.AF.INET6 => .INET6, + else => unreachable, + }; + } + + pub fn fmt(self: *const sockaddr, buf: *[inet.INET6_ADDRSTRLEN]u8) [:0]const u8 { + const addr_src: *const anyopaque = if (self.family() == AF.INET) @ptrCast(&self.sin.addr) else @ptrCast(&self.sin6.addr); + const formatted = std.mem.sliceTo(ares.ares_inet_ntop(self.family().int(), addr_src, buf, buf.len) orelse { + std.debug.panic("Invariant violation: SocketAddress created with invalid IPv6 address ({any})", .{self}); + }, 0); + if (comptime bun.Environment.isDebug) bun.assertWithLocation(bun.strings.isAllASCII(formatted), @src()); + return formatted; + } + // 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); + + pub const in = inet.sockaddr_in; + pub const in6 = inet.sockaddr_in6; }; const WellKnownAddress = struct { @@ -631,7 +668,7 @@ const JSValue = JSC.JSValue; const isDebug = bun.Environment.isDebug; const allow_assert = bun.Environment.allow_assert; -const inet = if (bun.Environment.isWindows) +pub const inet = if (bun.Environment.isWindows) win: { const ws2 = std.os.windows.ws2_32; break :win struct { diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index b85d79176e..84f574fe55 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -448,4 +448,41 @@ export default [ }, }, }), + define({ + name: "BlockList", + construct: true, + call: false, + finalize: true, + estimatedSize: true, + // customInspect: true, + structuredClone: { transferable: false, tag: 251 }, + JSType: "0b11101110", + klass: { + isBlockList: { + fn: "isBlockList", + length: 1, + }, + }, + proto: { + addAddress: { + fn: "addAddress", + length: 1, + }, + addRange: { + fn: "addRange", + length: 2, + }, + addSubnet: { + fn: "addSubnet", + length: 2, + }, + check: { + fn: "check", + length: 1, + }, + rules: { + getter: "rules", + }, + }, + }), ]; diff --git a/src/bun.js/bindings/JSGlobalObject.zig b/src/bun.js/bindings/JSGlobalObject.zig index 9629e2c81a..d8d31cf3d3 100644 --- a/src/bun.js/bindings/JSGlobalObject.zig +++ b/src/bun.js/bindings/JSGlobalObject.zig @@ -89,6 +89,17 @@ pub const JSGlobalObject = opaque { return this.ERR(.INVALID_ARG_VALUE, "The \"{s}\" argument is invalid. Received {}", .{ argname, actual_string_value }).throw(); } + pub fn throwInvalidArgumentValueCustom( + this: *JSGlobalObject, + argname: []const u8, + value: JSValue, + message: []const u8, + ) bun.JSError { + const actual_string_value = try determineSpecificType(this, value); + defer actual_string_value.deref(); + return this.ERR(.INVALID_ARG_VALUE, "The \"{s}\" argument {s}. Received {}", .{ argname, message, 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}" diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 44de989749..09f3f0f708 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -81,9 +81,9 @@ pub const Classes = struct { pub const NodeHTTPResponse = api.NodeHTTPResponse; pub const FrameworkFileSystemRouter = bun.bake.FrameworkRouter.JSFrameworkRouter; pub const DNSResolver = api.DNS.DNSResolver; - pub const S3Client = webcore.S3Client; pub const S3Stat = webcore.S3Stat; pub const HTMLBundle = api.HTMLBundle; pub const RedisClient = api.Valkey; + pub const BlockList = api.BlockList; }; diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 6c4a4291eb..7e0a8662f5 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -243,6 +243,7 @@ enum SerializationTag { // bun types start at 254 and decrease with each addition Bun__X509CertificateTag = 253, Bun__KeyObjectTag = 252, + Bun__nodenet_BlockList = 251, ErrorTag = 255 }; diff --git a/src/bun.js/node/net/BlockList.zig b/src/bun.js/node/net/BlockList.zig new file mode 100644 index 0000000000..067c6f151f --- /dev/null +++ b/src/bun.js/node/net/BlockList.zig @@ -0,0 +1,240 @@ +const std = @import("std"); +const bun = @import("bun"); +const C = bun.c; +const Environment = bun.Environment; +const JSC = bun.JSC; +const string = bun.string; +const Output = bun.Output; +const ZigString = JSC.ZigString; +const validators = @import("./../util/validators.zig"); +const SocketAddress = bun.JSC.GeneratedClassesList.SocketAddress; +const sockaddr = SocketAddress.sockaddr; + +const RefCount = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", deinit, .{}); +pub const new = bun.TrivialNew(@This()); +pub const ref = RefCount.ref; +pub const deref = RefCount.deref; + +const js = JSC.Codegen.JSBlockList; +pub const fromJS = js.fromJS; +pub const toJS = js.toJS; + +ref_count: RefCount = .init(), +globalThis: *JSC.JSGlobalObject, +da_rules: std.ArrayList(Rule), +mutex: bun.Mutex = .{}, + +pub fn constructor(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!*@This() { + _ = callFrame; + const ptr = @This().new(.{ + .globalThis = globalThis, + .da_rules = .init(bun.default_allocator), + }); + return ptr; +} + +pub fn estimatedSize(this: *@This()) usize { + this.mutex.lock(); + defer this.mutex.unlock(); + return @sizeOf(@This()) + (@sizeOf(Rule) * this.da_rules.items.len); +} + +pub fn finalize(this: *@This()) void { + this.deref(); +} + +pub fn deinit(this: *@This()) void { + this.da_rules.deinit(); + bun.destroy(this); +} + +pub fn isBlockList(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + _ = globalThis; + const value = callframe.argumentsAsArray(1)[0]; + return .jsBoolean(value.as(@This()) != null); +} + +pub fn addAddress(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + this.mutex.lock(); + defer this.mutex.unlock(); + const arguments = callframe.argumentsAsArray(2); + const address_js, var family_js = arguments; + if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); + const address = if (address_js.as(SocketAddress)) |sa| sa._addr else blk: { + try validators.validateString(globalThis, address_js, "address", .{}); + try validators.validateString(globalThis, family_js, "family", .{}); + break :blk (try SocketAddress.initFromAddrFamily(globalThis, address_js, family_js))._addr; + }; + try this.da_rules.insert(0, .{ .addr = address }); + return .jsUndefined(); +} + +pub fn addRange(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + this.mutex.lock(); + defer this.mutex.unlock(); + const arguments = callframe.argumentsAsArray(3); + const start_js, const end_js, var family_js = arguments; + if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); + const start = if (start_js.as(SocketAddress)) |sa| sa._addr else blk: { + try validators.validateString(globalThis, start_js, "start", .{}); + try validators.validateString(globalThis, family_js, "family", .{}); + break :blk (try SocketAddress.initFromAddrFamily(globalThis, start_js, family_js))._addr; + }; + const end = if (end_js.as(SocketAddress)) |sa| sa._addr else blk: { + try validators.validateString(globalThis, end_js, "end", .{}); + try validators.validateString(globalThis, family_js, "family", .{}); + break :blk (try SocketAddress.initFromAddrFamily(globalThis, end_js, family_js))._addr; + }; + if (_compare(start, end)) |ord| { + if (ord.compare(.gt)) { + return globalThis.throwInvalidArgumentValueCustom("start", start_js, "must come before end"); + } + } + try this.da_rules.insert(0, .{ .range = .{ .start = start, .end = end } }); + return .jsUndefined(); +} + +pub fn addSubnet(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + this.mutex.lock(); + defer this.mutex.unlock(); + const arguments = callframe.argumentsAsArray(3); + const network_js, const prefix_js, var family_js = arguments; + if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); + const network = if (network_js.as(SocketAddress)) |sa| sa._addr else blk: { + try validators.validateString(globalThis, network_js, "network", .{}); + try validators.validateString(globalThis, family_js, "family", .{}); + break :blk (try SocketAddress.initFromAddrFamily(globalThis, network_js, family_js))._addr; + }; + var prefix: u8 = 0; + switch (network.sin.family) { + std.posix.AF.INET => prefix = @intCast(try validators.validateInt32(globalThis, prefix_js, "prefix", .{}, 0, 32)), + std.posix.AF.INET6 => prefix = @intCast(try validators.validateInt32(globalThis, prefix_js, "prefix", .{}, 0, 128)), + else => {}, + } + try this.da_rules.insert(0, .{ .subnet = .{ .network = network, .prefix = prefix } }); + return .jsUndefined(); +} + +pub fn check(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + this.mutex.lock(); + defer this.mutex.unlock(); + const arguments = callframe.argumentsAsArray(2); + const address_js, var family_js = arguments; + if (family_js.isUndefined()) family_js = bun.String.static("ipv4").toJS(globalThis); + const address = if (address_js.as(SocketAddress)) |sa| sa._addr else blk: { + try validators.validateString(globalThis, address_js, "address", .{}); + try validators.validateString(globalThis, family_js, "family", .{}); + break :blk (SocketAddress.initFromAddrFamily(globalThis, address_js, family_js) catch |err| { + bun.debugAssert(err == error.JSError); + globalThis.clearException(); + return .jsBoolean(false); + })._addr; + }; + for (this.da_rules.items) |item| { + switch (item) { + .addr => |a| { + const order = _compare(address, a) orelse continue; + if (order.compare(.eq)) return .jsBoolean(true); + }, + .range => |r| { + const os = _compare(address, r.start) orelse continue; + const oe = _compare(address, r.end) orelse continue; + if (os.compare(.gte) and oe.compare(.lte)) return .jsBoolean(true); + }, + .subnet => |s| { + if (address.as_v4()) |ip_addr| if (s.network.as_v4()) |subnet_addr| { + if (s.prefix == 32) if (ip_addr == subnet_addr) (return .jsBoolean(true)) else continue; + const one: u32 = 1; + const mask_addr = ((one << @intCast(s.prefix)) - 1) << @intCast(32 - s.prefix); + const ip_net: u32 = @byteSwap(ip_addr) & mask_addr; + const subnet_net: u32 = @byteSwap(subnet_addr) & mask_addr; + if (ip_net == subnet_net) return .jsBoolean(true); + }; + if (address.sin.family == std.posix.AF.INET6 and s.network.sin.family == std.posix.AF.INET6) { + const ip_addr: u128 = @bitCast(address.sin6.addr); + const subnet_addr: u128 = @bitCast(s.network.sin6.addr); + if (s.prefix == 128) if (ip_addr == subnet_addr) (return .jsBoolean(true)) else continue; + const one: u128 = 1; + const mask_addr = ((one << @intCast(s.prefix)) - 1) << @intCast(128 - s.prefix); + const ip_net: u128 = @byteSwap(ip_addr) & mask_addr; + const subnet_net: u128 = @byteSwap(subnet_addr) & mask_addr; + if (ip_net == subnet_net) return .jsBoolean(true); + } + }, + } + } + return .jsBoolean(false); +} + +pub fn rules(this: *@This(), globalThis: *JSC.JSGlobalObject) JSC.JSValue { + this.mutex.lock(); + defer this.mutex.unlock(); + var list = std.ArrayList(JSC.JSValue).initCapacity(bun.default_allocator, this.da_rules.items.len) catch bun.outOfMemory(); + defer list.deinit(); + for (this.da_rules.items) |rule| { + switch (rule) { + .addr => |a| { + var buf: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); + list.appendAssumeCapacity(bun.String.createFormatForJS(globalThis, "Address: {s} {s}", .{ a.family().upper(), a.fmt(&buf) })); + }, + .range => |r| { + var buf_s: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); + var buf_e: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); + list.appendAssumeCapacity(bun.String.createFormatForJS(globalThis, "Range: {s} {s}-{s}", .{ r.start.family().upper(), r.start.fmt(&buf_s), r.end.fmt(&buf_e) })); + }, + .subnet => |s| { + var buf: [SocketAddress.inet.INET6_ADDRSTRLEN]u8 = @splat(0); + list.appendAssumeCapacity(bun.String.createFormatForJS(globalThis, "Subnet: {s} {s}/{d}", .{ s.network.family().upper(), s.network.fmt(&buf), s.prefix })); + }, + } + } + return JSC.JSArray.create(globalThis, list.items); +} + +pub fn onStructuredCloneSerialize(this: *@This(), globalThis: *JSC.JSGlobalObject, ctx: *anyopaque, writeBytes: *const fn (*anyopaque, ptr: [*]const u8, len: u32) callconv(JSC.conv) void) void { + _ = globalThis; + this.mutex.lock(); + defer this.mutex.unlock(); + this.ref(); + const writer = StructuredCloneWriter.Writer{ .context = .{ .ctx = ctx, .impl = writeBytes } }; + try writer.writeInt(usize, @intFromPtr(this), .little); +} + +const StructuredCloneWriter = struct { + ctx: *anyopaque, + impl: *const fn (*anyopaque, ptr: [*]const u8, len: u32) callconv(JSC.conv) void, + + pub const Writer = std.io.Writer(@This(), Error, write); + pub const Error = error{}; + + fn write(this: StructuredCloneWriter, bytes: []const u8) Error!usize { + this.impl(this.ctx, bytes.ptr, @as(u32, @truncate(bytes.len))); + return bytes.len; + } +}; + +pub fn onStructuredCloneDeserialize(globalThis: *JSC.JSGlobalObject, ptr: [*]u8, end: [*]u8) bun.JSError!JSC.JSValue { + const total_length: usize = @intFromPtr(end) - @intFromPtr(ptr); + var buffer_stream = std.io.fixedBufferStream(ptr[0..total_length]); + const reader = buffer_stream.reader(); + + const int = reader.readInt(usize, .little) catch return globalThis.throw("BlockList.onStructuredCloneDeserialize failed", .{}); + const this: *@This() = @ptrFromInt(int); + return this.toJS(globalThis); +} + +pub const Rule = union(enum) { + addr: sockaddr, + range: struct { start: sockaddr, end: sockaddr }, + subnet: struct { network: sockaddr, prefix: u8 }, +}; + +fn _compare(l: sockaddr, r: sockaddr) ?std.math.Order { + if (l.as_v4()) |l_4| if (r.as_v4()) |r_4| return std.math.order(@byteSwap((l_4)), @byteSwap((r_4))); + if (l.sin.family == std.posix.AF.INET6 and r.sin.family == std.posix.AF.INET6) return _compare_ipv6(l.sin6, r.sin6); + return null; +} + +fn _compare_ipv6(l: sockaddr.in6, r: sockaddr.in6) std.math.Order { + return std.math.order(@byteSwap((@as(u128, @bitCast(l.addr)))), @byteSwap((@as(u128, @bitCast(r.addr))))); +} diff --git a/src/bun.js/node/node_net_binding.zig b/src/bun.js/node/node_net_binding.zig index 0582c15535..b22bd62994 100644 --- a/src/bun.js/node/node_net_binding.zig +++ b/src/bun.js/node/node_net_binding.zig @@ -6,6 +6,7 @@ const JSC = bun.JSC; const string = bun.string; const Output = bun.Output; const ZigString = JSC.ZigString; +const validators = @import("./util/validators.zig"); // // @@ -67,21 +68,14 @@ pub fn setDefaultAutoSelectFamilyAttemptTimeout(global: *JSC.JSGlobalObject) JSC return globalThis.throw("missing argument", .{}); } const arg = arguments.slice()[0]; - if (!arg.isInt32AsAnyInt()) { - return globalThis.throwInvalidArguments("autoSelectFamilyAttemptTimeoutDefault", .{}); - } - const value: u32 = @max(10, arg.coerceToInt32(globalThis)); - autoSelectFamilyAttemptTimeoutDefault = value; + var value = try validators.validateInt32(globalThis, arg, "value", .{}, 1, null); + if (value < 10) value = 10; + autoSelectFamilyAttemptTimeoutDefault = @intCast(value); return JSC.jsNumber(value); } }).setter, 1, .{}); } -pub fn createBinding(global: *JSC.JSGlobalObject) JSC.JSValue { - const SocketAddress = bun.JSC.GeneratedClassesList.SocketAddress; - const net = JSC.JSValue.createEmptyObjectWithNullPrototype(global); +pub const SocketAddress = bun.JSC.Codegen.JSSocketAddress.getConstructor; - net.put(global, "SocketAddress", SocketAddress.js.getConstructor(global)); - - return net; -} +pub const BlockList = JSC.Codegen.JSBlockList.getConstructor; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 25df8fd801..eca4f2a6f0 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -202,7 +202,8 @@ export class ClassDefinition { configurable?: boolean; enumerable?: boolean; - structuredClone?: boolean | { transferable: boolean; tag: number }; + structuredClone?: { transferable: boolean; tag: number }; + customInspect?: boolean; callbacks?: Record; @@ -245,7 +246,7 @@ export function define( estimatedSize = false, call = false, construct = false, - structuredClone = false, + structuredClone, ...rest } = {} as Partial, ): ClassDefinition { diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index a980fcfaff..fa764a40fc 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -426,6 +426,15 @@ JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); "onStructuredCloneDeserialize", )}(JSC::JSGlobalObject*, const uint8_t*, const uint8_t*);` + "\n"; } + if (obj.customInspect) { + externs += `extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${protoSymbolName(typeName, "customInspect")}(JSC::JSGlobalObject*, JSC::CallFrame*);\n`; + + specialSymbols += ` + this->putDirect(vm, builtinNames(vm).inspectCustomPublicName(), JSFunction::create(vm, globalObject, 2, String("[nodejs.util.inspect.custom]"_s), ${protoSymbolName( + typeName, + "customInspect", + )}, ImplementationVisibility::Public), PropertyAttribute::Function | 0);`; + } if (obj.finalize) { externs += `extern JSC_CALLCONV void JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "finalize")}(void*);` + "\n"; @@ -1778,6 +1787,7 @@ function generateZig( values = [], hasPendingActivity = false, structuredClone = false, + customInspect = false, getInternalProperties = false, callbacks = {}, } = {} as ClassDefinition, @@ -1802,6 +1812,10 @@ function generateZig( exports.set("onStructuredCloneDeserialize", symbolName(typeName, "onStructuredCloneDeserialize")); } + if (customInspect) { + exports.set("customInspect", symbolName(typeName, "customInspect")); + } + proto = { ...Object.fromEntries(Object.entries(own || {}).map(([name, getterName]) => [name, { getter: getterName }])), ...proto, @@ -2059,6 +2073,19 @@ const JavaScriptCoreBindings = struct { `; } + if (customInspect) { + // TODO: perhaps exposing this on classes directly isn't the best API choice long term + // it would be better to make a different signature that accepts a writer, then a generated-only function that returns a js string + // the writer function can integrate with our native console.log implementation, the generated function can call the writer version and collect the result + exports.set("customInspect", protoSymbolName(typeName, "customInspect")); + output += ` + pub fn ${protoSymbolName(typeName, "customInspect")}(thisValue: *${typeName}, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { + if (comptime Environment.enable_logs) JSC.markBinding(@src()); + return @call(.always_inline, JSC.toJSHostValue, .{globalObject, @call(.always_inline, ${typeName}.customInspect, .{thisValue, globalObject, callFrame})}); + } + `; + } + return ( output.trim() + ` diff --git a/src/codegen/generate-node-errors.ts b/src/codegen/generate-node-errors.ts index 4db13ccba1..aafbe8c4e1 100644 --- a/src/codegen/generate-node-errors.ts +++ b/src/codegen/generate-node-errors.ts @@ -12,8 +12,9 @@ const extra_count = NodeErrors.map(x => x.slice(3)) .reduce((ac, cv) => ac + cv.length, 0); const count = NodeErrors.length + extra_count; -if (count > 65536) { - throw new Error("NodeError count exceeds u16"); +if (count > 1 << 16) { + // increase size of the enums below to have more tags + throw new Error(`NodeError can't fit ${count} codes in a u16`); } let enumHeader = ``; @@ -166,9 +167,9 @@ for (const [code, constructor, name, ...other_constructors] of NodeErrors) { ? `${constructor.name} & { name: "${name}", code: "${code}" }` : `${constructor.name} & { code: "${code}" }`; dts += ` -/** +/** * Construct an {@link ${constructor.name} ${constructor.name}} with the \`"${code}"\` error code. - * + * * To override this, update ErrorCode.cpp. To remove this generated type, mention \`"${code}"\` in builtins.d.ts. */ declare function $${code}(message: string): ${namedError};\n`; diff --git a/src/js/internal/net.ts b/src/js/internal/net.ts deleted file mode 100644 index f995f2c7bf..0000000000 --- a/src/js/internal/net.ts +++ /dev/null @@ -1,14 +0,0 @@ -const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket, getBufferedAmount] = $zig( - "socket.zig", - "createNodeTLSBinding", -); -const { SocketAddress } = $zig("node_net_binding.zig", "createBinding"); - -export default { - addServerName, - upgradeDuplexToTLS, - isNamedPipeSocket, - getBufferedAmount, - SocketAddress, - normalizedArgsSymbol: Symbol("normalizedArgs"), -}; diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index 7251471c31..0dd094da1e 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -53,7 +53,7 @@ const { validateAbortSignal, } = require("internal/validators"); -const { isIP } = require("./net"); +const { isIP } = require("node:net"); const EventEmitter = require("node:events"); diff --git a/src/js/node/dns.ts b/src/js/node/dns.ts index b0c5636353..c6e6ca7704 100644 --- a/src/js/node/dns.ts +++ b/src/js/node/dns.ts @@ -1,7 +1,7 @@ // Hardcoded module "node:dns" const dns = Bun.dns; const utilPromisifyCustomSymbol = Symbol.for("nodejs.util.promisify.custom"); -const { isIP } = require("./net"); +const { isIP } = require("node:net"); const { validateFunction, validateArray, diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 6949318d18..82dfd467ca 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -22,20 +22,24 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. const { Duplex } = require("node:stream"); const EventEmitter = require("node:events"); -const { - SocketAddress, - addServerName, - upgradeDuplexToTLS, - isNamedPipeSocket, - normalizedArgsSymbol, - getBufferedAmount, -} = require("internal/net"); +const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket, getBufferedAmount] = $zig( + "socket.zig", + "createNodeTLSBinding", +); +const normalizedArgsSymbol = Symbol("normalizedArgs"); const { ExceptionWithHostPort } = require("internal/shared"); import type { SocketListener, SocketHandler } from "bun"; import type { ServerOpts } from "node:net"; const { getTimerDuration } = require("internal/timers"); const { validateFunction, validateNumber, validateAbortSignal } = require("internal/validators"); +const getDefaultAutoSelectFamily = $zig("node_net_binding.zig", "getDefaultAutoSelectFamily"); +const setDefaultAutoSelectFamily = $zig("node_net_binding.zig", "setDefaultAutoSelectFamily"); +const getDefaultAutoSelectFamilyAttemptTimeout = $zig("node_net_binding.zig", "getDefaultAutoSelectFamilyAttemptTimeout"); // prettier-ignore +const setDefaultAutoSelectFamilyAttemptTimeout = $zig("node_net_binding.zig", "setDefaultAutoSelectFamilyAttemptTimeout"); // prettier-ignore +const SocketAddress = $zig("node_net_binding.zig", "SocketAddress"); +const BlockList = $zig("node_net_binding.zig", "BlockList"); + // IPv4 Segment const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"; const v4Str = `(?:${v4Seg}\\.){3}${v4Seg}`; @@ -1675,17 +1679,6 @@ function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; } -// TODO: -class BlockList { - constructor() {} - - addSubnet(_net, _prefix, _type) {} - - check(_address, _type) { - return false; - } -} - export default { createServer, Server, @@ -1697,10 +1690,10 @@ export default { Socket, _normalizeArgs: normalizeArgs, - getDefaultAutoSelectFamily: $zig("node_net_binding.zig", "getDefaultAutoSelectFamily"), - setDefaultAutoSelectFamily: $zig("node_net_binding.zig", "setDefaultAutoSelectFamily"), - getDefaultAutoSelectFamilyAttemptTimeout: $zig("node_net_binding.zig", "getDefaultAutoSelectFamilyAttemptTimeout"), - setDefaultAutoSelectFamilyAttemptTimeout: $zig("node_net_binding.zig", "setDefaultAutoSelectFamilyAttemptTimeout"), + getDefaultAutoSelectFamily, + setDefaultAutoSelectFamily, + getDefaultAutoSelectFamilyAttemptTimeout, + setDefaultAutoSelectFamilyAttemptTimeout, BlockList, SocketAddress, diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index 6a4c751c54..1e60a9a37a 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -2,7 +2,7 @@ const { isArrayBufferView, isTypedArray } = require("node:util/types"); const net = require("node:net"); const { Duplex } = require("node:stream"); -const { addServerName } = require("internal/net"); +const [addServerName] = $zig("socket.zig", "createNodeTLSBinding"); const { throwNotImplemented } = require("internal/shared"); const { throwOnInvalidTLSArray } = require("internal/tls"); diff --git a/src/ptr/CowSlice.zig b/src/ptr/CowSlice.zig index 7172d5ee99..108a34efd1 100644 --- a/src/ptr/CowSlice.zig +++ b/src/ptr/CowSlice.zig @@ -275,7 +275,7 @@ pub fn CowSliceZ(T: type, comptime sentinel: ?T) type { const cow_str_assertions = Environment.isDebug; const DebugData = if (cow_str_assertions) struct { - mutex: std.Thread.Mutex = .{}, + mutex: bun.Mutex = .{}, allocator: Allocator, /// number of active borrows borrows: usize = 0, diff --git a/src/string.zig b/src/string.zig index 671703ebc9..af4c10c70b 100644 --- a/src/string.zig +++ b/src/string.zig @@ -849,6 +849,14 @@ pub const String = extern struct { return BunString__createUTF8ForJS(globalObject, utf8_slice.ptr, utf8_slice.len); } + pub fn createFormatForJS(globalObject: *JSC.JSGlobalObject, comptime fmt: [:0]const u8, args: anytype) JSC.JSValue { + JSC.markBinding(@src()); + var builder = std.ArrayList(u8).init(bun.default_allocator); + defer builder.deinit(); + builder.writer().print(fmt, args) catch bun.outOfMemory(); + return BunString__createUTF8ForJS(globalObject, builder.items.ptr, builder.items.len); + } + pub fn parseDate(this: *String, globalObject: *JSC.JSGlobalObject) f64 { JSC.markBinding(@src()); return Bun__parseDate(globalObject, this); diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index 2fed558217..37b2b8e51b 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -19,6 +19,7 @@ const words: Record "std.StringHashMap(": { reason: "bun.StringHashMap has a faster `eql`" }, "std.enums.tagName(": { reason: "Use bun.tagName instead", limit: 2 }, "std.unicode": { reason: "Use bun.strings instead", limit: 33 }, + "std.Thread.Mutex": {reason: "Use bun.Mutex instead", limit: 1 }, "allocator.ptr ==": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, "allocator.ptr !=": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior", limit: 1 }, diff --git a/test/js/node/cluster.test.ts b/test/js/node/cluster.test.ts index 2e168261b7..2e2792042d 100644 --- a/test/js/node/cluster.test.ts +++ b/test/js/node/cluster.test.ts @@ -33,7 +33,7 @@ if (cluster.isPrimary) { bunRun(joinP(dir, "index.ts"), bunEnv); }); -test("cloneable and non-transferable not-equals", () => { +test("cloneable and non-transferable not-equals (BunFile)", () => { const dir = tempDirWithFiles("bun-test", { "index.ts": ` import cluster from "cluster"; @@ -68,3 +68,37 @@ if (cluster.isPrimary) { }); bunRun(joinP(dir, "index.ts"), bunEnv); }); + +test("cloneable and non-transferable not-equals (net.BlockList)", () => { + const dir = tempDirWithFiles("bun-test", { + "index.ts": ` +import cluster from "cluster"; +import net from "net"; +import { expect } from "bun:test"; +if (cluster.isPrimary) { + cluster.settings.serialization = "advanced"; + const worker = cluster.fork(); + const blocklist = new net.BlockList(); + console.log("P", "O", blocklist); + blocklist.addAddress("123.123.123.123"); + worker.on("online", function () { + worker.send({ blocklist }); + }); + worker.on("message", function (data) { + worker.kill(); + const { blocklist } = data; + console.log("P", "M", blocklist); + expect(blocklist.rules).toBeUndefined(); + expect(blocklist).toBeEmptyObject(); + process.exit(0); + }); +} else { + process.on("message", msg => { + console.log("W", msg); + process.send!(msg); + }); +} +`, + }); + bunRun(joinP(dir, "index.ts"), bunEnv); +}); diff --git a/test/js/node/test/parallel/test-blocklist-clone.js b/test/js/node/test/parallel/test-blocklist-clone.js new file mode 100644 index 0000000000..4264b36f54 --- /dev/null +++ b/test/js/node/test/parallel/test-blocklist-clone.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); + +const { + BlockList, +} = require('net'); + +const { + ok, + notStrictEqual, +} = require('assert'); + +const blocklist = new BlockList(); +blocklist.addAddress('123.123.123.123'); + +const mc = new MessageChannel(); + +mc.port1.onmessage = common.mustCall(({ data }) => { + notStrictEqual(data, blocklist); + ok(data.check('123.123.123.123')); + ok(!data.check('123.123.123.124')); + + data.addAddress('123.123.123.124'); + ok(blocklist.check('123.123.123.124')); + ok(data.check('123.123.123.124')); + + mc.port1.close(); +}); + +mc.port2.postMessage(blocklist); diff --git a/test/js/node/test/parallel/test-blocklist.js b/test/js/node/test/parallel/test-blocklist.js new file mode 100644 index 0000000000..a30ca8160a --- /dev/null +++ b/test/js/node/test/parallel/test-blocklist.js @@ -0,0 +1,284 @@ +'use strict'; + +require('../common'); + +const { + BlockList, + SocketAddress, +} = require('net'); +const assert = require('assert'); +const util = require('util'); + +{ + const blockList = new BlockList(); + + [1, [], {}, null, 1n, undefined, null].forEach((i) => { + assert.throws(() => blockList.addAddress(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, [], {}, null, 1n, null].forEach((i) => { + assert.throws(() => blockList.addAddress('1.1.1.1', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + assert.throws(() => blockList.addAddress('1.1.1.1', 'foo'), { + code: 'ERR_INVALID_ARG_VALUE' + }); + + [1, [], {}, null, 1n, undefined, null].forEach((i) => { + assert.throws(() => blockList.addRange(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => blockList.addRange('1.1.1.1', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, [], {}, null, 1n, null].forEach((i) => { + assert.throws(() => blockList.addRange('1.1.1.1', '1.1.1.2', i), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + assert.throws(() => blockList.addRange('1.1.1.1', '1.1.1.2', 'foo'), { + code: 'ERR_INVALID_ARG_VALUE' + }); +} + +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addAddress('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', 'ipv6'); + blockList.addAddress('::ffff:1.1.1.2', 'ipv6'); + + assert(blockList.check('1.1.1.1')); + assert(!blockList.check('1.1.1.1', 'ipv6')); + assert(!blockList.check('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17')); + assert(blockList.check('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', 'ipv6')); + + assert(blockList.check('::ffff:1.1.1.1', 'ipv6')); + assert(blockList.check('::ffff:1.1.1.1', 'IPV6')); + + assert(blockList.check('1.1.1.2')); + + assert(!blockList.check('1.2.3.4')); + assert(!blockList.check('::1', 'ipv6')); +} + +{ + const blockList = new BlockList(); + const sa1 = new SocketAddress({ address: '1.1.1.1' }); + const sa2 = new SocketAddress({ + address: '8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', + family: 'ipv6' + }); + const sa3 = new SocketAddress({ address: '1.1.1.2' }); + + blockList.addAddress(sa1); + blockList.addAddress(sa2); + blockList.addAddress('::ffff:1.1.1.2', 'ipv6'); + + assert(blockList.check('1.1.1.1')); + assert(blockList.check(sa1)); + assert(!blockList.check('1.1.1.1', 'ipv6')); + assert(!blockList.check('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17')); + assert(blockList.check('8592:757c:efae:4e45:fb5d:d62a:0d00:8e17', 'ipv6')); + assert(blockList.check(sa2)); + + assert(blockList.check('::ffff:1.1.1.1', 'ipv6')); + assert(blockList.check('::ffff:1.1.1.1', 'IPV6')); + + assert(blockList.check('1.1.1.2')); + assert(blockList.check(sa3)); + + assert(!blockList.check('1.2.3.4')); + assert(!blockList.check('::1', 'ipv6')); +} + +{ + const blockList = new BlockList(); + blockList.addRange('1.1.1.1', '1.1.1.10'); + blockList.addRange('::1', '::f', 'ipv6'); + + assert(!blockList.check('1.1.1.0')); + for (let n = 1; n <= 10; n++) + assert(blockList.check(`1.1.1.${n}`)); + assert(!blockList.check('1.1.1.11')); + + assert(!blockList.check('::0', 'ipv6')); + for (let n = 0x1; n <= 0xf; n++) { + assert(blockList.check(`::${n.toString(16)}`, 'ipv6'), + `::${n.toString(16)} check failed`); + } + assert(!blockList.check('::10', 'ipv6')); +} + +{ + const blockList = new BlockList(); + const sa1 = new SocketAddress({ address: '1.1.1.1' }); + const sa2 = new SocketAddress({ address: '1.1.1.10' }); + const sa3 = new SocketAddress({ address: '::1', family: 'ipv6' }); + const sa4 = new SocketAddress({ address: '::f', family: 'ipv6' }); + + blockList.addRange(sa1, sa2); + blockList.addRange(sa3, sa4); + + assert(!blockList.check('1.1.1.0')); + for (let n = 1; n <= 10; n++) + assert(blockList.check(`1.1.1.${n}`)); + assert(!blockList.check('1.1.1.11')); + + assert(!blockList.check('::0', 'ipv6')); + for (let n = 0x1; n <= 0xf; n++) { + assert(blockList.check(`::${n.toString(16)}`, 'ipv6'), + `::${n.toString(16)} check failed`); + } + assert(!blockList.check('::10', 'ipv6')); +} + +{ + const blockList = new BlockList(); + blockList.addSubnet('1.1.1.0', 16); + blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + + assert(blockList.check('1.1.0.1')); + assert(blockList.check('1.1.1.1')); + assert(!blockList.check('1.2.0.1')); + assert(blockList.check('::ffff:1.1.0.1', 'ipv6')); + + assert(blockList.check('8592:757c:efae:4e45:f::', 'ipv6')); + assert(blockList.check('8592:757c:efae:4e45::f', 'ipv6')); + assert(!blockList.check('8592:757c:efae:4f45::f', 'ipv6')); +} + +{ + const blockList = new BlockList(); + const sa1 = new SocketAddress({ address: '1.1.1.0' }); + const sa2 = new SocketAddress({ address: '1.1.1.1' }); + blockList.addSubnet(sa1, 16); + blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6'); + + assert(blockList.check('1.1.0.1')); + assert(blockList.check(sa2)); + assert(!blockList.check('1.2.0.1')); + assert(blockList.check('::ffff:1.1.0.1', 'ipv6')); + + assert(blockList.check('8592:757c:efae:4e45:f::', 'ipv6')); + assert(blockList.check('8592:757c:efae:4e45::f', 'ipv6')); + assert(!blockList.check('8592:757c:efae:4f45::f', 'ipv6')); +} + +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addRange('10.0.0.1', '10.0.0.10'); + blockList.addSubnet('8592:757c:efae:4e45::', 64, 'IpV6'); // Case insensitive + + const rulesCheck = [ + 'Subnet: IPv6 8592:757c:efae:4e45::/64', + 'Range: IPv4 10.0.0.1-10.0.0.10', + 'Address: IPv4 1.1.1.1', + ]; + assert.deepStrictEqual(blockList.rules, rulesCheck); + + assert(blockList.check('1.1.1.1')); + assert(blockList.check('10.0.0.5')); + assert(blockList.check('::ffff:10.0.0.5', 'ipv6')); + assert(blockList.check('8592:757c:efae:4e45::f', 'ipv6')); + + assert(!blockList.check('123.123.123.123')); + assert(!blockList.check('8592:757c:efaf:4e45:fb5d:d62a:0d00:8e17', 'ipv6')); + assert(!blockList.check('::ffff:123.123.123.123', 'ipv6')); +} + +{ + // This test validates boundaries of non-aligned CIDR bit prefixes + const blockList = new BlockList(); + blockList.addSubnet('10.0.0.0', 27); + blockList.addSubnet('8592:757c:efaf::', 51, 'ipv6'); + + for (let n = 0; n <= 31; n++) + assert(blockList.check(`10.0.0.${n}`)); + assert(!blockList.check('10.0.0.32')); + + assert(blockList.check('8592:757c:efaf:0:0:0:0:0', 'ipv6')); + assert(blockList.check('8592:757c:efaf:1fff:ffff:ffff:ffff:ffff', 'ipv6')); + assert(!blockList.check('8592:757c:efaf:2fff:ffff:ffff:ffff:ffff', 'ipv6')); +} + +{ + // Regression test for https://github.com/nodejs/node/issues/39074 + const blockList = new BlockList(); + + blockList.addRange('10.0.0.2', '10.0.0.10'); + + // IPv4 checks against IPv4 range. + assert(blockList.check('10.0.0.2')); + assert(blockList.check('10.0.0.10')); + assert(!blockList.check('192.168.0.3')); + assert(!blockList.check('2.2.2.2')); + assert(!blockList.check('255.255.255.255')); + + // IPv6 checks against IPv4 range. + assert(blockList.check('::ffff:0a00:0002', 'ipv6')); + assert(blockList.check('::ffff:0a00:000a', 'ipv6')); + assert(!blockList.check('::ffff:c0a8:0003', 'ipv6')); + assert(!blockList.check('::ffff:0202:0202', 'ipv6')); + assert(!blockList.check('::ffff:ffff:ffff', 'ipv6')); +} + +{ + const blockList = new BlockList(); + assert.throws(() => blockList.addRange('1.1.1.2', '1.1.1.1'), /ERR_INVALID_ARG_VALUE/); +} + +{ + const blockList = new BlockList(); + assert.throws(() => blockList.addSubnet(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.addSubnet('1.1.1.1', ''), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.addSubnet('1.1.1.1', NaN), /ERR_OUT_OF_RANGE/); + assert.throws(() => blockList.addSubnet('', 1, 1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.addSubnet('', 1, ''), /ERR_INVALID_ARG_VALUE/); + + assert.throws(() => blockList.addSubnet('1.1.1.1', -1, 'ipv4'), /ERR_OUT_OF_RANGE/); + assert.throws(() => blockList.addSubnet('1.1.1.1', 33, 'ipv4'), /ERR_OUT_OF_RANGE/); + + assert.throws(() => blockList.addSubnet('::', -1, 'ipv6'), /ERR_OUT_OF_RANGE/); + assert.throws(() => blockList.addSubnet('::', 129, 'ipv6'), /ERR_OUT_OF_RANGE/); +} + +{ + const blockList = new BlockList(); + assert.throws(() => blockList.check(1), /ERR_INVALID_ARG_TYPE/); + assert.throws(() => blockList.check('', 1), /ERR_INVALID_ARG_TYPE/); +} + +// { +// const blockList = new BlockList(); +// const ret = util.inspect(blockList, { depth: -1 }); +// assert.strictEqual(ret, '[BlockList]'); +// } + +// { +// const blockList = new BlockList(); +// const ret = util.inspect(blockList, { depth: null }); +// assert(ret.includes('rules: []')); +// } + +{ + // Test for https://github.com/nodejs/node/issues/43360 + const blocklist = new BlockList(); + blocklist.addSubnet('1.1.1.1', 32, 'ipv4'); + + assert(blocklist.check('1.1.1.1')); + assert(!blocklist.check('1.1.1.2')); + assert(!blocklist.check('2.3.4.5')); +} + +{ + assert(BlockList.isBlockList(new BlockList())); + assert(!BlockList.isBlockList({})); +} diff --git a/test/js/web/workers/message-channel.test.ts b/test/js/web/workers/message-channel.test.ts index cebf3f9a4c..dca6b35894 100644 --- a/test/js/web/workers/message-channel.test.ts +++ b/test/js/web/workers/message-channel.test.ts @@ -277,8 +277,7 @@ test("cloneable and transferable equals", async () => { await promise; }); -test("cloneable and non-transferable equals", async () => { - const assert = require("assert"); +test("cloneable and non-transferable equals (BunFile)", async () => { const mc = new MessageChannel(); const file = Bun.file(import.meta.filename); expect(file).toBeInstanceOf(Blob); // Bun.BunFile isnt exposed to JS @@ -300,3 +299,27 @@ test("cloneable and non-transferable equals", async () => { mc.port2.postMessage(file); await promise; }); + +test("cloneable and non-transferable equals (net.BlockList)", async () => { + const net = require("node:net"); + const mc = new MessageChannel(); + const blocklist = new net.BlockList(); + blocklist.addAddress("123.123.123.123"); + const { promise, resolve, reject } = Promise.withResolvers(); + mc.port1.onmessage = ({ data }) => { + try { + expect(data).toBeInstanceOf(net.BlockList); + expect(data.check("123.123.123.123")).toBeTrue(); + expect(!data.check("123.123.123.124")).toBeTrue(); + data.addAddress("123.123.123.124"); + expect(blocklist.check("123.123.123.124")).toBeTrue(); + expect(data.check("123.123.123.124")).toBeTrue(); + mc.port1.close(); + resolve(); + } catch (e) { + reject(e); + } + }; + mc.port2.postMessage(blocklist); + await promise; +}); diff --git a/test/js/web/workers/structured-clone.test.ts b/test/js/web/workers/structured-clone.test.ts index 5e99b9d5bc..0ce2ccc833 100644 --- a/test/js/web/workers/structured-clone.test.ts +++ b/test/js/web/workers/structured-clone.test.ts @@ -205,6 +205,20 @@ describe("structured clone", () => { }); }); + describe("net.BlockList works", () => { + test("simple", () => { + const net = require("node:net"); + const blocklist = new net.BlockList(); + blocklist.addAddress("123.123.123.123"); + const newlist = structuredClone(blocklist); + expect(newlist.check("123.123.123.123")).toBeTrue(); + expect(!newlist.check("123.123.123.124")).toBeTrue(); + newlist.addAddress("123.123.123.124"); + expect(blocklist.check("123.123.123.124")).toBeTrue(); + expect(newlist.check("123.123.123.124")).toBeTrue(); + }); + }); + describe("transferables", () => { test("ArrayBuffer", () => { const buffer = Uint8Array.from([1]).buffer;