You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 7-8.RSVP
Socket
Socket
Sign inDemoInstall

mockttp

Package Overview
Dependencies
Maintainers
1
Versions
122
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.1.0 to 1.2.0

custom-typings/dns2.d.ts

9

custom-typings/node-type-extensions.d.ts

@@ -98,2 +98,11 @@ // There's a few places where we attach extra data to some node objects during

export const sensitiveHeaders: Symbol | undefined;
}
declare module "http" {
import * as dns from 'dns';
interface ClientRequestArgs {
// __promisify__ here is a type hack, causes problems, so we strip it
lookup?: Omit<typeof dns.lookup, '__promisify__'>;
}
}

4

dist/rules/base-rule-builder.d.ts

@@ -56,2 +56,6 @@ /**

/**
* Match only requests whose bodies include the given string.
*/
withBodyIncluding(content: string): this;
/**
* Match only requests whose bodies exactly match the given

@@ -58,0 +62,0 @@ * object, when parsed as JSON.

@@ -85,2 +85,9 @@ "use strict";

/**
* Match only requests whose bodies include the given string.
*/
withBodyIncluding(content) {
this.matchers.push(new matchers_1.RawBodyIncludesMatcher(content));
return this;
}
/**
* Match only requests whose bodies exactly match the given

@@ -87,0 +94,0 @@ * object, when parsed as JSON.

@@ -97,2 +97,9 @@ /**

}
export declare class RawBodyIncludesMatcher extends Serializable implements RequestMatcher {
content: string;
readonly type = "raw-body-includes";
constructor(content: string);
matches(request: OngoingRequest): Promise<boolean>;
explain(): string;
}
export declare class RegexBodyMatcher extends Serializable implements RequestMatcher {

@@ -142,2 +149,3 @@ readonly type = "raw-body-regexp";

'raw-body-regexp': typeof RegexBodyMatcher;
'raw-body-includes': typeof RawBodyIncludesMatcher;
'json-body': typeof JsonBodyMatcher;

@@ -144,0 +152,0 @@ 'json-body-matching': typeof JsonBodyFlexibleMatcher;

@@ -240,2 +240,18 @@ "use strict";

exports.RawBodyMatcher = RawBodyMatcher;
class RawBodyIncludesMatcher extends serialization_1.Serializable {
constructor(content) {
super();
this.content = content;
this.type = 'raw-body-includes';
}
matches(request) {
return __awaiter(this, void 0, void 0, function* () {
return (yield request.body.asText()).includes(this.content);
});
}
explain() {
return `with a body including '${this.content}'`;
}
}
exports.RawBodyIncludesMatcher = RawBodyIncludesMatcher;
class RegexBodyMatcher extends serialization_1.Serializable {

@@ -254,3 +270,3 @@ constructor(regex) {

explain() {
return `with body matching /${unescapeRegexp(this.regexString)}/`;
return `with a body matching /${unescapeRegexp(this.regexString)}/`;
}

@@ -275,3 +291,3 @@ }

explain() {
return `with ${JSON.stringify(this.body)} as a JSON body`;
return `with a JSON body equivalent to ${JSON.stringify(this.body)}`;
}

@@ -296,3 +312,3 @@ }

explain() {
return `with JSON body including ${JSON.stringify(this.body)}`;
return `with a JSON body including ${JSON.stringify(this.body)}`;
}

@@ -336,2 +352,3 @@ }

'raw-body-regexp': RegexBodyMatcher,
'raw-body-includes': RawBodyIncludesMatcher,
'json-body': JsonBodyMatcher,

@@ -343,3 +360,14 @@ 'json-body-matching': JsonBodyFlexibleMatcher,

return __awaiter(this, void 0, void 0, function* () {
return _.every(yield Promise.all(matchers.map((matcher) => matcher.matches(req))));
return new Promise((resolve, reject) => {
const resultsPromises = matchers.map((matcher) => matcher.matches(req));
resultsPromises.forEach((maybePromiseResult) => __awaiter(this, void 0, void 0, function* () {
const result = yield maybePromiseResult;
if (!result)
resolve(false); // Resolve mismatches immediately
}));
// Otherwise resolve as normal: all true matches, exceptions reject.
Promise.all(resultsPromises)
.then((result) => resolve(_.every(result)))
.catch((e) => reject(e));
});
});

@@ -346,0 +374,0 @@ }

@@ -100,5 +100,41 @@ /**

}
export interface PassThroughLookupOptions {
/**
* The maximum time to cache a DNS response. Up to this limit,
* responses will be cached according to their own TTL. Defaults
* to Infinity.
*/
maxTtl?: number;
/**
* How long to cache a DNS ENODATA or ENOTFOUND response. Defaults
* to 0.15.
*/
errorTtl?: number;
/**
* The primary servers to use. DNS queries will be resolved against
* these servers first. If no data is available, queries will fall
* back to dns.lookup, and use the OS's default DNS servers.
*
* This defaults to dns.getServers().
*/
servers?: string[];
}
export interface PassThroughHandlerOptions {
/**
* The forwarding configuration for the passthrough rule.
* This generally shouldn't be used explicitly unless you're
* building rule data by hand. Instead, call `thenPassThrough`
* to send data directly or `thenForwardTo` with options to
* configure traffic forwarding.
*/
forwarding?: ForwardingOptions;
/**
* A list of hostnames for which server certificate errors should be ignored
* (none, by default).
*/
ignoreHostCertificateErrors?: string[];
/**
* A mapping of hosts to client certificates to use, in the form of
* `{ key, cert }` objects (none, by default)
*/
clientCertificateHostMap?: {

@@ -110,3 +146,44 @@ [host: string]: {

};
/**
* Custom DNS options, to allow configuration of the resolver used
* when forwarding requests upstream. Passing any option switches
* from using node's default dns.lookup function to using the
* cacheable-lookup module, which will cache responses.
*/
lookupOptions?: PassThroughLookupOptions;
/**
* A callback that will be passed the full request before it is passed through,
* and which returns an object that defines how the the request content should
* be changed before it's passed to the upstream server.
*
* The callback should return an object that definies how the request
* should be changed. All fields on the object are optional. The possible
* fields are:
*
* - `method` (a replacement HTTP verb, capitalized)
* - `url` (a full URL to send the request to)
* - `response` (a response callback result: if provided this will be used
* directly, the request will not be passed through at all, and any
* beforeResponse callback will never fire)
* - `headers` (object with string keys & values, replaces all headers if set)
* - `body` (string or buffer, replaces the body if set)
* - `json` (object, to be sent as a JSON-encoded body, taking precedence
* over `body` if both are set)
*/
beforeRequest?: (req: CompletedRequest) => MaybePromise<CallbackRequestResult>;
/**
* A callback that will be passed the full response before it is passed through,
* and which returns an object that defines how the the response content should
* before it's returned to the client.
*
* The callback should return an object that definies how the response
* should be changed. All fields on the object are optional. The possible
* fields are:
*
* - `status` (number, will replace the HTTP status code)
* - `headers` (object with string keys & values, replaces all headers if set)
* - `body` (string or buffer, replaces the body if set)
* - `json` (object, to be sent as a JSON-encoded body, taking precedence
* over `body` if both are set)
*/
beforeResponse?: (res: PassThroughResponse) => MaybePromise<CallbackResponseResult>;

@@ -125,2 +202,3 @@ }

};
lookupOptions?: PassThroughLookupOptions;
hasBeforeRequestCallback?: boolean;

