Compare commits

...

5 Commits

Author SHA1 Message Date
Don Isaac
adc431e319 Merge branch 'main' into don/net/blocklist 2025-02-07 12:25:21 -08:00
Don Isaac
b123577a6a cleanup 2025-02-06 12:13:59 -05:00
Don Isaac
81a4469e6d Merge branch 'main' of github.com:oven-sh/bun into don/net/blocklist 2025-02-06 12:11:43 -05:00
Don Isaac
cfbea14b2e feat(node/net): add missing BlockList shims 2025-02-05 18:47:21 -05:00
Don Isaac
1e164743a6 refactor(node/net): isIP et al mirror node's implementation 2025-02-05 18:47:21 -05:00
5 changed files with 222 additions and 50 deletions

View File

@@ -1,3 +1,45 @@
const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket] = $zig("socket.zig", "createNodeTLSBinding");
export default { addServerName, upgradeDuplexToTLS, isNamedPipeSocket };
// 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}`;
var IPv4Reg: RegExp | undefined;
// IPv6 Segment
const v6Seg = "(?:[0-9a-fA-F]{1,4})";
var IPv6Reg: RegExp | undefined;
function isIPv4(s: string): boolean {
return (IPv4Reg ??= new RegExp(`^${v4Str}$`)).test(s);
}
function isIPv6(s: string): boolean {
return (IPv6Reg ??= new RegExp(
"^(?:" +
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
`(?:${v6Seg}:){5}(?::${v4Str}|(?::${v6Seg}){1,2}|:)|` +
`(?:${v6Seg}:){4}(?:(?::${v6Seg}){0,1}:${v4Str}|(?::${v6Seg}){1,3}|:)|` +
`(?:${v6Seg}:){3}(?:(?::${v6Seg}){0,2}:${v4Str}|(?::${v6Seg}){1,4}|:)|` +
`(?:${v6Seg}:){2}(?:(?::${v6Seg}){0,3}:${v4Str}|(?::${v6Seg}){1,5}|:)|` +
`(?:${v6Seg}:){1}(?:(?::${v6Seg}){0,4}:${v4Str}|(?::${v6Seg}){1,6}|:)|` +
`(?::(?:(?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
")(?:%[0-9a-zA-Z-.:]{1,})?$",
)).test(s);
}
function isIP(s: string): 6 | 4 | 0;
function isIP(s: any): 6 | 4 | 0 {
if (isIPv4(s)) return 4;
if (isIPv6(s)) return 6;
return 0;
}
export default {
addServerName,
upgradeDuplexToTLS,
isNamedPipeSocket,
isIPv4,
isIPv6,
isIP,
};

View File

@@ -0,0 +1,37 @@
type IpType = "ipv4" | "ipv6";
type AddressLike = string; /* | net.SocketAddress */
const kHandle = Symbol("kHandle");
/** @todo */
type BlockListHandle = unknown;
/**
* TODO: migrate this to native code
* @see https://nodejs.org/api/net.html#class-netblocklist
*/
class BlockList {
public static isBlockList(value: unknown): value is BlockList {
return value?.[kHandle] !== undefined;
}
private [kHandle]: BlockListHandle;
constructor() {
// TODO
this[kHandle] = kHandle;
}
public addAddress(net: AddressLike /* | net.SocketAddress */, type: IpType = "ipv4"): void {}
public addRange(start: AddressLike, end: AddressLike, type: IpType = "ipv4"): void {}
public addSubnet(subnet: AddressLike, prefix: number, type: IpType = "ipv4"): void {}
public check(address: AddressLike, type: IpType = "ipv4"): boolean {
return false;
}
get rules(): string[] {
return [];
}
}
export default {
BlockList,
};

View File

@@ -20,48 +20,12 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import type { BlockList as BlockListType } from "node:net";
const { Duplex } = require("node:stream");
const EventEmitter = require("node:events");
const { addServerName, upgradeDuplexToTLS, isNamedPipeSocket } = require("../internal/net");
const { addServerName, upgradeDuplexToTLS, isNamedPipeSocket, isIP, isIPv4, isIPv6 } = require("../internal/net");
const { ExceptionWithHostPort } = require("internal/shared");
// 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}`;
var IPv4Reg;
// IPv6 Segment
const v6Seg = "(?:[0-9a-fA-F]{1,4})";
var IPv6Reg;
const DEFAULT_IPV4_ADDR = "0.0.0.0";
const DEFAULT_IPV6_ADDR = "::";
function isIPv4(s) {
return (IPv4Reg ??= new RegExp(`^${v4Str}$`)).test(s);
}
function isIPv6(s) {
return (IPv6Reg ??= new RegExp(
"^(?:" +
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
`(?:${v6Seg}:){5}(?::${v4Str}|(?::${v6Seg}){1,2}|:)|` +
`(?:${v6Seg}:){4}(?:(?::${v6Seg}){0,1}:${v4Str}|(?::${v6Seg}){1,3}|:)|` +
`(?:${v6Seg}:){3}(?:(?::${v6Seg}){0,2}:${v4Str}|(?::${v6Seg}){1,4}|:)|` +
`(?:${v6Seg}:){2}(?:(?::${v6Seg}){0,3}:${v4Str}|(?::${v6Seg}){1,5}|:)|` +
`(?:${v6Seg}:){1}(?:(?::${v6Seg}){0,4}:${v4Str}|(?::${v6Seg}){1,6}|:)|` +
`(?::(?:(?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
")(?:%[0-9a-zA-Z-.:]{1,})?$",
)).test(s);
}
function isIP(s) {
if (isIPv4(s)) return 4;
if (isIPv6(s)) return 6;
return 0;
}
const { connect: bunConnect } = Bun;
var { setTimeout } = globalThis;
@@ -1529,16 +1493,7 @@ function toNumber(x) {
return (x = Number(x)) >= 0 ? x : false;
}
// TODO:
class BlockList {
constructor() {}
addSubnet(net, prefix, type) {}
check(address, type) {
return false;
}
}
var BlockList: BlockListType | undefined;
export default {
createServer,
@@ -1557,7 +1512,9 @@ export default {
getDefaultAutoSelectFamilyAttemptTimeout: $zig("node_net_binding.zig", "getDefaultAutoSelectFamilyAttemptTimeout"),
setDefaultAutoSelectFamilyAttemptTimeout: $zig("node_net_binding.zig", "setDefaultAutoSelectFamilyAttemptTimeout"),
BlockList,
get BlockList(): BlockListType {
return (BlockList ??= require("internal/net/blocklist").BlockList);
},
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/net.js#L2456
Stream: Socket,
};

View File

@@ -0,0 +1,45 @@
import { BlockList } from "node:net";
import { describe, beforeEach, afterAll, it, expect } from "bun:test";
describe("BlockList", () => {
let b: BlockList;
beforeEach(() => {
b = new BlockList();
});
afterAll(() => {
// @ts-expect-error -- we're cleaning up
b = undefined;
});
describe(".isBlockList(value)", () => {
it('returns "true" for instances of BlockList', () => {
expect(BlockList.isBlockList(b)).toBeTrue();
});
it.each([1, undefined, null, true, false, "string", {}, new Map(), new Set()])(`%p is not a BlockList`, value => {
expect(BlockList.isBlockList(value)).toBeFalse();
});
});
describe("#rules", () => {
it("is an array", () => {
expect(b.rules).toBeInstanceOf(Array);
expect(b.rules).toStrictEqual([]);
});
it("cannot be overridden", () => {
// NOTE: this doesn't throw in Node; it just no-ops
// @ts-expect-error
expect(() => (b.rules = true)).toThrow(TypeError);
expect(b.rules).toStrictEqual([]);
});
it("cannot be manually added to", () => {
expect(b.rules).toBeEmpty();
// @ts-expect-error
b.rules.push("foo");
expect(b.rules).toBeEmpty();
});
});
});

View File

@@ -0,0 +1,91 @@
import { isIP, isIPv4, isIPv6 } from "node:net";
import { describe, beforeEach, it, expect } from "bun:test";
// common tests
describe.each(
[isIP, isIPv4, isIPv6].map(fn => [fn.name, fn] as const), // for pretty test names
)("net.%s", (_name, fn) => {
const NOT_IP = fn === isIP ? 0 : false;
// bad values
it.each([undefined, null, NaN, 0, 1, true, false, {}, function foo() {}])(
"given invalid input, returns false (%p)",
(input: any) => {
expect(fn(input)).toBe(NOT_IP);
},
);
it(`when called without any arguments, returns ${NOT_IP}`, () => {
// @ts-expect-error -- intentionally testing without arguments
expect(fn()).toBe(NOT_IP);
});
it.each(["", "foobar", "1", "localhost", "www.example.com"])(
"does not consider %p to be an ip address",
(input: string) => {
expect(fn(input)).toBe(NOT_IP);
},
);
it.each(["127.0.0.1/24"])("CIDR blocks are not IP addresses", (cidr: string) => {
expect(fn(cidr)).toBe(NOT_IP);
});
}); // </net.isIP*>
// valid and well formed (but invalid) IPv4 addresses
describe("IP version 4", () => {
describe.each([
"127.0.0.1", //
"0.0.0.0",
"255.255.255.255",
])("given a valid address", (input: string) => {
it(`net.isIPv4("${input}") === true`, () => {
expect(isIPv4(input)).toBe(true);
});
it(`net.isIP("${input}") === 4`, () => {
expect(isIP(input)).toBe(4);
});
it(`net.isIPv6("${input}") === false`, () => {
expect(isIPv6(input)).toBe(false);
});
}); // </valid>
describe.each(["256.256.256.256", "-1.0.0.0", "127.000.000.001"])(
"given a well-formed but invalid address",
(input: string) => {
it(`net.isIPv4("${input}") === false`, () => {
expect(isIPv4(input)).toBe(false);
});
it(`net.isIP("${input}") === 0`, () => {
expect(isIP(input)).toBe(0);
});
it(`net.isIPv6("${input}") === false`, () => {
expect(isIPv6(input)).toBe(false);
});
},
); // </well-formed but invalid>
}); // </IPv4>
describe("IP version 6", () => {
describe.each([
"::1",
"0:0:0:0:0:0:0:1",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334", //
])("given a valid address", (input: string) => {
it(`net.isIPv6("${input}") === true`, () => {
expect(isIPv6(input)).toBe(true);
});
it(`net.isIP("${input}") === 6`, () => {
expect(isIP(input)).toBe(6);
});
it(`net.isIPv4("${input}") === false`, () => {
expect(isIPv4(input)).toBe(false);
});
});
});