UNPKG

@amadeus-it-group/kassette

Version:

Development server, used mainly for testing, which proxies requests and is able to easily manage local mocks.

1,391 lines (1,358 loc) 115 kB
'use strict'; var http = require('http'); var http2 = require('http2'); var net = require('net'); var tls = require('tls'); var picocolors = require('picocolors'); var dateFns = require('date-fns'); var nodePath = require('path'); var mimeTypes = require('mime-types'); var fs = require('fs'); var http2Wrapper = require('http2-wrapper'); var https = require('https'); var url = require('url'); var perf_hooks = require('perf_hooks'); var crypto = require('crypto'); var istextorbinary = require('istextorbinary'); var events = require('events'); var yaml = require('yaml'); var rechoir = require('rechoir'); var interpret = require('interpret'); var nodeForge = require('node-forge'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var nodePath__namespace = /*#__PURE__*/_interopNamespaceDefault(nodePath); var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto); var yaml__namespace = /*#__PURE__*/_interopNamespaceDefault(yaml); //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// /** For Node.js Stream-like objects, fetches all the data and returns it as a buffer */ async function readAll(message) { return new Promise((resolve) => { const chunks = []; message.on('data', (chunk) => chunks.push(chunk)); message.on('end', () => resolve(Buffer.concat(chunks))); }); } //////////////////////////////////////////////////////////////////////////////// // Models //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Complex transformations //////////////////////////////////////////////////////////////////////////////// const flatten = (input) => { if (!Array.isArray(input)) { return [input]; } return input.map(flatten).flat(); }; /** * Turns the `NonSanitizedArray` `array` into a `SanitizedArray` */ const sanitize = (array) => flatten(array).filter((value) => value != null); // -------------------------------------------------------------------- internal //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// /** Sanitizes the given array of `parts` (flattening and rejecting void values) and joins the resulting parts into a string */ const safeBuildString = (parts) => sanitize(parts).join(''); var _CONF$3 = { /** The width of the separator between text blocks in the console. */ separatorLength: 40, /** The date-time pattern for displaying timestamps for timestamped messages. */ timestampFormat: 'yyyy/MM/dd hh:mm:ssaaa xxx', }; // ------------------------------------------------------------------------- 3rd //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// let userConsole = null; function getConsole() { return userConsole != null ? userConsole : console; } function createGlobalLogger(console) { userConsole = console; } //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// /** * Logs a message and potentially — depending on the given input — * a timestamp, * some data to be highlighted, * a checked/unchecked mark, * an extra line. */ function logInfo({ message, timestamp, data, checked, extraLine }) { getConsole().log(safeBuildString([ !timestamp ? null : [getTimestamp(), ' - '], message, data == null ? null : [': ', picocolors.bold(picocolors.green(data))], checked == null ? null : [': ', picocolors.bold(checked ? picocolors.green('✓') : picocolors.red('✗'))], !extraLine ? null : '\n', ])); } /** * Logs an error message and possibly the given error. */ function logError({ message, exception }) { getConsole().error(picocolors.bold(picocolors.red(message))); if (exception != null) { getConsole().error([exception.stack, exception.message].filter((value) => value != null)[0]); } } const separator = { message: ['\n', '-'.repeat(_CONF$3.separatorLength), '\n'].join(''), }; /** * Logs a separator line, with extra lines before and after. */ const logSeparator = () => logInfo(separator); //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// const _isChunk = (value) => value != null && value.hasOwnProperty('text'); /** * Formats a string made of several parts. * * `chunks` is an array of either: * * - values to be converted to strings * - `StringChunk` chunks to be formatted and converted to strings */ function buildString(chunks) { return chunks .map((chunk) => { if (!_isChunk(chunk)) { return chunk; } let { text } = chunk; if (chunk.color != null) { text = picocolors[chunk.color](text); if (chunk.bright == null || chunk.bright) { text = picocolors.bold(text); } } return text; }) .join(''); } /** * Returns a timestamp string, which is the current date-time formatted. */ const getTimestamp = () => dateFns.format(new Date(), _CONF$3.timestampFormat); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; //////////////////////////////////////////////////////////////////////////////// // Caching property //////////////////////////////////////////////////////////////////////////////// /** * Stores the result of the first call to the getter and returns that result directly for subsequent calls * * Applies to: class getters */ const CachedProperty = () => (_target, _propertyKey, descriptor) => { const descriptorKey = descriptor.value != null ? 'value' : 'get'; const method = descriptor[descriptorKey]; const values = new Map(); descriptor[descriptorKey] = function (...args) { let value; if (values.has(this)) { value = values.get(this); } else { value = method.apply(this, args); values.set(this, value); } return value; }; }; /** Serializes given `value` into JSON using a 4 spaces indentation */ const stringifyPretty = (value) => JSON.stringify(value, null, 4); // ------------------------------------------------------------------------- std // -------------------------------------------------------------------- internal //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// /** splits the `url` path portion into an array of path parts */ const splitPath = (path) => path.split('/'); /** returns the array of path parts of the given Node.js `url` */ const getPathParts = ({ pathname }) => pathname === '/' ? [] : splitPath(pathname).slice(1); /** builds an URL from the given `URLSpec` object */ const build$1 = ({ protocol, hostname, port, pathname, search }) => safeBuildString([ [protocol, protocol.endsWith(':') ? null : ':'], '//', hostname, port == null ? null : [':', port], pathname == null ? null : [pathname.startsWith('/') ? null : '/', pathname], search == null || search === '' ? null : [search.startsWith('?') ? null : '?', search], ]); function sanitizePath(value) { const cleaned = sanitize(value); const leading = cleaned[0] === ''; const trailing = cleaned[cleaned.length - 1] === ''; const final = cleaned.filter((part) => part.length > 0); if (leading) { final.unshift(''); } if (trailing) { final.push(''); } return final; } function joinPath(value) { return sanitizePath(value).join('/'); } // ------------------------------------------------------------------------- std //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// /** tells if file system node at given `path` exists */ async function exists(path) { try { await fs.promises.access(path, fs.constants.W_OK | fs.constants.R_OK); return true; } catch (exception) { if (exception.code === 'ENOENT') { return false; } throw exception; } } /** creates the hierarchy of folders given by `path` */ async function ensurePath(path) { await fs.promises.mkdir(nodePath__namespace.parse(nodePath__namespace.resolve(path)).dir, { recursive: true }); } /** writes `content` into the file at given `path` */ async function writeFile(path, content) { await ensurePath(path); return fs.promises.writeFile(path, content); } //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// class FileHandler { constructor(spec) { if (typeof spec === 'string') { spec = { name: nodePath__namespace.basename(spec), root: nodePath__namespace.dirname(spec), }; } this._spec = spec; } /** the root of the file */ get _root() { return this._spec.root; } get name() { return this._spec.name; } get path() { return nodePath__namespace.join(this._root, this.name); } async exists() { return exists(this.path); } async read() { try { return await fs.promises.readFile(this.path); } catch (exception) { if (exception.code === 'ENOENT') { return null; } else { throw exception; } } } async write(content) { return writeFile(this.path, content != null ? content : Buffer.alloc(0)); } async stat() { try { const stat = await fs.promises.stat(this.path); return { mtime: stat.mtimeMs, size: stat.size, }; } catch (exception) { if (exception.code === 'ENOENT') { return null; } throw exception; } } } __decorate([ CachedProperty() ], FileHandler.prototype, "_root", null); __decorate([ CachedProperty() ], FileHandler.prototype, "name", null); __decorate([ CachedProperty() ], FileHandler.prototype, "path", null); //////////////////////////////////////////////////////////////////////////////// // Models //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Implementation //////////////////////////////////////////////////////////////////////////////// class UserProperty { constructor(_spec = {}) { this._spec = _spec; ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// this._userInputIsSet = false; this._inputCache = { value: undefined, invalid: true, }; this._outputCache = { value: undefined, invalid: true, }; } ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// set(value) { this._userInputIsSet = true; this._userInput = value; this.resetInputCache(); } unset() { this._userInputIsSet = false; this._userInput = undefined; this.resetInputCache(); } ////////////////////////////////////////////////////////////////////////////// // Input ////////////////////////////////////////////////////////////////////////////// get inputAndOrigin() { if (!this._inputCache.invalid) { return this._inputCache.value; } let input; let origin; if (this.userInputIsSet) { origin = 'user'; input = this.userInput; } else if (this._spec.getDefaultInput != null) { origin = 'default'; input = this._spec.getDefaultInput(); } else { origin = 'none'; } const value = { input, inputOrigin: origin }; this._inputCache.value = value; this._inputCache.invalid = false; return value; } get userInputIsSet() { return this._userInputIsSet; } get userInput() { return this._userInput; } get hasDefaultInput() { return this._spec.getDefaultInput != null; } get defaultInput() { if (this._spec.getDefaultInput != null) { return this._spec.getDefaultInput(); } } get input() { return this.inputAndOrigin.input; } get inputOrigin() { return this.inputAndOrigin.inputOrigin; } resetInputCache() { this._inputCache.invalid = true; this._inputCache.value = undefined; this.resetOutputCache(); } ////////////////////////////////////////////////////////////////////////////// // Output ////////////////////////////////////////////////////////////////////////////// get output() { if (!this._outputCache.invalid) { return this._outputCache.value; } let transform = this._spec.transform; if (transform == null) { transform = ({ input }) => input; } const value = transform(this.inputAndOrigin); this._outputCache.value = value; this._outputCache.invalid = false; return value; } resetOutputCache() { this._outputCache.invalid = true; this._outputCache.value = undefined; } } /** * Application custom errors. */ /** * The base type for all custom application errors; * stores the `original` error, if any (otherwise returns `null`). */ class AppError extends Error { constructor(original, type, message) { super(message); this.original = original; this.type = type; } } /** * Tells if the given value is an application error. */ const isAppError = (error) => error instanceof AppError; //////////////////////////////////////////////////////////////////////////////// // Specific //////////////////////////////////////////////////////////////////////////////// /** * @param original The original file access error * @param path The path of the configuration file */ class FileConfigurationError extends AppError { constructor(original, path) { super(original, 'file_configuration', `File configuration could not be loaded: ${path}`); this.path = path; this.name = 'FileConfigurationError'; } } class MissingRemoteURLError extends AppError { constructor() { super(null, 'missing_remote_url', 'Remote URL is not specified, cannot forward the request to the backend. Specify "*" to read the URL from the request (when using kassette as a browser proxy).'); this.name = 'MissingRemoteURLError'; } } // copied from https://github.com/nodejs/node/blob/db8ff56629e74e8c997947b8d3960db64c1ce4f9/lib/internal/http2/util.js#L113C1-L154 const singleValueHeaders = new Set([ ':status', ':method', ':authority', ':scheme', ':path', ':protocol', 'access-control-allow-credentials', 'access-control-max-age', 'access-control-request-method', 'age', 'authorization', 'content-encoding', 'content-language', 'content-length', 'content-location', 'content-md5', 'content-range', 'content-type', 'date', 'dnt', 'etag', 'expires', 'from', 'host', 'if-match', 'if-modified-since', 'if-none-match', 'if-range', 'if-unmodified-since', 'last-modified', 'location', 'max-forwards', 'proxy-authorization', 'range', 'referer', 'retry-after', 'tk', 'upgrade-insecure-requests', 'user-agent', 'x-content-type-options', ]); const caseMapCache = new WeakMap(); const getCaseMap = (headers) => { let res = caseMapCache.get(headers); if (!res) { res = new Map(); caseMapCache.set(headers, res); } return res; }; const proxyHandler = { get(target, property, receiver) { var _a; if (typeof property === 'string') { property = (_a = getCaseMap(target).get(property.toLowerCase())) !== null && _a !== void 0 ? _a : property; } return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { if (typeof property === 'string') { const caseMap = getCaseMap(target); const key = property.toLowerCase(); const previousCase = caseMap.get(key); if (previousCase == null || previousCase !== property) { caseMap.set(key, property); if (previousCase != null) { delete target[previousCase]; } } } return Reflect.set(target, property, value, receiver); }, deleteProperty(target, property) { if (typeof property === 'string') { const caseMap = getCaseMap(target); const key = property.toLowerCase(); const previousCase = caseMap.get(key); if (previousCase != null) { property = previousCase; caseMap.delete(key); } } return Reflect.deleteProperty(target, property); }, }; const headersContainer = () => new Proxy(Object.create(null), proxyHandler); const appendHeader = (headers, headerName, headerValue) => { let result = headerValue; const existingHeader = headers[headerName]; if (Array.isArray(existingHeader)) { existingHeader.push(headerValue); result = existingHeader; } else if (typeof existingHeader === 'string') { result = [existingHeader, headerValue]; } headers[headerName] = result; }; const processRawHeaders = (rawHeaders) => { const headers = headersContainer(); for (let i = 0, l = rawHeaders.length; i < l; i += 2) { appendHeader(headers, rawHeaders[i], rawHeaders[i + 1]); } return headers; }; // ------------------------------------------------------------------------- std //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// /** * Implementation of the wrapper around a fetched response. * * @param original The original response object (that can be retrieved through `original`) * @param buffer The body of the response (that can be retrieved through `body`) */ class ServerResponse { constructor(original, body) { this.original = original; this.body = body; } get headers() { return processRawHeaders(this.original.rawHeaders); } get status() { return { code: this.original.statusCode, message: this.original.statusMessage, }; } } __decorate([ CachedProperty() ], ServerResponse.prototype, "headers", null); __decorate([ CachedProperty() ], ServerResponse.prototype, "status", null); var _CONF$2 = { messages: { handlingRequest: 'Handling input request', listening: 'Listening on port', sendingRequest: 'Sending request to', requestComplete: 'Request complete', socketError: 'Socket error', }, }; // ------------------------------------------------------------------------- std //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// const timestamp = () => { let value; return { get value() { return value; }, get defined() { return value != null; }, trigger() { value = perf_hooks.performance.now(); }, }; }; const computeDiffFactory = (curTime) => (nextTime) => { if (nextTime.defined) { const diff = nextTime.value - curTime; curTime = nextTime.value; return diff; } return -1; }; const timingCollector = () => { const timeStart = timestamp(); const timeInit = timestamp(); const timeLookup = timestamp(); const timeConnect = timestamp(); const timeTlsConnect = timestamp(); const timeSendComplete = timestamp(); const timeReceiveResponse = timestamp(); const timeEnd = timestamp(); return { start: timeStart.trigger, socket(socket) { if (timeInit.defined) { // note: with HTTP/2, this method is called twice: // once for the underlying socket (for which we need the timings) // and the second time for the http/2 stream (that we can ignore) return; } timeInit.trigger(); if (socket.connecting) { socket.once('lookup', timeLookup.trigger); socket.once('connect', timeConnect.trigger); if (socket instanceof tls.TLSSocket) { socket.once('secureConnect', timeTlsConnect.trigger); } } }, finish: timeSendComplete.trigger, response: timeReceiveResponse.trigger, end: timeEnd.trigger, total() { return timeEnd.value - timeStart.value; }, timings() { const computeDiff = computeDiffFactory(timeStart.value); return { blocked: computeDiff(timeInit), dns: computeDiff(timeLookup), connect: computeDiff(timeTlsConnect.defined ? timeTlsConnect : timeConnect), send: computeDiff(timeSendComplete), wait: computeDiff(timeReceiveResponse), receive: computeDiff(timeEnd), ssl: timeTlsConnect.defined && timeConnect.defined ? timeTlsConnect.value - timeConnect.value : -1, }; }, }; }; const forceHttp1 = { ALPNProtocols: ['http/1.1'], resolveProtocol: async () => ({ alpnProtocol: 'http/1.1' }), }; /** Returns a server response with a fetched body, as well as timing information */ async function sendRequest({ baseUrl, original, skipLog, }) { var _a; const targetURL = new url.URL(baseUrl); const url$1 = build$1({ protocol: targetURL.protocol, hostname: targetURL.hostname, port: targetURL.port === '' ? null : targetURL.port, // XXX 2018-12-12T11:55:24+01:00 // could the base url provide a base path (to be prefixed or suffixed?)? // and a base set of query parameters to be merged (with which having precedence?)? pathname: original.url.pathname, search: original.url.search, }); if (!skipLog) { logInfo({ timestamp: true, message: _CONF$2.messages.sendingRequest, data: url$1 }); } const requestOptions = { url: url$1, method: original.method, body: original.body, headers: Object.assign(headersContainer(), original.headers, { 'Accept-Encoding': 'identity', }), }; delete requestOptions.headers.host; // remove any http/2.0-only header: delete requestOptions.headers[':method']; delete requestOptions.headers[':scheme']; delete requestOptions.headers[':authority']; delete requestOptions.headers[':path']; const timings = timingCollector(); timings.start(); const request = await http2Wrapper.auto(requestOptions.url, { // forces the use of http/1.x in case http/1.x is used in the original request: ...(((_a = original.original) === null || _a === void 0 ? void 0 : _a.httpVersionMajor) < 2 ? forceHttp1 : {}), rejectUnauthorized: false, secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, method: requestOptions.method, headers: requestOptions.headers, agent: { http: http.globalAgent, https: https.globalAgent, http2: Object.create(http2Wrapper.globalAgent, { createConnection: { async value(origin, options) { const res = await http2Wrapper.globalAgent.createConnection.call(this, origin, options); timings.socket(res); return res; }, }, }), }, }); const response = await new Promise((resolve, reject) => request .on('response', resolve) .on('error', reject) .on('finish', timings.finish) .on('socket', timings.socket) .end(requestOptions.body && requestOptions.body.length > 0 ? requestOptions.body : undefined)); timings.response(); const body = await readAll(response); timings.end(); if (!skipLog) { logInfo({ timestamp: true, message: _CONF$2.messages.requestComplete }); } return { response: new ServerResponse(response, body), time: timings.total(), timings: timings.timings(), requestOptions, }; } function isFilter(value) { return value.filter != null; } function isListing(value) { return value.keys != null; } //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// async function computeChecksum(mock, spec) { var _a, _b; const type = (_a = spec.type) !== null && _a !== void 0 ? _a : 'sha256'; const format = (_b = spec.format) !== null && _b !== void 0 ? _b : 'hex'; const content = await computeContent(mock, spec); const checksum = crypto__namespace.createHash(type).update(content).digest(format); return { checksum, content }; } function identity(value) { return value; } async function computeContent(mock, spec) { const parts = []; function push(header, data, includeWhenNull = true) { if (data != null || includeWhenNull) { parts.push(header, data, ''); } } push('protocol', await processSpec(spec.protocol, false, () => mock.request.protocol.toLowerCase()), false); push('hostname', await processSpec(spec.hostname, false, async (spec) => { var _a; const filter = (_a = spec.filter) !== null && _a !== void 0 ? _a : identity; return await filter(mock.request.hostname.toLowerCase()); }), false); push('port', await processSpec(spec.port, false, () => mock.request.port), false); push('method', await processSpec(spec.method, false, () => mock.request.method.toLowerCase())); push('pathname', await processSpec(spec.pathname, false, async (spec) => { var _a; const filter = (_a = spec.filter) !== null && _a !== void 0 ? _a : identity; return await filter(mock.request.pathname); })); push('body', await processSpec(spec.body, true, async (spec) => { var _a; const filter = (_a = spec.filter) !== null && _a !== void 0 ? _a : identity; return (await filter(mock.request.body)).toString(); })); push('query', await processSpec(spec.query, true, async (spec) => await processList(spec, mock.request.queryParameters, true))); push('headers', await processSpec(spec.headers, false, async (spec) => await processList(spec, mock.request.headers, false))); push('custom data', stringifyPretty(spec.customData)); return sanitize(parts).join('\n'); } async function processList(spec, input, defaultCaseSensitive) { var _a, _b, _c; let output; if (isFilter(spec)) { output = await spec.filter(input); } else { const caseSensitive = (_a = spec.caseSensitive) !== null && _a !== void 0 ? _a : defaultCaseSensitive; let properties; if (!isListing(spec)) { properties = Object.keys(input); } else { const mode = (_b = spec.mode) !== null && _b !== void 0 ? _b : 'whitelist'; const keys = (_c = spec.keys) !== null && _c !== void 0 ? _c : []; if (mode === 'whitelist') { properties = keys; } else { let disallowedKeys = keys; if (!caseSensitive) disallowedKeys = disallowedKeys.map((key) => key.toLowerCase()); properties = []; for (const key of Object.keys(input)) { let checkedKey = key; if (!caseSensitive) checkedKey = checkedKey.toLowerCase(); if (!disallowedKeys.includes(checkedKey)) properties.push(key); } } } output = {}; for (const key of properties) { let targetKey = key; if (!caseSensitive) targetKey = targetKey.toLowerCase(); output[targetKey] = input[key]; } } return stringifyPretty(output); } //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// async function processSpec(spec, includedByDefault, process) { const newSpec = normalizeSpec(spec, includedByDefault); if (!newSpec.include) return null; return await process(newSpec); } function normalizeSpec(spec, defaultValue) { if (spec === true) return { include: true }; if (spec === false) return { include: false }; if (spec == null) { if (defaultValue != null) return { include: defaultValue }; return { include: false }; } if (spec.include == null) return Object.assign({}, spec, { include: true }); return spec; } //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// var _CONF$1 = { // Files of a single mock /** The name of the file containing the serialized data representing the mock */ dataFilename: 'data.json', inputRequestBaseFilename: 'input-request', forwardedRequestBaseFilename: 'forwarded-request', checksumFilename: 'checksum', /** A static list of headers to ignore when serving the mock */ ignoredHeaders: new Set([ 'content-length', 'transfer-encoding', 'connection', 'keep-alive', ':status', ]), /** Default values for an empty mock payload */ emptyPayload: { data: { headers: {}, ignoredHeaders: {}, /** The status code we want the mock to have */ status: { code: 200, message: 'OK', }, time: 50, }, /** The content type we want the body file to have */ bodyContentType: 'application/json', body: {}, }, /** The set of messages displayed to the end user */ messages: { writingHarFile: 'Writing har file at', writingInputRequestData: 'Writing input request data file at', writingInputRequestBody: 'Writing input request body file at', writingForwardedRequestData: 'Writing forwarded request data file at', writingForwardedRequestBody: 'Writing forwarded request body file at', writingData: 'Writing data file at', writingBody: 'Writing body file at', writingChecksumFile: 'Writing checksum file at', servingMockDirectly: 'Serving local mock directly', inexistentMock: 'Local mock did not exist, creating empty payload', alreadyExistingMock: 'Mock exists already, serving it', fetchingMock: 'Mock does not exist yet, recording it and serving it', requestFailed: 'Request failed', }, }; const emptyHar = () => ({ log: { version: '1.2', creator: { name: 'kassette', /* The version is inserted here at build time by rollup: */ version: "1.7.2", }, entries: [], }, }); const rawHeadersToHarHeaders = (rawHeaders) => { const res = []; for (let i = 0, l = rawHeaders.length; i < l; i += 2) { res.push({ name: rawHeaders[i], value: rawHeaders[i + 1], }); } return res; }; const toHarHeaders = (headers) => { const res = []; if (headers) { for (const name of Object.keys(headers)) { const values = headers[name]; for (const value of Array.isArray(values) ? values : [values]) { if (value != null) { res.push({ name, value: `${value}`, }); } } } } return res; }; const fromHarHeaders = (harHeaders) => { const headers = headersContainer(); for (const header of harHeaders !== null && harHeaders !== void 0 ? harHeaders : []) { appendHeader(headers, header.name, header.value); } return headers; }; const toHarHttpVersion = (nodeHttpVersion) => `HTTP/${nodeHttpVersion !== null && nodeHttpVersion !== void 0 ? nodeHttpVersion : '1.1'}`; const fromHarHttpVersion = (harHttpVersion) => { var _a; return (_a = harHttpVersion === null || harHttpVersion === void 0 ? void 0 : harHttpVersion.replace(/^HTTP\//i, '')) !== null && _a !== void 0 ? _a : '1.1'; }; const toHarContentBase64 = (body, mimeType) => ({ mimeType: mimeType !== null && mimeType !== void 0 ? mimeType : '', size: body.length, text: body.toString('base64'), encoding: 'base64', }); const toHarContent = (body, mimeType, parseMimeTypesAsJson = []) => { var _a, _b; if (Buffer.isBuffer(body)) { if (istextorbinary.isBinary(mimeType ? `file.${mimeTypes.extension(mimeType)}` : null, body)) { return toHarContentBase64(body, mimeType); } return { ...checkMimeTypeListAndParseBody(parseMimeTypesAsJson, body, mimeType), size: (_a = body === null || body === void 0 ? void 0 : body.length) !== null && _a !== void 0 ? _a : 0, }; } return { mimeType: mimeType !== null && mimeType !== void 0 ? mimeType : '', size: (_b = body === null || body === void 0 ? void 0 : body.length) !== null && _b !== void 0 ? _b : 0, text: body !== null && body !== void 0 ? body : '', }; }; const fromHarContent = (content) => { if ((content === null || content === void 0 ? void 0 : content.text) !== undefined) { return Buffer.from(content.text, content.encoding === 'base64' ? 'base64' : 'binary'); } if ((content === null || content === void 0 ? void 0 : content.json) !== undefined) { return Buffer.from(stringifyPretty(content.json), 'utf8'); } return Buffer.alloc(0); }; const checkMimeTypeListAndParseBody = (parseMimeTypesAsJson, body, mimeType) => { if ((mimeType && parseMimeTypesAsJson.includes(mimeType)) || (!mimeType && parseMimeTypesAsJson.includes(''))) { try { return { mimeType, json: JSON.parse(body.toString('utf-8')), }; } catch (error) { } } return { mimeType: mimeType !== null && mimeType !== void 0 ? mimeType : '', text: body.toString('binary'), }; }; const toHarPostData = (body, mimeType, parseMimeTypesAsJson = []) => { if (body && body.length > 0) { return checkMimeTypeListAndParseBody(parseMimeTypesAsJson, body, mimeType); } return undefined; }; const toHarQueryString = (searchParams) => { const res = []; for (const [name, value] of searchParams) { res.push({ name, value }); } return res; }; class StructuredFile extends events.EventEmitter { constructor(_fileHandler, _fileFormat) { super(); this._fileHandler = _fileHandler; this._fileFormat = _fileFormat; this._modified = false; this._readingPromise = null; this._writingPromise = null; this._nextWritingPromise = null; this._lastReadModifiedTime = 0; this._lastReadSize = 0; this._busyState = false; } _markBusy() { if (!this._busyState) { this._busyState = true; this.emit('busy'); } } async _markNonBusy() { await Promise.resolve(); const busy = !!(this._modified || this._readingPromise || this._writingPromise); if (!busy && this._busyState) { this._busyState = false; this.emit('nonBusy'); } } _markModified() { this._modified = true; this._markBusy(); } _afterRead() { } async _doRead() { var _a; try { this._markBusy(); const stat = await this._fileHandler.stat(); if (stat) { if (this._lastReadModifiedTime !== stat.mtime || this._lastReadSize !== stat.size) { const content = await this._fileHandler.read(); this._lastReadModifiedTime = stat.mtime; this._lastReadSize = (_a = content === null || content === void 0 ? void 0 : content.length) !== null && _a !== void 0 ? _a : 0; this._fileContent = content ? this._fileFormat.parse(content) : null; this._afterRead(); } } else { this._fileContent = null; this._lastReadSize = 0; this._lastReadModifiedTime = 0; this._afterRead(); } } finally { this._readingPromise = null; this._markNonBusy(); } } async _read() { if (this._writingPromise || this._modified) { return; } if (!this._readingPromise) { this._readingPromise = this._doRead(); } return this._readingPromise; } async _doWrite() { var _a; try { this._markBusy(); this._nextWritingPromise = null; const content = this._fileFormat.stringify(this._fileContent); this._modified = false; await this._fileHandler.write(content); // after the file is written, let's read back the modification time // unfortunately, there could be a race condition here if the file is modified by another process between the previous write // and the following stat if the size does not change const newStat = await this._fileHandler.stat(); this._lastReadSize = content.length; this._lastReadModifiedTime = (_a = newStat === null || newStat === void 0 ? void 0 : newStat.mtime) !== null && _a !== void 0 ? _a : 0; } finally { this._writingPromise = null; this._markNonBusy(); } } async _waitAndWriteAgain() { try { await this._writingPromise; } catch { /* ignore previous error */ } this._writingPromise = this._doWrite(); return this._writingPromise; } async _write() { if (this._writingPromise) { if (!this._nextWritingPromise) { this._nextWritingPromise = this._waitAndWriteAgain(); } return this._nextWritingPromise; } this._writingPromise = this._doWrite(); return this._writingPromise; } get path() { return this._fileHandler.path; } } const harFileMap = new Map(); const createHarFile = (path, keepDelay, format) => { let timeout = null; const harFile = new HarFile(new FileHandler(path), format); const removeFromMap = () => { const item = harFileMap.get(path); if (item === harFile) { harFileMap.delete(path); } }; const cancelTimeout = () => { if (timeout) { clearTimeout(timeout); timeout = null; } }; const planTimeout = () => { cancelTimeout(); timeout = setTimeout(removeFromMap, keepDelay); timeout.unref(); }; harFile.addListener('busy', cancelTimeout); harFile.addListener('nonBusy', planTimeout); planTimeout(); harFileMap.set(path, harFile); return harFile; }; const getHarFile = (path, keepDelay, format) => { var _a; return (_a = harFileMap.get(path)) !== null && _a !== void 0 ? _a : createHarFile(path, keepDelay, format); }; const defaultHarKeyManager = (entry, key) => { var _a, _b, _c; const defaultKey = joinPath((_a = entry._kassetteMockKey) !== null && _a !== void 0 ? _a : [(_b = entry.request) === null || _b === void 0 ? void 0 : _b.method, (_c = entry.request) === null || _c === void 0 ? void 0 : _c.url]); if (key && key !== defaultKey) { entry._kassetteMockKey = key; return key; } return defaultKey; }; const callKeyManager = (keyManager, entry, key) => { const nonSanitized = keyManager(entry, key); if (nonSanitized == null) { return; } const res = joinPath(nonSanitized); return res; }; class HarFile extends StructuredFile { constructor() { super(...arguments); this._keysMaps = new WeakMap(); } _afterRead() { this._keysMaps = new WeakMap(); } _getKeys(keyManager) { var _a, _b; let res = this._keysMaps.get(keyManager); if (!res) { res = new Map(); this._keysMaps.set(keyManager, res); const entries = (_b = (_a = this._fileContent) === null || _a === void 0 ? void 0 : _a.log.entries) !== null && _b !== void 0 ? _b : []; for (let i = 0, l = entries.length; i < l; i++) { const key = callKeyManager(keyManager, entries[i]); if (key && !res.has(key)) { res.set(key, i); } } } return res; } async getEntry(key, keyManager) { var _a, _b; if (key) { await this._read(); const keys = this._getKeys(keyManager); const entryIndex = (_a = keys.get(key)) !== null && _a !== void 0 ? _a : -1; if (entryIndex > -1) { return (_b = this._fileContent) === null || _b === void 0 ? void 0 : _b.log.entries[entryIndex]; } } } async setEntry(key, entry, keyManager) { var _a; key = callKeyManager(keyManager, entry, key); await this._read(); let content = this._fileContent; if (!content) { content = emptyHar(); this._fileContent = content; } const keys = this._getKeys(keyManager); let entryIndex = key == null ? -1 : ((_a = keys.get(key)) !== null && _a !== void 0 ? _a : -1); if (entryIndex === -1) { entryIndex = content.log.entries.length; if (key) { keys.set(key, entryIndex); } } content.log.entries[entryIndex] = entry; // remove other keys maps as they are outdated with the new entry: this._keysMaps = new WeakMap(); this._keysMaps.set(keyManager, keys); this._markModified(); await this._write(); } } const jsonFormat = { stringify(content) { return Buffer.from(stringifyPretty(content), 'utf8'); }, parse(content) { return JSON.parse(content.toString('utf8')); }, }; const yamlFormat = { stringify(content) { return Buffer.from(yaml__namespace.stringify(content, { blockQuote: 'literal' }), 'utf8'); }, parse(content) { return yaml__namespace.parse(content.toString('utf8')); }, }; const yamlRegExp = /\.ya?ml$/i; const detectHarFormat = (fileName) => yamlRegExp.test(fileName) ? yamlFormat : jsonFormat; // ------------------------------------------------------------------------- std //////////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////// const isRemotePayload = (payload) => payload.origin === 'remote' && 'requestOptions' in payload; const splitFromHarHeaders = (headers) => { const mainHeaders = []; const ignoredHeaders = []; for (const header of headers !== null && headers !== void 0 ? headers : []) { (_CONF$1.ignoredHeaders.has(header.name.toLowerCase()) ? ignoredHeaders : mainHeaders).push(header); } return { headers: fromHarHeaders(mainHeaders), ignoredHeaders: fromHarHeaders(ignoredHeaders) }; }; class Mock { get _localPayload() { return this.__localPayload; } set _localPayload(payload) { this.__localPayload = payload; this._delay.resetOutputCache(); } constructor(_spec) { this._spec = _spec; ////////////////////////////////////////////////////////////////////////////// // Properties & constructor ////////////////////////////////////////////////////////////////////////////// this._localPath = new UserProperty({ transform: ({ inputOrigin, input }) => inputOrigin === 'none' ? this.defaultLocalPath : joinPath(input), }); this._delay = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.delay.value, transform: ({ input }) => { if (input === 'recorded' && this._localPayload != null) { return this._localPayload.payload.data.time; } if (input == null || input === 'recorded') { return _CONF$1.emptyPayload.data.time; } return input; }, }); this._skipLog = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.skipLog.value, }); this._mode = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.mode.value, }); this._mocksFolder = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.mocksFolder.value, transform: ({ input }) => { const folder = joinPath(input); return joinPath([nodePath__namespace.isAbsolute(folder) ? null : this.options.root, folder]); }, }); this._mocksHarFile = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.mocksHarFile.value, transform: ({ input }) => { const file = joinPath(input); return joinPath([nodePath__namespace.isAbsolute(file) ? null : this.options.root, file]); }, }); this._mocksHarKeyManager = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.mocksHarKeyManager.value, }); this._harMimeTypesParseJson = new UserProperty({ getDefaultInput: () => this.options.userConfiguration.harMimeTypesParseJson.value, }); this._mockHarKey = new UserProperty({ transform: ({ inputOrigin, input }) => inputOrigin === 'none' ? this.defaultMockHarKey : joinPath(input), }); this._mocksFormat = new UserProperty