@@ -141,2 +219,5 @@ hasBeforeResponseCallback?: boolean;

readonly beforeResponse?: (res: PassThroughResponse) => MaybePromise<CallbackResponseResult>;
readonly lookupOptions: PassThroughLookupOptions | undefined;
private _cacheableLookupInstance;
private lookup;
constructor(options?: PassThroughHandlerOptions);

@@ -143,0 +224,0 @@ explain(): string;

@@ -21,2 +21,3 @@ "use strict";

const h2Client = require("http2-wrapper");
const cacheable_lookup_1 = require("cacheable-lookup");
const base64_arraybuffer_1 = require("base64-arraybuffer");

@@ -396,2 +397,3 @@ const stream_1 = require("stream");

}
this.lookupOptions = options.lookupOptions;
this.clientCertificateHostMap = options.clientCertificateHostMap || {};

@@ -401,2 +403,18 @@ this.beforeRequest = options.beforeRequest;

}
lookup() {
if (!this.lookupOptions)
return undefined;
if (!this._cacheableLookupInstance) {
this._cacheableLookupInstance = new cacheable_lookup_1.default({
maxTtl: this.lookupOptions.maxTtl,
errorTtl: this.lookupOptions.errorTtl,
// As little caching of "use the fallback server" as possible:
fallbackDuration: 1
});
if (this.lookupOptions.servers) {
this._cacheableLookupInstance.servers = this.lookupOptions.servers;
}
}
return this._cacheableLookupInstance.lookup;
}
explain() {

@@ -542,3 +560,3 @@ return this.forwarding

path,
headers, agent: agent, rejectUnauthorized: checkServerCertificate }, clientCert), (serverRes) => (() => __awaiter(this, void 0, void 0, function* () {
headers, lookup: this.lookup(), agent: agent, rejectUnauthorized: checkServerCertificate }, clientCert), (serverRes) => (() => __awaiter(this, void 0, void 0, function* () {
serverRes.on('error', reject);

@@ -710,3 +728,3 @@ let serverStatusCode = serverRes.statusCode;

forwarding: this.forwarding
} : {}), { ignoreHostCertificateErrors: this.ignoreHostCertificateErrors, clientCertificateHostMap: _.mapValues(this.clientCertificateHostMap, ({ pfx, passphrase }) => ({ pfx: serialization_1.serializeBuffer(pfx), passphrase })), hasBeforeRequestCallback: !!this.beforeRequest, hasBeforeResponseCallback: !!this.beforeResponse });
} : {}), { lookupOptions: this.lookupOptions, ignoreHostCertificateErrors: this.ignoreHostCertificateErrors, clientCertificateHostMap: _.mapValues(this.clientCertificateHostMap, ({ pfx, passphrase }) => ({ pfx: serialization_1.serializeBuffer(pfx), passphrase })), hasBeforeRequestCallback: !!this.beforeRequest, hasBeforeResponseCallback: !!this.beforeResponse });
}

