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 0.14.5 to 0.14.6

typedoc/classes/mockruledata.jsonbodyflexiblematcherdata.html

2

custom-typings/http-proxy-agent.d.ts

@@ -5,3 +5,3 @@ declare module 'http-proxy-agent' {

class HttpProxyAgent extends Agent {
constructor (uri: string | { protocol?: string; host?: string; hostname?: string; port?: string });
constructor (uri: string | { protocol?: string; host?: string; hostname?: string; port?: number });
}

@@ -8,0 +8,0 @@

@@ -5,3 +5,3 @@ declare module 'https-proxy-agent' {

class HttpsProxyAgent extends Agent {
constructor (uri: string | { protocol?: string; host?: string; hostname?: string; port?: string });
constructor (uri: string | { protocol?: string; host?: string; hostname?: string; port?: number });
}

@@ -8,0 +8,0 @@

@@ -22,3 +22,3 @@ /**

}
declare type SubscribableEvent = 'request' | 'response' | 'abort';
declare type SubscribableEvent = 'request' | 'response' | 'abort' | 'tlsClientError';
/**

@@ -25,0 +25,0 @@ * A Mockttp implementation, controlling a remote Mockttp standalone server.

@@ -51,3 +51,4 @@ "use strict";

'response',
'abort'
'abort',
'tlsClientError'
];

@@ -245,4 +246,8 @@ /**

on(event, callback) {
// Ignore unknown events
if (!_.includes(SUBSCRIBABLE_EVENTS, event))
return Promise.resolve();
// Ignore subscription events, if not supported by the server
if (!this.typeHasField('Subscription', 'failedTlsRequest'))
return Promise.resolve();
const standaloneStreamServer = this.mockServerOptions.standaloneServerUrl.replace(/^http/, 'ws');

@@ -254,3 +259,4 @@ const url = `${standaloneStreamServer}/server/${this.port}/subscription`;

response: 'responseCompleted',
abort: 'requestAborted'
abort: 'requestAborted',
tlsClientError: 'failedTlsRequest'
}[event];

@@ -308,2 +314,12 @@ // Note the typeHasField checks - these are a quick hack for backward compatibility,

},
tlsClientError: {
operationName: 'OnTlsClientError',
query: `subscription OnTlsClientError {
${queryResultName} {
failureCause
hostname
remoteIpAddress
}
}`
}
}[event];

@@ -321,3 +337,3 @@ client.request(query).subscribe({

}
else {
else if (event !== 'tlsClientError') {
data.timingEvents = {}; // For backward compat

@@ -324,0 +340,0 @@ }

import MockRuleBuilder from "./rules/mock-rule-builder";
import { ProxyConfig, MockedEndpoint, CompletedRequest, CompletedResponse } from "./types";
import { ProxyConfig, MockedEndpoint, CompletedRequest, CompletedResponse, TlsRequest } from "./types";
import { MockRuleData } from "./rules/mock-rule-types";

@@ -152,2 +152,20 @@ import { CAOptions } from './util/tls';

on(event: 'abort', callback: (req: CompletedRequest) => void): Promise<void>;
/**
* Subscribe to hear about requests that start a TLS handshake, but fail to complete it.
* Not all clients report TLS errors explicitly, so this event fires for explicitly
* reported TLS errors, and for TLS connections that are immediately closed with no
* data sent.
*
* This is typically useful to detect clients who aren't correctly configured to trust
* the configured HTTPS certificate. The callback is given the host name provided
* by the client via SNI, if SNI was used (it almost always is).
*
* This is only useful in some niche use cases, such as logging all requests seen
* by the server, independently of the rules defined.
*
* The callback will be called asynchronously from request handling. This function
* returns a promise, and the callback is not guaranteed to be registered until
* the promise is resolved.
*/
on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise<void>;
}

@@ -154,0 +172,0 @@ export interface MockttpOptions {

@@ -90,2 +90,18 @@ /**

}
export declare class JsonBodyMatcherData extends Serializable {
body: {};
readonly type: 'json-body';
constructor(body: {});
buildMatcher(): ((request: OngoingRequest) => Promise<boolean>) & {
explain: () => string;
};
}
export declare class JsonBodyFlexibleMatcherData extends Serializable {
body: {};
readonly type: 'json-body-matching';
constructor(body: {});
buildMatcher(): ((request: OngoingRequest) => Promise<boolean>) & {
explain: () => string;
};
}
export declare class CookieMatcherData extends Serializable {

@@ -103,3 +119,3 @@ cookie: {

}
export declare type MatcherData = (WildcardMatcherData | MethodMatcherData | SimplePathMatcherData | RegexPathMatcherData | HeaderMatcherData | QueryMatcherData | FormDataMatcherData | RawBodyMatcherData | RegexBodyMatcherData | CookieMatcherData);
export declare type MatcherData = (WildcardMatcherData | MethodMatcherData | SimplePathMatcherData | RegexPathMatcherData | HeaderMatcherData | QueryMatcherData | FormDataMatcherData | RawBodyMatcherData | RegexBodyMatcherData | JsonBodyMatcherData | JsonBodyFlexibleMatcherData | CookieMatcherData);
export declare const MatcherDataLookup: {

@@ -115,4 +131,6 @@ 'wildcard': typeof WildcardMatcherData;

'raw-body-regexp': typeof RegexBodyMatcherData;
'json-body': typeof JsonBodyMatcherData;
'json-body-matching': typeof JsonBodyFlexibleMatcherData;
'cookie': typeof CookieMatcherData;
};
export declare function buildMatchers(matcherPartData: MatcherData[]): RequestMatcher;

@@ -135,6 +135,40 @@ "use strict";

let bodyToMatch = new RegExp(this.regexString);
return _.assign((request) => __awaiter(this, void 0, void 0, function* () { return bodyToMatch.test(yield request.body.asText()); }), { explain: () => `for body matching /${this.regexString}/` });
return _.assign((request) => __awaiter(this, void 0, void 0, function* () { return bodyToMatch.test(yield request.body.asText()); }), { explain: () => `with body matching /${this.regexString}/` });
}
}
exports.RegexBodyMatcherData = RegexBodyMatcherData;
class JsonBodyMatcherData extends serialization_1.Serializable {
constructor(body) {
super();
this.body = body;
this.type = 'json-body';
}
buildMatcher() {
return _.assign((request) => __awaiter(this, void 0, void 0, function* () {
const receivedBody = yield (request.body.asJson().catch(() => undefined));
if (receivedBody === undefined)
return false;
else
return _.isEqual(receivedBody, this.body);
}), { explain: () => `with ${JSON.stringify(this.body)} as a JSON body` });
}
}
exports.JsonBodyMatcherData = JsonBodyMatcherData;
class JsonBodyFlexibleMatcherData extends serialization_1.Serializable {
constructor(body) {
super();
this.body = body;
this.type = 'json-body-matching';
}
buildMatcher() {
return _.assign((request) => __awaiter(this, void 0, void 0, function* () {
const receivedBody = yield (request.body.asJson().catch(() => undefined));
if (receivedBody === undefined)
return false;
else
return _.isMatch(receivedBody, this.body);
}), { explain: () => `with JSON body including ${JSON.stringify(this.body)}` });
}
}
exports.JsonBodyFlexibleMatcherData = JsonBodyFlexibleMatcherData;
class CookieMatcherData extends serialization_1.Serializable {

@@ -170,2 +204,4 @@ constructor(cookie) {

'raw-body-regexp': RegexBodyMatcherData,
'json-body': JsonBodyMatcherData,
'json-body-matching': JsonBodyFlexibleMatcherData,
'cookie': CookieMatcherData,

@@ -172,0 +208,0 @@ };

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

/**
* Match only requests whose bodies exactly match the given
* object, when parsed as JSON.
*
* Note that this only tests that the body can be parsed
* as JSON - it doesn't require a content-type header.
*/
withJsonBody(json: {}): MockRuleBuilder;
/**
* Match only requests whose bodies match (contain equivalent
* values, ignoring extra values) the given object, when
* parsed as JSON. Matching behaviour is the same as Lodash's
* _.isMatch method.
*
* Note that this only tests that the body can be parsed
* as JSON - it doesn't require a content-type header.
*/
withJsonBodyIncluding(json: {}): MockRuleBuilder;
/**
* Match only requests that include the given cookies

@@ -117,4 +135,9 @@ */

*/
thenJSON(status: number, data: object, headers?: OutgoingHttpHeaders): Promise<MockedEndpoint>;
thenJson(status: number, data: object, headers?: OutgoingHttpHeaders): Promise<MockedEndpoint>;
/**
* Deprecated alias for thenJson
* @deprecated
*/
thenJSON: (status: number, data: object, headers?: OutgoingHttpHeaders) => Promise<MockedEndpoint>;
/**
* Call the given callback for any matched requests that are received,

@@ -121,0 +144,0 @@ * and build a response from the result.

@@ -41,2 +41,7 @@ "use strict";

this.matchers = [];
/**
* Deprecated alias for thenJson
* @deprecated
*/
this.thenJSON = this.thenJson;
if (methodOrAddRule instanceof Function) {

@@ -90,2 +95,26 @@ this.matchers.push(new matchers_1.WildcardMatcherData());

/**
* Match only requests whose bodies exactly match the given
* object, when parsed as JSON.
*
* Note that this only tests that the body can be parsed
* as JSON - it doesn't require a content-type header.
*/
withJsonBody(json) {
this.matchers.push(new matchers_1.JsonBodyMatcherData(json));
return this;
}
/**
* Match only requests whose bodies match (contain equivalent
* values, ignoring extra values) the given object, when
* parsed as JSON. Matching behaviour is the same as Lodash's
* _.isMatch method.
*
* Note that this only tests that the body can be parsed
* as JSON - it doesn't require a content-type header.
*/
withJsonBodyIncluding(json) {
this.matchers.push(new matchers_1.JsonBodyFlexibleMatcherData(json));
return this;
}
/**
* Match only requests that include the given cookies

@@ -167,3 +196,3 @@ */

*/
thenJSON(status, data, headers = {}) {
thenJson(status, data, headers = {}) {
const defaultHeaders = { 'Content-Type': 'application/json' };

@@ -170,0 +199,0 @@ lodash_1.merge(defaultHeaders, headers);

/// <reference types="node" />
import http = require('http');
import { TlsRequest } from '../types';
import { DestroyableServer } from '../util/destroyable-server';

@@ -11,2 +12,8 @@ import { CAOptions } from '../util/tls';

}
declare module "tls" {
interface TLSSocket {
servername?: string;
initialRemoteAddress?: string;
}
}
export declare type ComboServerOptions = {

@@ -16,2 +23,2 @@ debug: boolean;

};
export declare function createComboServer(options: ComboServerOptions, requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void): Promise<DestroyableServer>;
export declare function createComboServer(options: ComboServerOptions, requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void, tlsClientErrorListener: (req: TlsRequest) => void): Promise<DestroyableServer>;

@@ -17,6 +17,52 @@ "use strict";

const socket_util_1 = require("../util/socket-util");
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
// sockets as soon as they're available, without waiting for the handshake to fully
// complete, so we can easily access them if the handshake fails.
const originalSocketInit = tls.TLSSocket.prototype._init;
tls.TLSSocket.prototype._init = function () {
originalSocketInit.apply(this, arguments);
const tlsSocket = this;
const loadSNI = tlsSocket._handle.oncertcb;
tlsSocket._handle.oncertcb = function (info) {
// Workaround for https://github.com/mscdex/httpolyglot/pull/11
if (tlsSocket.server.disableTlsHalfOpen)
tlsSocket.allowHalfOpen = false;
tlsSocket.initialRemoteAddress = tlsSocket._parent.remoteAddress;
tlsSocket.servername = info.servername;
return loadSNI.apply(this, arguments);
};
};
// Takes an established TLS socket, calls the error listener if it's silently closed
function ifTlsDropped(socket, errorCallback) {
new Promise((resolve, reject) => {
socket.once('data', resolve);
socket.once('close', reject);
socket.once('end', reject);
}).catch(() => {
// To get here, the socket must have connected & done the TLS handshake, but then
// closed/ended without ever sending any data. We can fairly confidently assume
// in that case that it's rejected our certificate.
errorCallback();
});
}
function getCauseFromError(error) {
const cause = (/alert certificate/.test(error.message) || /alert unknown ca/.test(error.message))
// The client explicitly told us it doesn't like the certificate
? 'cert-rejected'
: /no shared cipher/.test(error.message)
// The client refused to negotiate a cipher. Probably means it didn't like the
// cert so refused to continue, but it could genuinely not have a shared cipher.
? 'no-shared-cipher'
: (/ECONNRESET/.test(error.message) || error.code === 'ECONNRESET')
// The client sent no TLS alert, it just hard RST'd the connection
? 'reset'
: 'unknown'; // Something else.
if (cause === 'unknown')
console.log('Unknown TLS error:', error);
return cause;
}
// The low-level server that handles all the sockets & TLS. The server will correctly call the
// given handler for both HTTP & HTTPS direct connections, or connections when used as an
// either HTTP or HTTPS proxy, all on the same port.
function createComboServer(options, requestListener) {
function createComboServer(options, requestListener, tlsClientErrorListener) {
return __awaiter(this, void 0, void 0, function* () {

@@ -49,2 +95,19 @@ if (!options.https) {

}, requestListener);
// Used in our oncertcb monkeypatch above, as a workaround for https://github.com/mscdex/httpolyglot/pull/11
server.disableTlsHalfOpen = true;
server.on('tlsClientError', (error, socket) => {
// These only work because of oncertcb monkeypatch above
tlsClientErrorListener({
failureCause: getCauseFromError(error),
hostname: socket.servername,
remoteIpAddress: socket.initialRemoteAddress
});
});
server.on('secureConnection', (tlsSocket) => ifTlsDropped(tlsSocket, () => {
tlsClientErrorListener({
failureCause: 'closed',
hostname: tlsSocket.servername,
remoteIpAddress: tlsSocket.remoteAddress
});
}));
// If the server receives a HTTP/HTTPS CONNECT request, do some magic to proxy & intercept it

@@ -85,2 +148,29 @@ server.addListener('connect', (req, socket) => {

});
// Wait for:
// * connect, not dropped -> all good
// * _tlsError before connect -> cert rejected
// * sudden end before connect -> cert rejected
new Promise((resolve, reject) => {
tlsSocket.on('secure', () => {
resolve();
ifTlsDropped(tlsSocket, () => {
tlsClientErrorListener({
failureCause: 'closed',
hostname: targetHost,
remoteIpAddress: socket.remoteAddress
});
});
});
tlsSocket.on('_tlsError', (error) => {
reject(getCauseFromError(error));
});
tlsSocket.on('end', () => {
// Delay, so that simultaneous specific errors reject first
setTimeout(() => reject('closed'), 1);
});
}).catch((cause) => tlsClientErrorListener({
failureCause: cause,
hostname: targetHost,
remoteIpAddress: socket.remoteAddress
}));
// This is a little crazy, but only a little. We create a one-off server to handle HTTP parsing, but

@@ -87,0 +177,0 @@ // never listen on any ports or anything, we just hand it a live socket. Setup is pretty cheap here

/**
* @module Mockttp
*/
import { CompletedRequest, CompletedResponse } from "../types";
import { CompletedRequest, CompletedResponse, TlsRequest } from "../types";
import { MockRuleData } from "../rules/mock-rule-types";

@@ -34,5 +34,7 @@ import { Mockttp, AbstractMockttp, MockttpOptions } from "../mockttp";

on(event: 'abort', callback: (req: CompletedRequest) => void): Promise<void>;
on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise<void>;
private announceRequestAsync;
private announceResponseAsync;
private announceAbortAsync;
private announceTlsErrorAsync;
private handleRequest;

@@ -39,0 +41,0 @@ private isComplete;

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

https: this.httpsOptions
}, this.app);
}, this.app, this.announceTlsErrorAsync.bind(this));
this.server.listen(port);

@@ -178,2 +178,12 @@ // Handle websocket connections too (ignore for now, just forward on)

}
announceTlsErrorAsync(request) {
return __awaiter(this, void 0, void 0, function* () {
// We can get falsey but set hostname values - drop them
if (!request.hostname)
delete request.hostname;
if (this.debug)
console.warn(`TLS client error: ${JSON.stringify(request)}`);
this.eventEmitter.emit('tlsClientError', request);
});
}
handleRequest(rawRequest, rawResponse) {

@@ -180,0 +190,0 @@ return __awaiter(this, void 0, void 0, function* () {

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

const REQUEST_ABORTED_TOPIC = 'request-aborted';
const TLS_CLIENT_ERROR_TOPIC = 'tls-client-error';
function astToObject(ast) {

@@ -140,2 +141,7 @@ return _.zipObject(ast.fields.map((f) => f.name.value), ast.fields.map((f) => parseAnyAst(f.value)));

});
mockServer.on('tlsClientError', (request) => {
pubsub.publish(TLS_CLIENT_ERROR_TOPIC, {
failedTlsRequest: request
});
});
return Object.assign({ Query: {

@@ -171,2 +177,5 @@ mockedEndpoints: () => {

},
failedTlsRequest: {
subscribe: () => pubsub.asyncIterator(TLS_CLIENT_ERROR_TOPIC)
}
}, Request: {

@@ -173,0 +182,0 @@ body: (request) => {

@@ -38,2 +38,7 @@ /**

}
export declare type TlsRequest = {
hostname?: string;
remoteIpAddress: string;
failureCause: 'closed' | 'reset' | 'cert-rejected' | 'no-shared-cipher' | 'unknown';
};
export interface OngoingRequest extends Request, EventEmitter {

@@ -40,0 +45,0 @@ id: string;

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

@@ -69,3 +69,2 @@ "main": "dist/main.js",

"@types/source-map-support": "^0.4.0",
"@types/url-search-params": "^0.10.0",
"@types/uuid": "^3.4.0",

@@ -101,3 +100,2 @@ "@types/ws": "^5.1.2",

"typescript": "^3.4.3",
"url-search-params": "^0.10.0",
"webpack": "^3.7.1",

@@ -104,0 +102,0 @@ "ws": "^6.1.2"

@@ -1,2 +0,2 @@

# Mockttp [![Travis Build Status](https://img.shields.io/travis/httptoolkit/mockttp.svg)](https://travis-ci.org/httptoolkit/mockttp) [![Try Mockttp on RunKit](https://badge.runkitcdn.com/mockttp.svg)](https://npm.runkit.com/mockttp)
# 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)

@@ -3,0 +3,0 @@ > _Part of [HTTP Toolkit](https://httptoolkit.tech): powerful tools for building, testing & debugging HTTP(S)_

@@ -56,3 +56,3 @@ /**

type SubscribableEvent = 'request' | 'response' | 'abort';
type SubscribableEvent = 'request' | 'response' | 'abort' | 'tlsClientError';

@@ -62,3 +62,4 @@ const SUBSCRIBABLE_EVENTS: SubscribableEvent[] = [

'response',
'abort'
'abort',
'tlsClientError'
];

@@ -257,3 +258,6 @@

public on(event: SubscribableEvent, callback: (data: any) => void): Promise<void> {
// Ignore unknown events
if (!_.includes(SUBSCRIBABLE_EVENTS, event)) return Promise.resolve();
// Ignore subscription events, if not supported by the server
if (!this.typeHasField('Subscription', 'failedTlsRequest')) return Promise.resolve();

@@ -267,3 +271,4 @@ const standaloneStreamServer = this.mockServerOptions.standaloneServerUrl.replace(/^http/, 'ws');

response: 'responseCompleted',
abort: 'requestAborted'
abort: 'requestAborted',
tlsClientError: 'failedTlsRequest'
}[event];

@@ -323,2 +328,12 @@

},
tlsClientError: {
operationName: 'OnTlsClientError',
query: `subscription OnTlsClientError {
${queryResultName} {
failureCause
hostname
remoteIpAddress
}
}`
}
}[event];

@@ -336,3 +351,3 @@

data.timingEvents = JSON.parse(data.timingEvents);
} else {
} else if (event !== 'tlsClientError') {
data.timingEvents = {}; // For backward compat

@@ -339,0 +354,0 @@ }

@@ -7,3 +7,3 @@ /**

import MockRuleBuilder from "./rules/mock-rule-builder";
import { ProxyConfig, MockedEndpoint, Method, CompletedRequest, CompletedResponse } from "./types";
import { ProxyConfig, MockedEndpoint, Method, CompletedRequest, CompletedResponse, TlsRequest } from "./types";
import { MockRuleData } from "./rules/mock-rule-types";

@@ -168,2 +168,21 @@ import { CAOptions } from './util/tls';

on(event: 'abort', callback: (req: CompletedRequest) => void): Promise<void>;
/**
* Subscribe to hear about requests that start a TLS handshake, but fail to complete it.
* Not all clients report TLS errors explicitly, so this event fires for explicitly
* reported TLS errors, and for TLS connections that are immediately closed with no
* data sent.
*
* This is typically useful to detect clients who aren't correctly configured to trust
* the configured HTTPS certificate. The callback is given the host name provided
* by the client via SNI, if SNI was used (it almost always is).
*
* This is only useful in some niche use cases, such as logging all requests seen
* by the server, independently of the rules defined.
*
* The callback will be called asynchronously from request handling. This function
* returns a promise, and the callback is not guaranteed to be registered until
* the promise is resolved.
*/
on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise<void>;
}

@@ -170,0 +189,0 @@

@@ -178,3 +178,3 @@ /**

bodyToMatch.test(await request.body.asText())
, { explain: () => `for body matching /${this.regexString}/` });
, { explain: () => `with body matching /${this.regexString}/` });
}

@@ -184,2 +184,42 @@

export class JsonBodyMatcherData extends Serializable {
readonly type: 'json-body' = 'json-body';
constructor(
public body: {}
) {
super();
}
buildMatcher() {
return _.assign(async (request: OngoingRequest) => {
const receivedBody = await (request.body.asJson().catch(() => undefined));
if (receivedBody === undefined) return false;
else return _.isEqual(receivedBody, this.body)
}, { explain: () => `with ${JSON.stringify(this.body)} as a JSON body` });
}
}
export class JsonBodyFlexibleMatcherData extends Serializable {
readonly type: 'json-body-matching' = 'json-body-matching';
constructor(
public body: {}
) {
super();
}
buildMatcher() {
return _.assign(async (request: OngoingRequest) => {
const receivedBody = await (request.body.asJson().catch(() => undefined));
if (receivedBody === undefined) return false;
else return _.isMatch(receivedBody, this.body)
}, { explain: () => `with JSON body including ${JSON.stringify(this.body)}` });
}
}
export class CookieMatcherData extends Serializable {

@@ -224,2 +264,4 @@ readonly type: 'cookie' = 'cookie';

RegexBodyMatcherData |
JsonBodyMatcherData |
JsonBodyFlexibleMatcherData |
CookieMatcherData

@@ -238,2 +280,4 @@ );

'raw-body-regexp': RegexBodyMatcherData,
'json-body': JsonBodyMatcherData,
'json-body-matching': JsonBodyFlexibleMatcherData,
'cookie': CookieMatcherData,

@@ -240,0 +284,0 @@ };

@@ -37,3 +37,5 @@ /**

CookieMatcherData,
RegexBodyMatcherData
RegexBodyMatcherData,
JsonBodyMatcherData,
JsonBodyFlexibleMatcherData
} from "./matchers";

@@ -147,2 +149,32 @@

/**
* Match only requests whose bodies exactly match the given
* object, when parsed as JSON.
*
* Note that this only tests that the body can be parsed
* as JSON - it doesn't require a content-type header.
*/
withJsonBody(json: {}): MockRuleBuilder {
this.matchers.push(
new JsonBodyMatcherData(json)
);
return this;
}
/**
* Match only requests whose bodies match (contain equivalent
* values, ignoring extra values) the given object, when
* parsed as JSON. Matching behaviour is the same as Lodash's
* _.isMatch method.
*
* Note that this only tests that the body can be parsed
* as JSON - it doesn't require a content-type header.
*/
withJsonBodyIncluding(json: {}): MockRuleBuilder {
this.matchers.push(
new JsonBodyFlexibleMatcherData(json)
);
return this;
}
/**
* Match only requests that include the given cookies

@@ -232,3 +264,3 @@ */

*/
thenJSON(status: number, data: object, headers: OutgoingHttpHeaders = {}): Promise<MockedEndpoint> {
thenJson(status: number, data: object, headers: OutgoingHttpHeaders = {}): Promise<MockedEndpoint> {
const defaultHeaders = { 'Content-Type': 'application/json' };

@@ -247,2 +279,8 @@ merge(defaultHeaders, headers);

/**
* Deprecated alias for thenJson
* @deprecated
*/
thenJSON = this.thenJson;
/**
* Call the given callback for any matched requests that are received,

@@ -249,0 +287,0 @@ * and build a response from the result.

@@ -6,2 +6,4 @@ import _ = require('lodash');

import httpolyglot = require('httpolyglot');
import { TlsRequest } from '../types';
import destroyable, { DestroyableServer } from '../util/destroyable-server';

@@ -24,4 +26,67 @@ import { getCA, CAOptions } from '../util/tls';

declare module "tls" {
interface TLSSocket {
// This is a real field that actually exists - unclear why it's not
// in the type definitions.
servername?: string;
// We cache the initially set remote address on sockets, because it's cleared
// before the TLS error callback is called, exactly when we want to read it.
initialRemoteAddress?: string;
}
}
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
// sockets as soon as they're available, without waiting for the handshake to fully
// complete, so we can easily access them if the handshake fails.
const originalSocketInit = (<any>tls.TLSSocket.prototype)._init;
(<any>tls.TLSSocket.prototype)._init = function () {
originalSocketInit.apply(this, arguments);
const tlsSocket = this;
const loadSNI = tlsSocket._handle.oncertcb;
tlsSocket._handle.oncertcb = function (info: any) {
// Workaround for https://github.com/mscdex/httpolyglot/pull/11
if (tlsSocket.server.disableTlsHalfOpen) tlsSocket.allowHalfOpen = false;
tlsSocket.initialRemoteAddress = tlsSocket._parent.remoteAddress;
tlsSocket.servername = info.servername;
return loadSNI.apply(this, arguments);
};
};
export type ComboServerOptions = { debug: boolean, https?: CAOptions };
// Takes an established TLS socket, calls the error listener if it's silently closed
function ifTlsDropped(socket: tls.TLSSocket, errorCallback: () => void) {
new Promise((resolve, reject) => {
socket.once('data', resolve);
socket.once('close', reject);
socket.once('end', reject);
}).catch(() => {
// To get here, the socket must have connected & done the TLS handshake, but then
// closed/ended without ever sending any data. We can fairly confidently assume
// in that case that it's rejected our certificate.
errorCallback();
});
}
function getCauseFromError(error: Error & { code?: string }) {
const cause = (/alert certificate/.test(error.message) || /alert unknown ca/.test(error.message))
// The client explicitly told us it doesn't like the certificate
? 'cert-rejected'
: /no shared cipher/.test(error.message)
// The client refused to negotiate a cipher. Probably means it didn't like the
// cert so refused to continue, but it could genuinely not have a shared cipher.
? 'no-shared-cipher'
: (/ECONNRESET/.test(error.message) || error.code === 'ECONNRESET')
// The client sent no TLS alert, it just hard RST'd the connection
? 'reset'
: 'unknown'; // Something else.
if (cause === 'unknown') console.log('Unknown TLS error:', error);
return cause;
}
// The low-level server that handles all the sockets & TLS. The server will correctly call the

@@ -32,3 +97,4 @@ // given handler for both HTTP & HTTPS direct connections, or connections when used as an

options: ComboServerOptions,
requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void
requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void,
tlsClientErrorListener: (req: TlsRequest) => void
): Promise<DestroyableServer> {

@@ -63,2 +129,23 @@ if (!options.https) {

// Used in our oncertcb monkeypatch above, as a workaround for https://github.com/mscdex/httpolyglot/pull/11
(<any>server).disableTlsHalfOpen = true;
server.on('tlsClientError', (error: Error, socket: tls.TLSSocket) => {
// These only work because of oncertcb monkeypatch above
tlsClientErrorListener({
failureCause: getCauseFromError(error),
hostname: socket.servername,
remoteIpAddress: socket.initialRemoteAddress!
});
});
server.on('secureConnection', (tlsSocket: tls.TLSSocket) =>
ifTlsDropped(tlsSocket, () => {
tlsClientErrorListener({
failureCause: 'closed',
hostname: tlsSocket.servername,
remoteIpAddress: tlsSocket.remoteAddress!
});
})
);
// If the server receives a HTTP/HTTPS CONNECT request, do some magic to proxy & intercept it

@@ -102,2 +189,30 @@ server.addListener('connect', (req: http.IncomingMessage, socket: net.Socket) => {

// Wait for:
// * connect, not dropped -> all good
// * _tlsError before connect -> cert rejected
// * sudden end before connect -> cert rejected
new Promise((resolve, reject) => {
tlsSocket.on('secure', () => {
resolve();
ifTlsDropped(tlsSocket, () => {
tlsClientErrorListener({
failureCause: 'closed',
hostname: targetHost,
remoteIpAddress: socket.remoteAddress!
});
});
});
tlsSocket.on('_tlsError', (error) => {
reject(getCauseFromError(error));
});
tlsSocket.on('end', () => {
// Delay, so that simultaneous specific errors reject first
setTimeout(() => reject('closed'), 1);
});
}).catch((cause) => tlsClientErrorListener({
failureCause: cause,
hostname: targetHost,
remoteIpAddress: socket.remoteAddress!
}));
// This is a little crazy, but only a little. We create a one-off server to handle HTTP parsing, but

@@ -104,0 +219,0 @@ // never listen on any ports or anything, we just hand it a live socket. Setup is pretty cheap here

@@ -14,3 +14,3 @@ /**

import { OngoingRequest, CompletedRequest, CompletedResponse, OngoingResponse } from "../types";
import { OngoingRequest, CompletedRequest, CompletedResponse, OngoingResponse, TlsRequest } from "../types";
import { MockRuleData } from "../rules/mock-rule-types";

@@ -85,3 +85,3 @@ import { CAOptions } from '../util/tls';

https: this.httpsOptions
}, this.app);
}, this.app, this.announceTlsErrorAsync.bind(this));

@@ -164,2 +164,3 @@ this.server!.listen(port);

public on(event: 'abort', callback: (req: CompletedRequest) => void): Promise<void>;
public on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise<void>;
public on(event: string, callback: (...args: any[]) => void): Promise<void> {

@@ -201,2 +202,9 @@ this.eventEmitter.on(event, callback);

private async announceTlsErrorAsync(request: TlsRequest) {
// We can get falsey but set hostname values - drop them
if (!request.hostname) delete request.hostname;
if (this.debug) console.warn(`TLS client error: ${JSON.stringify(request)}`);
this.eventEmitter.emit('tlsClientError', request);
}
private async handleRequest(rawRequest: express.Request, rawResponse: express.Response) {

@@ -203,0 +211,0 @@ if (this.debug) console.log(`Handling request for ${rawRequest.url}`);

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

const REQUEST_ABORTED_TOPIC = 'request-aborted';
const TLS_CLIENT_ERROR_TOPIC = 'tls-client-error';

@@ -155,2 +156,8 @@ function astToObject<T>(ast: ObjectValueNode): T {

mockServer.on('tlsClientError', (request) => {
pubsub.publish(TLS_CLIENT_ERROR_TOPIC, {
failedTlsRequest: request
})
});
return <any> {

@@ -194,2 +201,5 @@ Query: {

},
failedTlsRequest: {
subscribe: () => pubsub.asyncIterator(TLS_CLIENT_ERROR_TOPIC)
}
},

@@ -196,0 +206,0 @@

@@ -51,2 +51,8 @@ /**

export type TlsRequest = {
hostname?: string;
remoteIpAddress: string;
failureCause: 'closed' | 'reset' | 'cert-rejected' | 'no-shared-cipher' | 'unknown'
};
export interface OngoingRequest extends Request, EventEmitter {

@@ -53,0 +59,0 @@ id: string;

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 too big to display

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

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

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

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

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

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

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

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

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