nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
335 lines (286 loc) • 11.6 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/tls.js
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import * as internalUtil from "nstdlib/lib/internal/util";
import {
isArrayBufferView,
isUint8Array,
} from "nstdlib/lib/internal/util/types";
import * as net from "nstdlib/lib/net";
import { getOptionValue } from "nstdlib/lib/internal/options";
import {
getRootCertificates,
getSSLCiphers,
} from "nstdlib/stub/binding/crypto";
import { Buffer } from "nstdlib/lib/buffer";
import { canonicalizeIP } from "nstdlib/stub/binding/cares_wrap";
import * as _tls_common from "nstdlib/lib/_tls_common";
import * as _tls_wrap from "nstdlib/lib/_tls_wrap";
import { createSecurePair } from "nstdlib/lib/internal/tls/secure-pair";
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
const {
ERR_TLS_CERT_ALTNAME_FORMAT,
ERR_TLS_CERT_ALTNAME_INVALID,
ERR_OUT_OF_RANGE,
} = __codes__;
internalUtil.assertCrypto();
// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations
// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more
// renegotiations are seen. The settings are applied to all remote client
// connections.
const _export_CLIENT_RENEG_LIMIT_ = 3;
export { _export_CLIENT_RENEG_LIMIT_ as CLIENT_RENEG_LIMIT };
const _export_CLIENT_RENEG_WINDOW_ = 600;
export { _export_CLIENT_RENEG_WINDOW_ as CLIENT_RENEG_WINDOW };
const _export_DEFAULT_CIPHERS_ = getOptionValue("--tls-cipher-list");
export { _export_DEFAULT_CIPHERS_ as DEFAULT_CIPHERS };
const _export_DEFAULT_ECDH_CURVE_ = "auto";
export { _export_DEFAULT_ECDH_CURVE_ as DEFAULT_ECDH_CURVE };
if (getOptionValue("--tls-min-v1.0")) exports.DEFAULT_MIN_VERSION = "TLSv1";
else if (getOptionValue("--tls-min-v1.1"))
exports.DEFAULT_MIN_VERSION = "TLSv1.1";
else if (getOptionValue("--tls-min-v1.2"))
exports.DEFAULT_MIN_VERSION = "TLSv1.2";
else if (getOptionValue("--tls-min-v1.3"))
exports.DEFAULT_MIN_VERSION = "TLSv1.3";
else exports.DEFAULT_MIN_VERSION = "TLSv1.2";
if (getOptionValue("--tls-max-v1.3")) exports.DEFAULT_MAX_VERSION = "TLSv1.3";
else if (getOptionValue("--tls-max-v1.2"))
exports.DEFAULT_MAX_VERSION = "TLSv1.2";
else exports.DEFAULT_MAX_VERSION = "TLSv1.3"; // Will depend on node version.
const _export_getCiphers_ = internalUtil.cachedResult(() =>
internalUtil.filterDuplicateStrings(getSSLCiphers(), true),
);
export { _export_getCiphers_ as getCiphers };
let rootCertificates;
function cacheRootCertificates() {
rootCertificates = Object.freeze(getRootCertificates());
}
Object.defineProperty(exports, "rootCertificates", {
__proto__: null,
configurable: false,
enumerable: true,
get: () => {
// Out-of-line caching to promote inlining the getter.
if (!rootCertificates) cacheRootCertificates();
return rootCertificates;
},
});
// Convert protocols array into valid OpenSSL protocols list
// ("\x06spdy/2\x08http/1.1\x08http/1.0")
function convertProtocols(protocols) {
const lens = new Array(protocols.length);
const buff = Buffer.allocUnsafe(
protocols.reduce((p, c, i) => {
const len = Buffer.byteLength(c);
if (len > 255) {
throw new ERR_OUT_OF_RANGE(
"The byte length of the protocol at index " +
`${i} exceeds the maximum length.`,
"<= 255",
len,
true,
);
}
lens[i] = len;
return p + 1 + len;
}, 0),
);
let offset = 0;
for (let i = 0, c = protocols.length; i < c; i++) {
buff[offset++] = lens[i];
buff.write(protocols[i], offset);
offset += lens[i];
}
return buff;
}
const _export_convertALPNProtocols_ = function convertALPNProtocols(
protocols,
out,
) {
// If protocols is Array - translate it into buffer
if (Array.isArray(protocols)) {
out.ALPNProtocols = convertProtocols(protocols);
} else if (isUint8Array(protocols)) {
// Copy new buffer not to be modified by user.
out.ALPNProtocols = Buffer.from(protocols);
} else if (isArrayBufferView(protocols)) {
out.ALPNProtocols = Buffer.from(
protocols.buffer.slice(
protocols.byteOffset,
protocols.byteOffset + protocols.byteLength,
),
);
}
};
export { _export_convertALPNProtocols_ as convertALPNProtocols };
function unfqdn(host) {
return host.replace(/[.]$/, "");
}
// String#toLowerCase() is locale-sensitive so we use
// a conservative version that only lowercases A-Z.
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) {
// Empty strings, null, undefined, etc. never match.
if (!pattern) return false;
const patternParts = splitHost(pattern);
if (hostParts.length !== patternParts.length) return false;
// Pattern has empty components, e.g. "bad..example.com".
if (patternParts.includes("")) return false;
// RFC 6125 allows IDNA U-labels (Unicode) in names but we have no
// good way to detect their encoding or normalize them so we simply
// reject them. Control characters and blanks are rejected as well
// because nothing good can come from accepting them.
const isBad = (s) => /[^\u0021-\u007F]/u.test(s);
if (patternParts.some(isBad)) return false;
// Check host parts from right to left first.
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("*");
// Short-circuit when the subdomain does not contain a wildcard.
// RFC 6125 does not allow wildcard substitution for components
// containing IDNA A-labels (Punycode) so match those verbatim.
if (patternSubdomainParts.length === 1 || patternSubdomain.includes("xn--"))
return hostSubdomain === patternSubdomain;
if (!wildcards) return false;
// More than one wildcard is always wrong.
if (patternSubdomainParts.length > 2) return false;
// *.tld wildcards are not allowed.
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;
}
// This pattern is used to determine the length of escaped sequences within
// the subject alt names string. It allows any valid JSON string literal.
// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly.
const jsonStringPattern =
// eslint-disable-next-line no-control-regex
/^"(?:[^"\\\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)) {
// There is a quote character and there is no separator before the quote.
currentToken += altNames.substring(offset, nextQuote);
const match = jsonStringPattern.exec(altNames.substring(nextQuote));
if (!match) {
throw new ERR_TLS_CERT_ALTNAME_FORMAT();
}
currentToken += JSONParse(match[0]);
offset = nextQuote + match[0].length;
} else if (nextSep !== -1) {
// There is a separator and no quote before it.
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;
}
const _export_checkServerIdentity_ = 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); // Remove trailing dot for error messages.
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 {
// Match against Common Name only if no supported identifiers exist.
const cn = subject.CN;
if (Array.isArray(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 { _export_checkServerIdentity_ as checkServerIdentity };
const _export_createSecureContext_ = _tls_common.createSecureContext;
export { _export_createSecureContext_ as createSecureContext };
const _export_SecureContext_ = _tls_common.SecureContext;
export { _export_SecureContext_ as SecureContext };
const _export_TLSSocket_ = _tls_wrap.TLSSocket;
export { _export_TLSSocket_ as TLSSocket };
const _export_Server_ = _tls_wrap.Server;
export { _export_Server_ as Server };
const _export_createServer_ = _tls_wrap.createServer;
export { _export_createServer_ as createServer };
const _export_connect_ = _tls_wrap.connect;
export { _export_connect_ as connect };
const _export_createSecurePair_ = internalUtil.deprecate(
createSecurePair,
"tls.createSecurePair() is deprecated. Please use " +
"tls.TLSSocket instead.",
"DEP0064",
);
export { _export_createSecurePair_ as createSecurePair };