@@ -737,3 +755,3 @@ static deserialize(data, channel) {

forwarding: { targetHost: data.forwardToLocation }
} : {}), { forwarding: data.forwarding, ignoreHostCertificateErrors: data.ignoreHostCertificateErrors, clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap, ({ pfx, passphrase }) => ({ pfx: serialization_1.deserializeBuffer(pfx), passphrase })) }));
} : {}), { forwarding: data.forwarding, lookupOptions: data.lookupOptions, ignoreHostCertificateErrors: data.ignoreHostCertificateErrors, clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap, ({ pfx, passphrase }) => ({ pfx: serialization_1.deserializeBuffer(pfx), passphrase })) }));
}

@@ -740,0 +758,0 @@ }

38

dist/rules/requests/request-rule-builder.d.ts

@@ -151,34 +151,5 @@ /**

* This method takes options to configure how the request is passed
* through. The available options are:
* through. See {@link PassThroughHandlerOptions} for the full details
* of the options available.
*
* * ignoreHostCertificateErrors, a list of hostnames for which server
* certificate errors should be ignored (none, by default).
* * clientCertificateHostMap, a mapping of hosts to client certificates to use,
* in the form of { key, cert } objects (none, by default)
* * beforeRequest, a callback that will be passed the full request
* before it is passed through, and which returns an object that defines
* how the the request content should be changed before it's passed
* to the upstream server (details below).
* * beforeResponse, a callback that will be passed the full response
* before it is completed, and which returns an object that defines
* how the the response content should be changed before it's returned
* to the client (details below).
*
* The beforeRequest & beforeResponse callbacks should return objects
* defining how the request/response should be changed. All fields on
* the object are optional. The valid fields are:
*
* Valid fields are:
* - Request only: `method` (a replacement HTTP verb, capitalized)
* - Request only: `url` (a full URL to send the request to)
* - Request only: `response` (a response callback result: if provided
* this will be used directly, the request will not be passed through
* at all, and any beforeResponse callback will never fire)
* - Response only: `status` (number, will replace the HTTP status code)
* - Both: `headers` (object with string keys & values, replaces all
* headers if set)
* - Both: `body` (string or buffer, replaces the body if set)
* - Both: `json` (object, to be sent as a JSON-encoded body, taking
* precedence over `body` if both are set)
*
* Calling this method registers the rule with the server, so it

@@ -203,4 +174,5 @@ * starts to handle requests.

*
* This method also takes options to configure how the request is passed
* through, see thenPassThrough for more details.
* This method takes options to configure how the request is passed
* through. See {@link PassThroughHandlerOptions} for the full details
* of the options available.
*

@@ -207,0 +179,0 @@ * Calling this method registers the rule with the server, so it

@@ -181,34 +181,5 @@ "use strict";

* This method takes options to configure how the request is passed
* through. The available options are:
* through. See {@link PassThroughHandlerOptions} for the full details
* of the options available.
*
* * ignoreHostCertificateErrors, a list of hostnames for which server
* certificate errors should be ignored (none, by default).
* * clientCertificateHostMap, a mapping of hosts to client certificates to use,
* in the form of { key, cert } objects (none, by default)
* * beforeRequest, a callback that will be passed the full request
* before it is passed through, and which returns an object that defines
* how the the request content should be changed before it's passed
* to the upstream server (details below).
* * beforeResponse, a callback that will be passed the full response
* before it is completed, and which returns an object that defines
* how the the response content should be changed before it's returned
* to the client (details below).
*
* The beforeRequest & beforeResponse callbacks should return objects
* defining how the request/response should be changed. All fields on
* the object are optional. The valid fields are:
*
* Valid fields are:
* - Request only: `method` (a replacement HTTP verb, capitalized)
* - Request only: `url` (a full URL to send the request to)
* - Request only: `response` (a response callback result: if provided
* this will be used directly, the request will not be passed through
* at all, and any beforeResponse callback will never fire)
* - Response only: `status` (number, will replace the HTTP status code)
* - Both: `headers` (object with string keys & values, replaces all
* headers if set)
* - Both: `body` (string or buffer, replaces the body if set)
* - Both: `json` (object, to be sent as a JSON-encoded body, taking
* precedence over `body` if both are set)
*
* Calling this method registers the rule with the server, so it

@@ -240,4 +211,5 @@ * starts to handle requests.

*
* This method also takes options to configure how the request is passed
* through, see thenPassThrough for more details.
* This method takes options to configure how the request is passed
* through. See {@link PassThroughHandlerOptions} for the full details
* of the options available.
*

@@ -244,0 +216,0 @@ * Calling this method registers the rule with the server, so it

@@ -49,5 +49,12 @@ "use strict";

if (this.completionChecker) {
// If we have a specific rule, use that
return this.completionChecker.isComplete(this.requestCount);
}
else if (this.requestCount === 0) {
// Otherwise, by default we're definitely incomplete if we've seen no requests
return false;
}
else {
// And we're _maybe_ complete if we've seen at least one request. In reality, we're incomplete
// but we should be used anyway if we're at any point we're the last matching rule for a request.
return null;

@@ -54,0 +61,0 @@ }

@@ -8,3 +8,3 @@ /**

import { OngoingRequest, Explainable } from "../../types";
import { CloseConnectionHandler, TimeoutHandler, ForwardingOptions } from '../requests/request-handlers';
import { CloseConnectionHandler, TimeoutHandler, ForwardingOptions, PassThroughLookupOptions } from '../requests/request-handlers';
export interface WebSocketHandler extends Explainable, Serializable {

@@ -15,4 +15,22 @@ type: keyof typeof WsHandlerLookup;

export interface PassThroughWebSocketHandlerOptions {
/**
* The forwarding configuration for the passthrough rule.
* This generally shouldn't be used explicitly unless you're
* building rule data by hand. Instead, call `thenPassThrough`
* to send data directly or `thenForwardTo` with options to
* configure traffic forwarding.
*/
forwarding?: ForwardingOptions;
/**
* A list of hostnames for which server certificate errors should be
* ignored (none, by default).
*/
ignoreHostCertificateErrors?: string[];
/**
* Custom DNS options, to allow configuration of the resolver used
* when forwarding requests upstream. Passing any option switches
* from using node's default dns.lookup function to using the
* cacheable-lookup module, which will cache responses.
*/
lookupOptions?: PassThroughLookupOptions;
}

