Compare commits

...

3 Commits

Author SHA1 Message Date
Don Isaac
2fdf9c489a add some tests 2025-03-04 16:24:12 -08:00
Don Isaac
3f089a80d1 internal socket class 2025-03-04 14:14:32 -08:00
Don Isaac
2142083508 refactor(net): extract isIP, isIPv4, and isIPv6 2025-03-03 14:05:47 -08:00
8 changed files with 1440 additions and 1134 deletions

View File

@@ -1,4 +1,19 @@
const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket] = $zig("socket.zig", "createNodeTLSBinding");
const { SocketAddress } = $zig("node_net_binding.zig", "createBinding");
export default { addServerName, upgradeDuplexToTLS, isNamedPipeSocket, SocketAddress };
const bunTlsSymbol = Symbol.for("::buntls::");
const bunSocketServerHandlers = Symbol.for("::bunsocket_serverhandlers::");
const bunSocketServerConnections = Symbol.for("::bunnetserverconnections::");
const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::");
export default {
addServerName,
upgradeDuplexToTLS,
isNamedPipeSocket,
SocketAddress,
// symbols
bunTlsSymbol,
bunSocketServerHandlers,
bunSocketServerConnections,
bunSocketServerOptions,
};

39
src/js/internal/net/ip.ts Normal file
View File

@@ -0,0 +1,39 @@
// 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;
function isIPv4(s): boolean {
return (IPv4Reg ??= new RegExp(`^${v4Str}$`)).test(s);
}
function isIPv6(s): 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): 0 | 4 | 6 {
if (isIPv4(s)) return 4;
if (isIPv6(s)) return 6;
return 0;
}
export default {
isIP,
isIPv4,
isIPv6,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
// Hardcoded module "node:tls"
const { isArrayBufferView, isArrayBuffer, isTypedArray } = require("node:util/types");
const { addServerName } = require("../internal/net");
const { Socket: InternalTCPSocket } = require("internal/net/socket");
const net = require("node:net");
const { Duplex } = require("node:stream");
const { Server: NetServer, [Symbol.for("::bunternal::")]: InternalTCPSocket } = net;
const { Server: NetServer } = net;
const { rootCertificates, canonicalizeIP } = $cpp("NodeTLS.cpp", "createNodeTLSBinding");

View File

@@ -5,6 +5,19 @@ import { expect, it, jest } from "bun:test";
import { closeSync } from "fs";
import { bunEnv, bunExe, expectMaxObjectTypeCount, getMaxFD, isWindows, tls } from "harness";
const stripMimallocLogs = (logs: string) =>
logs
.split("\n")
.filter(line => !line.startsWith("mimalloc:"))
.join("\n");
function withoutMimalloc(out: string): string;
function withoutMimalloc(out: ReadableStream): Promise<string>;
function withoutMimalloc(out) {
if (typeof out === "string") return stripMimallocLogs(out);
else return new Response(out).text().then(stripMimallocLogs);
}
it("should throw when a socket from a file descriptor has a bad file descriptor", async () => {
const open = jest.fn();
const close = jest.fn();
@@ -140,7 +153,7 @@ it("should keep process alive only when active", async () => {
});
expect(await exited).toBe(0);
expect(await new Response(stderr).text()).toBe("");
expect(await withoutMimalloc(stderr)).toBe("");
var lines = (await new Response(stdout).text()).split(/\r?\n/);
expect(
lines.filter(function (line) {
@@ -188,7 +201,7 @@ it("connect() should return the socket object", async () => {
});
expect(await exited).toBe(0);
expect(await new Response(stderr).text()).toBe("");
expect(await withoutMimalloc(stderr)).toBe("");
});
it("listen() should throw connection error for invalid host", () => {

View File

@@ -0,0 +1,46 @@
const net = require("node:net");
const server = new net.Server();
const client = new net.Socket();
const serverEmit = server.emit,
clientEmit = client.emit;
const verboseEmit = (name, originalEmit) => {
const log = (...args) => console.log(`[${name}]`, ...args);
return function verboseEmit(...args) {
const [eventName, ...rest] = args;
switch (eventName) {
case "data":
log("data:", ...rest.map(d => d.toString()));
break;
default:
if (args[1] && args[1] instanceof Error) {
log(eventName, args[1].message);
} else {
log(eventName);
}
}
return originalEmit.apply(this, args);
};
};
Object.defineProperty(server, "emit", { value: verboseEmit("server", serverEmit) });
Object.defineProperty(client, "emit", { value: verboseEmit("client", clientEmit) });
server.on("connection", socket => {
socket.on("data", data => {
console.log("[server] socket data:", data.toString());
socket.write(data);
process.nextTick(() => socket.end());
});
socket.on("close", () => server.close());
});
client.on("data", () => client.end());
server.listen(0, () => {
client.connect(server.address(), () => {
client.write("ping");
});
});

View File

@@ -0,0 +1,55 @@
import { bunRun } from "harness";
import path from "node:path";
const fixturePath = (...segs: string[]): string => path.join(import.meta.dirname, "fixtures", "socket", ...segs);
function cleanLogs(text: string): string {
return text
.split("\n")
.filter(line => !line.startsWith("mimalloc:"))
.join("\n");
}
describe("Given a ping server/client", () => {
let logs: string[], clientLogs, serverLogs;
beforeAll(() => {
logs = bunRun(fixturePath("ping.fixture.js")).stdout.split("\n");
clientLogs = logs.filter(line => line.startsWith("[client]"));
serverLogs = logs.filter(line => line.startsWith("[server]"));
});
it("no errors occur", () => {
expect(logs.find(line => /error/i.test(line))).toBeUndefined();
});
describe("the client", () => {
it("emits a connect event", () => expect(clientLogs).toContain("[client] connect"));
it("emits a DNS 'lookup' event before attempting connect", () => {
expect(clientLogs).toContain("[client] lookup");
expect(clientLogs.indexOf("[client] lookup")).toBeLessThan(clientLogs.indexOf("[client] connectionAttempt"));
});
it("emits a 'connectionAttempt' event before connecting", () => {
expect(clientLogs).toContain("[client] connectionAttempt");
expect(clientLogs.indexOf("[client] connectionAttempt")).toBeLessThan(clientLogs.indexOf("[client] connect"));
});
it('receives "ping" from the server', () => expect(clientLogs).toContain("[client] data: ping"));
it("emits 'prefinish' before 'finish'", () => {
expect(clientLogs).toContain("[client] prefinish");
expect(clientLogs).toContain("[client] finish");
expect(clientLogs.indexOf("[client] prefinish")).toBeLessThan(clientLogs.indexOf("[client] finish"));
});
it("finishes with a close event", () => {
expect(logs).toContain("[client] close");
expect(clientLogs.at(-1)).toEqual("[client] close");
});
}); // the client
describe("the server", () => {
it("emits a 'connection' event", () => expect(serverLogs).toContain("[server] connection"));
it("receives 'ping' from the client", () => expect(serverLogs).toContain("[server] socket data: ping"));
it("finishes with a 'close' event", () => {
expect(logs).toContain("[server] close");
expect(serverLogs.at(-1)).toEqual("[server] close");
});
}); // the server
});