Files
bun.sh/src/js/node/net.ts
2025-03-04 14:14:32 -08:00

578 lines
16 KiB
TypeScript

// Hardcoded module "node:net"
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
const { Duplex } = require("node:stream");
const EventEmitter = require("node:events");
const {
SocketAddress,
addServerName,
bunTlsSymbol,
bunSocketServerHandlers,
bunSocketServerConnections,
bunSocketServerOptions,
} = require("internal/net");
const { isIP, isIPv4, isIPv6 } = require("internal/net/ip");
const { Socket: InternalSocket } = require("internal/net/socket");
const { ExceptionWithHostPort } = require("internal/shared");
import type { SocketListener } from "bun";
import type { ServerOpts, Server as ServerType } from "node:net";
var { setTimeout } = globalThis;
const kRealListen = Symbol("kRealListen");
/**
* @class Socket
*/
function Socket(options) {
return new InternalSocket(options);
}
Socket.prototype = InternalSocket.prototype;
Object.defineProperty(Socket, Symbol.hasInstance, {
value(instance) {
return instance instanceof InternalSocket;
},
});
function createConnection(port, host, connectListener) {
if (typeof port === "object") {
// port is option pass Socket options and let connect handle connection options
return new Socket(port).connect(port, host, connectListener);
}
// port is path or host, let connect handle this
return new Socket().connect(port, host, connectListener);
}
const connect = createConnection;
type MaybeListener = SocketListener<unknown> | null;
function Server(): void;
function Server(options?: null | undefined): void;
function Server(connectionListener: () => {}): void;
function Server(options: ServerOpts, connectionListener?: () => {}): void;
function Server(options?, connectionListener?): void {
if (!(this instanceof Server)) {
return new Server(options, connectionListener);
}
EventEmitter.$apply(this, []);
if (typeof options === "function") {
connectionListener = options;
options = {};
} else if (options == null || typeof options === "object") {
options = { ...options };
} else {
throw $ERR_INVALID_ARG_TYPE("options", ["Object", "Function"], options);
}
$assert(typeof Duplex.getDefaultHighWaterMark === "function");
// https://nodejs.org/api/net.html#netcreateserveroptions-connectionlistener
const {
maxConnections, //
allowHalfOpen = false,
keepAlive = false,
keepAliveInitialDelay = 0,
highWaterMark = Duplex.getDefaultHighWaterMark(),
pauseOnConnect = false,
noDelay = false,
} = options;
this._connections = 0;
this._handle = null as MaybeListener;
this._usingWorkers = false;
this.workers = [];
this._unref = false;
this.listeningId = 1;
this[bunSocketServerConnections] = 0;
this[bunSocketServerOptions] = undefined;
this.allowHalfOpen = allowHalfOpen;
this.keepAlive = keepAlive;
this.keepAliveInitialDelay = keepAliveInitialDelay;
this.highWaterMark = highWaterMark;
this.pauseOnConnect = Boolean(pauseOnConnect);
this.noDelay = noDelay;
this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0;
// TODO: options.blockList
options.connectionListener = connectionListener;
this[bunSocketServerOptions] = options;
}
$toClass(Server, "Server", EventEmitter);
Object.defineProperty(Server.prototype, "listening", {
get() {
return !!this._handle;
},
});
Server.prototype.ref = function ref() {
this._handle?.ref();
return this;
};
Server.prototype.unref = function unref() {
this._handle?.unref();
return this;
};
Server.prototype.close = function close(callback) {
if (typeof callback === "function") {
if (!this._handle) {
this.once("close", function close() {
callback($ERR_SERVER_NOT_RUNNING());
});
} else {
this.once("close", callback);
}
}
if (this._handle) {
this._handle.stop(false);
this._handle = null;
}
this._emitCloseIfDrained();
return this;
};
Server.prototype[Symbol.asyncDispose] = function asyncDispose() {
const { resolve, reject, promise } = Promise.withResolvers();
this.close(function (err, ...args) {
if (err) reject(err);
else resolve(...args);
});
return promise;
};
Server.prototype._emitCloseIfDrained = function _emitCloseIfDrained() {
if (this._handle || this[bunSocketServerConnections] > 0) {
return;
}
process.nextTick(() => {
this.emit("close");
});
};
Server.prototype.address = function address() {
const server = this._handle;
if (server) {
const unix = server.unix;
if (unix) {
return unix;
}
//TODO: fix adress when host is passed
let address = server.hostname;
const type = isIP(address);
const port = server.port;
if (typeof port === "number") {
return {
port,
address,
family: type ? `IPv${type}` : undefined,
};
}
if (type) {
return {
address,
family: type ? `IPv${type}` : undefined,
};
}
return address;
}
return null;
};
Server.prototype.getConnections = function getConnections(callback) {
if (typeof callback === "function") {
//in Bun case we will never error on getConnections
//node only errors if in the middle of the couting the server got disconnected, what never happens in Bun
//if disconnected will only pass null as well and 0 connected
callback(null, this._handle ? this[bunSocketServerConnections] : 0);
}
return this;
};
Server.prototype.listen = function listen(port, hostname, onListen) {
let backlog;
let path;
let exclusive = false;
let allowHalfOpen = false;
let reusePort = false;
let ipv6Only = false;
//port is actually path
if (typeof port === "string") {
if (Number.isSafeInteger(hostname)) {
if (hostname > 0) {
//hostname is backlog
backlog = hostname;
}
} else if (typeof hostname === "function") {
//hostname is callback
onListen = hostname;
}
path = port;
hostname = undefined;
port = undefined;
} else {
if (typeof hostname === "function") {
onListen = hostname;
hostname = undefined;
}
if (typeof port === "function") {
onListen = port;
port = 0;
} else if (typeof port === "object") {
const options = port;
options.signal?.addEventListener("abort", () => this.close());
hostname = options.host;
exclusive = options.exclusive;
path = options.path;
port = options.port;
ipv6Only = options.ipv6Only;
allowHalfOpen = options.allowHalfOpen;
reusePort = options.reusePort;
const isLinux = process.platform === "linux";
if (!Number.isSafeInteger(port) || port < 0) {
if (path) {
const isAbstractPath = path.startsWith("\0");
if (isLinux && isAbstractPath && (options.writableAll || options.readableAll)) {
const message = `The argument 'options' can not set readableAll or writableAll to true when path is abstract unix socket. Received ${JSON.stringify(options)}`;
const error = new TypeError(message);
error.code = "ERR_INVALID_ARG_VALUE";
throw error;
}
hostname = path;
port = undefined;
} else {
let message = 'The argument \'options\' must have the property "port" or "path"';
try {
message = `${message}. Received ${JSON.stringify(options)}`;
} catch {}
const error = new TypeError(message);
error.code = "ERR_INVALID_ARG_VALUE";
throw error;
}
} else if (!Number.isSafeInteger(port) || port < 0) {
port = 0;
}
// port <number>
// host <string>
// path <string> Will be ignored if port is specified. See Identifying paths for IPC connections.
// backlog <number> Common parameter of server.listen() functions.
// exclusive <boolean> Default: false
// readableAll <boolean> For IPC servers makes the pipe readable for all users. Default: false.
// writableAll <boolean> For IPC servers makes the pipe writable for all users. Default: false.
// ipv6Only <boolean> For TCP servers, setting ipv6Only to true will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound. Default: false.
// signal <AbortSignal> An AbortSignal that may be used to close a listening server.
if (typeof options.callback === "function") onListen = options?.callback;
} else if (!Number.isSafeInteger(port) || port < 0) {
port = 0;
}
hostname = hostname || "::";
}
if (this._handle) {
throw $ERR_SERVER_ALREADY_LISTEN();
}
if (onListen != null) {
this.once("listening", onListen);
}
try {
var tls = undefined;
var TLSSocketClass = undefined;
const bunTLS = this[bunTlsSymbol];
const options = this[bunSocketServerOptions];
let contexts: Map<string, any> | null = null;
if (typeof bunTLS === "function") {
[tls, TLSSocketClass] = bunTLS.$call(this, port, hostname, false);
options.servername = tls.serverName;
options.InternalSocketClass = TLSSocketClass;
contexts = tls.contexts;
if (!tls.requestCert) {
tls.rejectUnauthorized = false;
}
} else {
options.InternalSocketClass = InternalSocket;
}
listenInCluster(
this,
null,
port,
4,
backlog,
undefined,
exclusive,
ipv6Only,
allowHalfOpen,
reusePort,
undefined,
undefined,
path,
hostname,
tls,
contexts,
onListen,
);
} catch (err) {
setTimeout(emitErrorNextTick, 1, this, err);
}
return this;
};
Server.prototype[kRealListen] = function realListen(
path,
port,
hostname,
exclusive,
ipv6Only,
allowHalfOpen,
reusePort,
tls,
contexts,
onListen,
) {
if (path) {
this._handle = Bun.listen({
unix: path,
tls,
allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false,
reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false,
ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false,
exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false,
socket: InternalSocket[bunSocketServerHandlers],
});
} else {
this._handle = Bun.listen({
port,
hostname,
tls,
allowHalfOpen: allowHalfOpen || this[bunSocketServerOptions]?.allowHalfOpen || false,
reusePort: reusePort || this[bunSocketServerOptions]?.reusePort || false,
ipv6Only: ipv6Only || this[bunSocketServerOptions]?.ipv6Only || false,
exclusive: exclusive || this[bunSocketServerOptions]?.exclusive || false,
socket: InternalSocket[bunSocketServerHandlers],
});
}
//make this instance available on handlers
this._handle.data = this;
if (contexts) {
for (const [name, context] of contexts) {
addServerName(this._handle, name, context);
}
}
// We must schedule the emitListeningNextTick() only after the next run of
// the event loop's IO queue. Otherwise, the server may not actually be listening
// when the 'listening' event is emitted.
//
// That leads to all sorts of confusion.
//
// process.nextTick() is not sufficient because it will run before the IO queue.
setTimeout(emitListeningNextTick, 1, this);
};
Server.prototype.getsockname = function getsockname(out) {
out.port = this.address().port;
return out;
};
function emitErrorNextTick(self, error) {
self.emit("error", error);
}
class ConnResetException extends Error {
constructor(msg) {
super(msg);
this.code = "ECONNRESET";
}
get ["constructor"]() {
return Error;
}
}
function emitListeningNextTick(self) {
if (!self._handle) return;
self.emit("listening");
}
let cluster;
function listenInCluster(
server,
address,
port,
addressType,
backlog,
fd,
exclusive,
ipv6Only,
allowHalfOpen,
reusePort,
flags,
options,
path,
hostname,
tls,
contexts,
onListen,
) {
exclusive = !!exclusive;
if (cluster === undefined) cluster = require("node:cluster");
if (cluster.isPrimary || exclusive) {
server[kRealListen](path, port, hostname, exclusive, ipv6Only, allowHalfOpen, reusePort, tls, contexts, onListen);
return;
}
const serverQuery = {
address: address,
port: port,
addressType: addressType,
fd: fd,
flags,
backlog,
...options,
};
cluster._getServer(server, serverQuery, function listenOnPrimaryHandle(err, handle) {
err = checkBindError(err, port, handle);
if (err) {
throw new ExceptionWithHostPort(err, "bind", address, port);
}
server[kRealListen](path, port, hostname, exclusive, ipv6Only, allowHalfOpen, reusePort, tls, contexts, onListen);
});
}
function createServer(options, connectionListener) {
return new Server(options, connectionListener);
}
function normalizeArgs(args) {
while (args[args.length - 1] == null) args.pop();
let arr;
if (args.length === 0) {
arr = [{}, null];
return arr;
}
const arg0 = args[0];
let options: any = {};
if (typeof arg0 === "object" && arg0 !== null) {
options = arg0;
} else if (isPipeName(arg0)) {
options.path = arg0;
} else {
options.port = arg0;
if (args.length > 1 && typeof args[1] === "string") {
options.host = args[1];
}
}
const cb = args[args.length - 1];
if (typeof cb !== "function") arr = [options, null];
else arr = [options, cb];
return arr;
}
function checkBindError(err, port, handle) {
// EADDRINUSE may not be reported until we call listen() or connect().
// To complicate matters, a failed bind() followed by listen() or connect()
// will implicitly bind to a random port. Ergo, check that the socket is
// bound to the expected port before calling listen() or connect().
if (err === 0 && port > 0 && handle.getsockname) {
const out = {};
err = handle.getsockname(out);
if (err === 0 && port !== out.port) {
$debug(`checkBindError, bound to ${out.port} instead of ${port}`);
const UV_EADDRINUSE = -4091;
err = UV_EADDRINUSE;
}
}
return err;
}
function isPipeName(s) {
return typeof s === "string" && toNumber(s) === false;
}
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,
createConnection,
connect,
isIP,
isIPv4,
isIPv6,
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"),
BlockList,
SocketAddress,
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/net.js#L2456
Stream: Socket,
};