@@ -24,2 +42,5 @@ export declare class PassThroughWebSocketHandler extends Serializable implements WebSocketHandler {

private wsServer?;
readonly lookupOptions: PassThroughLookupOptions | undefined;
private _cacheableLookupInstance;
private lookup;
constructor(options?: PassThroughWebSocketHandlerOptions);

@@ -26,0 +47,0 @@ explain(): string;

@@ -19,2 +19,3 @@ "use strict";

const common_tags_1 = require("common-tags");
const cacheable_lookup_1 = require("cacheable-lookup");
const serialization_1 = require("../../util/serialization");

@@ -95,3 +96,20 @@ const request_handlers_1 = require("../requests/request-handlers");

this.forwarding = options.forwarding;
this.lookupOptions = options.lookupOptions;
}
lookup() {
if (!this.lookupOptions)
return undefined;
if (!this._cacheableLookupInstance) {
this._cacheableLookupInstance = new cacheable_lookup_1.default({
maxTtl: this.lookupOptions.maxTtl,
errorTtl: this.lookupOptions.errorTtl,
// As little caching of "use the fallback server" as possible:
fallbackDuration: 1
});
if (this.lookupOptions.servers) {
this._cacheableLookupInstance.servers = this.lookupOptions.servers;
}
}
return this._cacheableLookupInstance.lookup;
}
explain() {

@@ -176,2 +194,3 @@ return this.forwarding

maxPayload: 0,
lookup: this.lookup(),
headers: _.omitBy(headers, (_v, headerName) => headerName.toLowerCase().startsWith('sec-websocket') ||

@@ -178,0 +197,0 @@ headerName.toLowerCase() === 'connection') // Simplify to string - doesn't matter though, only used by http module anyway

@@ -39,7 +39,5 @@ /**

* This method takes options to configure how the request is passed
* through. The available options are:
* through. See {@link PassThroughWebSocketHandlerOptions} for the full
* details of the options available.
*
* * ignoreHostCertificateErrors, a list of hostnames for which server
* certificate errors should be ignored (none, by default).
*
* Calling this method registers the rule with the server, so it

@@ -64,4 +62,5 @@ * starts to handle requests.

*
* This method also takes options to configure how the request is passed
* through, see thenPassThrough for more details.
* This method takes options to configure how the request is passed
* through. See {@link PassThroughWebSocketHandlerOptions} for the full
* details of the options available.
*

@@ -68,0 +67,0 @@ * Calling this method registers the rule with the server, so it

@@ -50,7 +50,5 @@ "use strict";

* This method takes options to configure how the request is passed
* through. The available options are:
* through. See {@link PassThroughWebSocketHandlerOptions} for the full
* details of the options available.
*
* * ignoreHostCertificateErrors, a list of hostnames for which server
* certificate errors should be ignored (none, by default).
*
* Calling this method registers the rule with the server, so it

@@ -82,4 +80,5 @@ * starts to handle requests.

*
* This method also takes options to configure how the request is passed
* through, see thenPassThrough for more details.
* This method takes options to configure how the request is passed
* through. See {@link PassThroughWebSocketHandlerOptions} for the full
* details of the options available.
*

@@ -86,0 +85,0 @@ * Calling this method registers the rule with the server, so it

@@ -49,5 +49,12 @@ "use strict";

if (this.completionChecker) {
// If we have a specific rule, use that
return this.completionChecker.isComplete(this.requestCount);
}
else if (this.requestCount === 0) {
// Otherwise, by default we're definitely incomplete if we've seen no requests
return false;
}
else {
// And we're _maybe_ complete if we've seen at least one request. In reality, we're incomplete
// but we should be used anyway if we're at any point we're the last matching rule for a request.
return null;

@@ -54,0 +61,0 @@ }

@@ -65,3 +65,3 @@ /**

handleWebSocket(rawRequest: ExtendedRawRequest, socket: net.Socket, head: Buffer): Promise<void>;
private isComplete;
private findMatchingRule;
private getUnmatchedRequestExplanation;

@@ -68,0 +68,0 @@ private sendUnmatchedRequestError;

@@ -69,14 +69,2 @@ "use strict";

};
this.isComplete = (rule, matchingRules) => {
const isDefinitelyComplete = rule.isComplete();
if (isDefinitelyComplete !== null) {
return isDefinitelyComplete;
}
else if (matchingRules[matchingRules.length - 1] === rule) {
return false;
}
else {
return rule.requests.length !== 0;
}
};
this.initialDebugSetting = this.debug;

@@ -345,4 +333,3 @@ this.httpsOptions = options.https;

});
let nextRulePromise = promise_1.filter(this.requestRules, (r) => r.matches(request))
.then((matchingRules) => matchingRules.filter((r) => !this.isComplete(r, matchingRules))[0]);
let nextRulePromise = this.findMatchingRule(this.requestRules, request);
// Async: once we know what the next rule is, ping a request event

@@ -405,4 +392,3 @@ nextRulePromise

});
let nextRulePromise = promise_1.filter(this.webSocketRules, (r) => r.matches(request))
.then((matchingRules) => matchingRules.filter((r) => !this.isComplete(r, matchingRules))[0]);
let nextRulePromise = this.findMatchingRule(this.webSocketRules, request);
try {

@@ -435,2 +421,25 @@ let nextRule = yield nextRulePromise;

}
findMatchingRule(rules, request) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// Start all rules matching immediately
const rulesMatches = rules.map((r) => ({ rule: r, match: r.matches(request) }));
// Evaluate the matches one by one, and immediately use the first
for (let { rule, match } of rulesMatches) {
if ((yield match) && rule.isComplete() === false) {
// The first matching incomplete rule we find is the one we should use
return rule;
}
}
// There are no incomplete & matching rules! One last option: if the last matching rule is
// maybe-incomplete (i.e. default completion status but has seen >0 requests) then it should
// match anyway. This allows us to add rules and have the last repeat indefinitely.
const lastMatchingRule = (_a = _.last(yield promise_1.filter(rulesMatches, m => m.match))) === null || _a === void 0 ? void 0 : _a.rule;
if (!lastMatchingRule || lastMatchingRule.isComplete())
return undefined;
// Otherwise, must be a rule with isComplete === null, i.e. no specific completion check:
else
return lastMatchingRule;
});
}
getUnmatchedRequestExplanation(request) {

@@ -437,0 +446,0 @@ return __awaiter(this, void 0, void 0, function* () {

@@ -55,3 +55,3 @@ "use strict";

else
resolve(result);
resolve(strictOriginMatch(origin, result));
});

@@ -58,0 +58,0 @@ });

{
"name": "mockttp",
"version": "1.1.0",
"version": "1.2.0",
"description": "Mock HTTP server for testing HTTP clients and stubbing webservices",

@@ -8,2 +8,4 @@ "main": "dist/main.js",

"dist/main.js": "./dist/main-browser.js",
"dns": false,
"os": false,
"fs": false,

@@ -15,3 +17,4 @@ "net": false,

"http2": false,
"http2-wrapper": false
"http2-wrapper": false,
"cacheable-lookup": false
},

@@ -87,2 +90,3 @@ "types": "dist/main.d.ts",

"chai-fetch": "^0.3.1",
"dns2": "^1.4.2",
"fs-extra": "^8.1.0",

@@ -128,2 +132,3 @@ "http-proxy-agent": "^2.0.0",

"brotli": "^1.3.2",
"cacheable-lookup": "^5.0.4",
"common-tags": "^1.8.0",

@@ -130,0 +135,0 @@ "connect": "^3.7.0",

@@ -27,2 +27,3 @@ /**

RawBodyMatcher,
RawBodyIncludesMatcher,
WildcardMatcher,

@@ -126,2 +127,10 @@ CookieMatcher,

/**
* Match only requests whose bodies include the given string.
*/
withBodyIncluding(content: string): this {
this.matchers.push(new RawBodyIncludesMatcher(content));
return this;
}
/**
* Match only requests whose bodies exactly match the given

@@ -128,0 +137,0 @@ * object, when parsed as JSON.

@@ -287,2 +287,20 @@ /**

export class RawBodyIncludesMatcher extends Serializable implements RequestMatcher {
readonly type = 'raw-body-includes';
constructor(
public content: string
) {
super();
}
async matches(request: OngoingRequest) {
return (await request.body.asText()).includes(this.content);
}
explain() {
return `with a body including '${this.content}'`;
}
}
export class RegexBodyMatcher extends Serializable implements RequestMatcher {

@@ -303,3 +321,3 @@ readonly type = 'raw-body-regexp';

explain() {
return `with body matching /${unescapeRegexp(this.regexString)}/`;
return `with a body matching /${unescapeRegexp(this.regexString)}/`;
}

@@ -326,3 +344,3 @@

explain() {
return `with ${JSON.stringify(this.body)} as a JSON body`;
return `with a JSON body equivalent to ${JSON.stringify(this.body)}`;
}

@@ -349,3 +367,3 @@

explain() {
return `with JSON body including ${JSON.stringify(this.body)}`;
return `with a JSON body including ${JSON.stringify(this.body)}`;
}

@@ -395,2 +413,3 @@

'raw-body-regexp': RegexBodyMatcher,
'raw-body-includes': RawBodyIncludesMatcher,
'json-body': JsonBodyMatcher,

@@ -401,8 +420,16 @@ 'json-body-matching': JsonBodyFlexibleMatcher,

export async function matchesAll(req: OngoingRequest, matchers: RequestMatcher[]) {
return _.every(
await Promise.all(
matchers.map((matcher) => matcher.matches(req))
)
);
export async function matchesAll(req: OngoingRequest, matchers: RequestMatcher[]): Promise<boolean> {
return new Promise((resolve, reject) => {
const resultsPromises = matchers.map((matcher) => matcher.matches(req));
resultsPromises.forEach(async (maybePromiseResult) => {
const result = await maybePromiseResult;
if (!result) resolve(false); // Resolve mismatches immediately
});
// Otherwise resolve as normal: all true matches, exceptions reject.
Promise.all(resultsPromises)
.then((result) => resolve(_.every(result)))
.catch((e) => reject(e));
});
}

@@ -409,0 +436,0 @@

@@ -12,2 +12,3 @@ /**

import * as h2Client from 'http2-wrapper';
import CacheableLookup from 'cacheable-lookup';
import { encode as encodeBase64, decode as decodeBase64 } from 'base64-arraybuffer';

@@ -397,9 +398,92 @@ import { Readable, Transform } from 'stream';

export interface PassThroughLookupOptions {
/**
* The maximum time to cache a DNS response. Up to this limit,
* responses will be cached according to their own TTL. Defaults
* to Infinity.
*/
maxTtl?: number;
/**
* How long to cache a DNS ENODATA or ENOTFOUND response. Defaults
* to 0.15.
*/
errorTtl?: number;
/**
* The primary servers to use. DNS queries will be resolved against
* these servers first. If no data is available, queries will fall
* back to dns.lookup, and use the OS's default DNS servers.
*
* This defaults to dns.getServers().
*/
servers?: string[];
}
export interface PassThroughHandlerOptions {
/**
* The forwarding configuration for the passthrough rule.
* This generally shouldn't be used explicitly unless you're
* building rule data by hand. Instead, call `thenPassThrough`
* to send data directly or `thenForwardTo` with options to
* configure traffic forwarding.
*/
forwarding?: ForwardingOptions,
/**
* A list of hostnames for which server certificate errors should be ignored
* (none, by default).
*/
ignoreHostCertificateErrors?: string[];
/**
* A mapping of hosts to client certificates to use, in the form of
* `{ key, cert }` objects (none, by default)
*/
clientCertificateHostMap?: {
[host: string]: { pfx: Buffer, passphrase?: string }
};
/**
* Custom DNS options, to allow configuration of the resolver used
* when forwarding requests upstream. Passing any option switches
* from using node's default dns.lookup function to using the
* cacheable-lookup module, which will cache responses.
*/
lookupOptions?: PassThroughLookupOptions;
/**
* A callback that will be passed the full request before it is passed through,
* and which returns an object that defines how the the request content should
* be changed before it's passed to the upstream server.
*
* The callback should return an object that definies how the request
* should be changed. All fields on the object are optional. The possible
* fields are:
*
* - `method` (a replacement HTTP verb, capitalized)
* - `url` (a full URL to send the request to)
* - `response` (a response callback result: if provided this will be used
* directly, the request will not be passed through at all, and any
* beforeResponse callback will never fire)
* - `headers` (object with string keys & values, replaces all headers if set)
* - `body` (string or buffer, replaces the body if set)
* - `json` (object, to be sent as a JSON-encoded body, taking precedence
* over `body` if both are set)
*/
beforeRequest?: (req: CompletedRequest) => MaybePromise<CallbackRequestResult>;
/**
* A callback that will be passed the full response before it is passed through,
* and which returns an object that defines how the the response content should
* before it's returned to the client.
*
* The callback should return an object that definies how the response
* should be changed. All fields on the object are optional. The possible
* fields are:
*
* - `status` (number, will replace the HTTP status code)
* - `headers` (object with string keys & values, replaces all headers if set)
* - `body` (string or buffer, replaces the body if set)
* - `json` (object, to be sent as a JSON-encoded body, taking precedence
* over `body` if both are set)
*/
beforeResponse?: (res: PassThroughResponse) => MaybePromise<CallbackResponseResult>;

@@ -414,2 +498,3 @@ }

