mockttp
Advanced tools
Comparing version 1.0.3 to 1.0.4
@@ -90,2 +90,6 @@ // There's a few places where we attach extra data to some node objects during | ||
} | ||
// A constant symbol used in-band in HTTP/2 header objects to list the headers | ||
// that shouldn't/weren't automatically compressed. Only defined in Node 15+. | ||
export const sensitiveHeaders: Symbol | undefined; | ||
} |
@@ -18,2 +18,3 @@ "use strict"; | ||
const http = require("http"); | ||
const http2 = require("http2"); | ||
const https = require("https"); | ||
@@ -352,3 +353,3 @@ const h2Client = require("http2-wrapper"); | ||
const invalidHeaders = _(modifiedHeaders) | ||
.pickBy((value, name) => name.startsWith(':') && | ||
.pickBy((value, name) => name.toString().startsWith(':') && | ||
// We allow returning a preexisting header value - that's ignored | ||
@@ -450,5 +451,3 @@ // silently, so that mutating & returning the provided headers is always safe. | ||
const completedRequest = yield request_utils_1.waitForCompletedRequest(clientReq); | ||
const modifiedReq = yield this.beforeRequest(Object.assign(completedRequest, { | ||
headers: _.clone(completedRequest.headers) // Clone headers so we can ignore mutations | ||
})); | ||
const modifiedReq = yield this.beforeRequest(Object.assign(Object.assign({}, completedRequest), { headers: _.clone(completedRequest.headers) })); | ||
if (modifiedReq.response) { | ||
@@ -466,3 +465,3 @@ // The callback has provided a full response: don't passthrough at all, just use it. | ||
headersManuallyModified = !!modifiedReq.headers; | ||
validateCustomHeaders(clientReq.headers, modifiedReq.headers, OVERRIDABLE_REQUEST_PSEUDOHEADERS // These are handled by getCorrectPseudoheaders above | ||
validateCustomHeaders(completedRequest.headers, modifiedReq.headers, OVERRIDABLE_REQUEST_PSEUDOHEADERS // These are handled by getCorrectPseudoheaders above | ||
); | ||
@@ -529,3 +528,3 @@ if (modifiedReq.json) { | ||
// We drop all incoming pseudoheaders, and regenerate them (except legally modified ones) | ||
headers = _.pickBy(headers, (value, key) => !key.startsWith(':') || | ||
headers = _.pickBy(headers, (value, key) => !key.toString().startsWith(':') || | ||
(headersManuallyModified && | ||
@@ -558,2 +557,3 @@ OVERRIDABLE_REQUEST_PSEUDOHEADERS.includes(key))); | ||
body = yield request_utils_1.streamToBuffer(serverRes); | ||
const cleanHeaders = request_utils_1.cleanUpHeaders(serverHeaders); | ||
modifiedRes = yield this.beforeResponse({ | ||
@@ -563,6 +563,6 @@ id: clientReq.id, | ||
statusMessage: serverRes.statusMessage, | ||
headers: _.clone(serverHeaders), | ||
headers: _.clone(cleanHeaders), | ||
body: request_utils_1.buildBodyReader(body, serverHeaders) | ||
}); | ||
validateCustomHeaders(serverHeaders, modifiedRes.headers); | ||
validateCustomHeaders(cleanHeaders, modifiedRes.headers); | ||
serverStatusCode = modifiedRes.statusCode || | ||
@@ -595,6 +595,7 @@ modifiedRes.status || | ||
const headerValue = serverHeaders[header]; | ||
if (headerValue === undefined) | ||
if (headerValue === undefined || | ||
header === http2.sensitiveHeaders || | ||
header === ':status' // H2 status gets set by writeHead below | ||
) | ||
return; | ||
if (header === ':status') | ||
return; // H2 status gets set by writeHead below | ||
try { | ||
@@ -601,0 +602,0 @@ clientRes.setHeader(header, headerValue); |
@@ -90,4 +90,5 @@ "use strict"; | ||
const host = req.headers[':authority'] || req.headers['host']; | ||
const absoluteUrl = `${req.protocol}://${host}${req.path}`; | ||
if (!req.headers[':path']) { | ||
req.url = new url.URL(req.url, `${req.protocol}://${host}`).toString(); | ||
req.url = new url.URL(absoluteUrl).toString(); | ||
} | ||
@@ -99,3 +100,3 @@ else { | ||
Object.defineProperty(req, 'url', { | ||
value: new url.URL(req.url, `${req.protocol}://${host}`).toString() | ||
value: new url.URL(absoluteUrl).toString() | ||
}); | ||
@@ -102,0 +103,0 @@ } |
@@ -31,4 +31,22 @@ /** | ||
}) => (req: http.IncomingMessage, _res: http.ServerResponse, next: () => void) => void; | ||
/** | ||
* Translate from internal header representations (basically Node's header representations) to a | ||
* mildly more consistent & simplified model that we expose externally: numbers as strings, and | ||
* no sensitiveHeaders symbol for HTTP/2. | ||
*/ | ||
export declare function cleanUpHeaders(headers: Headers): {}; | ||
/** | ||
* Build an initiated request: the external representation of a request | ||
* that's just started. | ||
*/ | ||
export declare function buildInitiatedRequest(request: OngoingRequest): InitiatedRequest; | ||
/** | ||
* Build an aborted request: the external representation of a request | ||
* that's been aborted. | ||
*/ | ||
export declare function buildAbortedRequest(request: OngoingRequest): InitiatedRequest; | ||
/** | ||
* Build a completed request: the external representation of a request | ||
* that's been completely received (but not necessarily replied to). | ||
*/ | ||
export declare function waitForCompletedRequest(request: OngoingRequest): Promise<CompletedRequest>; | ||
@@ -38,2 +56,6 @@ export declare function trackResponse(response: http.ServerResponse, timingEvents: TimingEvents, tags: string[], options: { | ||
}): OngoingResponse; | ||
/** | ||
* Build a completed response: the external representation of a response | ||
* that's been completely written out and sent back to the client. | ||
*/ | ||
export declare function waitForCompletedResponse(response: OngoingResponse): Promise<CompletedResponse>; | ||
@@ -40,0 +62,0 @@ export declare function tryToParseHttp(input: Buffer, socket: net.Socket): PartiallyParsedHttpRequest; |
@@ -17,2 +17,3 @@ "use strict"; | ||
const tls_1 = require("tls"); | ||
const http2 = require("http2"); | ||
const stream = require("stream"); | ||
@@ -77,3 +78,3 @@ const querystring = require("querystring"); | ||
const h1Headers = _.omitBy(h2Headers, (_value, key) => { | ||
return key.startsWith(':'); | ||
return key === http2.sensitiveHeaders || key.toString().startsWith(':'); | ||
}); | ||
@@ -264,9 +265,27 @@ if (!h1Headers['host'] && h2Headers[':authority']) { | ||
}; | ||
/** | ||
* Translate from internal header representations (basically Node's header representations) to a | ||
* mildly more consistent & simplified model that we expose externally: numbers as strings, and | ||
* no sensitiveHeaders symbol for HTTP/2. | ||
*/ | ||
function cleanUpHeaders(headers) { | ||
return _.mapValues(_.omit(headers, ...(http2.sensitiveHeaders ? [http2.sensitiveHeaders] : [])), (headerValue) => _.isNumber(headerValue) ? headerValue.toString() : headerValue); | ||
} | ||
exports.cleanUpHeaders = cleanUpHeaders; | ||
/** | ||
* Build an initiated request: the external representation of a request | ||
* that's just started. | ||
*/ | ||
function buildInitiatedRequest(request) { | ||
return Object.assign(Object.assign({}, _.pick(request, 'id', 'matchedRuleId', 'protocol', 'httpVersion', 'method', 'url', 'path', 'hostname', 'headers', 'tags')), { timingEvents: request.timingEvents }); | ||
return Object.assign(Object.assign({}, _.pick(request, 'id', 'matchedRuleId', 'protocol', 'httpVersion', 'method', 'url', 'path', 'hostname', 'headers', 'tags')), { headers: cleanUpHeaders(request.headers), timingEvents: request.timingEvents }); | ||
} | ||
exports.buildInitiatedRequest = buildInitiatedRequest; | ||
/** | ||
* Build an aborted request: the external representation of a request | ||
* that's been aborted. | ||
*/ | ||
function buildAbortedRequest(request) { | ||
const requestData = buildInitiatedRequest(request); | ||
return Object.assign(requestData, { | ||
headers: cleanUpHeaders(request.headers), | ||
// Exists for backward compat: really Abort events should have no body at all | ||
@@ -277,2 +296,6 @@ body: exports.buildBodyReader(Buffer.alloc(0), {}) | ||
exports.buildAbortedRequest = buildAbortedRequest; | ||
/** | ||
* Build a completed request: the external representation of a request | ||
* that's been completely received (but not necessarily replied to). | ||
*/ | ||
function waitForCompletedRequest(request) { | ||
@@ -283,3 +306,3 @@ return __awaiter(this, void 0, void 0, function* () { | ||
const requestData = buildInitiatedRequest(request); | ||
return Object.assign(requestData, { body }); | ||
return Object.assign(requestData, { body, headers: cleanUpHeaders(request.headers) }); | ||
}); | ||
@@ -334,2 +357,6 @@ } | ||
exports.trackResponse = trackResponse; | ||
/** | ||
* Build a completed response: the external representation of a response | ||
* that's been completely written out and sent back to the client. | ||
*/ | ||
function waitForCompletedResponse(response) { | ||
@@ -346,7 +373,3 @@ return __awaiter(this, void 0, void 0, function* () { | ||
]).assign({ | ||
headers: _.mapValues(response.getHeaders(), (headerValue) => { | ||
return _.isNumber(headerValue) | ||
? headerValue.toString() // HTTP :status sneaks in as a number | ||
: headerValue; | ||
}), | ||
headers: cleanUpHeaders(response.getHeaders()), | ||
body: body | ||
@@ -353,0 +376,0 @@ }).valueOf(); |
{ | ||
"name": "mockttp", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"description": "Mock HTTP server for testing HTTP clients and stubbing webservices", | ||
@@ -31,3 +31,4 @@ "main": "dist/main.js", | ||
"build:src": "tsc && cp src/standalone/schema.gql dist/standalone/schema.gql && chmod +x ./dist/standalone/standalone-bin.js", | ||
"build:doc": "typedoc --ignoreCompilerErrors --excludeExternals --readme none --exclude '**/+(main-browser|standalone-bin).ts' --out typedoc/ src/ custom-typings/", | ||
"build:doc": "typedoc --ignoreCompilerErrors --excludeExternals --readme none --exclude '**/+(main-browser|standalone-bin).ts' --out typedoc/ src/ custom-typings/ ", | ||
"postbuild:doc": "touch ./typedoc/.nojekyll", | ||
"test": "npm run build && npm run test:node && npm run test:browser", | ||
@@ -38,3 +39,3 @@ "test:node": "NODE_EXTRA_CA_CERTS=./test/fixtures/test-ca.pem mocha -r ts-node/register 'test/**/*.spec.ts'", | ||
"standalone": "npm run build && node -e 'require(\".\").getStandalone({ debug: true }).start()'", | ||
"ci": "npm run test && catch-uncommitted", | ||
"ci-tests": "npm run test && catch-uncommitted", | ||
"prepack": "npm run build" | ||
@@ -41,0 +42,0 @@ }, |
@@ -1,2 +0,2 @@ | ||
# Mockttp [![Travis Build Status](https://img.shields.io/travis/httptoolkit/mockttp.svg)](https://travis-ci.org/httptoolkit/mockttp) [![Available on NPM](https://img.shields.io/npm/v/mockttp.svg)](https://npmjs.com/package/mockttp) [![Try Mockttp on RunKit](https://badge.runkitcdn.com/mockttp.svg)](https://npm.runkit.com/mockttp) | ||
# Mockttp [![Build Status](https://github.com/httptoolkit/mockttp/workflows/CI/badge.svg)](https://github.com/httptoolkit/mockttp/actions) [![Available on NPM](https://img.shields.io/npm/v/mockttp.svg)](https://npmjs.com/package/mockttp) [![Try Mockttp on RunKit](https://badge.runkitcdn.com/mockttp.svg)](https://npm.runkit.com/mockttp) | ||
@@ -3,0 +3,0 @@ > _Part of [HTTP Toolkit](https://httptoolkit.tech): powerful tools for building, testing & debugging HTTP(S)_ |
@@ -9,2 +9,3 @@ /** | ||
import http = require('http'); | ||
import http2 = require('http2'); | ||
import https = require('https'); | ||
@@ -27,3 +28,4 @@ import * as h2Client from 'http2-wrapper'; | ||
h2HeadersToH1, | ||
isAbsoluteUrl | ||
isAbsoluteUrl, | ||
cleanUpHeaders | ||
} from '../util/request-utils'; | ||
@@ -580,3 +582,3 @@ import { isLocalPortActive } from '../util/socket-util'; | ||
.pickBy((value, name) => | ||
name.startsWith(':') && | ||
name.toString().startsWith(':') && | ||
// We allow returning a preexisting header value - that's ignored | ||
@@ -706,7 +708,6 @@ // silently, so that mutating & returning the provided headers is always safe. | ||
const completedRequest = await waitForCompletedRequest(clientReq); | ||
const modifiedReq = await this.beforeRequest( | ||
Object.assign(completedRequest, { | ||
headers: _.clone(completedRequest.headers) // Clone headers so we can ignore mutations | ||
}) | ||
); | ||
const modifiedReq = await this.beforeRequest({ | ||
...completedRequest, | ||
headers: _.clone(completedRequest.headers) | ||
}); | ||
@@ -732,3 +733,3 @@ if (modifiedReq.response) { | ||
validateCustomHeaders( | ||
clientReq.headers, | ||
completedRequest.headers, | ||
modifiedReq.headers, | ||
@@ -812,3 +813,3 @@ OVERRIDABLE_REQUEST_PSEUDOHEADERS // These are handled by getCorrectPseudoheaders above | ||
headers = _.pickBy(headers, (value, key) => | ||
!key.startsWith(':') || | ||
!key.toString().startsWith(':') || | ||
(headersManuallyModified && | ||
@@ -852,2 +853,3 @@ OVERRIDABLE_REQUEST_PSEUDOHEADERS.includes(key as any) | ||
body = await streamToBuffer(serverRes); | ||
const cleanHeaders = cleanUpHeaders(serverHeaders); | ||
@@ -858,7 +860,7 @@ modifiedRes = await this.beforeResponse({ | ||
statusMessage: serverRes.statusMessage, | ||
headers: _.clone(serverHeaders), | ||
headers: _.clone(cleanHeaders), | ||
body: buildBodyReader(body, serverHeaders) | ||
}); | ||
validateCustomHeaders(serverHeaders, modifiedRes.headers); | ||
validateCustomHeaders(cleanHeaders, modifiedRes.headers); | ||
@@ -899,4 +901,7 @@ serverStatusCode = modifiedRes.statusCode || | ||
const headerValue = serverHeaders[header]; | ||
if (headerValue === undefined) return; | ||
if (header === ':status') return; // H2 status gets set by writeHead below | ||
if ( | ||
headerValue === undefined || | ||
(header as unknown) === http2.sensitiveHeaders || | ||
header === ':status' // H2 status gets set by writeHead below | ||
) return; | ||
@@ -903,0 +908,0 @@ try { |
@@ -112,6 +112,8 @@ /** | ||
req.path = req.url; | ||
const host = req.headers[':authority'] || req.headers['host']; | ||
const absoluteUrl = `${req.protocol}://${host}${req.path}`; | ||
if (!req.headers[':path']) { | ||
req.url = new url.URL(req.url!, `${req.protocol}://${host}`).toString(); | ||
req.url = new url.URL(absoluteUrl).toString(); | ||
} else { | ||
@@ -122,3 +124,3 @@ // Node's HTTP/2 compat logic maps .url to headers[':path']. We want them to | ||
Object.defineProperty(req, 'url', { | ||
value: new url.URL(req.url!, `${req.protocol}://${host}`).toString() | ||
value: new url.URL(absoluteUrl).toString() | ||
}); | ||
@@ -125,0 +127,0 @@ } |
@@ -99,4 +99,4 @@ /** | ||
export function h2HeadersToH1(h2Headers: Headers): Headers { | ||
const h1Headers = _.omitBy(h2Headers, (_value, key) => { | ||
return key.startsWith(':') | ||
const h1Headers = _.omitBy(h2Headers, (_value, key: string | Symbol) => { | ||
return key === http2.sensitiveHeaders || key.toString().startsWith(':') | ||
}); | ||
@@ -321,2 +321,19 @@ | ||
/** | ||
* Translate from internal header representations (basically Node's header representations) to a | ||
* mildly more consistent & simplified model that we expose externally: numbers as strings, and | ||
* no sensitiveHeaders symbol for HTTP/2. | ||
*/ | ||
export function cleanUpHeaders(headers: Headers) { | ||
return _.mapValues( | ||
_.omit(headers, ...(http2.sensitiveHeaders ? [http2.sensitiveHeaders as any] : [])), | ||
(headerValue: undefined | string | string[] | number) => | ||
_.isNumber(headerValue) ? headerValue.toString() : headerValue | ||
); | ||
} | ||
/** | ||
* Build an initiated request: the external representation of a request | ||
* that's just started. | ||
*/ | ||
export function buildInitiatedRequest(request: OngoingRequest): InitiatedRequest { | ||
@@ -336,2 +353,3 @@ return { | ||
), | ||
headers: cleanUpHeaders(request.headers), | ||
timingEvents: request.timingEvents | ||
@@ -341,5 +359,10 @@ }; | ||
/** | ||
* Build an aborted request: the external representation of a request | ||
* that's been aborted. | ||
*/ | ||
export function buildAbortedRequest(request: OngoingRequest): InitiatedRequest { | ||
const requestData = buildInitiatedRequest(request); | ||
return Object.assign(requestData, { | ||
headers: cleanUpHeaders(request.headers), | ||
// Exists for backward compat: really Abort events should have no body at all | ||
@@ -350,2 +373,6 @@ body: buildBodyReader(Buffer.alloc(0), {}) | ||
/** | ||
* Build a completed request: the external representation of a request | ||
* that's been completely received (but not necessarily replied to). | ||
*/ | ||
export async function waitForCompletedRequest(request: OngoingRequest): Promise<CompletedRequest> { | ||
@@ -356,3 +383,3 @@ const body = await waitForBody(request.body, request.headers); | ||
const requestData = buildInitiatedRequest(request); | ||
return Object.assign(requestData, { body }); | ||
return Object.assign(requestData, { body, headers: cleanUpHeaders(request.headers) }); | ||
} | ||
@@ -427,2 +454,6 @@ | ||
/** | ||
* Build a completed response: the external representation of a response | ||
* that's been completely written out and sent back to the client. | ||
*/ | ||
export async function waitForCompletedResponse(response: OngoingResponse): Promise<CompletedResponse> { | ||
@@ -439,7 +470,3 @@ const body = await waitForBody(response.body, response.getHeaders()); | ||
]).assign({ | ||
headers: _.mapValues(response.getHeaders(), (headerValue) => { | ||
return _.isNumber(headerValue) | ||
? headerValue.toString() // HTTP :status sneaks in as a number | ||
: headerValue; | ||
}) as Headers, | ||
headers: cleanUpHeaders(response.getHeaders()), | ||
body: body | ||
@@ -446,0 +473,0 @@ }).valueOf(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
737871
13387
14