diff options
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 4 | ||||
-rw-r--r-- | src/bun.js/node/node_os.zig | 260 | ||||
-rw-r--r-- | src/darwin_c.zig | 33 | ||||
-rw-r--r-- | src/linux_c.zig | 12 | ||||
-rw-r--r-- | test/bun.js/os.test.js | 3 |
5 files changed, 307 insertions, 5 deletions
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index ebe5f83ed..1479ef977 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3629,12 +3629,12 @@ pub const JSValue = enum(JSValueReprInt) { return zig_str; } - pub fn get(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) ?JSValue { + pub fn get(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { const value = getIfPropertyExistsImpl(this, global, property.ptr, @intCast(u32, property.len)); return if (@enumToInt(value) != 0) value else return null; } - pub fn getTruthy(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) ?JSValue { + pub fn getTruthy(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { if (get(this, global, property)) |prop| { if (prop.isEmptyOrUndefinedOrNull()) return null; return prop; diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index eb7ae3c2d..9bf1bbb86 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -381,10 +381,193 @@ pub const Os = struct { pub fn networkInterfaces(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - // TODO: - return JSC.JSValue.createEmptyObject(globalThis, 0); + // getifaddrs sets a pointer to a linked list + var interface_start: ?*C.ifaddrs = null; + const rc = C.getifaddrs(&interface_start); + if (rc != 0) { + const err = JSC.SystemError{ + .message = JSC.ZigString.init("A system error occurred: getifaddrs returned an error"), + .code = JSC.ZigString.init(@as(string, @tagName(JSC.Node.ErrorCode.ERR_SYSTEM_ERROR))), + .errno = @enumToInt(std.os.errno(rc)), + .syscall = JSC.ZigString.init("getifaddrs"), + }; + + globalThis.vm().throwError(globalThis, err.toErrorInstance(globalThis)); + return JSC.JSValue.jsUndefined(); + } + defer C.freeifaddrs(interface_start); + + + const helpers = struct { + // We'll skip interfaces that aren't actually available + pub fn skip(iface: *C.ifaddrs) bool { + // Skip interfaces that aren't actually available + if (iface.ifa_flags & C.IFF_RUNNING == 0) return true; + if (iface.ifa_flags & C.IFF_UP == 0) return true; + if (iface.ifa_addr == null) return true; + + return false; + } + + // We won't actually return link-layer interfaces but we need them for + // extracting the MAC address + pub fn isLinkLayer(iface: *C.ifaddrs) bool { + if (iface.ifa_addr == null) return false; + return if (comptime Environment.isLinux) + return iface.ifa_addr.*.sa_family == std.os.AF.PACKET + else if (comptime Environment.isMac) + return iface.ifa_addr.?.*.family == std.os.AF.LINK + else unreachable; + } + + pub fn isLoopback(iface: *C.ifaddrs) bool { + return iface.ifa_flags & C.IFF_LOOPBACK == C.IFF_LOOPBACK; + } + }; + + + // The list currently contains entries for link-layer interfaces + // and the IPv4, IPv6 interfaces. We only want to return the latter two + // but need the link-layer entries to determine MAC address. + // So, on our first pass through the linked list we'll count the number of + // INET interfaces only. + var num_inet_interfaces: usize = 0; + var it = interface_start; + while (it) |iface| : (it = iface.ifa_next) { + if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; + num_inet_interfaces += 1; + } + + var ret = JSC.JSValue.createEmptyObject(globalThis, 8); + + // Second pass through, populate each interface object + it = interface_start; + while (it) |iface| : (it = iface.ifa_next) { + if (helpers.skip(iface) or helpers.isLinkLayer(iface)) continue; + + const interface_name = std.mem.sliceTo(iface.ifa_name, 0); + const addr = std.net.Address.initPosix(@alignCast(4, @ptrCast(*std.os.sockaddr, iface.ifa_addr))); + const netmask = std.net.Address.initPosix(@alignCast(4, @ptrCast(*std.os.sockaddr, iface.ifa_netmask))); + + var interface = JSC.JSValue.createEmptyObject(globalThis, 7); + + // address <string> The assigned IPv4 or IPv6 address + // cidr <string> The assigned IPv4 or IPv6 address with the routing prefix in CIDR notation. If the netmask is invalid, this property is set to null. + { + // Compute the CIDR suffix; returns null if the netmask cannot + // be converted to a CIDR suffix + const maybe_suffix: ?u8 = switch (addr.any.family) { + std.os.AF.INET => netmaskToCIDRSuffix(netmask.in.sa.addr), + std.os.AF.INET6 => netmaskToCIDRSuffix(@bitCast(u128, netmask.in6.sa.addr)), + else => null + }; + + // Format the address and then, if valid, the CIDR suffix; both + // the address and cidr values can be slices into this same buffer + // e.g. addr_str = "192.168.88.254", cidr_str = "192.168.88.254/24" + var buf: [64]u8 = undefined; + const addr_str = formatAddress(addr, &buf) catch unreachable; + var cidr = JSC.JSValue.null; + if (maybe_suffix) |suffix| { + //NOTE addr_str might not start at buf[0] due to slicing in formatAddress + const start = @ptrToInt(addr_str.ptr) - @ptrToInt(&buf[0]); + // Start writing the suffix immediately after the address + const suffix_str = std.fmt.bufPrint(buf[start + addr_str.len..], "/{}", .{ suffix }) catch unreachable; + // The full cidr value is the address + the suffix + const cidr_str = buf[start..start + addr_str.len + suffix_str.len]; + cidr = JSC.ZigString.init(cidr_str).withEncoding().toValueGC(globalThis); + } + + interface.put(globalThis, JSC.ZigString.static("address"), + JSC.ZigString.init(addr_str).withEncoding().toValueGC(globalThis)); + interface.put(globalThis, JSC.ZigString.static("cidr"), cidr); + } + + // netmask <string> The IPv4 or IPv6 network mask + { + var buf: [64]u8 = undefined; + const str = formatAddress(netmask, &buf) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("netmask"), + JSC.ZigString.init(str).withEncoding().toValueGC(globalThis)); + } + + // family <string> Either IPv4 or IPv6 + interface.put(globalThis, JSC.ZigString.static("family"), + (switch (addr.any.family) { + std.os.AF.INET => JSC.ZigString.static("IPv4"), + std.os.AF.INET6 => JSC.ZigString.static("IPv6"), + else => JSC.ZigString.static("unknown"), + }).toValue(globalThis) + ); + + // mac <string> The MAC address of the network interface + { + // We need to search for the link-layer interface whose name matches this one + var ll_it = interface_start; + const maybe_ll_addr = while (ll_it) |ll_iface| : (ll_it = ll_iface.ifa_next) { + if (helpers.skip(ll_iface) or !helpers.isLinkLayer(ll_iface)) continue; + + const ll_name = bun.sliceTo(ll_iface.ifa_name, 0); + if (!strings.hasPrefix(ll_name, interface_name)) continue; + if (ll_name.len > interface_name.len and ll_name[interface_name.len] != ':') continue; + + // This is the correct link-layer interface entry for the current interface, + // cast to a link-layer socket address + if (comptime Environment.isLinux) { + break @ptrCast(?*std.os.sockaddr.ll, @alignCast(4, ll_iface.ifa_addr)); + } else if (comptime Environment.isMac) { + break @ptrCast(?*C.sockaddr_dl, @alignCast(2, ll_iface.ifa_addr)); + } else unreachable; + } else null; + + if (maybe_ll_addr) |ll_addr| { + // Encode its link-layer address. We need 2*6 bytes for the + // hex characters and 5 for the colon separators + var mac_buf: [17]u8 = undefined; + var addr_data = if (comptime Environment.isLinux) ll_addr.addr + else if (comptime Environment.isMac) ll_addr.sdl_data[ll_addr.sdl_nlen..] + else unreachable; + const mac = std.fmt.bufPrint(&mac_buf, + "{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}:{x:0>2}", + .{ + addr_data[0], addr_data[1], addr_data[2], + addr_data[3], addr_data[4], addr_data[5], + } + ) catch unreachable; + interface.put(globalThis, JSC.ZigString.static("mac"), + JSC.ZigString.init(mac).withEncoding().toValueGC(globalThis)); + } + } + + // internal <boolean> true if the network interface is a loopback or similar interface that is not remotely accessible; otherwise false + interface.put(globalThis, JSC.ZigString.static("internal"), + JSC.JSValue.jsBoolean(helpers.isLoopback(iface))); + + // scopeid <number> The numeric IPv6 scope ID (only specified when family is IPv6) + if (addr.any.family == std.os.AF.INET6) { + interface.put(globalThis, JSC.ZigString.static("scope_id"), + JSC.JSValue.jsNumber(addr.in6.sa.scope_id)); + } + + + // Does this entry already exist? + if (ret.get(globalThis, interface_name)) |array| { + // Add this interface entry to the existing array + const next_index = @intCast(u32, array.getLengthOfArray(globalThis)); + array.putIndex(globalThis, next_index, interface); + } else { + // Add it as an array with this interface as an element + const member_name = JSC.ZigString.init(interface_name); + var array = JSC.JSValue.createEmptyArray(globalThis, 1); + array.putIndex(globalThis, 0, interface); + ret.put(globalThis, &member_name, array); + } + } + + return ret; } + pub fn platform(globalThis: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); @@ -561,3 +744,76 @@ pub const Os = struct { return JSC.ZigString.static(comptime getMachineName()).toValue(globalThis); } }; + +fn formatAddress(address: std.net.Address, into: []u8) ![]u8 { + // std.net.Address.format includes `:<port>` and square brackets (IPv6) + // while Node does neither. This uses format then strips these to bring + // the result into conformance with Node. + var result = try std.fmt.bufPrint(into, "{}", .{address}); + + // Strip `:<port>` + if (std.mem.lastIndexOfScalar(u8, result, ':')) |colon| { + result = result[0..colon]; + } + // Strip brackets + if (result[0] == '[' and result[result.len-1] == ']') { + result = result[1..result.len-1]; + } + return result; +} + +/// Given a netmask returns a CIDR suffix. Returns null if the mask is not valid. +/// `@TypeOf(mask)` must be one of u32 (IPv4) or u128 (IPv6) +fn netmaskToCIDRSuffix(mask: anytype) ?u8 { + const T = @TypeOf(mask); + comptime std.debug.assert(T == u32 or T == u128); + + const mask_bits = @byteSwap(mask); + + // Validity check: set bits should be left-contiguous + const first_zero = @clz(~mask_bits); + const last_one = @bitSizeOf(T) - @ctz(mask_bits); + if (first_zero < @bitSizeOf(T) and first_zero < last_one) return null; + return first_zero; +} +test "netmaskToCIDRSuffix" { + const ipv4_tests = .{ + .{ "255.255.255.255", 32 }, + .{ "255.255.255.254", 31 }, + .{ "255.255.255.252", 30 }, + .{ "255.255.255.128", 25 }, + .{ "255.255.255.0", 24 }, + .{ "255.255.128.0", 17 }, + .{ "255.255.0.0", 16 }, + .{ "255.128.0.0", 9 }, + .{ "255.0.0.0", 8 }, + .{ "224.0.0.0", 3 }, + .{ "192.0.0.0", 2 }, + .{ "128.0.0.0", 1 }, + .{ "0.0.0.0", 0 }, + + // invalid masks + .{ "255.0.0.255", null }, + .{ "128.0.0.255", null }, + .{ "128.0.0.1", null }, + }; + inline for (ipv4_tests) |t| { + const addr = try std.net.Address.parseIp4(t[0], 0); + try std.testing.expectEqual(@as(?u8, t[1]), netmaskToCIDRSuffix(addr.in.sa.addr)); + } + + const ipv6_tests = .{ + .{ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128 }, + .{ "ffff:ffff:ffff:ffff::", 64 }, + .{ "::", 0 }, + + // invalid masks + .{ "ff00:1::", null }, + .{ "0:1::", null }, + }; + inline for (ipv6_tests) |t| { + const addr = try std.net.Address.parseIp6(t[0], 0); + const bits = @bitCast(u128, addr.in6.sa.addr); + try std.testing.expectEqual(@as(?u8, t[1]), netmaskToCIDRSuffix(bits)); + } +} diff --git a/src/darwin_c.zig b/src/darwin_c.zig index f2e22c85e..d62e665b9 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -751,3 +751,36 @@ pub const RemoveFileFlags = struct { }; pub const removefile_state_t = opaque {}; pub extern fn removefileat(fd: c_int, path: [*c]const u8, state: ?*removefile_state_t, flags: u32) c_int; + +// As of Zig v0.11.0-dev.1393+38eebf3c4, ifaddrs.h is not included in the headers +pub const ifaddrs = extern struct { + ifa_next: ?*ifaddrs, + ifa_name: [*:0]u8, + ifa_flags: c_uint, + ifa_addr: ?*std.os.sockaddr, + ifa_netmask: ?*std.os.sockaddr, + ifa_dstaddr: ?*std.os.sockaddr, + ifa_data: *anyopaque, +}; +pub extern fn getifaddrs(*?*ifaddrs) c_int; +pub extern fn freeifaddrs(?*ifaddrs) void; + +const net_if_h = @cImport({ @cInclude("net/if.h"); }); +pub const IFF_RUNNING = net_if_h.IFF_RUNNING; +pub const IFF_UP = net_if_h.IFF_UP; +pub const IFF_LOOPBACK = net_if_h.IFF_LOOPBACK; +pub const sockaddr_dl = extern struct { + sdl_len: u8, // Total length of sockaddr */ + sdl_family: u8, // AF_LINK */ + sdl_index: u16, // if != 0, system given index for interface */ + sdl_type: u8, // interface type */ + sdl_nlen: u8, // interface name length, no trailing 0 reqd. */ + sdl_alen: u8, // link level address length */ + sdl_slen: u8, // link layer selector length */ + sdl_data: [12]u8, // minimum work area, can be larger; contains both if name and ll address */ + //#ifndef __APPLE__ + // /* For TokenRing */ + // u_short sdl_rcf; /* source routing control */ + // u_short sdl_route[16]; /* source routing information */ + //#endif +}; diff --git a/src/linux_c.zig b/src/linux_c.zig index 04faf210b..ae9300477 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -480,3 +480,15 @@ pub fn posix_spawn_file_actions_addchdir_np(actions: *posix_spawn_file_actions_t } pub extern fn vmsplice(fd: c_int, iovec: [*]const std.os.iovec, iovec_count: usize, flags: u32) isize; + + +const net_c = @cImport({ + @cInclude("ifaddrs.h"); // getifaddrs, freeifaddrs + @cInclude("net/if.h"); // IFF_RUNNING, IFF_UP +}); +pub const ifaddrs = net_c.ifaddrs; +pub const getifaddrs = net_c.getifaddrs; +pub const freeifaddrs = net_c.freeifaddrs; +pub const IFF_RUNNING = net_c.IFF_RUNNING; +pub const IFF_UP = net_c.IFF_UP; +pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK; diff --git a/test/bun.js/os.test.js b/test/bun.js/os.test.js index 87c03d5d4..122969337 100644 --- a/test/bun.js/os.test.js +++ b/test/bun.js/os.test.js @@ -110,7 +110,8 @@ it("networkInterfaces", () => { expect(typeof nI.family === "string").toBe(true); expect(typeof nI.mac === "string").toBe(true); expect(typeof nI.internal === "boolean").toBe(true); - expect(typeof nI.cidr).toBe("string"); + if (nI.cidr) // may be null + expect(typeof nI.cidr).toBe("string"); } } }); |