clientCertificateHostMap?: { [host: string]: { pfx: string, passphrase?: string } };
lookupOptions?: PassThroughLookupOptions;

@@ -623,2 +708,24 @@ hasBeforeRequestCallback?: boolean;

public readonly lookupOptions: PassThroughLookupOptions | undefined;
private _cacheableLookupInstance: CacheableLookup | undefined;
private lookup() {
if (!this.lookupOptions) return undefined;
if (!this._cacheableLookupInstance) {
this._cacheableLookupInstance = new CacheableLookup({
maxTtl: this.lookupOptions.maxTtl,
errorTtl: this.lookupOptions.errorTtl,
// As little caching of "use the fallback server" as possible:
fallbackDuration: 1
});
if (this.lookupOptions.servers) {
this._cacheableLookupInstance.servers = this.lookupOptions.servers;
}
}
return this._cacheableLookupInstance.lookup;
}
constructor(options: PassThroughHandlerOptions = {}) {

@@ -648,2 +755,3 @@ super();

this.lookupOptions = options.lookupOptions;
this.clientCertificateHostMap = options.clientCertificateHostMap || {};

@@ -832,2 +940,3 @@

headers,
lookup: this.lookup(),
agent: agent as http.Agent,

@@ -1042,2 +1151,3 @@ rejectUnauthorized: checkServerCertificate,

} : {},
lookupOptions: this.lookupOptions,
ignoreHostCertificateErrors: this.ignoreHostCertificateErrors,

