UNPKG

mockttp-mvs

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

211 lines 10.2 kB
"use strict"; 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