mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
578 lines
16 KiB
TypeScript
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,
|
|
};
|