@@ -1095,2 +1205,3 @@ clientCertificateHostMap: _.mapValues(this.clientCertificateHostMap,

forwarding: data.forwarding,
lookupOptions: data.lookupOptions,
ignoreHostCertificateErrors: data.ignoreHostCertificateErrors,

@@ -1097,0 +1208,0 @@ clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap,

@@ -281,34 +281,5 @@ /**

* This method takes options to configure how the request is passed
* through. The available options are:
* through. See {@link PassThroughHandlerOptions} for the full details
* of the options available.
*
* * ignoreHostCertificateErrors, a list of hostnames for which server
* certificate errors should be ignored (none, by default).
* * clientCertificateHostMap, a mapping of hosts to client certificates to use,
* in the form of { key, cert } objects (none, by default)
* * beforeRequest, a callback that will be passed the full request
* before it is passed through, and which returns an object that defines
* how the the request content should be changed before it's passed
* to the upstream server (details below).
* * beforeResponse, a callback that will be passed the full response
* before it is completed, and which returns an object that defines
* how the the response content should be changed before it's returned
* to the client (details below).
*
* The beforeRequest & beforeResponse callbacks should return objects
* defining how the request/response should be changed. All fields on
* the object are optional. The valid fields are:
*
* Valid fields are:
* - Request only: `method` (a replacement HTTP verb, capitalized)
* - Request only: `url` (a full URL to send the request to)
* - Request only: `response` (a response callback result: if provided
* this will be used directly, the request will not be passed through
* at all, and any beforeResponse callback will never fire)
* - Response only: `status` (number, will replace the HTTP status code)
* - Both: `headers` (object with string keys & values, replaces all
* headers if set)
* - Both: `body` (string or buffer, replaces the body if set)
* - Both: `json` (object, to be sent as a JSON-encoded body, taking
* precedence over `body` if both are set)
*
* Calling this method registers the rule with the server, so it

@@ -342,4 +313,5 @@ * starts to handle requests.

*
* This method also takes options to configure how the request is passed
* through, see thenPassThrough for more details.
* This method takes options to configure how the request is passed
* through. See {@link PassThroughHandlerOptions} for the full details
* of the options available.
*

@@ -346,0 +318,0 @@ * Calling this method registers the rule with the server, so it

@@ -78,4 +78,10 @@ /**

if (this.completionChecker) {
// If we have a specific rule, use that
return this.completionChecker.isComplete(this.requestCount);
} else if (this.requestCount === 0) {
// Otherwise, by default we're definitely incomplete if we've seen no requests
return false;
} else {
// And we're _maybe_ complete if we've seen at least one request. In reality, we're incomplete
// but we should be used anyway if we're at any point we're the last matching rule for a request.
return null;

@@ -82,0 +88,0 @@ }

@@ -11,2 +11,3 @@ /**

import { stripIndent } from 'common-tags';
import CacheableLookup from 'cacheable-lookup';

@@ -25,3 +26,4 @@ import {

TimeoutHandler,
ForwardingOptions
ForwardingOptions,
PassThroughLookupOptions
} from '../requests/request-handlers';

@@ -103,4 +105,24 @@ import { streamToBuffer, isHttp2 } from '../../util/request-utils';

export interface PassThroughWebSocketHandlerOptions {
forwarding?: ForwardingOptions;
/**
* The forwarding configuration for the passthrough rule.
* This generally shouldn't be used explicitly unless you're
* building rule data by hand. Instead, call `thenPassThrough`
* to send data directly or `thenForwardTo` with options to
* configure traffic forwarding.
*/
forwarding?: ForwardingOptions,
/**
* A list of hostnames for which server certificate errors should be
* ignored (none, by default).
*/
ignoreHostCertificateErrors?: string[];
/**
* Custom DNS options, to allow configuration of the resolver used
* when forwarding requests upstream. Passing any option switches
* from using node's default dns.lookup function to using the
* cacheable-lookup module, which will cache responses.
*/
lookupOptions?: PassThroughLookupOptions;
}

