mirror of
https://github.com/denoland/deno.git
synced 2024-12-30 02:59:11 -05:00
960776cd32
This commit changes "node:http" module to add support for the "createConnection" option when the "request()" API is called. Closes https://github.com/denoland/deno/issues/19507 --------- Signed-off-by: Yoshiya Hinosawa <stibium121@gmail.com> Signed-off-by: Satya Rohith <me@satyarohith.com> Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com> Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com> Co-authored-by: crowlkats <crowlkats@toaxl.com>
492 lines
14 KiB
TypeScript
492 lines
14 KiB
TypeScript
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
|
|
|
|
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
|
// deno-lint-ignore-file no-explicit-any prefer-primordials
|
|
|
|
import {
|
|
ObjectAssign,
|
|
StringPrototypeReplace,
|
|
} from "ext:deno_node/internal/primordials.mjs";
|
|
import assert from "ext:deno_node/internal/assert.mjs";
|
|
import * as net from "node:net";
|
|
import { createSecureContext } from "node:_tls_common";
|
|
import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts";
|
|
import { connResetException } from "ext:deno_node/internal/errors.ts";
|
|
import { emitWarning } from "node:process";
|
|
import { debuglog } from "ext:deno_node/internal/util/debuglog.ts";
|
|
import {
|
|
constants as TCPConstants,
|
|
TCP,
|
|
} from "ext:deno_node/internal_binding/tcp_wrap.ts";
|
|
import {
|
|
constants as PipeConstants,
|
|
Pipe,
|
|
} from "ext:deno_node/internal_binding/pipe_wrap.ts";
|
|
import { EventEmitter } from "node:events";
|
|
import { kEmptyObject } from "ext:deno_node/internal/util.mjs";
|
|
import { nextTick } from "ext:deno_node/_next_tick.ts";
|
|
import { kHandle } from "ext:deno_node/internal/stream_base_commons.ts";
|
|
import {
|
|
isAnyArrayBuffer,
|
|
isArrayBufferView,
|
|
} from "ext:deno_node/internal/util/types.ts";
|
|
|
|
const kConnectOptions = Symbol("connect-options");
|
|
const kIsVerified = Symbol("verified");
|
|
const kPendingSession = Symbol("pendingSession");
|
|
const kRes = Symbol("res");
|
|
|
|
let debug = debuglog("tls", (fn) => {
|
|
debug = fn;
|
|
});
|
|
|
|
function onConnectEnd(this: any) {
|
|
// NOTE: This logic is shared with _http_client.js
|
|
if (!this._hadError) {
|
|
const options = this[kConnectOptions];
|
|
this._hadError = true;
|
|
const error: any = connResetException(
|
|
"Client network socket disconnected " +
|
|
"before secure TLS connection was " +
|
|
"established",
|
|
);
|
|
error.path = options.path;
|
|
error.host = options.host;
|
|
error.port = options.port;
|
|
error.localAddress = options.localAddress;
|
|
this.destroy(error);
|
|
}
|
|
}
|
|
|
|
export class TLSSocket extends net.Socket {
|
|
_tlsOptions: any;
|
|
_secureEstablished: boolean;
|
|
_securePending: boolean;
|
|
_newSessionPending: boolean;
|
|
_controlReleased: boolean;
|
|
secureConnecting: boolean;
|
|
_SNICallback: any;
|
|
servername: string | null;
|
|
alpnProtocol: string | boolean | null;
|
|
alpnProtocols: string[] | null;
|
|
authorized: boolean;
|
|
authorizationError: any;
|
|
[kRes]: any;
|
|
[kIsVerified]: boolean;
|
|
[kPendingSession]: any;
|
|
[kConnectOptions]: any;
|
|
ssl: any;
|
|
|
|
_start() {
|
|
this[kHandle].afterConnect();
|
|
}
|
|
|
|
constructor(socket: any, opts: any = kEmptyObject) {
|
|
const tlsOptions = { ...opts };
|
|
|
|
const hostname = opts.servername ?? opts.host ?? socket._host;
|
|
tlsOptions.hostname = hostname;
|
|
|
|
const _cert = tlsOptions?.secureContext?.cert;
|
|
const _key = tlsOptions?.secureContext?.key;
|
|
|
|
let caCerts = tlsOptions?.secureContext?.ca;
|
|
if (typeof caCerts === "string") caCerts = [caCerts];
|
|
else if (isArrayBufferView(caCerts) || isAnyArrayBuffer(caCerts)) {
|
|
caCerts = [new TextDecoder().decode(caCerts)];
|
|
}
|
|
tlsOptions.caCerts = caCerts;
|
|
tlsOptions.alpnProtocols = opts.ALPNProtocols;
|
|
|
|
super({
|
|
handle: _wrapHandle(tlsOptions, socket),
|
|
...opts,
|
|
manualStart: true, // This prevents premature reading from TLS handle
|
|
});
|
|
if (socket) {
|
|
this._parent = socket;
|
|
}
|
|
this._tlsOptions = tlsOptions;
|
|
this._secureEstablished = false;
|
|
this._securePending = false;
|
|
this._newSessionPending = false;
|
|
this._controlReleased = false;
|
|
this.secureConnecting = true;
|
|
this._SNICallback = null;
|
|
this.servername = null;
|
|
this.alpnProtocol = null;
|
|
this.alpnProtocols = tlsOptions.ALPNProtocols;
|
|
this.authorized = false;
|
|
this.authorizationError = null;
|
|
this[kRes] = null;
|
|
this[kIsVerified] = false;
|
|
this[kPendingSession] = null;
|
|
|
|
this.ssl = new class {
|
|
verifyError() {
|
|
return null; // Never fails, rejectUnauthorized is always true in Deno.
|
|
}
|
|
}();
|
|
|
|
// deno-lint-ignore no-this-alias
|
|
const tlssock = this;
|
|
|
|
/** Wraps the given socket and adds the tls capability to the underlying
|
|
* handle */
|
|
function _wrapHandle(tlsOptions: any, wrap: net.Socket | undefined) {
|
|
let handle: any;
|
|
|
|
if (wrap) {
|
|
handle = wrap._handle;
|
|
}
|
|
|
|
const options = tlsOptions;
|
|
if (!handle) {
|
|
handle = options.pipe
|
|
? new Pipe(PipeConstants.SOCKET)
|
|
: new TCP(TCPConstants.SOCKET);
|
|
}
|
|
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
|
|
// Patches `afterConnect` hook to replace TCP conn with TLS conn
|
|
const afterConnect = handle.afterConnect;
|
|
handle.afterConnect = async (req: any, status: number) => {
|
|
options.hostname ??= undefined; // coerce to undefined if null, startTls expects hostname to be undefined
|
|
if (tlssock._isNpmAgent) {
|
|
// skips the TLS handshake for @npmcli/agent as it's handled by
|
|
// onSocket handler of ClientRequest object.
|
|
tlssock.emit("secure");
|
|
tlssock.removeListener("end", onConnectEnd);
|
|
return afterConnect.call(handle, req, status);
|
|
}
|
|
|
|
try {
|
|
const conn = await Deno.startTls(handle[kStreamBaseField], options);
|
|
try {
|
|
const hs = await conn.handshake();
|
|
if (hs.alpnProtocol) {
|
|
tlssock.alpnProtocol = hs.alpnProtocol;
|
|
} else {
|
|
tlssock.alpnProtocol = false;
|
|
}
|
|
} catch {
|
|
// Don't interrupt "secure" event to let the first read/write
|
|
// operation emit the error.
|
|
}
|
|
|
|
// Assign the TLS connection to the handle and resume reading.
|
|
handle[kStreamBaseField] = conn;
|
|
handle.upgrading = false;
|
|
if (!handle.pauseOnCreate) {
|
|
handle.readStart();
|
|
}
|
|
|
|
resolve();
|
|
|
|
tlssock.emit("secure");
|
|
tlssock.removeListener("end", onConnectEnd);
|
|
} catch {
|
|
// TODO(kt3k): Handle this
|
|
}
|
|
return afterConnect.call(handle, req, status);
|
|
};
|
|
|
|
handle.upgrading = promise;
|
|
(handle as any).verifyError = function () {
|
|
return null; // Never fails, rejectUnauthorized is always true in Deno.
|
|
};
|
|
// Pretends `handle` is `tls_wrap.wrap(handle, ...)` to make some npm modules happy
|
|
// An example usage of `_parentWrap` in npm module:
|
|
// https://github.com/szmarczak/http2-wrapper/blob/51eeaf59ff9344fb192b092241bfda8506983620/source/utils/js-stream-socket.js#L6
|
|
handle._parent = handle;
|
|
handle._parentWrap = wrap;
|
|
|
|
return handle;
|
|
}
|
|
}
|
|
|
|
_tlsError(err: Error) {
|
|
this.emit("_tlsError", err);
|
|
if (this._controlReleased) {
|
|
return err;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_releaseControl() {
|
|
if (this._controlReleased) {
|
|
return false;
|
|
}
|
|
this._controlReleased = true;
|
|
this.removeListener("error", this._tlsError);
|
|
return true;
|
|
}
|
|
|
|
getEphemeralKeyInfo() {
|
|
return {};
|
|
}
|
|
|
|
isSessionReused() {
|
|
return false;
|
|
}
|
|
|
|
setSession(_session: any) {
|
|
// TODO(kt3k): implement this
|
|
}
|
|
|
|
setServername(_servername: any) {
|
|
// TODO(kt3k): implement this
|
|
}
|
|
|
|
getPeerCertificate(_detailed: boolean) {
|
|
// TODO(kt3k): implement this
|
|
return {
|
|
subject: "localhost",
|
|
subjectaltname: "IP Address:127.0.0.1, IP Address:::1",
|
|
};
|
|
}
|
|
}
|
|
|
|
function normalizeConnectArgs(listArgs: any) {
|
|
const args = net._normalizeArgs(listArgs);
|
|
const options = args[0];
|
|
const cb = args[1];
|
|
|
|
// If args[0] was options, then normalize dealt with it.
|
|
// If args[0] is port, or args[0], args[1] is host, port, we need to
|
|
// find the options and merge them in, normalize's options has only
|
|
// the host/port/path args that it knows about, not the tls options.
|
|
// This means that options.host overrides a host arg.
|
|
if (listArgs[1] !== null && typeof listArgs[1] === "object") {
|
|
ObjectAssign(options, listArgs[1]);
|
|
} else if (listArgs[2] !== null && typeof listArgs[2] === "object") {
|
|
ObjectAssign(options, listArgs[2]);
|
|
}
|
|
|
|
return cb ? [options, cb] : [options];
|
|
}
|
|
|
|
let ipServernameWarned = false;
|
|
|
|
export function Server(options: any, listener: any) {
|
|
return new ServerImpl(options, listener);
|
|
}
|
|
|
|
export class ServerImpl extends EventEmitter {
|
|
listener?: Deno.TlsListener;
|
|
#closed = false;
|
|
constructor(public options: any, listener: any) {
|
|
super();
|
|
if (listener) {
|
|
this.on("secureConnection", listener);
|
|
}
|
|
}
|
|
|
|
listen(port: any, callback: any): this {
|
|
const key = this.options.key?.toString();
|
|
const cert = this.options.cert?.toString();
|
|
// TODO(kt3k): The default host should be "localhost"
|
|
const hostname = this.options.host ?? "0.0.0.0";
|
|
|
|
this.listener = Deno.listenTls({ port, hostname, cert, key });
|
|
|
|
callback?.call(this);
|
|
this.#listen(this.listener);
|
|
return this;
|
|
}
|
|
|
|
async #listen(listener: Deno.TlsListener) {
|
|
while (!this.#closed) {
|
|
try {
|
|
// Creates TCP handle and socket directly from Deno.TlsConn.
|
|
// This works as TLS socket. We don't use TLSSocket class for doing
|
|
// this because Deno.startTls only supports client side tcp connection.
|
|
// TODO(@satyarohith): set TLSSocket.alpnProtocol when we use TLSSocket class.
|
|
const handle = new TCP(TCPConstants.SOCKET, await listener.accept());
|
|
const socket = new net.Socket({ handle });
|
|
this.emit("secureConnection", socket);
|
|
} catch (e) {
|
|
if (e instanceof Deno.errors.BadResource) {
|
|
this.#closed = true;
|
|
}
|
|
// swallow
|
|
}
|
|
}
|
|
}
|
|
|
|
close(cb?: (err?: Error) => void): this {
|
|
if (this.listener) {
|
|
this.listener.close();
|
|
}
|
|
cb?.();
|
|
nextTick(() => {
|
|
this.emit("close");
|
|
});
|
|
return this;
|
|
}
|
|
|
|
address() {
|
|
const addr = this.listener!.addr as Deno.NetAddr;
|
|
return {
|
|
port: addr.port,
|
|
address: addr.hostname,
|
|
};
|
|
}
|
|
}
|
|
|
|
Server.prototype = ServerImpl.prototype;
|
|
|
|
export function createServer(options: any, listener: any) {
|
|
return new ServerImpl(options, listener);
|
|
}
|
|
|
|
function onConnectSecure(this: TLSSocket) {
|
|
this.authorized = true;
|
|
this.secureConnecting = false;
|
|
debug("client emit secureConnect. authorized:", this.authorized);
|
|
this.emit("secureConnect");
|
|
|
|
this.removeListener("end", onConnectEnd);
|
|
}
|
|
|
|
export function connect(...args: any[]) {
|
|
args = normalizeConnectArgs(args);
|
|
let options = args[0];
|
|
const cb = args[1];
|
|
const allowUnauthorized = getAllowUnauthorized();
|
|
|
|
options = {
|
|
rejectUnauthorized: !allowUnauthorized,
|
|
ciphers: DEFAULT_CIPHERS,
|
|
checkServerIdentity,
|
|
minDHSize: 1024,
|
|
...options,
|
|
};
|
|
|
|
if (!options.keepAlive) {
|
|
options.singleUse = true;
|
|
}
|
|
|
|
assert(typeof options.checkServerIdentity === "function");
|
|
assert(
|
|
typeof options.minDHSize === "number",
|
|
"options.minDHSize is not a number: " + options.minDHSize,
|
|
);
|
|
assert(
|
|
options.minDHSize > 0,
|
|
"options.minDHSize is not a positive number: " +
|
|
options.minDHSize,
|
|
);
|
|
|
|
const context = options.secureContext || createSecureContext(options);
|
|
|
|
const tlssock = new TLSSocket(options.socket, {
|
|
allowHalfOpen: options.allowHalfOpen,
|
|
pipe: !!options.path,
|
|
secureContext: context,
|
|
isServer: false,
|
|
requestCert: true,
|
|
rejectUnauthorized: options.rejectUnauthorized !== false,
|
|
session: options.session,
|
|
ALPNProtocols: options.ALPNProtocols,
|
|
requestOCSP: options.requestOCSP,
|
|
enableTrace: options.enableTrace,
|
|
pskCallback: options.pskCallback,
|
|
highWaterMark: options.highWaterMark,
|
|
onread: options.onread,
|
|
signal: options.signal,
|
|
...options, // Caveat emptor: Node does not do this.
|
|
});
|
|
|
|
// rejectUnauthorized property can be explicitly defined as `undefined`
|
|
// causing the assignment to default value (`true`) fail. Before assigning
|
|
// it to the tlssock connection options, explicitly check if it is false
|
|
// and update rejectUnauthorized property. The property gets used by TLSSocket
|
|
// connection handler to allow or reject connection if unauthorized
|
|
options.rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
|
|
tlssock[kConnectOptions] = options;
|
|
|
|
if (cb) {
|
|
tlssock.once("secureConnect", cb);
|
|
}
|
|
|
|
if (!options.socket) {
|
|
// If user provided the socket, it's their responsibility to manage its
|
|
// connectivity. If we created one internally, we connect it.
|
|
if (options.timeout) {
|
|
tlssock.setTimeout(options.timeout);
|
|
}
|
|
|
|
tlssock.connect(options, tlssock._start);
|
|
}
|
|
|
|
tlssock._releaseControl();
|
|
|
|
if (options.session) {
|
|
tlssock.setSession(options.session);
|
|
}
|
|
|
|
if (options.servername) {
|
|
if (!ipServernameWarned && net.isIP(options.servername)) {
|
|
emitWarning(
|
|
"Setting the TLS ServerName to an IP address is not permitted by " +
|
|
"RFC 6066. This will be ignored in a future version.",
|
|
"DeprecationWarning",
|
|
"DEP0123",
|
|
);
|
|
ipServernameWarned = true;
|
|
}
|
|
tlssock.setServername(options.servername);
|
|
}
|
|
|
|
if (options.socket) {
|
|
tlssock._start();
|
|
}
|
|
|
|
tlssock.on("secure", onConnectSecure);
|
|
tlssock.prependListener("end", onConnectEnd);
|
|
|
|
return tlssock;
|
|
}
|
|
|
|
function getAllowUnauthorized() {
|
|
return false;
|
|
}
|
|
|
|
// TODO(kt3k): Implement this when Deno provides APIs for getting peer
|
|
// certificates.
|
|
export function checkServerIdentity(_hostname: string, _cert: any) {
|
|
}
|
|
|
|
function unfqdn(host: string): string {
|
|
return StringPrototypeReplace(host, /[.]$/, "");
|
|
}
|
|
|
|
// Order matters. Mirrors ALL_CIPHER_SUITES from rustls/src/suites.rs but
|
|
// using openssl cipher names instead. Mutable in Node but not (yet) in Deno.
|
|
export const DEFAULT_CIPHERS = [
|
|
// TLSv1.3 suites
|
|
"AES256-GCM-SHA384",
|
|
"AES128-GCM-SHA256",
|
|
"TLS_CHACHA20_POLY1305_SHA256",
|
|
// TLSv1.2 suites
|
|
"ECDHE-ECDSA-AES256-GCM-SHA384",
|
|
"ECDHE-ECDSA-AES128-GCM-SHA256",
|
|
"ECDHE-ECDSA-CHACHA20-POLY1305",
|
|
"ECDHE-RSA-AES256-GCM-SHA384",
|
|
"ECDHE-RSA-AES128-GCM-SHA256",
|
|
"ECDHE-RSA-CHACHA20-POLY1305",
|
|
].join(":");
|
|
|
|
export default {
|
|
TLSSocket,
|
|
connect,
|
|
createServer,
|
|
checkServerIdentity,
|
|
DEFAULT_CIPHERS,
|
|
Server,
|
|
unfqdn,
|
|
};
|