import {
ArrayIsArray,
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,
ERR_TLS_CERT_ALTNAME_INVALID,
} 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";
import { startTlsInternal } from "ext:deno_net/02_tls.js";
import { core, internals } from "ext:core/mod.js";
import {
op_node_tls_handshake,
op_node_tls_start,
op_tls_canonicalize_ipv4_address,
op_tls_key_null,
op_tls_key_static,
} from "ext:core/ops";
const kConnectOptions = Symbol("connect-options");
const kIsVerified = Symbol("verified");
const kPendingSession = Symbol("pendingSession");
const kRes = Symbol("res");
const tlsStreamRids = new Uint32Array(2);
let debug = debuglog("tls", (fn) => {
debug = fn;
});
function canonicalizeIP(ip) {
return op_tls_canonicalize_ipv4_address(ip);
}
function onConnectEnd() {
if (!this._hadError) {
const options = this[kConnectOptions];
this._hadError = true;
const error = 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 {
_start() {
this[kHandle].afterConnectTls();
}
constructor(socket, opts = kEmptyObject) {
const tlsOptions = { ...opts };
let hostname = opts.servername ?? opts.host ?? socket?._host ??
"localhost";
if (hostname && hostname.endsWith(".")) {
hostname = hostname.slice(0, -1);
}
tlsOptions.hostname = hostname;
const cert = tlsOptions?.secureContext?.cert;
const key = tlsOptions?.secureContext?.key;
const hasTlsKey = key != undefined &&
cert != undefined;
const keyPair = hasTlsKey
? op_tls_key_static(cert, key)
: op_tls_key_null();
let caCerts = tlsOptions?.secureContext?.ca;
if (typeof caCerts === "string") {
caCerts = [caCerts];
} else if (isArrayBufferView(caCerts) || isAnyArrayBuffer(caCerts)) {
caCerts = [new TextDecoder().decode(caCerts)];
} else if (Array.isArray(caCerts)) {
caCerts = caCerts.map((cert) => {
if (typeof cert === "string") {
return cert;
} else if (isArrayBufferView(cert) || isAnyArrayBuffer(cert)) {
return new TextDecoder().decode(cert);
}
return cert;
});
}
tlsOptions.keyPair = keyPair;
tlsOptions.caCerts = caCerts;
tlsOptions.alpnProtocols = opts.ALPNProtocols;
tlsOptions.rejectUnauthorized = opts.rejectUnauthorized !== false;
try {
if (
opts.checkServerIdentity &&
typeof opts.checkServerIdentity == "function" &&
opts.checkServerIdentity() == undefined
) {
tlsOptions.unsafelyDisableHostnameVerification = true;
}
} catch { }
super({
handle: _wrapHandle(tlsOptions, socket),
...opts,
manualStart: true, });
if (socket) {
this.on("close", () => this._parent?.emit("close"));
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.encrypted = true;
this.authorized = false;
this.authorizationError = null;
this[kRes] = null;
this[kIsVerified] = false;
this[kPendingSession] = null;
this.ssl = new class {
verifyError() {
return null; }
}();
const tlssock = this;
function _wrapHandle(tlsOptions, socket) {
let handle;
let wrap;
if (socket) {
if (socket instanceof net.Socket && socket._handle) {
wrap = socket;
} else {
wrap = new JSStreamSocket(socket);
}
handle = wrap._handle;
}
const options = tlsOptions;
if (!handle) {
handle = options.pipe
? new Pipe(PipeConstants.SOCKET)
: new TCP(TCPConstants.SOCKET);
}
const { promise, resolve } = Promise.withResolvers();
handle.afterConnectTls = async function () {
options.hostname ??= undefined; if (tlssock._needsSockInitWorkaround) {
tlssock.emit("secure");
tlssock.removeListener("end", onConnectEnd);
return;
}
try {
const conn = await startTls(
wrap,
this,
options,
);
try {
const hs = await conn.handshake();
if (hs?.alpnProtocol) {
tlssock.alpnProtocol = hs.alpnProtocol;
} else {
tlssock.alpnProtocol = false;
}
} catch {
}
this[kStreamBaseField] = conn;
tlssock._tlsUpgraded = true;
this.upgrading = false;
if (!this.pauseOnCreate) {
this.readStart();
}
this.afterConnectTlsResolve?.();
delete this.afterConnectTlsResolve;
tlssock.emit("secure");
tlssock.removeListener("end", onConnectEnd);
} catch {
}
};
handle.afterConnectTlsResolve = resolve;
handle.upgrading = promise;
handle.verifyError = function () {
return null; };
handle._parent = handle;
handle._parentWrap = socket;
return handle;
}
}
_tlsError(err) {
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) {
}
setServername(_servername) {
}
setMaxSendFragment(_maxSendFragment) {
}
getPeerCertificate(detailed = false) {
const conn = this[kHandle]?.[kStreamBaseField];
if (conn) return conn[internals.getPeerCertificate](detailed);
}
getCipher() {
return "";
}
}
class JSStreamSocket {
#rid;
#channelRid;
#closed = false;
constructor(stream) {
this.stream = stream;
}
init(options) {
op_node_tls_start(options, tlsStreamRids);
this.#rid = tlsStreamRids[0];
this.#channelRid = tlsStreamRids[1];
const channelRid = this.#channelRid;
this.stream.on("data", (data) => {
core.write(channelRid, data);
});
const buf = new Uint8Array(1024 * 16);
(async () => {
while (true) {
try {
const readPromise = core.read(channelRid, buf);
core.unrefOpPromise(readPromise);
const nread = await readPromise;
this.stream.write(buf.slice(0, nread));
} catch {
break;
}
}
})();
this.stream.on("close", () => {
this.close();
});
}
close() {
if (this.#closed) return;
this.#closed = true;
if (this.#rid !== undefined) {
try {
core.close(this.#rid);
} catch {
}
this.#rid = undefined;
}
if (this.#channelRid !== undefined) {
try {
core.close(this.#channelRid);
} catch {
}
this.#channelRid = undefined;
}
}
handshake() {
return op_node_tls_handshake(this.#rid);
}
read(buf) {
const promise = core.read(this.#rid, buf);
core.unrefOpPromise(promise);
return promise;
}
write(data) {
return core.write(this.#rid, data);
}
}
function startTls(wrap, handle, options) {
if (wrap instanceof JSStreamSocket) {
options.caCerts ??= [];
wrap.init(options);
return wrap;
} else {
return startTlsInternal(handle[kStreamBaseField], options);
}
}
function normalizeConnectArgs(listArgs) {
const args = net._normalizeArgs(listArgs);
const options = args[0];
const cb = args[1];
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, listener) {
return new ServerImpl(options, listener);
}
export class ServerImpl extends EventEmitter {
listener;
#closed = false;
#unrefed = false;
constructor(options, listener) {
super();
this.options = options;
if (listener) {
this.on("secureConnection", listener);
}
}
unref() {
this.#unrefed = true;
if (this.listener) {
this.listener.unref();
}
}
ref() {
this.#unrefed = false;
if (this.listener) {
this.listener.ref();
}
}
listen(port, callback) {
const key = this.options.key?.toString();
const cert = this.options.cert?.toString();
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) {
if (this.#unrefed) {
listener.unref();
return;
}
while (!this.#closed) {
try {
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;
}
}
}
}
close(cb) {
if (this.listener) {
this.listener.close();
}
cb?.();
nextTick(() => {
this.emit("close");
});
return this;
}
address() {
const addr = this.listener.addr;
return {
port: addr.port,
address: addr.hostname,
};
}
}
Server.prototype = ServerImpl.prototype;
export function createServer(options, listener) {
return new ServerImpl(options, listener);
}
function onConnectSecure() {
this.authorized = true;
this.secureConnecting = false;
debug("client emit secureConnect. authorized:", this.authorized);
this.emit("secureConnect");
this.removeListener("end", onConnectEnd);
}
export function connect(...args) {
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, });
options.rejectUnauthorized = options.rejectUnauthorized !== false;
tlssock[kConnectOptions] = options;
if (cb) {
tlssock.once("secureConnect", cb);
}
if (!options.socket) {
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;
}
const jsonStringPattern =
/^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/;
function splitEscapedAltNames(altNames) {
const result = [];
let currentToken = "";
let offset = 0;
while (offset !== altNames.length) {
const nextSep = altNames.indexOf(",", offset);
const nextQuote = altNames.indexOf('"', offset);
if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) {
currentToken += altNames.substring(offset, nextQuote);
const match = jsonStringPattern.exec(altNames.substring(nextQuote));
if (!match) {
throw new ERR_TLS_CERT_ALTNAME_FORMAT();
}
currentToken += JSON.parse(match[0]);
offset = nextQuote + match[0].length;
} else if (nextSep !== -1) {
currentToken += altNames.substring(offset, nextSep);
result.push(currentToken);
currentToken = "";
offset = nextSep + 2;
} else {
currentToken += altNames.substring(offset);
offset = altNames.length;
}
}
result.push(currentToken);
return result;
}
function unfqdn(host) {
return StringPrototypeReplace(host, /[.]$/, "");
}
function toLowerCase(c) {
return String.fromCharCode(32 + c.charCodeAt(0));
}
function splitHost(host) {
return unfqdn(host).replace(/[A-Z]/g, toLowerCase).split(".");
}
function check(hostParts, pattern, wildcards) {
if (!pattern) {
return false;
}
const patternParts = splitHost(pattern);
if (hostParts.length !== patternParts.length) {
return false;
}
if (patternParts.includes("")) {
return false;
}
const isBad = (s) => /[^\u0021-\u007F]/u.test(s);
if (patternParts.some(isBad)) {
return false;
}
for (let i = hostParts.length - 1; i > 0; i -= 1) {
if (hostParts[i] !== patternParts[i]) {
return false;
}
}
const hostSubdomain = hostParts[0];
const patternSubdomain = patternParts[0];
const patternSubdomainParts = patternSubdomain.split("*", 3);
if (
patternSubdomainParts.length === 1 ||
patternSubdomain.includes("xn--")
) {
return hostSubdomain === patternSubdomain;
}
if (!wildcards) {
return false;
}
if (patternSubdomainParts.length > 2) {
return false;
}
if (patternParts.length <= 2) {
return false;
}
const { 0: prefix, 1: suffix } = patternSubdomainParts;
if (prefix.length + suffix.length > hostSubdomain.length) {
return false;
}
if (!hostSubdomain.startsWith(prefix)) {
return false;
}
if (!hostSubdomain.endsWith(suffix)) {
return false;
}
return true;
}
export function checkServerIdentity(hostname, cert) {
const subject = cert.subject;
const altNames = cert.subjectaltname;
const dnsNames = [];
const ips = [];
hostname = "" + hostname;
if (altNames) {
const splitAltNames = altNames.includes('"')
? splitEscapedAltNames(altNames)
: altNames.split(", ");
splitAltNames.forEach((name) => {
if (name.startsWith("DNS:")) {
dnsNames.push(name.slice(4));
} else if (name.startsWith("IP Address:")) {
ips.push(canonicalizeIP(name.slice(11)));
}
});
}
let valid = false;
let reason = "Unknown reason";
hostname = unfqdn(hostname);
if (net.isIP(hostname)) {
valid = ips.includes(canonicalizeIP(hostname));
if (!valid) {
reason = `IP: ${hostname} is not in the cert's list: ` + ips.join(", ");
}
} else if (dnsNames.length > 0 || subject?.CN) {
const hostParts = splitHost(hostname);
const wildcard = (pattern) => check(hostParts, pattern, true);
if (dnsNames.length > 0) {
valid = dnsNames.some(wildcard);
if (!valid) {
reason =
`Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
}
} else {
const cn = subject.CN;
if (ArrayIsArray(cn)) {
valid = cn.some(wildcard);
} else if (cn) {
valid = wildcard(cn);
}
if (!valid) {
reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
}
}
} else {
reason = "Cert does not contain a DNS name";
}
if (!valid) {
return new ERR_TLS_CERT_ALTNAME_INVALID(reason, hostname, cert);
}
}
export const DEFAULT_CIPHERS = [
"AES256-GCM-SHA384",
"AES128-GCM-SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"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,
};