@@ -116,2 +138,25 @@

// Same lookup configuration as normal request PassThroughHandler:
public readonly lookupOptions: PassThroughLookupOptions | undefined;
private _cacheableLookupInstance: CacheableLookup | undefined;
private lookup() {
if (!this.lookupOptions) return undefined;
if (!this._cacheableLookupInstance) {
this._cacheableLookupInstance = new CacheableLookup({
maxTtl: this.lookupOptions.maxTtl,
errorTtl: this.lookupOptions.errorTtl,
// As little caching of "use the fallback server" as possible:
fallbackDuration: 1
});
if (this.lookupOptions.servers) {
this._cacheableLookupInstance.servers = this.lookupOptions.servers;
}
}
return this._cacheableLookupInstance.lookup;
}
constructor(options: PassThroughWebSocketHandlerOptions = {}) {

@@ -139,2 +184,4 @@ super();

this.forwarding = options.forwarding;
this.lookupOptions = options.lookupOptions;
}

@@ -238,2 +285,3 @@

maxPayload: 0,
lookup: this.lookup(),
headers: _.omitBy(headers, (_v, headerName) =>

@@ -243,3 +291,3 @@ headerName.toLowerCase().startsWith('sec-websocket') ||

) as { [key: string]: string } // Simplify to string - doesn't matter though, only used by http module anyway
});
} as WebSocket.ClientOptions);

