mirror of
https://github.com/denoland/deno.git
synced 2024-12-26 17:19:06 -05:00
d59599fc18
partially unblocks #25470 This PR aligns the resolution of `localhost` hostname to Node.js behavior. In Node.js `dns.lookup("localhost", (_, addr) => console.log(addr))` prints ipv6 address `::1`, but it prints ipv4 address `127.0.0.1` in Deno. That difference causes some errors in the work of enabling `createConnection` option in `http.request` (#25470). This PR fixes the issue by aligning `dns.lookup` behavior to Node.js. This PR also changes the following behaviors (resolving TODOs): - `http.createServer` now listens on ipv6 address `[::]` by default on linux/mac - `net.createServer` now listens on ipv6 address `[::]` by default on linux/mac These changes are also alignments to Node.js behaviors.
2482 lines
67 KiB
TypeScript
2482 lines
67 KiB
TypeScript
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
// 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.
|
|
|
|
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
|
// deno-lint-ignore-file prefer-primordials
|
|
|
|
import { notImplemented } from "ext:deno_node/_utils.ts";
|
|
import { BlockList, SocketAddress } from "ext:deno_node/internal/blocklist.mjs";
|
|
|
|
import { EventEmitter } from "node:events";
|
|
import {
|
|
isIP,
|
|
isIPv4,
|
|
isIPv6,
|
|
normalizedArgsSymbol,
|
|
} from "ext:deno_node/internal/net.ts";
|
|
import { Duplex } from "node:stream";
|
|
import {
|
|
asyncIdSymbol,
|
|
defaultTriggerAsyncIdScope,
|
|
newAsyncId,
|
|
ownerSymbol,
|
|
} from "ext:deno_node/internal/async_hooks.ts";
|
|
import {
|
|
ERR_INVALID_ADDRESS_FAMILY,
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_INVALID_FD_TYPE,
|
|
ERR_INVALID_IP_ADDRESS,
|
|
ERR_MISSING_ARGS,
|
|
ERR_SERVER_ALREADY_LISTEN,
|
|
ERR_SERVER_NOT_RUNNING,
|
|
ERR_SOCKET_CLOSED,
|
|
errnoException,
|
|
exceptionWithHostPort,
|
|
genericNodeError,
|
|
uvExceptionWithHostPort,
|
|
} from "ext:deno_node/internal/errors.ts";
|
|
import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
|
|
import { Encodings } from "ext:deno_node/_utils.ts";
|
|
import { isUint8Array } from "ext:deno_node/internal/util/types.ts";
|
|
import {
|
|
kAfterAsyncWrite,
|
|
kBuffer,
|
|
kBufferCb,
|
|
kBufferGen,
|
|
kHandle,
|
|
kUpdateTimer,
|
|
onStreamRead,
|
|
setStreamTimeout,
|
|
writeGeneric,
|
|
writevGeneric,
|
|
} from "ext:deno_node/internal/stream_base_commons.ts";
|
|
import { kTimeout } from "ext:deno_node/internal/timers.mjs";
|
|
import { nextTick } from "ext:deno_node/_next_tick.ts";
|
|
import {
|
|
DTRACE_NET_SERVER_CONNECTION,
|
|
DTRACE_NET_STREAM_END,
|
|
} from "ext:deno_node/internal/dtrace.ts";
|
|
import { Buffer } from "node:buffer";
|
|
import type { LookupOneOptions } from "ext:deno_node/internal/dns/utils.ts";
|
|
import {
|
|
validateAbortSignal,
|
|
validateFunction,
|
|
validateInt32,
|
|
validateNumber,
|
|
validatePort,
|
|
validateString,
|
|
} from "ext:deno_node/internal/validators.mjs";
|
|
import {
|
|
constants as TCPConstants,
|
|
TCP,
|
|
TCPConnectWrap,
|
|
} from "ext:deno_node/internal_binding/tcp_wrap.ts";
|
|
import {
|
|
constants as PipeConstants,
|
|
Pipe,
|
|
PipeConnectWrap,
|
|
} from "ext:deno_node/internal_binding/pipe_wrap.ts";
|
|
import { ShutdownWrap } from "ext:deno_node/internal_binding/stream_wrap.ts";
|
|
import { assert } from "ext:deno_node/_util/asserts.ts";
|
|
import { isWindows } from "ext:deno_node/_util/os.ts";
|
|
import { ADDRCONFIG, lookup as dnsLookup } from "node:dns";
|
|
import { codeMap } from "ext:deno_node/internal_binding/uv.ts";
|
|
import { guessHandleType } from "ext:deno_node/internal_binding/util.ts";
|
|
import { debuglog } from "ext:deno_node/internal/util/debuglog.ts";
|
|
import type { DuplexOptions } from "ext:deno_node/_stream.d.ts";
|
|
import type { BufferEncoding } from "ext:deno_node/_global.d.ts";
|
|
import type { Abortable } from "ext:deno_node/_events.d.ts";
|
|
import { channel } from "node:diagnostics_channel";
|
|
|
|
let debug = debuglog("net", (fn) => {
|
|
debug = fn;
|
|
});
|
|
|
|
const kLastWriteQueueSize = Symbol("lastWriteQueueSize");
|
|
const kSetNoDelay = Symbol("kSetNoDelay");
|
|
const kBytesRead = Symbol("kBytesRead");
|
|
const kBytesWritten = Symbol("kBytesWritten");
|
|
|
|
const DEFAULT_IPV4_ADDR = "0.0.0.0";
|
|
const DEFAULT_IPV6_ADDR = "::";
|
|
|
|
type Handle = TCP | Pipe;
|
|
|
|
interface HandleOptions {
|
|
pauseOnCreate?: boolean;
|
|
manualStart?: boolean;
|
|
handle?: Handle;
|
|
}
|
|
|
|
interface OnReadOptions {
|
|
buffer: Uint8Array | (() => Uint8Array);
|
|
/**
|
|
* This function is called for every chunk of incoming data.
|
|
*
|
|
* Two arguments are passed to it: the number of bytes written to buffer and
|
|
* a reference to buffer.
|
|
*
|
|
* Return `false` from this function to implicitly `pause()` the socket.
|
|
*/
|
|
callback(bytesWritten: number, buf: Uint8Array): boolean;
|
|
}
|
|
|
|
interface ConnectOptions {
|
|
/**
|
|
* If specified, incoming data is stored in a single buffer and passed to the
|
|
* supplied callback when data arrives on the socket.
|
|
*
|
|
* Note: this will cause the streaming functionality to not provide any data,
|
|
* however events like `"error"`, `"end"`, and `"close"` will still be
|
|
* emitted as normal and methods like `pause()` and `resume()` will also
|
|
* behave as expected.
|
|
*/
|
|
onread?: OnReadOptions;
|
|
}
|
|
|
|
interface SocketOptions extends ConnectOptions, HandleOptions, DuplexOptions {
|
|
/**
|
|
* If specified, wrap around an existing socket with the given file
|
|
* descriptor, otherwise a new socket will be created.
|
|
*/
|
|
fd?: number;
|
|
/**
|
|
* If set to `false`, then the socket will automatically end the writable
|
|
* side when the readable side ends. See `net.createServer()` and the `"end"`
|
|
* event for details. Default: `false`.
|
|
*/
|
|
allowHalfOpen?: boolean;
|
|
/**
|
|
* Allow reads on the socket when an fd is passed, otherwise ignored.
|
|
* Default: `false`.
|
|
*/
|
|
readable?: boolean;
|
|
/**
|
|
* Allow writes on the socket when an fd is passed, otherwise ignored.
|
|
* Default: `false`.
|
|
*/
|
|
writable?: boolean;
|
|
/** An Abort signal that may be used to destroy the socket. */
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
interface TcpNetConnectOptions extends TcpSocketConnectOptions, SocketOptions {
|
|
timeout?: number;
|
|
}
|
|
|
|
interface IpcNetConnectOptions extends IpcSocketConnectOptions, SocketOptions {
|
|
timeout?: number;
|
|
}
|
|
|
|
type NetConnectOptions = TcpNetConnectOptions | IpcNetConnectOptions;
|
|
|
|
interface AddressInfo {
|
|
address: string;
|
|
family?: string;
|
|
port: number;
|
|
}
|
|
|
|
type LookupFunction = (
|
|
hostname: string,
|
|
options: LookupOneOptions,
|
|
callback: (
|
|
err: ErrnoException | null,
|
|
address: string,
|
|
family: number,
|
|
) => void,
|
|
) => void;
|
|
|
|
interface TcpSocketConnectOptions extends ConnectOptions {
|
|
port: number;
|
|
host?: string;
|
|
localAddress?: string;
|
|
localPort?: number;
|
|
hints?: number;
|
|
family?: number;
|
|
lookup?: LookupFunction;
|
|
}
|
|
|
|
interface IpcSocketConnectOptions extends ConnectOptions {
|
|
path: string;
|
|
}
|
|
|
|
type SocketConnectOptions = TcpSocketConnectOptions | IpcSocketConnectOptions;
|
|
|
|
function _getNewAsyncId(handle?: Handle): number {
|
|
return !handle || typeof handle.getAsyncId !== "function"
|
|
? newAsyncId()
|
|
: handle.getAsyncId();
|
|
}
|
|
|
|
interface NormalizedArgs {
|
|
0: Partial<NetConnectOptions | ListenOptions>;
|
|
1: ConnectionListener | null;
|
|
[normalizedArgsSymbol]?: boolean;
|
|
}
|
|
|
|
const _noop = (_arrayBuffer: Uint8Array, _nread: number): undefined => {
|
|
return;
|
|
};
|
|
|
|
const netClientSocketChannel = channel("net.client.socket");
|
|
const netServerSocketChannel = channel("net.server.socket");
|
|
|
|
function _toNumber(x: unknown): number | false {
|
|
return (x = Number(x)) >= 0 ? (x as number) : false;
|
|
}
|
|
|
|
function _isPipeName(s: unknown): s is string {
|
|
return typeof s === "string" && _toNumber(s) === false;
|
|
}
|
|
|
|
function _createHandle(fd: number, isServer: boolean): Handle {
|
|
validateInt32(fd, "fd", 0);
|
|
|
|
const type = guessHandleType(fd);
|
|
|
|
if (type === "PIPE") {
|
|
return new Pipe(isServer ? PipeConstants.SERVER : PipeConstants.SOCKET);
|
|
}
|
|
|
|
if (type === "TCP") {
|
|
return new TCP(isServer ? TCPConstants.SERVER : TCPConstants.SOCKET);
|
|
}
|
|
|
|
throw new ERR_INVALID_FD_TYPE(type);
|
|
}
|
|
|
|
// Returns an array [options, cb], where options is an object,
|
|
// cb is either a function or null.
|
|
// Used to normalize arguments of `Socket.prototype.connect()` and
|
|
// `Server.prototype.listen()`. Possible combinations of parameters:
|
|
// - (options[...][, cb])
|
|
// - (path[...][, cb])
|
|
// - ([port][, host][...][, cb])
|
|
// For `Socket.prototype.connect()`, the [...] part is ignored
|
|
// For `Server.prototype.listen()`, the [...] part is [, backlog]
|
|
// but will not be handled here (handled in listen())
|
|
export function _normalizeArgs(args: unknown[]): NormalizedArgs {
|
|
let arr: NormalizedArgs;
|
|
|
|
if (args.length === 0) {
|
|
arr = [{}, null];
|
|
arr[normalizedArgsSymbol] = true;
|
|
|
|
return arr;
|
|
}
|
|
|
|
const arg0 = args[0] as Partial<NetConnectOptions> | number | string;
|
|
let options: Partial<SocketConnectOptions> = {};
|
|
|
|
if (typeof arg0 === "object" && arg0 !== null) {
|
|
// (options[...][, cb])
|
|
options = arg0;
|
|
} else if (_isPipeName(arg0)) {
|
|
// (path[...][, cb])
|
|
(options as IpcSocketConnectOptions).path = arg0;
|
|
} else {
|
|
// ([port][, host][...][, cb])
|
|
(options as TcpSocketConnectOptions).port = arg0;
|
|
|
|
if (args.length > 1 && typeof args[1] === "string") {
|
|
(options as TcpSocketConnectOptions).host = args[1];
|
|
}
|
|
}
|
|
|
|
const cb = args[args.length - 1];
|
|
|
|
if (!_isConnectionListener(cb)) {
|
|
arr = [options, null];
|
|
} else {
|
|
arr = [options, cb];
|
|
}
|
|
|
|
arr[normalizedArgsSymbol] = true;
|
|
|
|
return arr;
|
|
}
|
|
|
|
function _isTCPConnectWrap(
|
|
req: TCPConnectWrap | PipeConnectWrap,
|
|
): req is TCPConnectWrap {
|
|
return "localAddress" in req && "localPort" in req;
|
|
}
|
|
|
|
function _afterConnect(
|
|
status: number,
|
|
// deno-lint-ignore no-explicit-any
|
|
handle: any,
|
|
req: PipeConnectWrap | TCPConnectWrap,
|
|
readable: boolean,
|
|
writable: boolean,
|
|
) {
|
|
let socket = handle[ownerSymbol];
|
|
|
|
if (socket.constructor.name === "ReusedHandle") {
|
|
socket = socket.handle;
|
|
}
|
|
|
|
// Callback may come after call to destroy
|
|
if (socket.destroyed) {
|
|
return;
|
|
}
|
|
|
|
debug("afterConnect");
|
|
|
|
assert(socket.connecting);
|
|
|
|
socket.connecting = false;
|
|
socket._sockname = null;
|
|
|
|
if (status === 0) {
|
|
if (socket.readable && !readable) {
|
|
socket.push(null);
|
|
socket.read();
|
|
}
|
|
|
|
if (socket.writable && !writable) {
|
|
socket.end();
|
|
}
|
|
|
|
socket._unrefTimer();
|
|
|
|
socket.emit("connect");
|
|
socket.emit("ready");
|
|
|
|
// Start the first read, or get an immediate EOF.
|
|
// this doesn't actually consume any bytes, because len=0.
|
|
if (readable && !socket.isPaused()) {
|
|
socket.read(0);
|
|
}
|
|
} else {
|
|
socket.connecting = false;
|
|
let details;
|
|
|
|
if (_isTCPConnectWrap(req)) {
|
|
details = req.localAddress + ":" + req.localPort;
|
|
}
|
|
|
|
const ex = exceptionWithHostPort(
|
|
status,
|
|
"connect",
|
|
req.address,
|
|
(req as TCPConnectWrap).port,
|
|
details,
|
|
);
|
|
|
|
if (_isTCPConnectWrap(req)) {
|
|
ex.localAddress = req.localAddress;
|
|
ex.localPort = req.localPort;
|
|
}
|
|
|
|
socket.destroy(ex);
|
|
}
|
|
}
|
|
|
|
function _checkBindError(err: number, port: number, handle: TCP) {
|
|
// 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: AddressInfo | Record<string, never> = {};
|
|
err = handle.getsockname(out);
|
|
|
|
if (err === 0 && port !== out.port) {
|
|
err = codeMap.get("EADDRINUSE")!;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
function _isPipe(
|
|
options: Partial<SocketConnectOptions>,
|
|
): options is IpcSocketConnectOptions {
|
|
return "path" in options && !!options.path;
|
|
}
|
|
|
|
function _connectErrorNT(socket: Socket, err: Error) {
|
|
socket.destroy(err);
|
|
}
|
|
|
|
function _internalConnect(
|
|
socket: Socket,
|
|
address: string,
|
|
port: number,
|
|
addressType: number,
|
|
localAddress: string,
|
|
localPort: number,
|
|
flags: number,
|
|
) {
|
|
assert(socket.connecting);
|
|
|
|
let err;
|
|
|
|
if (localAddress || localPort) {
|
|
if (addressType === 4) {
|
|
localAddress = localAddress || DEFAULT_IPV4_ADDR;
|
|
err = (socket._handle as TCP).bind(localAddress, localPort);
|
|
} else {
|
|
// addressType === 6
|
|
localAddress = localAddress || DEFAULT_IPV6_ADDR;
|
|
err = (socket._handle as TCP).bind6(localAddress, localPort, flags);
|
|
}
|
|
|
|
debug(
|
|
"binding to localAddress: %s and localPort: %d (addressType: %d)",
|
|
localAddress,
|
|
localPort,
|
|
addressType,
|
|
);
|
|
|
|
err = _checkBindError(err, localPort, socket._handle as TCP);
|
|
|
|
if (err) {
|
|
const ex = exceptionWithHostPort(err, "bind", localAddress, localPort);
|
|
socket.destroy(ex);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (addressType === 6 || addressType === 4) {
|
|
const req = new TCPConnectWrap();
|
|
req.oncomplete = _afterConnect;
|
|
req.address = address;
|
|
req.port = port;
|
|
req.localAddress = localAddress;
|
|
req.localPort = localPort;
|
|
|
|
if (addressType === 4) {
|
|
err = (socket._handle as TCP).connect(req, address, port);
|
|
} else {
|
|
err = (socket._handle as TCP).connect6(req, address, port);
|
|
}
|
|
} else {
|
|
const req = new PipeConnectWrap();
|
|
req.oncomplete = _afterConnect;
|
|
req.address = address;
|
|
|
|
err = (socket._handle as Pipe).connect(req, address);
|
|
}
|
|
|
|
if (err) {
|
|
let details = "";
|
|
|
|
const sockname = socket._getsockname();
|
|
|
|
if (sockname) {
|
|
details = `${sockname.address}:${sockname.port}`;
|
|
}
|
|
|
|
const ex = exceptionWithHostPort(err, "connect", address, port, details);
|
|
socket.destroy(ex);
|
|
}
|
|
}
|
|
|
|
// Provide a better error message when we call end() as a result
|
|
// of the other side sending a FIN. The standard "write after end"
|
|
// is overly vague, and makes it seem like the user's code is to blame.
|
|
function _writeAfterFIN(
|
|
this: Socket,
|
|
// deno-lint-ignore no-explicit-any
|
|
chunk: any,
|
|
encoding?:
|
|
| BufferEncoding
|
|
| null
|
|
| ((error: Error | null | undefined) => void),
|
|
cb?: (error: Error | null | undefined) => void,
|
|
): boolean {
|
|
if (!this.writableEnded) {
|
|
return Duplex.prototype.write.call(
|
|
this,
|
|
chunk,
|
|
encoding as BufferEncoding | null,
|
|
// @ts-expect-error Using `call` seem to be interfering with the overload for write
|
|
cb,
|
|
);
|
|
}
|
|
|
|
if (typeof encoding === "function") {
|
|
cb = encoding;
|
|
encoding = null;
|
|
}
|
|
|
|
const err = genericNodeError(
|
|
"This socket has been ended by the other party",
|
|
{ code: "EPIPE" },
|
|
);
|
|
|
|
if (typeof cb === "function") {
|
|
defaultTriggerAsyncIdScope(this[asyncIdSymbol], nextTick, cb, err);
|
|
}
|
|
|
|
if (this._server) {
|
|
nextTick(() => this.destroy(err));
|
|
} else {
|
|
this.destroy(err);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function _tryReadStart(socket: Socket) {
|
|
// Not already reading, start the flow.
|
|
debug("Socket._handle.readStart");
|
|
socket._handle!.reading = true;
|
|
const err = socket._handle!.readStart();
|
|
|
|
if (err) {
|
|
socket.destroy(errnoException(err, "read"));
|
|
}
|
|
}
|
|
|
|
// Called when the "end" event is emitted.
|
|
function _onReadableStreamEnd(this: Socket) {
|
|
if (!this.allowHalfOpen) {
|
|
this.write = _writeAfterFIN;
|
|
}
|
|
}
|
|
|
|
// Called when creating new Socket, or when re-using a closed Socket
|
|
function _initSocketHandle(socket: Socket) {
|
|
socket._undestroy();
|
|
socket._sockname = undefined;
|
|
|
|
// Handle creation may be deferred to bind() or connect() time.
|
|
if (socket._handle) {
|
|
// deno-lint-ignore no-explicit-any
|
|
(socket._handle as any)[ownerSymbol] = socket;
|
|
socket._handle.onread = onStreamRead;
|
|
socket[asyncIdSymbol] = _getNewAsyncId(socket._handle);
|
|
|
|
let userBuf = socket[kBuffer];
|
|
|
|
if (userBuf) {
|
|
const bufGen = socket[kBufferGen];
|
|
|
|
if (bufGen !== null) {
|
|
userBuf = bufGen();
|
|
|
|
if (!isUint8Array(userBuf)) {
|
|
return;
|
|
}
|
|
|
|
socket[kBuffer] = userBuf;
|
|
}
|
|
|
|
socket._handle.useUserBuffer(userBuf);
|
|
}
|
|
}
|
|
}
|
|
|
|
function _lookupAndConnect(
|
|
self: Socket,
|
|
options: TcpSocketConnectOptions,
|
|
) {
|
|
const { localAddress, localPort } = options;
|
|
const host = options.host || "localhost";
|
|
let { port } = options;
|
|
|
|
if (localAddress && !isIP(localAddress)) {
|
|
throw new ERR_INVALID_IP_ADDRESS(localAddress);
|
|
}
|
|
|
|
if (localPort) {
|
|
validateNumber(localPort, "options.localPort");
|
|
}
|
|
|
|
if (typeof port !== "undefined") {
|
|
if (typeof port !== "number" && typeof port !== "string") {
|
|
throw new ERR_INVALID_ARG_TYPE(
|
|
"options.port",
|
|
["number", "string"],
|
|
port,
|
|
);
|
|
}
|
|
|
|
validatePort(port);
|
|
}
|
|
|
|
port |= 0;
|
|
|
|
// If host is an IP, skip performing a lookup
|
|
const addressType = isIP(host);
|
|
if (addressType) {
|
|
defaultTriggerAsyncIdScope(self[asyncIdSymbol], nextTick, () => {
|
|
if (self.connecting) {
|
|
defaultTriggerAsyncIdScope(
|
|
self[asyncIdSymbol],
|
|
_internalConnect,
|
|
self,
|
|
host,
|
|
port,
|
|
addressType,
|
|
localAddress,
|
|
localPort,
|
|
);
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (options.lookup !== undefined) {
|
|
validateFunction(options.lookup, "options.lookup");
|
|
}
|
|
|
|
const dnsOpts = {
|
|
family: options.family,
|
|
hints: options.hints || 0,
|
|
};
|
|
|
|
if (
|
|
!isWindows &&
|
|
dnsOpts.family !== 4 &&
|
|
dnsOpts.family !== 6 &&
|
|
dnsOpts.hints === 0
|
|
) {
|
|
dnsOpts.hints = ADDRCONFIG;
|
|
}
|
|
|
|
debug("connect: find host", host);
|
|
debug("connect: dns options", dnsOpts);
|
|
self._host = host;
|
|
const lookup = options.lookup || dnsLookup;
|
|
|
|
defaultTriggerAsyncIdScope(self[asyncIdSymbol], function () {
|
|
lookup(
|
|
host,
|
|
dnsOpts,
|
|
function emitLookup(
|
|
err: ErrnoException | null,
|
|
ip: string,
|
|
addressType: number,
|
|
) {
|
|
self.emit("lookup", err, ip, addressType, host);
|
|
|
|
// It's possible we were destroyed while looking this up.
|
|
// XXX it would be great if we could cancel the promise returned by
|
|
// the look up.
|
|
if (!self.connecting) {
|
|
return;
|
|
}
|
|
|
|
if (err) {
|
|
// net.createConnection() creates a net.Socket object and immediately
|
|
// calls net.Socket.connect() on it (that's us). There are no event
|
|
// listeners registered yet so defer the error event to the next tick.
|
|
nextTick(_connectErrorNT, self, err);
|
|
} else if (!isIP(ip)) {
|
|
err = new ERR_INVALID_IP_ADDRESS(ip);
|
|
|
|
nextTick(_connectErrorNT, self, err);
|
|
} else if (addressType !== 4 && addressType !== 6) {
|
|
err = new ERR_INVALID_ADDRESS_FAMILY(
|
|
`${addressType}`,
|
|
options.host!,
|
|
options.port,
|
|
);
|
|
|
|
nextTick(_connectErrorNT, self, err);
|
|
} else {
|
|
self._unrefTimer();
|
|
|
|
defaultTriggerAsyncIdScope(
|
|
self[asyncIdSymbol],
|
|
_internalConnect,
|
|
self,
|
|
ip,
|
|
port,
|
|
addressType,
|
|
localAddress,
|
|
localPort,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
function _afterShutdown(this: ShutdownWrap<TCP>) {
|
|
// deno-lint-ignore no-explicit-any
|
|
const self: any = this.handle[ownerSymbol];
|
|
|
|
debug("afterShutdown destroyed=%j", self.destroyed, self._readableState);
|
|
|
|
this.callback();
|
|
}
|
|
|
|
function _emitCloseNT(s: Socket | Server) {
|
|
debug("SERVER: emit close");
|
|
s.emit("close");
|
|
}
|
|
|
|
/**
|
|
* This class is an abstraction of a TCP socket or a streaming `IPC` endpoint
|
|
* (uses named pipes on Windows, and Unix domain sockets otherwise). It is also
|
|
* an `EventEmitter`.
|
|
*
|
|
* A `net.Socket` can be created by the user and used directly to interact with
|
|
* a server. For example, it is returned by `createConnection`,
|
|
* so the user can use it to talk to the server.
|
|
*
|
|
* It can also be created by Node.js and passed to the user when a connection
|
|
* is received. For example, it is passed to the listeners of a `"connection"` event emitted on a `Server`, so the user can use
|
|
* it to interact with the client.
|
|
*/
|
|
export class Socket extends Duplex {
|
|
// Problem with this is that users can supply their own handle, that may not
|
|
// have `handle.getAsyncId()`. In this case an `[asyncIdSymbol]` should
|
|
// probably be supplied by `async_hooks`.
|
|
[asyncIdSymbol] = -1;
|
|
|
|
[kHandle]: Handle | null = null;
|
|
[kSetNoDelay] = false;
|
|
[kLastWriteQueueSize] = 0;
|
|
// deno-lint-ignore no-explicit-any
|
|
[kTimeout]: any = null;
|
|
[kBuffer]: Uint8Array | boolean | null = null;
|
|
[kBufferCb]: OnReadOptions["callback"] | null = null;
|
|
[kBufferGen]: (() => Uint8Array) | null = null;
|
|
|
|
// Used after `.destroy()`
|
|
[kBytesRead] = 0;
|
|
[kBytesWritten] = 0;
|
|
|
|
// Reserved properties
|
|
server = null;
|
|
// deno-lint-ignore no-explicit-any
|
|
_server: any = null;
|
|
|
|
_peername?: AddressInfo | Record<string, never>;
|
|
_sockname?: AddressInfo | Record<string, never>;
|
|
_pendingData: Uint8Array | string | null = null;
|
|
_pendingEncoding = "";
|
|
_host: string | null = null;
|
|
// deno-lint-ignore no-explicit-any
|
|
_parent: any = null;
|
|
|
|
constructor(options: SocketOptions | number) {
|
|
if (typeof options === "number") {
|
|
// Legacy interface.
|
|
options = { fd: options };
|
|
} else {
|
|
options = { ...options };
|
|
}
|
|
|
|
// Default to *not* allowing half open sockets.
|
|
options.allowHalfOpen = Boolean(options.allowHalfOpen);
|
|
// For backwards compat do not emit close on destroy.
|
|
options.emitClose = false;
|
|
options.autoDestroy = true;
|
|
// Handle strings directly.
|
|
options.decodeStrings = false;
|
|
|
|
super(options);
|
|
|
|
if (options.handle) {
|
|
this._handle = options.handle;
|
|
this[asyncIdSymbol] = _getNewAsyncId(this._handle);
|
|
} else if (options.fd !== undefined) {
|
|
// REF: https://github.com/denoland/deno/issues/6529
|
|
notImplemented("net.Socket.prototype.constructor with fd option");
|
|
}
|
|
|
|
const onread = options.onread;
|
|
|
|
if (
|
|
onread !== null &&
|
|
typeof onread === "object" &&
|
|
(isUint8Array(onread.buffer) || typeof onread.buffer === "function") &&
|
|
typeof onread.callback === "function"
|
|
) {
|
|
if (typeof onread.buffer === "function") {
|
|
this[kBuffer] = true;
|
|
this[kBufferGen] = onread.buffer;
|
|
} else {
|
|
this[kBuffer] = onread.buffer;
|
|
}
|
|
|
|
this[kBufferCb] = onread.callback;
|
|
}
|
|
|
|
this.on("end", _onReadableStreamEnd);
|
|
|
|
_initSocketHandle(this);
|
|
|
|
// If we have a handle, then start the flow of data into the
|
|
// buffer. If not, then this will happen when we connect.
|
|
if (this._handle && options.readable !== false) {
|
|
if (options.pauseOnCreate) {
|
|
// Stop the handle from reading and pause the stream
|
|
this._handle.reading = false;
|
|
this._handle.readStop();
|
|
// @ts-expect-error This property shouldn't be modified
|
|
this.readableFlowing = false;
|
|
} else if (!options.manualStart) {
|
|
this.read(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initiate a connection on a given socket.
|
|
*
|
|
* Possible signatures:
|
|
*
|
|
* - `socket.connect(options[, connectListener])`
|
|
* - `socket.connect(path[, connectListener])` for `IPC` connections.
|
|
* - `socket.connect(port[, host][, connectListener])` for TCP connections.
|
|
* - Returns: `net.Socket` The socket itself.
|
|
*
|
|
* This function is asynchronous. When the connection is established, the `"connect"` event will be emitted. If there is a problem connecting,
|
|
* instead of a `"connect"` event, an `"error"` event will be emitted with
|
|
* the error passed to the `"error"` listener.
|
|
* The last parameter `connectListener`, if supplied, will be added as a listener
|
|
* for the `"connect"` event **once**.
|
|
*
|
|
* This function should only be used for reconnecting a socket after `"close"` has been emitted or otherwise it may lead to undefined
|
|
* behavior.
|
|
*/
|
|
connect(
|
|
options: SocketConnectOptions | NormalizedArgs,
|
|
connectionListener?: ConnectionListener,
|
|
): this;
|
|
connect(
|
|
port: number,
|
|
host: string,
|
|
connectionListener?: ConnectionListener,
|
|
): this;
|
|
connect(port: number, connectionListener?: ConnectionListener): this;
|
|
connect(path: string, connectionListener?: ConnectionListener): this;
|
|
connect(...args: unknown[]): this {
|
|
let normalized: NormalizedArgs;
|
|
|
|
// If passed an array, it's treated as an array of arguments that have
|
|
// already been normalized (so we don't normalize more than once). This has
|
|
// been solved before in https://github.com/nodejs/node/pull/12342, but was
|
|
// reverted as it had unintended side effects.
|
|
if (
|
|
Array.isArray(args[0]) &&
|
|
(args[0] as unknown as NormalizedArgs)[normalizedArgsSymbol]
|
|
) {
|
|
normalized = args[0] as unknown as NormalizedArgs;
|
|
} else {
|
|
normalized = _normalizeArgs(args);
|
|
}
|
|
|
|
const options = normalized[0];
|
|
const cb = normalized[1];
|
|
|
|
// `options.port === null` will be checked later.
|
|
if (
|
|
(options as TcpSocketConnectOptions).port === undefined &&
|
|
(options as IpcSocketConnectOptions).path == null
|
|
) {
|
|
throw new ERR_MISSING_ARGS(["options", "port", "path"]);
|
|
}
|
|
|
|
if (this.write !== Socket.prototype.write) {
|
|
this.write = Socket.prototype.write;
|
|
}
|
|
|
|
if (this.destroyed) {
|
|
this._handle = null;
|
|
this._peername = undefined;
|
|
this._sockname = undefined;
|
|
}
|
|
|
|
const { path } = options as IpcNetConnectOptions;
|
|
const pipe = _isPipe(options);
|
|
debug("pipe", pipe, path);
|
|
|
|
if (!this._handle) {
|
|
this._handle = pipe
|
|
? new Pipe(PipeConstants.SOCKET)
|
|
: new TCP(TCPConstants.SOCKET);
|
|
|
|
_initSocketHandle(this);
|
|
}
|
|
|
|
if (cb !== null) {
|
|
this.once("connect", cb);
|
|
}
|
|
|
|
this._unrefTimer();
|
|
|
|
this.connecting = true;
|
|
|
|
if (pipe) {
|
|
validateString(path, "options.path");
|
|
defaultTriggerAsyncIdScope(
|
|
this[asyncIdSymbol],
|
|
_internalConnect,
|
|
this,
|
|
path,
|
|
);
|
|
} else {
|
|
_lookupAndConnect(this, options as TcpSocketConnectOptions);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Pauses the reading of data. That is, `"data"` events will not be emitted.
|
|
* Useful to throttle back an upload.
|
|
*
|
|
* @return The socket itself.
|
|
*/
|
|
override pause(): this {
|
|
if (
|
|
!this.connecting &&
|
|
this._handle &&
|
|
this._handle.reading
|
|
) {
|
|
this._handle.reading = false;
|
|
|
|
if (!this.destroyed) {
|
|
const err = this._handle.readStop();
|
|
|
|
if (err) {
|
|
this.destroy(errnoException(err, "read"));
|
|
}
|
|
}
|
|
}
|
|
|
|
return Duplex.prototype.pause.call(this) as unknown as this;
|
|
}
|
|
|
|
/**
|
|
* Resumes reading after a call to `socket.pause()`.
|
|
*
|
|
* @return The socket itself.
|
|
*/
|
|
override resume(): this {
|
|
if (
|
|
this[kBuffer] &&
|
|
!this.connecting &&
|
|
this._handle &&
|
|
!this._handle.reading
|
|
) {
|
|
_tryReadStart(this);
|
|
}
|
|
|
|
return Duplex.prototype.resume.call(this) as this;
|
|
}
|
|
|
|
/**
|
|
* Sets the socket to timeout after `timeout` milliseconds of inactivity on
|
|
* the socket. By default `net.Socket` do not have a timeout.
|
|
*
|
|
* When an idle timeout is triggered the socket will receive a `"timeout"` event but the connection will not be severed. The user must manually call `socket.end()` or `socket.destroy()` to
|
|
* end the connection.
|
|
*
|
|
* If `timeout` is `0`, then the existing idle timeout is disabled.
|
|
*
|
|
* The optional `callback` parameter will be added as a one-time listener for the `"timeout"` event.
|
|
* @return The socket itself.
|
|
*/
|
|
setTimeout = setStreamTimeout;
|
|
|
|
/**
|
|
* Enable/disable the use of Nagle's algorithm.
|
|
*
|
|
* When a TCP connection is created, it will have Nagle's algorithm enabled.
|
|
*
|
|
* Nagle's algorithm delays data before it is sent via the network. It attempts
|
|
* to optimize throughput at the expense of latency.
|
|
*
|
|
* Passing `true` for `noDelay` or not passing an argument will disable Nagle's
|
|
* algorithm for the socket. Passing `false` for `noDelay` will enable Nagle's
|
|
* algorithm.
|
|
*
|
|
* @param noDelay
|
|
* @return The socket itself.
|
|
*/
|
|
setNoDelay(noDelay?: boolean): this {
|
|
if (!this._handle) {
|
|
this.once(
|
|
"connect",
|
|
noDelay ? this.setNoDelay : () => this.setNoDelay(noDelay),
|
|
);
|
|
|
|
return this;
|
|
}
|
|
|
|
// Backwards compatibility: assume true when `noDelay` is omitted
|
|
const newValue = noDelay === undefined ? true : !!noDelay;
|
|
|
|
if (
|
|
"setNoDelay" in this._handle &&
|
|
this._handle.setNoDelay &&
|
|
newValue !== this[kSetNoDelay]
|
|
) {
|
|
this[kSetNoDelay] = newValue;
|
|
this._handle.setNoDelay(newValue);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enable/disable keep-alive functionality, and optionally set the initial
|
|
* delay before the first keepalive probe is sent on an idle socket.
|
|
*
|
|
* Set `initialDelay` (in milliseconds) to set the delay between the last
|
|
* data packet received and the first keepalive probe. Setting `0` for`initialDelay` will leave the value unchanged from the default
|
|
* (or previous) setting.
|
|
*
|
|
* Enabling the keep-alive functionality will set the following socket options:
|
|
*
|
|
* - `SO_KEEPALIVE=1`
|
|
* - `TCP_KEEPIDLE=initialDelay`
|
|
* - `TCP_KEEPCNT=10`
|
|
* - `TCP_KEEPINTVL=1`
|
|
*
|
|
* @param enable
|
|
* @param initialDelay
|
|
* @return The socket itself.
|
|
*/
|
|
setKeepAlive(enable: boolean, initialDelay?: number): this {
|
|
if (!this._handle) {
|
|
this.once("connect", () => this.setKeepAlive(enable, initialDelay));
|
|
|
|
return this;
|
|
}
|
|
|
|
if ("setKeepAlive" in this._handle) {
|
|
this._handle.setKeepAlive(enable, ~~(initialDelay! / 1000));
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the bound `address`, the address `family` name and `port` of the
|
|
* socket as reported by the operating system:`{ port: 12346, family: "IPv4", address: "127.0.0.1" }`
|
|
*/
|
|
address(): AddressInfo | Record<string, never> {
|
|
return this._getsockname();
|
|
}
|
|
|
|
/**
|
|
* Calling `unref()` on a socket will allow the program to exit if this is the only
|
|
* active socket in the event system. If the socket is already `unref`ed calling`unref()` again will have no effect.
|
|
*
|
|
* @return The socket itself.
|
|
*/
|
|
unref(): this {
|
|
if (!this._handle) {
|
|
this.once("connect", this.unref);
|
|
|
|
return this;
|
|
}
|
|
|
|
if (typeof this._handle.unref === "function") {
|
|
this._handle.unref();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Opposite of `unref()`, calling `ref()` on a previously `unref`ed socket will_not_ let the program exit if it's the only socket left (the default behavior).
|
|
* If the socket is `ref`ed calling `ref` again will have no effect.
|
|
*
|
|
* @return The socket itself.
|
|
*/
|
|
ref(): this {
|
|
if (!this._handle) {
|
|
this.once("connect", this.ref);
|
|
|
|
return this;
|
|
}
|
|
|
|
if (typeof this._handle.ref === "function") {
|
|
this._handle.ref();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* This property shows the number of characters buffered for writing. The buffer
|
|
* may contain strings whose length after encoding is not yet known. So this number
|
|
* is only an approximation of the number of bytes in the buffer.
|
|
*
|
|
* `net.Socket` has the property that `socket.write()` always works. This is to
|
|
* help users get up and running quickly. The computer cannot always keep up
|
|
* with the amount of data that is written to a socket. The network connection
|
|
* simply might be too slow. Node.js will internally queue up the data written to a
|
|
* socket and send it out over the wire when it is possible.
|
|
*
|
|
* The consequence of this internal buffering is that memory may grow.
|
|
* Users who experience large or growing `bufferSize` should attempt to
|
|
* "throttle" the data flows in their program with `socket.pause()` and `socket.resume()`.
|
|
*
|
|
* @deprecated Use `writableLength` instead.
|
|
*/
|
|
get bufferSize(): number {
|
|
if (this._handle) {
|
|
return this.writableLength;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* The amount of received bytes.
|
|
*/
|
|
get bytesRead(): number {
|
|
return this._handle ? this._handle.bytesRead : this[kBytesRead];
|
|
}
|
|
|
|
/**
|
|
* The amount of bytes sent.
|
|
*/
|
|
get bytesWritten(): number | undefined {
|
|
let bytes = this._bytesDispatched;
|
|
const data = this._pendingData;
|
|
const encoding = this._pendingEncoding;
|
|
const writableBuffer = this.writableBuffer;
|
|
|
|
if (!writableBuffer) {
|
|
return undefined;
|
|
}
|
|
|
|
for (const el of writableBuffer) {
|
|
bytes += el!.chunk instanceof Buffer
|
|
? el!.chunk.length
|
|
: Buffer.byteLength(el!.chunk, el!.encoding);
|
|
}
|
|
|
|
if (Array.isArray(data)) {
|
|
// Was a writev, iterate over chunks to get total length
|
|
for (let i = 0; i < data.length; i++) {
|
|
const chunk = data[i];
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
if ((data as any).allBuffers || chunk instanceof Buffer) {
|
|
bytes += chunk.length;
|
|
} else {
|
|
bytes += Buffer.byteLength(chunk.chunk, chunk.encoding);
|
|
}
|
|
}
|
|
} else if (data) {
|
|
// Writes are either a string or a Buffer.
|
|
if (typeof data !== "string") {
|
|
bytes += (data as Buffer).length;
|
|
} else {
|
|
bytes += Buffer.byteLength(data, encoding);
|
|
}
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* If `true`,`socket.connect(options[, connectListener])` was
|
|
* called and has not yet finished. It will stay `true` until the socket becomes
|
|
* connected, then it is set to `false` and the `"connect"` event is emitted. Note
|
|
* that the `socket.connect(options[, connectListener])` callback is a listener for the `"connect"` event.
|
|
*/
|
|
connecting = false;
|
|
|
|
/**
|
|
* The string representation of the local IP address the remote client is
|
|
* connecting on. For example, in a server listening on `"0.0.0.0"`, if a client
|
|
* connects on `"192.168.1.1"`, the value of `socket.localAddress` would be`"192.168.1.1"`.
|
|
*/
|
|
get localAddress(): string {
|
|
return this._getsockname().address;
|
|
}
|
|
|
|
/**
|
|
* The numeric representation of the local port. For example, `80` or `21`.
|
|
*/
|
|
get localPort(): number {
|
|
return this._getsockname().port;
|
|
}
|
|
|
|
/**
|
|
* The string representation of the local IP family. `"IPv4"` or `"IPv6"`.
|
|
*/
|
|
get localFamily(): string | undefined {
|
|
return this._getsockname().family;
|
|
}
|
|
|
|
/**
|
|
* The string representation of the remote IP address. For example,`"74.125.127.100"` or `"2001:4860:a005::68"`. Value may be `undefined` if
|
|
* the socket is destroyed (for example, if the client disconnected).
|
|
*/
|
|
get remoteAddress(): string | undefined {
|
|
return this._getpeername().address;
|
|
}
|
|
|
|
/**
|
|
* The string representation of the remote IP family. `"IPv4"` or `"IPv6"`.
|
|
*/
|
|
get remoteFamily(): string | undefined {
|
|
const { family } = this._getpeername();
|
|
|
|
return family ? `IPv${family}` : family;
|
|
}
|
|
|
|
/**
|
|
* The numeric representation of the remote port. For example, `80` or `21`.
|
|
*/
|
|
get remotePort(): number | undefined {
|
|
return this._getpeername().port;
|
|
}
|
|
|
|
get pending(): boolean {
|
|
return !this._handle || this.connecting;
|
|
}
|
|
|
|
get readyState(): string {
|
|
if (this.connecting) {
|
|
return "opening";
|
|
} else if (this.readable && this.writable) {
|
|
return "open";
|
|
} else if (this.readable && !this.writable) {
|
|
return "readOnly";
|
|
} else if (!this.readable && this.writable) {
|
|
return "writeOnly";
|
|
}
|
|
return "closed";
|
|
}
|
|
|
|
/**
|
|
* Half-closes the socket. i.e., it sends a FIN packet. It is possible the
|
|
* server will still send some data.
|
|
*
|
|
* See `writable.end()` for further details.
|
|
*
|
|
* @param encoding Only used when data is `string`.
|
|
* @param cb Optional callback for when the socket is finished.
|
|
* @return The socket itself.
|
|
*/
|
|
override end(cb?: () => void): this;
|
|
override end(buffer: Uint8Array | string, cb?: () => void): this;
|
|
override end(
|
|
data: Uint8Array | string,
|
|
encoding?: Encodings,
|
|
cb?: () => void,
|
|
): this;
|
|
override end(
|
|
data?: Uint8Array | string | (() => void),
|
|
encoding?: Encodings | (() => void),
|
|
cb?: () => void,
|
|
): this {
|
|
Duplex.prototype.end.call(this, data, encoding as Encodings, cb);
|
|
DTRACE_NET_STREAM_END(this);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @param size Optional argument to specify how much data to read.
|
|
*/
|
|
override read(
|
|
size?: number,
|
|
): string | Uint8Array | Buffer | null | undefined {
|
|
if (
|
|
this[kBuffer] &&
|
|
!this.connecting &&
|
|
this._handle &&
|
|
!this._handle.reading
|
|
) {
|
|
_tryReadStart(this);
|
|
}
|
|
|
|
return Duplex.prototype.read.call(this, size);
|
|
}
|
|
|
|
destroySoon() {
|
|
if (this.writable) {
|
|
this.end();
|
|
}
|
|
|
|
if (this.writableFinished) {
|
|
this.destroy();
|
|
} else {
|
|
this.once("finish", this.destroy);
|
|
}
|
|
}
|
|
|
|
_unrefTimer() {
|
|
// deno-lint-ignore no-this-alias
|
|
for (let s = this; s !== null; s = s._parent) {
|
|
if (s[kTimeout]) {
|
|
s[kTimeout].refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
// The user has called .end(), and all the bytes have been
|
|
// sent out to the other side.
|
|
// deno-lint-ignore no-explicit-any
|
|
override _final(cb: any): any {
|
|
// If still connecting - defer handling `_final` until 'connect' will happen
|
|
if (this.pending) {
|
|
debug("_final: not yet connected");
|
|
return this.once("connect", () => this._final(cb));
|
|
}
|
|
|
|
if (!this._handle) {
|
|
return cb();
|
|
}
|
|
|
|
debug("_final: not ended, call shutdown()");
|
|
|
|
const req = new ShutdownWrap<Handle>();
|
|
req.oncomplete = _afterShutdown;
|
|
req.handle = this._handle;
|
|
req.callback = cb;
|
|
const err = this._handle.shutdown(req);
|
|
|
|
if (err === 1 || err === codeMap.get("ENOTCONN")) {
|
|
// synchronous finish
|
|
return cb();
|
|
} else if (err !== 0) {
|
|
return cb(errnoException(err, "shutdown"));
|
|
}
|
|
}
|
|
|
|
_onTimeout() {
|
|
const handle = this._handle;
|
|
const lastWriteQueueSize = this[kLastWriteQueueSize];
|
|
|
|
if (lastWriteQueueSize > 0 && handle) {
|
|
// `lastWriteQueueSize !== writeQueueSize` means there is
|
|
// an active write in progress, so we suppress the timeout.
|
|
const { writeQueueSize } = handle;
|
|
|
|
if (lastWriteQueueSize !== writeQueueSize) {
|
|
this[kLastWriteQueueSize] = writeQueueSize;
|
|
this._unrefTimer();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
debug("_onTimeout");
|
|
this.emit("timeout");
|
|
}
|
|
|
|
override _read(size?: number) {
|
|
debug("_read");
|
|
if (this.connecting || !this._handle) {
|
|
debug("_read wait for connection");
|
|
this.once("connect", () => this._read(size));
|
|
} else if (!this._handle.reading) {
|
|
_tryReadStart(this);
|
|
}
|
|
}
|
|
|
|
override _destroy(exception: Error | null, cb: (err: Error | null) => void) {
|
|
debug("destroy");
|
|
this.connecting = false;
|
|
|
|
// deno-lint-ignore no-this-alias
|
|
for (let s = this; s !== null; s = s._parent) {
|
|
clearTimeout(s[kTimeout]);
|
|
}
|
|
|
|
debug("close");
|
|
if (this._handle) {
|
|
debug("close handle");
|
|
const isException = exception ? true : false;
|
|
// `bytesRead` and `kBytesWritten` should be accessible after `.destroy()`
|
|
this[kBytesRead] = this._handle.bytesRead;
|
|
this[kBytesWritten] = this._handle.bytesWritten;
|
|
|
|
this._handle.close(() => {
|
|
this._handle!.onread = _noop;
|
|
this._handle = null;
|
|
this._sockname = undefined;
|
|
|
|
debug("emit close");
|
|
this.emit("close", isException);
|
|
});
|
|
cb(exception);
|
|
} else {
|
|
cb(exception);
|
|
nextTick(_emitCloseNT, this);
|
|
}
|
|
|
|
if (this._server) {
|
|
debug("has server");
|
|
this._server._connections--;
|
|
|
|
if (this._server._emitCloseIfDrained) {
|
|
this._server._emitCloseIfDrained();
|
|
}
|
|
}
|
|
}
|
|
|
|
_getpeername(): AddressInfo | Record<string, never> {
|
|
if (!this._handle || !("getpeername" in this._handle) || this.connecting) {
|
|
return this._peername || {};
|
|
} else if (!this._peername) {
|
|
this._peername = {};
|
|
this._handle.getpeername(this._peername);
|
|
}
|
|
|
|
return this._peername;
|
|
}
|
|
|
|
_getsockname(): AddressInfo | Record<string, never> {
|
|
if (!this._handle || !("getsockname" in this._handle)) {
|
|
return {};
|
|
} else if (!this._sockname) {
|
|
this._sockname = {};
|
|
this._handle.getsockname(this._sockname);
|
|
}
|
|
|
|
return this._sockname;
|
|
}
|
|
|
|
_writeGeneric(
|
|
writev: boolean,
|
|
// deno-lint-ignore no-explicit-any
|
|
data: any,
|
|
encoding: string,
|
|
cb: (error?: Error | null) => void,
|
|
) {
|
|
// If we are still connecting, then buffer this for later.
|
|
// The Writable logic will buffer up any more writes while
|
|
// waiting for this one to be done.
|
|
if (this.connecting) {
|
|
this._pendingData = data;
|
|
this._pendingEncoding = encoding;
|
|
this.once("connect", function connect(this: Socket) {
|
|
this._writeGeneric(writev, data, encoding, cb);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this._pendingData = null;
|
|
this._pendingEncoding = "";
|
|
|
|
if (!this._handle) {
|
|
cb(new ERR_SOCKET_CLOSED());
|
|
|
|
return false;
|
|
}
|
|
|
|
this._unrefTimer();
|
|
|
|
let req;
|
|
|
|
if (writev) {
|
|
req = writevGeneric(this, data, cb);
|
|
} else {
|
|
req = writeGeneric(this, data, encoding, cb);
|
|
}
|
|
if (req.async) {
|
|
this[kLastWriteQueueSize] = req.bytes;
|
|
}
|
|
}
|
|
|
|
// @ts-ignore Duplex defining as a property when want a method.
|
|
_writev(
|
|
// deno-lint-ignore no-explicit-any
|
|
chunks: Array<{ chunk: any; encoding: string }>,
|
|
cb: (error?: Error | null) => void,
|
|
) {
|
|
this._writeGeneric(true, chunks, "", cb);
|
|
}
|
|
|
|
override _write(
|
|
// deno-lint-ignore no-explicit-any
|
|
data: any,
|
|
encoding: string,
|
|
cb: (error?: Error | null) => void,
|
|
) {
|
|
this._writeGeneric(false, data, encoding, cb);
|
|
}
|
|
|
|
[kAfterAsyncWrite]() {
|
|
this[kLastWriteQueueSize] = 0;
|
|
}
|
|
|
|
get [kUpdateTimer]() {
|
|
return this._unrefTimer;
|
|
}
|
|
|
|
get _connecting(): boolean {
|
|
return this.connecting;
|
|
}
|
|
|
|
// Legacy alias. Having this is probably being overly cautious, but it doesn't
|
|
// really hurt anyone either. This can probably be removed safely if desired.
|
|
get _bytesDispatched(): number {
|
|
return this._handle ? this._handle.bytesWritten : this[kBytesWritten];
|
|
}
|
|
|
|
get _handle(): Handle | null {
|
|
return this[kHandle];
|
|
}
|
|
|
|
set _handle(v: Handle | null) {
|
|
this[kHandle] = v;
|
|
}
|
|
}
|
|
|
|
export const Stream = Socket;
|
|
|
|
// Target API:
|
|
//
|
|
// let s = net.connect({port: 80, host: 'google.com'}, function() {
|
|
// ...
|
|
// });
|
|
//
|
|
// There are various forms:
|
|
//
|
|
// connect(options, [cb])
|
|
// connect(port, [host], [cb])
|
|
// connect(path, [cb]);
|
|
//
|
|
export function connect(
|
|
options: NetConnectOptions,
|
|
connectionListener?: () => void,
|
|
): Socket;
|
|
export function connect(
|
|
port: number,
|
|
host?: string,
|
|
connectionListener?: () => void,
|
|
): Socket;
|
|
export function connect(path: string, connectionListener?: () => void): Socket;
|
|
export function connect(...args: unknown[]) {
|
|
const normalized = _normalizeArgs(args);
|
|
const options = normalized[0] as Partial<NetConnectOptions>;
|
|
debug("createConnection", normalized);
|
|
const socket = new Socket(options);
|
|
|
|
if (netClientSocketChannel.hasSubscribers) {
|
|
netClientSocketChannel.publish({
|
|
socket,
|
|
});
|
|
}
|
|
|
|
if (options.timeout) {
|
|
socket.setTimeout(options.timeout);
|
|
}
|
|
|
|
return socket.connect(normalized);
|
|
}
|
|
|
|
export const createConnection = connect;
|
|
|
|
export interface ListenOptions extends Abortable {
|
|
fd?: number;
|
|
port?: number | undefined;
|
|
host?: string | undefined;
|
|
backlog?: number | undefined;
|
|
path?: string | undefined;
|
|
exclusive?: boolean | undefined;
|
|
readableAll?: boolean | undefined;
|
|
writableAll?: boolean | undefined;
|
|
/**
|
|
* Default: `false`
|
|
*/
|
|
ipv6Only?: boolean | undefined;
|
|
}
|
|
|
|
type ConnectionListener = (socket: Socket) => void;
|
|
|
|
interface ServerOptions {
|
|
/**
|
|
* Indicates whether half-opened TCP connections are allowed.
|
|
* Default: false
|
|
*/
|
|
allowHalfOpen?: boolean | undefined;
|
|
/**
|
|
* Indicates whether the socket should be paused on incoming connections.
|
|
* Default: false
|
|
*/
|
|
pauseOnConnect?: boolean | undefined;
|
|
}
|
|
|
|
function _isServerSocketOptions(
|
|
options: unknown,
|
|
): options is null | undefined | ServerOptions {
|
|
return (
|
|
options === null ||
|
|
typeof options === "undefined" ||
|
|
typeof options === "object"
|
|
);
|
|
}
|
|
|
|
function _isConnectionListener(
|
|
connectionListener: unknown,
|
|
): connectionListener is ConnectionListener {
|
|
return typeof connectionListener === "function";
|
|
}
|
|
|
|
function _getFlags(ipv6Only?: boolean): number {
|
|
return ipv6Only === true ? TCPConstants.UV_TCP_IPV6ONLY : 0;
|
|
}
|
|
|
|
function _listenInCluster(
|
|
server: Server,
|
|
address: string | null,
|
|
port: number | null,
|
|
addressType: number | null,
|
|
backlog: number,
|
|
fd?: number | null,
|
|
exclusive?: boolean,
|
|
flags?: number,
|
|
) {
|
|
exclusive = !!exclusive;
|
|
|
|
// TODO(cmorten): here we deviate somewhat from the Node implementation which
|
|
// makes use of the https://nodejs.org/api/cluster.html module to run servers
|
|
// across a "cluster" of Node processes to take advantage of multi-core
|
|
// systems.
|
|
//
|
|
// Though Deno has has a Worker capability from which we could simulate this,
|
|
// for now we assert that we are _always_ on the primary process.
|
|
const isPrimary = true;
|
|
|
|
if (isPrimary || exclusive) {
|
|
// Will create a new handle
|
|
// _listen2 sets up the listened handle, it is still named like this
|
|
// to avoid breaking code that wraps this method
|
|
server._listen2(address, port, addressType, backlog, fd, flags);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
function _lookupAndListen(
|
|
server: Server,
|
|
port: number,
|
|
address: string,
|
|
backlog: number,
|
|
exclusive: boolean,
|
|
flags: number,
|
|
) {
|
|
dnsLookup(address, function doListen(err, ip, addressType) {
|
|
if (err) {
|
|
server.emit("error", err);
|
|
} else {
|
|
addressType = ip ? addressType : 4;
|
|
|
|
_listenInCluster(
|
|
server,
|
|
ip,
|
|
port,
|
|
addressType,
|
|
backlog,
|
|
null,
|
|
exclusive,
|
|
flags,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function _addAbortSignalOption(server: Server, options: ListenOptions) {
|
|
if (options?.signal === undefined) {
|
|
return;
|
|
}
|
|
|
|
validateAbortSignal(options.signal, "options.signal");
|
|
|
|
const { signal } = options;
|
|
|
|
const onAborted = () => {
|
|
server.close();
|
|
};
|
|
|
|
if (signal.aborted) {
|
|
nextTick(onAborted);
|
|
} else {
|
|
signal.addEventListener("abort", onAborted);
|
|
server.once("close", () => signal.removeEventListener("abort", onAborted));
|
|
}
|
|
}
|
|
|
|
// Returns handle if it can be created, or error code if it can't
|
|
export function _createServerHandle(
|
|
address: string | null,
|
|
port: number | null,
|
|
addressType: number | null,
|
|
fd?: number | null,
|
|
flags?: number,
|
|
): Handle | number {
|
|
let err = 0;
|
|
// Assign handle in listen, and clean up if bind or listen fails
|
|
let handle;
|
|
let isTCP = false;
|
|
|
|
if (typeof fd === "number" && fd >= 0) {
|
|
try {
|
|
handle = _createHandle(fd, true);
|
|
} catch (e) {
|
|
// Not a fd we can listen on. This will trigger an error.
|
|
debug("listen invalid fd=%d:", fd, (e as Error).message);
|
|
|
|
return codeMap.get("EINVAL")!;
|
|
}
|
|
|
|
err = handle.open(fd);
|
|
|
|
if (err) {
|
|
return err;
|
|
}
|
|
|
|
assert(!address && !port);
|
|
} else if (port === -1 && addressType === -1) {
|
|
handle = new Pipe(PipeConstants.SERVER);
|
|
|
|
if (isWindows) {
|
|
const instances = Number.parseInt(
|
|
Deno.env.get("NODE_PENDING_PIPE_INSTANCES") ?? "",
|
|
);
|
|
|
|
if (!Number.isNaN(instances)) {
|
|
handle.setPendingInstances!(instances);
|
|
}
|
|
}
|
|
} else {
|
|
handle = new TCP(TCPConstants.SERVER);
|
|
isTCP = true;
|
|
}
|
|
|
|
if (address || port || isTCP) {
|
|
debug("bind to", address || "any");
|
|
|
|
if (!address) {
|
|
// TODO(@bartlomieju): differs from Node which tries to bind to IPv6 first when no
|
|
// address is provided.
|
|
//
|
|
// Forcing IPv4 as a workaround for Deno not aligning with Node on
|
|
// implicit binding on Windows.
|
|
//
|
|
// REF: https://github.com/denoland/deno/issues/10762
|
|
|
|
// Try binding to ipv6 first
|
|
// err = (handle as TCP).bind6(DEFAULT_IPV6_ADDR, port ?? 0, flags ?? 0);
|
|
|
|
// if (err) {
|
|
// handle.close();
|
|
|
|
// Fallback to ipv4
|
|
return _createServerHandle(DEFAULT_IPV4_ADDR, port, 4, null, flags);
|
|
// }
|
|
} else if (addressType === 6) {
|
|
err = (handle as TCP).bind6(address, port ?? 0, flags ?? 0);
|
|
} else {
|
|
err = (handle as TCP).bind(address, port ?? 0);
|
|
}
|
|
}
|
|
|
|
if (err) {
|
|
handle.close();
|
|
|
|
return err;
|
|
}
|
|
|
|
return handle;
|
|
}
|
|
|
|
function _emitErrorNT(server: Server, err: Error) {
|
|
server.emit("error", err);
|
|
}
|
|
|
|
function _emitListeningNT(server: Server) {
|
|
// Ensure handle hasn't closed
|
|
if (server._handle) {
|
|
server.emit("listening");
|
|
}
|
|
}
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
function _onconnection(this: any, err: number, clientHandle?: Handle) {
|
|
// deno-lint-ignore no-this-alias
|
|
const handle = this;
|
|
const self = handle[ownerSymbol];
|
|
|
|
debug("onconnection");
|
|
|
|
if (err) {
|
|
self.emit("error", errnoException(err, "accept"));
|
|
|
|
return;
|
|
}
|
|
|
|
if (self.maxConnections && self._connections >= self.maxConnections) {
|
|
clientHandle!.close();
|
|
|
|
return;
|
|
}
|
|
|
|
const socket = self._createSocket(clientHandle);
|
|
this._connections++;
|
|
self.emit("connection", socket);
|
|
|
|
if (netServerSocketChannel.hasSubscribers) {
|
|
netServerSocketChannel.publish({
|
|
socket,
|
|
});
|
|
}
|
|
}
|
|
|
|
function _setupListenHandle(
|
|
this: Server,
|
|
address: string | null,
|
|
port: number | null,
|
|
addressType: number | null,
|
|
backlog: number,
|
|
fd?: number | null,
|
|
flags?: number,
|
|
) {
|
|
debug("setupListenHandle", address, port, addressType, backlog, fd);
|
|
|
|
// If there is not yet a handle, we need to create one and bind.
|
|
// In the case of a server sent via IPC, we don't need to do this.
|
|
if (this._handle) {
|
|
debug("setupListenHandle: have a handle already");
|
|
} else {
|
|
debug("setupListenHandle: create a handle");
|
|
|
|
let rval = null;
|
|
|
|
// Try to bind to the unspecified IPv6 address, see if IPv6 is available
|
|
if (!address && typeof fd !== "number") {
|
|
if (isWindows) {
|
|
address = DEFAULT_IPV4_ADDR;
|
|
addressType = 4;
|
|
} else {
|
|
address = DEFAULT_IPV6_ADDR;
|
|
addressType = 6;
|
|
}
|
|
}
|
|
|
|
if (rval === null) {
|
|
rval = _createServerHandle(address, port, addressType, fd, flags);
|
|
}
|
|
|
|
if (typeof rval === "number") {
|
|
const error = uvExceptionWithHostPort(rval, "listen", address, port);
|
|
nextTick(_emitErrorNT, this, error);
|
|
|
|
return;
|
|
}
|
|
|
|
this._handle = rval;
|
|
}
|
|
|
|
this[asyncIdSymbol] = _getNewAsyncId(this._handle);
|
|
this._handle.onconnection = _onconnection;
|
|
this._handle[ownerSymbol] = this;
|
|
|
|
// Use a backlog of 512 entries. We pass 511 to the listen() call because
|
|
// the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1);
|
|
// which will thus give us a backlog of 512 entries.
|
|
const err = this._handle.listen(backlog || 511);
|
|
|
|
if (err) {
|
|
const ex = uvExceptionWithHostPort(err, "listen", address, port);
|
|
this._handle.close();
|
|
this._handle = null;
|
|
|
|
defaultTriggerAsyncIdScope(
|
|
this[asyncIdSymbol],
|
|
nextTick,
|
|
_emitErrorNT,
|
|
this,
|
|
ex,
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Generate connection key, this should be unique to the connection
|
|
this._connectionKey = addressType + ":" + address + ":" + port;
|
|
|
|
// Unref the handle if the server was unref'ed prior to listening
|
|
if (this._unref) {
|
|
this.unref();
|
|
}
|
|
|
|
defaultTriggerAsyncIdScope(
|
|
this[asyncIdSymbol],
|
|
nextTick,
|
|
_emitListeningNT,
|
|
this,
|
|
);
|
|
}
|
|
|
|
/** This class is used to create a TCP or IPC server. */
|
|
export class Server extends EventEmitter {
|
|
[asyncIdSymbol] = -1;
|
|
|
|
allowHalfOpen = false;
|
|
pauseOnConnect = false;
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
_handle: any = null;
|
|
_connections = 0;
|
|
_usingWorkers = false;
|
|
// deno-lint-ignore no-explicit-any
|
|
_workers: any[] = [];
|
|
_unref = false;
|
|
_pipeName?: string;
|
|
_connectionKey?: string;
|
|
|
|
/**
|
|
* `net.Server` is an `EventEmitter` with the following events:
|
|
*
|
|
* - `"close"` - Emitted when the server closes. If connections exist, this
|
|
* event is not emitted until all connections are ended.
|
|
* - `"connection"` - Emitted when a new connection is made. `socket` is an
|
|
* instance of `net.Socket`.
|
|
* - `"error"` - Emitted when an error occurs. Unlike `net.Socket`, the
|
|
* `"close"` event will not be emitted directly following this event unless
|
|
* `server.close()` is manually called. See the example in discussion of
|
|
* `server.listen()`.
|
|
* - `"listening"` - Emitted when the server has been bound after calling
|
|
* `server.listen()`.
|
|
*/
|
|
constructor(connectionListener?: ConnectionListener);
|
|
constructor(options?: ServerOptions, connectionListener?: ConnectionListener);
|
|
constructor(
|
|
options?: ServerOptions | ConnectionListener,
|
|
connectionListener?: ConnectionListener,
|
|
) {
|
|
super();
|
|
|
|
if (_isConnectionListener(options)) {
|
|
this.on("connection", options);
|
|
} else if (_isServerSocketOptions(options)) {
|
|
this.allowHalfOpen = options?.allowHalfOpen || false;
|
|
this.pauseOnConnect = !!options?.pauseOnConnect;
|
|
|
|
if (_isConnectionListener(connectionListener)) {
|
|
this.on("connection", connectionListener);
|
|
}
|
|
} else {
|
|
throw new ERR_INVALID_ARG_TYPE("options", "Object", options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start a server listening for connections. A `net.Server` can be a TCP or
|
|
* an `IPC` server depending on what it listens to.
|
|
*
|
|
* Possible signatures:
|
|
*
|
|
* - `server.listen(handle[, backlog][, callback])`
|
|
* - `server.listen(options[, callback])`
|
|
* - `server.listen(path[, backlog][, callback])` for `IPC` servers
|
|
* - `server.listen([port[, host[, backlog]]][, callback])` for TCP servers
|
|
*
|
|
* This function is asynchronous. When the server starts listening, the `'listening'` event will be emitted. The last parameter `callback`will be added as a listener for the `'listening'`
|
|
* event.
|
|
*
|
|
* All `listen()` methods can take a `backlog` parameter to specify the maximum
|
|
* length of the queue of pending connections. The actual length will be determined
|
|
* by the OS through sysctl settings such as `tcp_max_syn_backlog` and `somaxconn` on Linux. The default value of this parameter is 511 (not 512).
|
|
*
|
|
* All `Socket` are set to `SO_REUSEADDR` (see [`socket(7)`](https://man7.org/linux/man-pages/man7/socket.7.html) for
|
|
* details).
|
|
*
|
|
* The `server.listen()` method can be called again if and only if there was an
|
|
* error during the first `server.listen()` call or `server.close()` has been
|
|
* called. Otherwise, an `ERR_SERVER_ALREADY_LISTEN` error will be thrown.
|
|
*
|
|
* One of the most common errors raised when listening is `EADDRINUSE`.
|
|
* This happens when another server is already listening on the requested`port`/`path`/`handle`. One way to handle this would be to retry
|
|
* after a certain amount of time:
|
|
*/
|
|
listen(
|
|
port?: number,
|
|
hostname?: string,
|
|
backlog?: number,
|
|
listeningListener?: () => void,
|
|
): this;
|
|
listen(
|
|
port?: number,
|
|
hostname?: string,
|
|
listeningListener?: () => void,
|
|
): this;
|
|
listen(port?: number, backlog?: number, listeningListener?: () => void): this;
|
|
listen(port?: number, listeningListener?: () => void): this;
|
|
listen(path: string, backlog?: number, listeningListener?: () => void): this;
|
|
listen(path: string, listeningListener?: () => void): this;
|
|
listen(options: ListenOptions, listeningListener?: () => void): this;
|
|
// deno-lint-ignore no-explicit-any
|
|
listen(handle: any, backlog?: number, listeningListener?: () => void): this;
|
|
// deno-lint-ignore no-explicit-any
|
|
listen(handle: any, listeningListener?: () => void): this;
|
|
listen(...args: unknown[]): this {
|
|
const normalized = _normalizeArgs(args);
|
|
let options = normalized[0] as Partial<ListenOptions>;
|
|
const cb = normalized[1];
|
|
|
|
if (this._handle) {
|
|
throw new ERR_SERVER_ALREADY_LISTEN();
|
|
}
|
|
|
|
if (cb !== null) {
|
|
this.once("listening", cb);
|
|
}
|
|
|
|
const backlogFromArgs: number =
|
|
// (handle, backlog) or (path, backlog) or (port, backlog)
|
|
_toNumber(args.length > 1 && args[1]) ||
|
|
(_toNumber(args.length > 2 && args[2]) as number); // (port, host, backlog)
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
options = (options as any)._handle || (options as any).handle || options;
|
|
const flags = _getFlags(options.ipv6Only);
|
|
|
|
// (handle[, backlog][, cb]) where handle is an object with a handle
|
|
if (options instanceof TCP) {
|
|
this._handle = options;
|
|
this[asyncIdSymbol] = this._handle.getAsyncId();
|
|
|
|
_listenInCluster(this, null, -1, -1, backlogFromArgs);
|
|
|
|
return this;
|
|
}
|
|
|
|
_addAbortSignalOption(this, options);
|
|
|
|
// (handle[, backlog][, cb]) where handle is an object with a fd
|
|
if (typeof options.fd === "number" && options.fd >= 0) {
|
|
_listenInCluster(this, null, null, null, backlogFromArgs, options.fd);
|
|
|
|
return this;
|
|
}
|
|
|
|
// ([port][, host][, backlog][, cb]) where port is omitted,
|
|
// that is, listen(), listen(null), listen(cb), or listen(null, cb)
|
|
// or (options[, cb]) where options.port is explicitly set as undefined or
|
|
// null, bind to an arbitrary unused port
|
|
if (
|
|
args.length === 0 ||
|
|
typeof args[0] === "function" ||
|
|
(typeof options.port === "undefined" && "port" in options) ||
|
|
options.port === null
|
|
) {
|
|
options.port = 0;
|
|
}
|
|
|
|
// ([port][, host][, backlog][, cb]) where port is specified
|
|
// or (options[, cb]) where options.port is specified
|
|
// or if options.port is normalized as 0 before
|
|
let backlog;
|
|
|
|
if (typeof options.port === "number" || typeof options.port === "string") {
|
|
validatePort(options.port, "options.port");
|
|
backlog = options.backlog || backlogFromArgs;
|
|
|
|
// start TCP server listening on host:port
|
|
if (options.host) {
|
|
_lookupAndListen(
|
|
this,
|
|
options.port | 0,
|
|
options.host,
|
|
backlog,
|
|
!!options.exclusive,
|
|
flags,
|
|
);
|
|
} else {
|
|
// Undefined host, listens on unspecified address
|
|
// Default addressType 4 will be used to search for primary server
|
|
_listenInCluster(
|
|
this,
|
|
null,
|
|
options.port | 0,
|
|
4,
|
|
backlog,
|
|
undefined,
|
|
options.exclusive,
|
|
);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
// (path[, backlog][, cb]) or (options[, cb])
|
|
// where path or options.path is a UNIX domain socket or Windows pipe
|
|
if (options.path && _isPipeName(options.path)) {
|
|
const pipeName = (this._pipeName = options.path);
|
|
backlog = options.backlog || backlogFromArgs;
|
|
|
|
_listenInCluster(
|
|
this,
|
|
pipeName,
|
|
-1,
|
|
-1,
|
|
backlog,
|
|
undefined,
|
|
options.exclusive,
|
|
);
|
|
|
|
if (!this._handle) {
|
|
// Failed and an error shall be emitted in the next tick.
|
|
// Therefore, we directly return.
|
|
return this;
|
|
}
|
|
|
|
let mode = 0;
|
|
|
|
if (options.readableAll === true) {
|
|
mode |= PipeConstants.UV_READABLE;
|
|
}
|
|
|
|
if (options.writableAll === true) {
|
|
mode |= PipeConstants.UV_WRITABLE;
|
|
}
|
|
|
|
if (mode !== 0) {
|
|
const err = this._handle.fchmod(mode);
|
|
|
|
if (err) {
|
|
this._handle.close();
|
|
this._handle = null;
|
|
|
|
throw errnoException(err, "uv_pipe_chmod");
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
if (!("port" in options || "path" in options)) {
|
|
throw new ERR_INVALID_ARG_VALUE(
|
|
"options",
|
|
options,
|
|
'must have the property "port" or "path"',
|
|
);
|
|
}
|
|
|
|
throw new ERR_INVALID_ARG_VALUE("options", options);
|
|
}
|
|
|
|
/**
|
|
* Stops the server from accepting new connections and keeps existing
|
|
* connections. This function is asynchronous, the server is finally closed
|
|
* when all connections are ended and the server emits a `"close"` event.
|
|
* The optional `callback` will be called once the `"close"` event occurs. Unlike
|
|
* that event, it will be called with an `Error` as its only argument if the server
|
|
* was not open when it was closed.
|
|
*
|
|
* @param cb Called when the server is closed.
|
|
*/
|
|
close(cb?: (err?: Error) => void): this {
|
|
if (typeof cb === "function") {
|
|
if (!this._handle) {
|
|
this.once("close", function close() {
|
|
cb(new ERR_SERVER_NOT_RUNNING());
|
|
});
|
|
} else {
|
|
this.once("close", cb);
|
|
}
|
|
}
|
|
|
|
if (this._handle) {
|
|
(this._handle as TCP).close();
|
|
this._handle = null;
|
|
}
|
|
|
|
if (this._usingWorkers) {
|
|
let left = this._workers.length;
|
|
const onWorkerClose = () => {
|
|
if (--left !== 0) {
|
|
return;
|
|
}
|
|
|
|
this._connections = 0;
|
|
this._emitCloseIfDrained();
|
|
};
|
|
|
|
// Increment connections to be sure that, even if all sockets will be closed
|
|
// during polling of workers, `close` event will be emitted only once.
|
|
this._connections++;
|
|
|
|
// Poll workers
|
|
for (let n = 0; n < this._workers.length; n++) {
|
|
this._workers[n].close(onWorkerClose);
|
|
}
|
|
} else {
|
|
this._emitCloseIfDrained();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns the bound `address`, the address `family` name, and `port` of the server
|
|
* as reported by the operating system if listening on an IP socket
|
|
* (useful to find which port was assigned when getting an OS-assigned address):`{ port: 12346, family: "IPv4", address: "127.0.0.1" }`.
|
|
*
|
|
* For a server listening on a pipe or Unix domain socket, the name is returned
|
|
* as a string.
|
|
*
|
|
* `server.address()` returns `null` before the `"listening"` event has been
|
|
* emitted or after calling `server.close()`.
|
|
*/
|
|
address(): AddressInfo | string | null {
|
|
if (this._handle && this._handle.getsockname) {
|
|
const out = {};
|
|
const err = this._handle.getsockname(out);
|
|
|
|
if (err) {
|
|
throw errnoException(err, "address");
|
|
}
|
|
|
|
return out as AddressInfo;
|
|
} else if (this._pipeName) {
|
|
return this._pipeName;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Asynchronously get the number of concurrent connections on the server. Works
|
|
* when sockets were sent to forks.
|
|
*
|
|
* Callback should take two arguments `err` and `count`.
|
|
*/
|
|
getConnections(cb: (err: Error | null, count: number) => void): this {
|
|
// deno-lint-ignore no-this-alias
|
|
const server = this;
|
|
|
|
function end(err: Error | null, connections?: number) {
|
|
defaultTriggerAsyncIdScope(
|
|
server[asyncIdSymbol],
|
|
nextTick,
|
|
cb,
|
|
err,
|
|
connections,
|
|
);
|
|
}
|
|
|
|
if (!this._usingWorkers) {
|
|
end(null, this._connections);
|
|
|
|
return this;
|
|
}
|
|
|
|
// Poll workers
|
|
let left = this._workers.length;
|
|
let total = this._connections;
|
|
|
|
function oncount(err: Error, count: number) {
|
|
if (err) {
|
|
left = -1;
|
|
|
|
return end(err);
|
|
}
|
|
|
|
total += count;
|
|
|
|
if (--left === 0) {
|
|
return end(null, total);
|
|
}
|
|
}
|
|
|
|
for (let n = 0; n < this._workers.length; n++) {
|
|
this._workers[n].getConnections(oncount);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Calling `unref()` on a server will allow the program to exit if this is the only
|
|
* active server in the event system. If the server is already `unref`ed calling `unref()` again will have no effect.
|
|
*/
|
|
unref(): this {
|
|
this._unref = true;
|
|
|
|
if (this._handle) {
|
|
this._handle.unref();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Opposite of `unref()`, calling `ref()` on a previously `unref`ed server will _not_ let the program exit if it's the only server left (the default behavior).
|
|
* If the server is `ref`ed calling `ref()` again will have no effect.
|
|
*/
|
|
ref(): this {
|
|
this._unref = false;
|
|
|
|
if (this._handle) {
|
|
this._handle.ref();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Indicates whether or not the server is listening for connections.
|
|
*/
|
|
get listening(): boolean {
|
|
return !!this._handle;
|
|
}
|
|
|
|
_createSocket(clientHandle) {
|
|
const socket = new Socket({
|
|
handle: clientHandle,
|
|
allowHalfOpen: this.allowHalfOpen,
|
|
pauseOnCreate: this.pauseOnConnect,
|
|
readable: true,
|
|
writable: true,
|
|
});
|
|
|
|
// TODO(@bartlomieju): implement noDelay and setKeepAlive
|
|
|
|
socket.server = this;
|
|
socket._server = this;
|
|
|
|
DTRACE_NET_SERVER_CONNECTION(socket);
|
|
|
|
return socket;
|
|
}
|
|
|
|
_listen2 = _setupListenHandle;
|
|
|
|
_emitCloseIfDrained() {
|
|
debug("SERVER _emitCloseIfDrained");
|
|
if (this._handle || this._connections) {
|
|
debug(
|
|
`SERVER handle? ${!!this._handle} connections? ${this._connections}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// We use setTimeout instead of nextTick here to avoid EADDRINUSE error
|
|
// when the same port listened immediately after the 'close' event.
|
|
// ref: https://github.com/denoland/deno_std/issues/2788
|
|
defaultTriggerAsyncIdScope(
|
|
this[asyncIdSymbol],
|
|
setTimeout,
|
|
_emitCloseNT,
|
|
0,
|
|
this,
|
|
);
|
|
}
|
|
|
|
_setupWorker(socketList: EventEmitter) {
|
|
this._usingWorkers = true;
|
|
this._workers.push(socketList);
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
socketList.once("exit", (socketList: any) => {
|
|
const index = this._workers.indexOf(socketList);
|
|
this._workers.splice(index, 1);
|
|
});
|
|
}
|
|
|
|
[EventEmitter.captureRejectionSymbol](
|
|
err: Error,
|
|
event: string,
|
|
sock: Socket,
|
|
) {
|
|
switch (event) {
|
|
case "connection": {
|
|
sock.destroy(err);
|
|
break;
|
|
}
|
|
default: {
|
|
this.emit("error", err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new TCP or IPC server.
|
|
*
|
|
* Accepts an `options` object with properties `allowHalfOpen` (default `false`)
|
|
* and `pauseOnConnect` (default `false`).
|
|
*
|
|
* If `allowHalfOpen` is set to `false`, then the socket will
|
|
* automatically end the writable side when the readable side ends.
|
|
*
|
|
* If `allowHalfOpen` is set to `true`, when the other end of the socket
|
|
* signals the end of transmission, the server will only send back the end of
|
|
* transmission when `socket.end()` is explicitly called. For example, in the
|
|
* context of TCP, when a FIN packed is received, a FIN packed is sent back
|
|
* only when `socket.end()` is explicitly called. Until then the connection is
|
|
* half-closed (non-readable but still writable). See `"end"` event and RFC 1122
|
|
* (section 4.2.2.13) for more information.
|
|
*
|
|
* `pauseOnConnect` indicates whether the socket should be paused on incoming
|
|
* connections.
|
|
*
|
|
* If `pauseOnConnect` is set to `true`, then the socket associated with each
|
|
* incoming connection will be paused, and no data will be read from its
|
|
* handle. This allows connections to be passed between processes without any
|
|
* data being read by the original process. To begin reading data from a paused
|
|
* socket, call `socket.resume()`.
|
|
*
|
|
* The server can be a TCP server or an IPC server, depending on what it
|
|
* `listen()` to.
|
|
*
|
|
* Here is an example of an TCP echo server which listens for connections on
|
|
* port 8124:
|
|
*
|
|
* @param options Socket options.
|
|
* @param connectionListener Automatically set as a listener for the `"connection"` event.
|
|
* @return A `net.Server`.
|
|
*/
|
|
export function createServer(
|
|
options?: ServerOptions,
|
|
connectionListener?: ConnectionListener,
|
|
): Server {
|
|
return new Server(options, connectionListener);
|
|
}
|
|
|
|
export { BlockList, isIP, isIPv4, isIPv6, SocketAddress };
|
|
|
|
export default {
|
|
_createServerHandle,
|
|
_normalizeArgs,
|
|
isIP,
|
|
isIPv4,
|
|
isIPv6,
|
|
BlockList,
|
|
SocketAddress,
|
|
connect,
|
|
createConnection,
|
|
createServer,
|
|
Server,
|
|
Socket,
|
|
Stream,
|
|
};
|