mockttp-mvs
Version:
Mock HTTP server for testing HTTP clients and stubbing webservices
211 lines • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.shouldUseStrictHttps = exports.getContentLengthAfterModification = exports.getH2HeadersAfterModification = exports.OVERRIDABLE_REQUEST_PSEUDOHEADERS = exports.getHostAfterModification = exports.buildOverriddenBody = exports.UPSTREAM_TLS_OPTIONS = void 0;
const _ = require("lodash");
const url = require("url");
const common_tags_1 = require("common-tags");
const util_1 = require("../util/util");
const buffer_utils_1 = require("../util/buffer-utils");
const request_utils_1 = require("../util/request-utils");
// TLS settings for proxied connections, intended to avoid TLS fingerprint blocking
// issues so far as possible, by closely emulating a Firefox Client Hello:
const NEW_CURVES_SUPPORTED = Number(process.versions.node.split('.')[0]) >= 17;
const SSL_OP_TLSEXT_PADDING = 1 << 4;
const SSL_OP_NO_ENCRYPT_THEN_MAC = 1 << 19;
// All settings are designed to exactly match Firefox v103, since that's a good baseline
// that seems to be widely accepted and is easy to emulate from Node.js.
exports.UPSTREAM_TLS_OPTIONS = {
ecdhCurve: [
'X25519',
'prime256v1',
'secp384r1',
'secp521r1',
...(NEW_CURVES_SUPPORTED
? [
'ffdhe2048',
'ffdhe3072'
] : [])
].join(':'),
sigalgs: [
'ecdsa_secp256r1_sha256',
'ecdsa_secp384r1_sha384',
'ecdsa_secp521r1_sha512',
'rsa_pss_rsae_sha256',
'rsa_pss_rsae_sha384',
'rsa_pss_rsae_sha512',
'rsa_pkcs1_sha256',
'rsa_pkcs1_sha384',
'rsa_pkcs1_sha512',
'ECDSA+SHA1',
'rsa_pkcs1_sha1'
].join(':'),
ciphers: [
'TLS_AES_128_GCM_SHA256',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_AES_256_GCM_SHA384',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-SHA',
'ECDHE-ECDSA-AES128-SHA',
'ECDHE-RSA-AES128-SHA',
'ECDHE-RSA-AES256-SHA',
'AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'AES128-SHA',
'AES256-SHA'
].join(':'),
secureOptions: SSL_OP_TLSEXT_PADDING | SSL_OP_NO_ENCRYPT_THEN_MAC,
...{
// Valid, but not included in Node.js TLS module types:
requestOSCP: true
}
};
// --- Various helpers for deriving parts of request/response data given partial overrides: ---
/**
* Takes a callback result and some headers, and returns a ready to send body, using the headers
* (and potentially modifying them) to match the content type & encoding.
*/
async function buildOverriddenBody(callbackResult, headers) {
// Raw bodies are easy: use them as is.
if (callbackResult?.rawBody)
return callbackResult?.rawBody;
// In the json/body case, we need to get the body and transform it into a buffer
// for consistent handling later, and encode it to match the headers.
let replacementBody;
if (callbackResult?.json) {
headers['content-type'] = 'application/json';
replacementBody = JSON.stringify(callbackResult?.json);
}
else {
replacementBody = callbackResult?.body;
}
if (replacementBody === undefined)
return replacementBody;
let rawBuffer;
if ((0, request_utils_1.isMockttpBody)(replacementBody)) {
// It's our own bodyReader instance. That's not supposed to happen, but
// it's ok, we just need to use the buffer data instead of the whole object
rawBuffer = Buffer.from(replacementBody.buffer);
}
else if (replacementBody === '') {
// For empty bodies, it's slightly more convenient if they're truthy
rawBuffer = Buffer.alloc(0);
}
else {
rawBuffer = (0, buffer_utils_1.asBuffer)(replacementBody);
}
return await (0, request_utils_1.encodeBodyBuffer)(rawBuffer, headers);
}
exports.buildOverriddenBody = buildOverriddenBody;
/**
* If you override some headers, they have implications for the effective URL we send the
* request to. If you override that and the URL at the same time, it gets complicated.
*
* This method calculates the correct header value we should use: prioritising the header
* value you provide, printing a warning if it's contradictory, or return the URL-inferred
* value to override the header correctly if you didn't specify.
*/
function deriveUrlLinkedHeader(originalHeaders, replacementHeaders, headerName, expectedValue // The inferred 'correct' value from the URL
) {
const replacementValue = replacementHeaders?.[headerName];
if (replacementValue !== undefined) {
if (replacementValue !== expectedValue && replacementValue === originalHeaders[headerName]) {
// If you rewrite the URL-based header wrongly, by explicitly setting it to the
// existing value, we accept it but print a warning. This would be easy to
// do if you mutate the existing headers, for example, and ignore the host.
console.warn((0, common_tags_1.oneLine) `
Passthrough callback overrode the URL and the ${headerName} header
with mismatched values, which may be a mistake. The URL implies
${expectedValue}, whilst the header was set to ${replacementValue}.
`);
}
// Whatever happens, if you explicitly set a value, we use it.
return replacementValue;
}
// If you didn't override the header at all, then we automatically ensure
// the correct value is set automatically.
return expectedValue;
}
/**
* Autocorrect the host header only in the case that if you didn't explicitly
* override it yourself for some reason (e.g. if you're testing bad behaviour).
*/
function getHostAfterModification(reqUrl, originalHeaders, replacementHeaders) {
return deriveUrlLinkedHeader(originalHeaders, replacementHeaders, 'host', url.parse(reqUrl).host);
}
exports.getHostAfterModification = getHostAfterModification;
exports.OVERRIDABLE_REQUEST_PSEUDOHEADERS = [
':authority',
':scheme'
];
/**
* Automatically update the :scheme and :authority headers to match the updated URL,
* as long as they weren't explicitly overriden themselves, in which case let them
* be set to any invalid value you like (e.g. to send a request to one server but
* pretend it was sent to another).
*/
function getH2HeadersAfterModification(reqUrl, originalHeaders, replacementHeaders) {
const parsedUrl = url.parse(reqUrl);
return {
':scheme': deriveUrlLinkedHeader(originalHeaders, replacementHeaders, ':scheme', parsedUrl.protocol.slice(0, -1)),
':authority': deriveUrlLinkedHeader(originalHeaders, replacementHeaders, ':authority', parsedUrl.host)
};
}
exports.getH2HeadersAfterModification = getH2HeadersAfterModification;
// Helper to handle content-length nicely for you when rewriting requests with callbacks
function getContentLengthAfterModification(body, originalHeaders, replacementHeaders, mismatchAllowed = false) {
// If there was a content-length header, it might now be wrong, and it's annoying
// to need to set your own content-length override when you just want to change
// the body. To help out, if you override the body but don't explicitly override
// the (now invalid) content-length, then we fix it for you.
if (!_.has(originalHeaders, 'content-length')) {
// Nothing to override - use the replacement value, or undefined
return (replacementHeaders || {})['content-length'];
}
if (!replacementHeaders) {
// There was a length set, and you've provided a body but not changed it.
// You probably just want to send this body and have it work correctly,
// so we should fix the content length for you automatically.
return (0, util_1.byteLength)(body).toString();
}
// There was a content length before, and you're replacing the headers entirely
const lengthOverride = replacementHeaders['content-length']?.toString();
// If you're setting the content-length to the same as the origin headers, even
// though that's the wrong value, it *might* be that you're just extending the
// existing headers, and you're doing this by accident (we can't tell for sure).
// We use invalid content-length as instructed, but print a warning just in case.
if (lengthOverride === originalHeaders['content-length'] &&
lengthOverride !== (0, util_1.byteLength)(body).toString() &&
!mismatchAllowed // Set for HEAD responses
) {
console.warn((0, common_tags_1.oneLine) `
Passthrough modifications overrode the body and the content-length header
with mismatched values, which may be a mistake. The body contains
${(0, util_1.byteLength)(body)} bytes, whilst the header was set to ${lengthOverride}.
`);
}
return lengthOverride;
}
exports.getContentLengthAfterModification = getContentLengthAfterModification;
// Function to check if we should skip https errors for the current hostname and port,
// based on the given config
function shouldUseStrictHttps(hostname, port, ignoreHostHttpsErrors) {
let skipHttpsErrors = false;
if (ignoreHostHttpsErrors === true) {
// Ignore cert errors if `ignoreHostHttpsErrors` is set to true, or
skipHttpsErrors = true;
}
else if (Array.isArray(ignoreHostHttpsErrors) && (
// if the whole hostname or host+port is whitelisted
_.includes(ignoreHostHttpsErrors, hostname) ||
_.includes(ignoreHostHttpsErrors, `${hostname}:${port}`))) {
skipHttpsErrors = true;
}
return !skipHttpsErrors;
}
exports.shouldUseStrictHttps = shouldUseStrictHttps;
//# sourceMappingURL=passthrough-handling.js.map