@@ -246,0 +294,0 @@ upstreamSocket.once('open', () => {

@@ -53,7 +53,5 @@ /**

* This method takes options to configure how the request is passed
* through. The available options are:
* through. See {@link PassThroughWebSocketHandlerOptions} for the full
* details of the options available.
*
* * ignoreHostCertificateErrors, a list of hostnames for which server
* certificate errors should be ignored (none, by default).
*
* Calling this method registers the rule with the server, so it

@@ -87,4 +85,5 @@ * starts to handle requests.

*
* This method also takes options to configure how the request is passed
* through, see thenPassThrough for more details.
* This method takes options to configure how the request is passed
* through. See {@link PassThroughWebSocketHandlerOptions} for the full
* details of the options available.
*

@@ -91,0 +90,0 @@ * Calling this method registers the rule with the server, so it

@@ -83,4 +83,10 @@ /**

if (this.completionChecker) {
// If we have a specific rule, use that
return this.completionChecker.isComplete(this.requestCount);
} else if (this.requestCount === 0) {
// Otherwise, by default we're definitely incomplete if we've seen no requests
return false;
} else {
// And we're _maybe_ complete if we've seen at least one request. In reality, we're incomplete
// but we should be used anyway if we're at any point we're the last matching rule for a request.
return null;

@@ -87,0 +93,0 @@ }

@@ -412,8 +412,3 @@ /**

let nextRulePromise = filter(this.requestRules, (r) => r.matches(request))
.then((matchingRules) =>
matchingRules.filter((r) =>
!this.isComplete(r, matchingRules)
)[0] as RequestRule | undefined
);
let nextRulePromise = this.findMatchingRule(this.requestRules, request);

@@ -477,8 +472,3 @@ // Async: once we know what the next rule is, ping a request event

let nextRulePromise = filter(this.webSocketRules, (r) => r.matches(request))
.then((matchingRules) =>
matchingRules.filter((r) =>
!this.isComplete(r, matchingRules)
)[0] as WebSocketRule | undefined
);
let nextRulePromise = this.findMatchingRule(this.webSocketRules, request);

@@ -508,14 +498,24 @@ try {

private isComplete = (
rule: RequestRule | WebSocketRule,
matchingRules: Array<RequestRule | WebSocketRule>
) => {
const isDefinitelyComplete = rule.isComplete();
if (isDefinitelyComplete !== null) {
return isDefinitelyComplete;
} else if (matchingRules[matchingRules.length - 1] === rule) {
return false;
} else {
return rule.requests.length !== 0;
private async findMatchingRule<R extends WebSocketRule | RequestRule>(
rules: Array<R>,
request: OngoingRequest
): Promise<R | undefined> {
// Start all rules matching immediately
const rulesMatches = rules.map((r) => ({ rule: r, match: r.matches(request) }));
// Evaluate the matches one by one, and immediately use the first
for (let { rule, match } of rulesMatches) {
if (await match && rule.isComplete() === false) {
// The first matching incomplete rule we find is the one we should use
return rule;
}
}
// There are no incomplete & matching rules! One last option: if the last matching rule is
// maybe-incomplete (i.e. default completion status but has seen >0 requests) then it should
// match anyway. This allows us to add rules and have the last repeat indefinitely.
const lastMatchingRule = _.last(await filter(rulesMatches, m => m.match))?.rule;
if (!lastMatchingRule || lastMatchingRule.isComplete()) return undefined;
// Otherwise, must be a rule with isComplete === null, i.e. no specific completion check:
else return lastMatchingRule;
}

@@ -522,0 +522,0 @@

@@ -64,3 +64,3 @@ /**

if (error) reject(error);
else resolve(result);
else resolve(strictOriginMatch(origin, result));
});

@@ -67,0 +67,0 @@ });

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

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

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

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc