Socket
Socket
Sign inDemoInstall

mockttp

Package Overview
Dependencies
38
Maintainers
1
Versions
120
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 3.4.0 to 3.5.0

9

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

@@ -13,4 +13,8 @@ // There's a few places where we attach extra data to some node objects during

// vice versa) all on one socket, this is the value for the final hop.
lastHopEncrypted?: boolean;
__lastHopEncrypted?: boolean;
// For CONNECT-based socket tunnels, this is the address that was listed in the
// last layer of the tunnelling so far.
__lastHopConnectAddress?: string;
// Normally only defined on TLSSocket, but useful to explicitly include here

@@ -104,5 +108,6 @@ // Undefined on plain HTTP, 'true' on TLSSocket.

// Treated the same as net.Socket, when we unwrap them in our combo server:
lastHopEncrypted?: net.Socket['lastHopEncrypted'];
__lastHopEncrypted?: net.Socket['__lastHopEncrypted'];
__lastHopConnectAddress?: net.Socket['__lastHopConnectAddress'];
__timingInfo?: net.Socket['__timingInfo'];
}
}

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

const DuplexPair = require("native-duplexpair");
const destroyable_server_1 = require("../util/destroyable-server");
const destroyable_server_1 = require("destroyable-server");
const error_1 = require("../util/error");

@@ -207,3 +207,3 @@ const promise_1 = require("../util/promise");

yield new Promise((resolve, reject) => {
this.server = (0, destroyable_server_1.destroyable)(this.app.listen(listenOptions, resolve));
this.server = (0, destroyable_server_1.makeDestroyable)(this.app.listen(listenOptions, resolve));
this.server.on('error', reject);

@@ -210,0 +210,0 @@ this.server.on('upgrade', (req, socket, head) => __awaiter(this, void 0, void 0, function* () {

@@ -25,2 +25,4 @@ "use strict";

const REQUEST_ABORTED_TOPIC = 'request-aborted';
const TLS_PASSTHROUGH_OPENED_TOPIC = 'tls-passthrough-opened';
const TLS_PASSTHROUGH_CLOSED_TOPIC = 'tls-passthrough-closed';
const TLS_CLIENT_ERROR_TOPIC = 'tls-client-error';

@@ -83,4 +85,4 @@ const CLIENT_ERROR_TOPIC = 'client-error';

requestAborted: Object.assign(evt, {
// Backward compat: old clients expect this to be present. In future this can be removed
// and abort events can switch from Request to InitiatedRequest in the schema.
// Backward compat: old clients expect this to be present. In future this can be
// removed and abort events can lose the 'body' in the schema.
body: Buffer.alloc(0)

@@ -90,2 +92,12 @@ })

});
mockServer.on('tls-passthrough-opened', (evt) => {
pubsub.publish(TLS_PASSTHROUGH_OPENED_TOPIC, {
tlsPassthroughOpened: evt
});
});
mockServer.on('tls-passthrough-closed', (evt) => {
pubsub.publish(TLS_PASSTHROUGH_CLOSED_TOPIC, {
tlsPassthroughClosed: evt
});
});
mockServer.on('tls-client-error', (evt) => {

@@ -171,2 +183,8 @@ pubsub.publish(TLS_CLIENT_ERROR_TOPIC, {

},
tlsPassthroughOpened: {
subscribe: () => pubsub.asyncIterator(TLS_PASSTHROUGH_OPENED_TOPIC)
},
tlsPassthroughClosed: {
subscribe: () => pubsub.asyncIterator(TLS_PASSTHROUGH_CLOSED_TOPIC)
},
failedTlsRequest: {

@@ -173,0 +191,0 @@ subscribe: () => pubsub.asyncIterator(TLS_CLIENT_ERROR_TOPIC)

@@ -32,4 +32,6 @@ "use strict";

webSocketClose: WebSocketClose!
requestAborted: Request!
failedTlsRequest: TlsRequest!
requestAborted: AbortedRequest!
tlsPassthroughOpened: TlsPassthroughEvent!
tlsPassthroughClosed: TlsPassthroughEvent!
failedTlsRequest: TlsHandshakeFailure!
failedClientRequest: ClientError!

@@ -61,4 +63,27 @@ }

type TlsPassthroughEvent {
id: String!
upstreamPort: Int!
hostname: String
remoteIpAddress: String!
remotePort: Int!
tags: [String!]!
timingEvents: Json!
}
type TlsHandshakeFailure {
failureCause: String!
hostname: String
remoteIpAddress: String!
remotePort: Int!
tags: [String!]!
timingEvents: Json!
}
# Old name for TlsHandshakeFailure, kept for backward compat
type TlsRequest {
failureCause: String!
hostname: String

@@ -133,2 +158,25 @@ remoteIpAddress: String!

type AbortedRequest {
id: ID!
timingEvents: Json!
tags: [String!]!
matchedRuleId: ID
protocol: String!
httpVersion: String!
method: String!
url: String!
path: String!
remoteIpAddress: String!
remotePort: Int!
hostname: String
headers: Json!
rawHeaders: Json!
body: Buffer!
error: Json
}
type Response {

@@ -135,0 +183,0 @@ id: ID!

@@ -321,4 +321,29 @@ "use strict";

${this.schema.asOptionalField('Request', 'tags')}
${this.schema.asOptionalField('AbortedRequest', 'error')}
}
}`,
'tls-passthrough-opened': (0, graphql_tag_1.default) `subscription OnTlsPassthroughOpened {
tlsPassthroughOpened {
id
upstreamPort
hostname
remoteIpAddress
remotePort
tags
timingEvents
}
}`,
'tls-passthrough-closed': (0, graphql_tag_1.default) `subscription OnTlsPassthroughClosed {
tlsPassthroughClosed {
id
upstreamPort
hostname
remoteIpAddress
remotePort
tags
timingEvents
}
}`,
'tls-client-error': (0, graphql_tag_1.default) `subscription OnTlsClientError {

@@ -329,5 +354,5 @@ failedTlsRequest {

remoteIpAddress
${this.schema.asOptionalField('TlsRequest', 'remotePort')}
${this.schema.asOptionalField('TlsRequest', 'tags')}
${this.schema.asOptionalField('TlsRequest', 'timingEvents')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'remotePort')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'tags')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'timingEvents')}
}

@@ -391,2 +416,6 @@ }`,

}
else if (event === 'abort') {
normalizeHttpMessage(data, event);
data.error = data.error ? JSON.parse(data.error) : undefined;
}
else {

@@ -393,0 +422,0 @@ normalizeHttpMessage(data, event);

@@ -5,6 +5,7 @@ export declare class SchemaIntrospector {

queryTypeDefined(queryType: string): boolean;
isTypeDefined(typeName: string): boolean;
typeHasField(typeName: string, fieldName: string): boolean;
asOptionalField(typeName: string, fieldName: string): string;
asOptionalField(typeName: string | string[], fieldName: string): string;
typeHasInputField(typeName: string, fieldName: string): boolean;
}
export declare const introspectionQuery = "\n query IntrospectionQuery {\n __schema {\n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n ...FullType\n }\n directives {\n name\n locations\n args {\n ...InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n fields(includeDeprecated: true) {\n name\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n }\n\n fragment InputValue on __InputValue {\n name\n type { ...TypeRef }\n defaultValue\n }\n\n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n";

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

}
isTypeDefined(typeName) {
return _.some(this.adminServerSchema.types, { name: typeName });
}
typeHasField(typeName, fieldName) {

@@ -20,3 +23,7 @@ const type = _.find(this.adminServerSchema.types, { name: typeName });

asOptionalField(typeName, fieldName) {
return (this.typeHasField(typeName, fieldName))
const possibleNames = !Array.isArray(typeName) ? [typeName] : typeName;
const firstAvailableName = possibleNames.find((name) => this.isTypeDefined(name));
if (!firstAvailableName)
return '';
return (this.typeHasField(firstAvailableName, fieldName))
? fieldName

@@ -23,0 +30,0 @@ : '';

@@ -23,2 +23,3 @@ import { Mockttp, MockttpOptions } from "./mockttp";

'close-connection': typeof requestHandlerDefinitions.CloseConnectionHandlerDefinition;
'reset-connection': typeof requestHandlerDefinitions.ResetConnectionHandlerDefinition;
timeout: typeof requestHandlerDefinitions.TimeoutHandlerDefinition;

@@ -38,2 +39,3 @@ 'json-rpc-response': typeof requestHandlerDefinitions.JsonRpcResponseHandlerDefinition;

'close-connection': typeof requestHandlerDefinitions.CloseConnectionHandlerDefinition;
'reset-connection': typeof requestHandlerDefinitions.ResetConnectionHandlerDefinition;
timeout: typeof requestHandlerDefinitions.TimeoutHandlerDefinition;

@@ -40,0 +42,0 @@ };

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

import { Mockttp, MockttpOptions, SubscribableEvent, PortRange } from "./mockttp";
import { Mockttp, MockttpOptions, MockttpHttpsOptions, SubscribableEvent, PortRange } from "./mockttp";
import { MockttpServer } from "./server/mockttp-server";

@@ -6,3 +6,5 @@ import { MockttpClientOptions } from "./client/mockttp-client";

export * from "./types";
export type { Mockttp, MockttpServer, MockttpAdminServer, MockttpOptions, MockttpClientOptions, MockttpAdminServerOptions, SubscribableEvent, PortRange };
export type { Mockttp, MockttpServer, MockttpAdminServer, MockttpOptions, MockttpHttpsOptions, MockttpClientOptions, MockttpAdminServerOptions, SubscribableEvent, PortRange };
export type { TlsHandshakeFailure as TlsRequest } from './types';
export type { CertDataOptions as HttpsOptions, CertPathOptions as HttpsPathOptions } from './util/tls';
import * as matchers from './rules/matchers';

@@ -24,3 +26,3 @@ import * as requestHandlers from './rules/requests/request-handlers';

export { generateCACertificate, generateSPKIFingerprint } from './util/tls';
export type { CAOptions, PEM, HttpsOptions, HttpsPathOptions } from './util/tls';
export type { CAOptions, PEM, CertDataOptions, CertPathOptions } from './util/tls';
export type { CachedDns, DnsLookupFunction } from './util/dns';

@@ -27,0 +29,0 @@ export type { Serialized, SerializedValue } from './serialization/serialization';

@@ -5,3 +5,3 @@ import * as cors from 'cors';

import { WebSocketRuleBuilder } from "./rules/websockets/websocket-rule-builder";
import { ProxyEnvConfig, MockedEndpoint, CompletedRequest, CompletedResponse, TlsRequest, InitiatedRequest, ClientError, WebSocketMessage, WebSocketClose } from "./types";
import { ProxyEnvConfig, MockedEndpoint, CompletedRequest, CompletedResponse, TlsPassthroughEvent, TlsHandshakeFailure, InitiatedRequest, ClientError, WebSocketMessage, WebSocketClose, AbortedRequest } from "./types";
import type { RequestRuleData } from "./rules/requests/request-rule";

@@ -431,4 +431,32 @@ import type { WebSocketRuleData } from "./rules/websockets/websocket-rule";

*/
on(event: 'abort', callback: (req: InitiatedRequest) => void): Promise<void>;
on(event: 'abort', callback: (req: AbortedRequest) => void): Promise<void>;
/**
* Subscribe to hear about TLS connections that are passed through the proxy without
* interception, due to the `tlsPassthrough` HTTPS option.
*
* 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 connection handling. This function
* returns a promise, and the callback is not guaranteed to be registered until
* the promise is resolved.
*
* @category Events
*/
on(event: 'tls-passthrough-opened', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
/**
* Subscribe to hear about closure of TLS connections that were passed through the
* proxy without interception, due to the `tlsPassthrough` HTTPS option.
*
* 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 connection handling. This function
* returns a promise, and the callback is not guaranteed to be registered until
* the promise is resolved.
*
* @category Events
*/
on(event: 'tls-passthrough-closed', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
/**
* Subscribe to hear about requests that start a TLS handshake, but fail to complete it.

@@ -452,3 +480,3 @@ * Not all clients report TLS errors explicitly, so this event fires for explicitly

*/
on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise<void>;
on(event: 'tls-client-error', callback: (req: TlsHandshakeFailure) => void): Promise<void>;
/**

@@ -577,2 +605,25 @@ * Subscribe to hear about requests that fail before successfully sending their

}
export declare type MockttpHttpsOptions = CAOptions & {
/**
* The domain name that will be used in the certificate for incoming TLS
* connections which don't use SNI to request a specific domain.
*/
defaultDomain?: string;
/**
* A list of hostnames where TLS interception should always be skipped.
*
* When a TLS connection is started that references a matching hostname in its
* server name indication (SNI) extension, or which uses a matching hostname
* in a preceeding CONNECT request to create a tunnel, the connection will be
* sent raw to the upstream hostname, without handling TLS within Mockttp (i.e.
* with no TLS interception performed).
*
* Each element in the list must be an object with a 'hostname' field for the
* hostname that should be matched. In future more options may be supported
* here for additional configuration of this behaviour.
*/
tlsPassthrough?: Array<{
hostname: string;
}>;
};
export interface MockttpOptions {

@@ -598,3 +649,3 @@ /**

*/
https?: CAOptions;
https?: MockttpHttpsOptions;
/**

@@ -646,3 +697,3 @@ * Should HTTP/2 be enabled? Can be true, false, or 'fallback'. If true,

}
export declare type SubscribableEvent = 'request-initiated' | 'request' | 'response' | 'websocket-request' | 'websocket-accepted' | 'websocket-message-received' | 'websocket-message-sent' | 'websocket-close' | 'abort' | 'tls-client-error' | 'client-error';
export declare type SubscribableEvent = 'request-initiated' | 'request' | 'response' | 'websocket-request' | 'websocket-accepted' | 'websocket-message-received' | 'websocket-message-sent' | 'websocket-close' | 'abort' | 'tls-passthrough-opened' | 'tls-passthrough-closed' | 'tls-client-error' | 'client-error';
/**

@@ -649,0 +700,0 @@ * @hidden

@@ -92,3 +92,3 @@ /// <reference types="node" />

}
export declare type CallbackResponseResult = CallbackResponseMessageResult | 'close';
export declare type CallbackResponseResult = CallbackResponseMessageResult | 'close' | 'reset';
/**

@@ -338,2 +338,26 @@ * Can be returned from callbacks to define parts of a response, or

/**
* Whether to simulate connection errors back to the client.
*
* By default (in most cases - see below) when an upstream request fails
* outright a 502 "Bad Gateway" response is sent to the downstream client,
* explicitly indicating the failure and containing the error that caused
* the issue in the response body.
*
* Only in the case of upstream connection reset errors is a connection reset
* normally sent back downstream to existing clients (this behaviour exists
* for backward compatibility, and will change to match other error behaviour
* in a future version).
*
* When this option is set to `true`, low-level connection failures will
* always trigger a downstream connection close/reset, rather than a 502
* response.
*
* This includes DNS failures, TLS connection errors, TCP connection resets,
* etc (but not HTTP non-200 responses, which are still proxied as normal).
* This is less convenient for debugging in a testing environment or when
* using a proxy intentionally, but can be more accurate when trying to
* transparently proxy network traffic, errors and all.
*/
simulateConnectionErrors?: boolean;
/**
* A set of data to automatically transform a request. This includes properties

@@ -516,2 +540,3 @@ * to support many transformation common use cases.

lookupOptions?: PassThroughLookupOptions;
simulateConnectionErrors?: boolean;
transformRequest?: Replace<RequestTransform, {

@@ -586,2 +611,3 @@ 'replaceBody'?: string;

readonly lookupOptions?: PassThroughLookupOptions;
readonly simulateConnectionErrors: boolean;
protected outgoingSockets: Set<net.Socket>;

@@ -599,2 +625,6 @@ constructor(options?: PassThroughHandlerOptions);

}
export declare class ResetConnectionHandlerDefinition extends Serializable implements RequestHandlerDefinition {
readonly type = "reset-connection";
explain(): string;
}
export declare class TimeoutHandlerDefinition extends Serializable implements RequestHandlerDefinition {

@@ -629,4 +659,5 @@ readonly type = "timeout";

'close-connection': typeof CloseConnectionHandlerDefinition;
'reset-connection': typeof ResetConnectionHandlerDefinition;
timeout: typeof TimeoutHandlerDefinition;
'json-rpc-response': typeof JsonRpcResponseHandlerDefinition;
};

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

Object.defineProperty(exports, "__esModule", { value: true });
exports.HandlerDefinitionLookup = exports.JsonRpcResponseHandlerDefinition = exports.TimeoutHandlerDefinition = exports.CloseConnectionHandlerDefinition = exports.PassThroughHandlerDefinition = exports.SERIALIZED_OMIT = exports.FileHandlerDefinition = exports.StreamHandlerDefinition = exports.CallbackHandlerDefinition = exports.SimpleHandlerDefinition = void 0;
exports.HandlerDefinitionLookup = exports.JsonRpcResponseHandlerDefinition = exports.TimeoutHandlerDefinition = exports.ResetConnectionHandlerDefinition = exports.CloseConnectionHandlerDefinition = exports.PassThroughHandlerDefinition = exports.SERIALIZED_OMIT = exports.FileHandlerDefinition = exports.StreamHandlerDefinition = exports.CallbackHandlerDefinition = exports.SimpleHandlerDefinition = void 0;
const _ = require("lodash");

@@ -189,2 +189,3 @@ const url = require("url");

this.proxyConfig = options.proxyConfig;
this.simulateConnectionErrors = !!options.simulateConnectionErrors;
this.clientCertificateHostMap = options.clientCertificateHostMap || {};

@@ -278,3 +279,3 @@ this.extraCACertificates = options.trustAdditionalCAs || [];

forwarding: this.forwarding
} : {}), { proxyConfig: (0, serialization_1.serializeProxyConfig)(this.proxyConfig, channel), lookupOptions: this.lookupOptions, ignoreHostCertificateErrors: this.ignoreHostHttpsErrors, extraCACertificates: this.extraCACertificates.map((certObject) => {
} : {}), { proxyConfig: (0, serialization_1.serializeProxyConfig)(this.proxyConfig, channel), lookupOptions: this.lookupOptions, simulateConnectionErrors: this.simulateConnectionErrors, ignoreHostCertificateErrors: this.ignoreHostHttpsErrors, extraCACertificates: this.extraCACertificates.map((certObject) => {
// We use toString to make sure that buffers always end up as

@@ -338,2 +339,12 @@ // as UTF-8 string, to avoid serialization issues. Strings are an

exports.CloseConnectionHandlerDefinition = CloseConnectionHandlerDefinition;
class ResetConnectionHandlerDefinition extends serialization_1.Serializable {
constructor() {
super(...arguments);
this.type = 'reset-connection';
}
explain() {
return 'reset the connection';
}
}
exports.ResetConnectionHandlerDefinition = ResetConnectionHandlerDefinition;
class TimeoutHandlerDefinition extends serialization_1.Serializable {

@@ -373,2 +384,3 @@ constructor() {

'close-connection': CloseConnectionHandlerDefinition,
'reset-connection': ResetConnectionHandlerDefinition,
'timeout': TimeoutHandlerDefinition,

@@ -375,0 +387,0 @@ 'json-rpc-response': JsonRpcResponseHandlerDefinition

@@ -5,5 +5,7 @@ import { TypedError } from 'typed-error';

import { RuleParameters } from '../rule-parameters';
import { CallbackHandlerDefinition, CallbackRequestResult, CallbackResponseMessageResult, CallbackResponseResult, CloseConnectionHandlerDefinition, FileHandlerDefinition, ForwardingOptions, HandlerDefinitionLookup, JsonRpcResponseHandlerDefinition, PassThroughHandlerDefinition, PassThroughHandlerOptions, PassThroughLookupOptions, PassThroughResponse, RequestHandlerDefinition, RequestTransform, ResponseTransform, SerializedCallbackHandlerData, SerializedPassThroughData, SerializedStreamHandlerData, SimpleHandlerDefinition, StreamHandlerDefinition, TimeoutHandlerDefinition } from './request-handler-definitions';
import { CallbackHandlerDefinition, CallbackRequestResult, CallbackResponseMessageResult, CallbackResponseResult, CloseConnectionHandlerDefinition, FileHandlerDefinition, ForwardingOptions, HandlerDefinitionLookup, JsonRpcResponseHandlerDefinition, PassThroughHandlerDefinition, PassThroughHandlerOptions, PassThroughLookupOptions, PassThroughResponse, RequestHandlerDefinition, RequestTransform, ResetConnectionHandlerDefinition, ResponseTransform, SerializedCallbackHandlerData, SerializedPassThroughData, SerializedStreamHandlerData, SimpleHandlerDefinition, StreamHandlerDefinition, TimeoutHandlerDefinition } from './request-handler-definitions';
export { CallbackRequestResult, CallbackResponseMessageResult, CallbackResponseResult, ForwardingOptions, PassThroughResponse, PassThroughHandlerOptions, PassThroughLookupOptions, RequestTransform, ResponseTransform };
export declare class AbortError extends TypedError {
readonly code?: string | undefined;
constructor(message: string, code?: string | undefined);
}

@@ -47,2 +49,10 @@ export interface RequestHandler extends RequestHandlerDefinition {

}
export declare class ResetConnectionHandler extends ResetConnectionHandlerDefinition {
constructor();
handle(request: OngoingRequest): Promise<void>;
/**
* @internal
*/
static deserialize(): ResetConnectionHandler;
}
export declare class TimeoutHandler extends TimeoutHandlerDefinition {

@@ -49,0 +59,0 @@ handle(): Promise<void>;

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

Object.defineProperty(exports, "__esModule", { value: true });
exports.HandlerLookup = exports.JsonRpcResponseHandler = exports.TimeoutHandler = exports.CloseConnectionHandler = exports.PassThroughHandler = exports.FileHandler = exports.StreamHandler = exports.CallbackHandler = exports.SimpleHandler = exports.AbortError = void 0;
exports.HandlerLookup = exports.JsonRpcResponseHandler = exports.TimeoutHandler = exports.ResetConnectionHandler = exports.CloseConnectionHandler = exports.PassThroughHandler = exports.FileHandler = exports.StreamHandler = exports.CallbackHandler = exports.SimpleHandler = exports.AbortError = void 0;
const _ = require("lodash");
const url = require("url");
const net = require("net");
const tls = require("tls");

@@ -41,2 +42,6 @@ const http = require("http");

class AbortError extends typed_error_1.TypedError {
constructor(message, code) {
super(message);
this.code = code;
}
}

@@ -97,4 +102,8 @@ exports.AbortError = AbortError;

request.socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
}
else if (outResponse === 'reset') {
(0, socket_util_1.resetSocket)(request.socket);
throw new AbortError('Connection reset intentionally by rule');
}
else {

@@ -392,4 +401,9 @@ yield writeResponseFromCallback(outResponse, response);

socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
}
else if (modifiedReq.response === 'reset') {
const socket = clientReq.socket;
(0, socket_util_1.resetSocket)(socket);
throw new AbortError('Connection reset intentionally by rule');
}
else {

@@ -586,4 +600,10 @@ // The callback has provided a full response: don't passthrough at all, just use it.

clientRes.socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
}
else if (modifiedRes === 'reset') {
// Dump the real response data and kill the client socket:
serverRes.resume();
(0, socket_util_1.resetSocket)(clientRes.socket);
throw new AbortError('Connection reset intentionally by rule');
}
validateCustomHeaders(serverHeaders, modifiedRes === null || modifiedRes === void 0 ? void 0 : modifiedRes.headers);

@@ -673,3 +693,3 @@ serverStatusCode = (modifiedRes === null || modifiedRes === void 0 ? void 0 : modifiedRes.statusCode) ||

serverReq.on('error', (e) => {
var _a;
var _a, _b;
if (serverReq.aborted)

@@ -684,7 +704,12 @@ return;

clientRes.tags.push('passthrough-error:' + e.code);
if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED') {
if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED' || this.simulateConnectionErrors) {
// The upstream socket closed: forcibly close the downstream stream to match
const socket = clientReq.socket;
socket.destroy();
reject(new AbortError('Upstream connection was reset'));
if ('resetAndDestroy' in socket) {
socket.resetAndDestroy();
}
else {
socket.destroy();
}
reject(new AbortError(`Upstream connection error: ${(_b = e.message) !== null && _b !== void 0 ? _b : e}`, e.code));
}

@@ -771,3 +796,3 @@ else {

forwarding: { targetHost: data.forwardToLocation }
} : {}), { forwarding: data.forwarding, lookupOptions: data.lookupOptions, ignoreHostHttpsErrors: data.ignoreHostCertificateErrors, trustAdditionalCAs: data.extraCACertificates, clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap, ({ pfx, passphrase }) => ({ pfx: (0, serialization_1.deserializeBuffer)(pfx), passphrase })) }));
} : {}), { forwarding: data.forwarding, lookupOptions: data.lookupOptions, simulateConnectionErrors: !!data.simulateConnectionErrors, ignoreHostHttpsErrors: data.ignoreHostCertificateErrors, trustAdditionalCAs: data.extraCACertificates, clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap, ({ pfx, passphrase }) => ({ pfx: (0, serialization_1.deserializeBuffer)(pfx), passphrase })) }));
}

@@ -781,3 +806,3 @@ }

socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
});

@@ -787,2 +812,27 @@ }

exports.CloseConnectionHandler = CloseConnectionHandler;
class ResetConnectionHandler extends request_handler_definitions_1.ResetConnectionHandlerDefinition {
constructor() {
super();
if (!net.Socket.prototype.resetAndDestroy) {
throw new Error('Reset handlers are only supported in Node v16.17+, v18.3.0+, or later');
}
}
handle(request) {
return __awaiter(this, void 0, void 0, function* () {
const socket = request.socket;
socket.resetAndDestroy();
throw new AbortError('Connection reset intentionally by rule');
});
}
/**
* @internal
*/
static deserialize() {
if (!net.Socket.prototype.resetAndDestroy) {
throw new Error('Reset handlers are only supported in Node v16.17+, v18.3.0+, or later');
}
return new ResetConnectionHandler();
}
}
exports.ResetConnectionHandler = ResetConnectionHandler;
class TimeoutHandler extends request_handler_definitions_1.TimeoutHandlerDefinition {

@@ -820,2 +870,3 @@ handle() {

'close-connection': CloseConnectionHandler,
'reset-connection': ResetConnectionHandler,
'timeout': TimeoutHandler,

@@ -822,0 +873,0 @@ 'json-rpc-response': JsonRpcResponseHandler

@@ -206,2 +206,21 @@ /// <reference types="node" />

/**
* Reset connections that match this rule immediately, sending a TCP
* RST packet directly, without any status code or response, and without
* cleanly closing the TCP connection.
*
* This is only supported in Node.js versions (>=16.17, >=18.3.0, or
* later), where `net.Socket` includes the `resetAndDestroy` method.
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
* This method returns a promise that resolves with a mocked endpoint.
* Wait for the promise to confirm that the rule has taken effect
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*
* @category Responses
*/
thenResetConnection(): Promise<MockedEndpoint>;
/**
* Hold open connections that match this rule, but never respond

@@ -208,0 +227,0 @@ * with anything at all, typically causing a timeout on the client side.

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

/**
* Reset connections that match this rule immediately, sending a TCP
* RST packet directly, without any status code or response, and without
* cleanly closing the TCP connection.
*
* This is only supported in Node.js versions (>=16.17, >=18.3.0, or
* later), where `net.Socket` includes the `resetAndDestroy` method.
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
* This method returns a promise that resolves with a mocked endpoint.
* Wait for the promise to confirm that the rule has taken effect
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*
* @category Responses
*/
thenResetConnection() {
const rule = Object.assign(Object.assign({}, this.buildBaseRuleData()), { handler: new request_handler_definitions_1.ResetConnectionHandlerDefinition() });
return this.addRule(rule);
}
/**
* Hold open connections that match this rule, but never respond

@@ -242,0 +264,0 @@ * with anything at all, typically causing a timeout on the client side.

/// <reference types="node" />
import { ClientServerChannel, Serializable, SerializedProxyConfig } from "../../serialization/serialization";
import { Explainable, Headers } from "../../types";
import { CloseConnectionHandlerDefinition, TimeoutHandlerDefinition, ForwardingOptions, PassThroughLookupOptions } from '../requests/request-handler-definitions';
import { CloseConnectionHandlerDefinition, ResetConnectionHandlerDefinition, TimeoutHandlerDefinition, ForwardingOptions, PassThroughLookupOptions } from '../requests/request-handler-definitions';
import { ProxyConfig } from '../proxy-config';

@@ -112,3 +112,3 @@ export interface WebSocketHandlerDefinition extends Explainable, Serializable {

}
export { CloseConnectionHandlerDefinition, TimeoutHandlerDefinition };
export { CloseConnectionHandlerDefinition, ResetConnectionHandlerDefinition, TimeoutHandlerDefinition };
export declare const WsHandlerDefinitionLookup: {

@@ -120,3 +120,4 @@ 'ws-passthrough': typeof PassThroughWebSocketHandlerDefinition;

'close-connection': typeof CloseConnectionHandlerDefinition;
'reset-connection': typeof ResetConnectionHandlerDefinition;
timeout: typeof TimeoutHandlerDefinition;
};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WsHandlerDefinitionLookup = exports.TimeoutHandlerDefinition = exports.CloseConnectionHandlerDefinition = exports.RejectWebSocketHandlerDefinition = exports.ListenWebSocketHandlerDefinition = exports.EchoWebSocketHandlerDefinition = exports.PassThroughWebSocketHandlerDefinition = void 0;
exports.WsHandlerDefinitionLookup = exports.TimeoutHandlerDefinition = exports.ResetConnectionHandlerDefinition = exports.CloseConnectionHandlerDefinition = exports.RejectWebSocketHandlerDefinition = exports.ListenWebSocketHandlerDefinition = exports.EchoWebSocketHandlerDefinition = exports.PassThroughWebSocketHandlerDefinition = void 0;
const url = require("url");

@@ -9,2 +9,3 @@ const common_tags_1 = require("common-tags");

Object.defineProperty(exports, "CloseConnectionHandlerDefinition", { enumerable: true, get: function () { return request_handler_definitions_1.CloseConnectionHandlerDefinition; } });
Object.defineProperty(exports, "ResetConnectionHandlerDefinition", { enumerable: true, get: function () { return request_handler_definitions_1.ResetConnectionHandlerDefinition; } });
Object.defineProperty(exports, "TimeoutHandlerDefinition", { enumerable: true, get: function () { return request_handler_definitions_1.TimeoutHandlerDefinition; } });

@@ -109,4 +110,5 @@ class PassThroughWebSocketHandlerDefinition extends serialization_1.Serializable {

'close-connection': request_handler_definitions_1.CloseConnectionHandlerDefinition,
'reset-connection': request_handler_definitions_1.ResetConnectionHandlerDefinition,
'timeout': request_handler_definitions_1.TimeoutHandlerDefinition
};
//# sourceMappingURL=websocket-handler-definitions.js.map

@@ -6,3 +6,3 @@ /// <reference types="node" />

import { OngoingRequest } from "../../types";
import { CloseConnectionHandler, TimeoutHandler } from '../requests/request-handlers';
import { CloseConnectionHandler, ResetConnectionHandler, TimeoutHandler } from '../requests/request-handlers';
import { RuleParameters } from '../rule-parameters';

@@ -41,3 +41,3 @@ import { EchoWebSocketHandlerDefinition, ListenWebSocketHandlerDefinition, PassThroughWebSocketHandlerDefinition, PassThroughWebSocketHandlerOptions, RejectWebSocketHandlerDefinition, SerializedPassThroughWebSocketData, WebSocketHandlerDefinition, WsHandlerDefinitionLookup } from './websocket-handler-definitions';

}
export { CloseConnectionHandler, TimeoutHandler };
export { CloseConnectionHandler, ResetConnectionHandler, TimeoutHandler };
export declare const WsHandlerLookup: typeof WsHandlerDefinitionLookup;

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

Object.defineProperty(exports, "__esModule", { value: true });
exports.WsHandlerLookup = exports.TimeoutHandler = exports.CloseConnectionHandler = exports.RejectWebSocketHandler = exports.ListenWebSocketHandler = exports.EchoWebSocketHandler = exports.PassThroughWebSocketHandler = void 0;
exports.WsHandlerLookup = exports.TimeoutHandler = exports.ResetConnectionHandler = exports.CloseConnectionHandler = exports.RejectWebSocketHandler = exports.ListenWebSocketHandler = exports.EchoWebSocketHandler = exports.PassThroughWebSocketHandler = void 0;
const _ = require("lodash");

@@ -22,2 +22,3 @@ const url = require("url");

Object.defineProperty(exports, "CloseConnectionHandler", { enumerable: true, get: function () { return request_handlers_1.CloseConnectionHandler; } });
Object.defineProperty(exports, "ResetConnectionHandler", { enumerable: true, get: function () { return request_handlers_1.ResetConnectionHandler; } });
Object.defineProperty(exports, "TimeoutHandler", { enumerable: true, get: function () { return request_handlers_1.TimeoutHandler; } });

@@ -217,6 +218,6 @@ const request_utils_1 = require("../../util/request-utils");

[hostname, port] = hostHeader.split(':');
// lastHopEncrypted is set in http-combo-server, for requests that have explicitly
// __lastHopEncrypted is set in http-combo-server, for requests that have explicitly
// CONNECTed upstream (which may then up/downgrade from the current encryption).
if (socket.lastHopEncrypted !== undefined) {
protocol = socket.lastHopEncrypted ? 'wss' : 'ws';
if (socket.__lastHopEncrypted !== undefined) {
protocol = socket.__lastHopEncrypted ? 'wss' : 'ws';
}

@@ -352,4 +353,5 @@ else {

'close-connection': request_handlers_1.CloseConnectionHandler,
'reset-connection': request_handlers_1.ResetConnectionHandler,
'timeout': request_handlers_1.TimeoutHandler
};
//# sourceMappingURL=websocket-handlers.js.map

@@ -140,2 +140,21 @@ /// <reference types="node" />

/**
* Reset connections that match this rule immediately, sending a TCP
* RST packet directly, without accepting the socket or sending any
* other response, and without cleanly closing the TCP connection.
*
* This is only supported in Node.js versions (>=16.17, >=18.3.0, or
* later), where `net.Socket` includes the `resetAndDestroy` method.
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
* This method returns a promise that resolves with a mocked endpoint.
* Wait for the promise to confirm that the rule has taken effect
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*
* @category Responses
*/
thenResetConnection(): Promise<MockedEndpoint>;
/**
* Hold open connections that match this rule, but never respond

@@ -142,0 +161,0 @@ * with anything at all, typically causing a timeout on the client side.

@@ -172,2 +172,24 @@ "use strict";

/**
* Reset connections that match this rule immediately, sending a TCP
* RST packet directly, without accepting the socket or sending any
* other response, and without cleanly closing the TCP connection.
*
* This is only supported in Node.js versions (>=16.17, >=18.3.0, or
* later), where `net.Socket` includes the `resetAndDestroy` method.
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
* This method returns a promise that resolves with a mocked endpoint.
* Wait for the promise to confirm that the rule has taken effect
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*
* @category Responses
*/
thenResetConnection() {
const rule = Object.assign(Object.assign({}, this.buildBaseRuleData()), { handler: new websocket_handler_definitions_1.ResetConnectionHandlerDefinition() });
return this.addRule(rule);
}
/**
* Hold open connections that match this rule, but never respond

@@ -174,0 +196,0 @@ * with anything at all, typically causing a timeout on the client side.

@@ -5,10 +5,10 @@ /// <reference types="node" />

import http = require('http');
import { TlsRequest } from '../types';
import { DestroyableServer } from '../util/destroyable-server';
import { CAOptions } from '../util/tls';
import { DestroyableServer } from 'destroyable-server';
import { TlsHandshakeFailure } from '../types';
import { MockttpHttpsOptions } from '../mockttp';
export declare type ComboServerOptions = {
debug: boolean;
https: CAOptions | undefined;
https: MockttpHttpsOptions | undefined;
http2: true | false | 'fallback';
};
export declare function createComboServer(options: ComboServerOptions, requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void, tlsClientErrorListener: (socket: tls.TLSSocket, req: TlsRequest) => void): Promise<DestroyableServer & net.Server>;
export declare function createComboServer(options: ComboServerOptions, requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void, tlsClientErrorListener: (socket: tls.TLSSocket, req: TlsHandshakeFailure) => void, tlsPassthroughListener: (socket: net.Socket, address: string, port?: number) => void): Promise<DestroyableServer<net.Server>>;

@@ -14,9 +14,11 @@ "use strict";

const _ = require("lodash");
const now = require("performance-now");
const net = require("net");
const tls = require("tls");
const destroyable_server_1 = require("destroyable-server");
const httpolyglot = require("@httptoolkit/httpolyglot");
const now = require("performance-now");
const destroyable_server_1 = require("../util/destroyable-server");
const read_tls_client_hello_1 = require("read-tls-client-hello");
const tls_1 = require("../util/tls");
const util_1 = require("../util/util");
const socket_util_1 = require("../util/socket-util");
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to

@@ -96,3 +98,5 @@ // sockets as soon as they're available, without waiting for the handshake to fully

? 'reset'
: 'unknown'; // Something \else.
: error.code === 'ERR_TLS_HANDSHAKE_TIMEOUT'
? 'handshake-timeout'
: 'unknown'; // Something else.
if (cause === 'unknown')

@@ -102,29 +106,7 @@ console.log('Unknown TLS error:', error);

}
function buildTimingInfo() {
return { initialSocket: Date.now(), initialSocketTimestamp: now() };
}
function buildTlsError(socket, cause) {
var _a, _b, _c;
const timingInfo = socket.__timingInfo ||
((_a = socket._parent) === null || _a === void 0 ? void 0 : _a.__timingInfo) ||
buildTimingInfo();
return {
failureCause: cause,
hostname: socket.servername,
// These only work because of oncertcb monkeypatch above
remoteIpAddress: socket.remoteAddress || // Normal case
((_b = socket._parent) === null || _b === void 0 ? void 0 : _b.remoteAddress) || // Pre-certCB error, e.g. timeout
socket.initialRemoteAddress,
remotePort: socket.remotePort ||
((_c = socket._parent) === null || _c === void 0 ? void 0 : _c.remotePort) ||
socket.initialRemotePort,
tags: [],
timingEvents: {
startTime: timingInfo.initialSocket,
connectTimestamp: timingInfo.initialSocketTimestamp,
tunnelTimestamp: timingInfo.tunnelSetupTimestamp,
handshakeTimestamp: timingInfo.tlsConnectedTimestamp,
failureTimestamp: now()
}
};
const eventData = (0, socket_util_1.buildSocketEventData)(socket);
eventData.failureCause = cause;
eventData.timingEvents.failureTimestamp = now();
return eventData;
}

@@ -134,3 +116,4 @@ // The low-level server that handles all the sockets & TLS. The server will correctly call the

// either HTTP or HTTPS proxy, all on the same port.
function createComboServer(options, requestListener, tlsClientErrorListener) {
function createComboServer(options, requestListener, tlsClientErrorListener, tlsPassthroughListener) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {

@@ -143,4 +126,4 @@ let server;

const ca = yield (0, tls_1.getCA)(options.https);
const defaultCert = ca.generateCertificate('localhost');
server = httpolyglot.createServer({
const defaultCert = ca.generateCertificate((_a = options.https.defaultDomain) !== null && _a !== void 0 ? _a : 'localhost');
const tlsServer = tls.createServer({
key: defaultCert.key,

@@ -171,9 +154,13 @@ cert: defaultCert.cert,

}
}, requestListener);
});
if ((_b = options.https.tlsPassthrough) === null || _b === void 0 ? void 0 : _b.length) {
passThroughMatchingTls(tlsServer, options.https.tlsPassthrough, tlsPassthroughListener);
}
server = httpolyglot.createServer(tlsServer, requestListener);
}
server.on('connection', (socket) => {
socket.__timingInfo = socket.__timingInfo || buildTimingInfo();
socket.__timingInfo = socket.__timingInfo || (0, socket_util_1.buildSocketTimingInfo)();
// All sockets are initially marked as using unencrypted upstream connections.
// If TLS is used, this is upgraded to 'true' by secureConnection below.
socket.lastHopEncrypted = false;
socket.__lastHopEncrypted = false;
// For actual sockets, set NODELAY to avoid any buffering whilst streaming. This is

@@ -193,6 +180,6 @@ // off by default in Node HTTP, but likely to be enabled soon & is default in curl.

else if (!socket.__timingInfo) {
socket.__timingInfo = buildTimingInfo();
socket.__timingInfo = (0, socket_util_1.buildSocketTimingInfo)();
}
socket.__timingInfo.tlsConnectedTimestamp = now();
socket.lastHopEncrypted = true;
socket.__lastHopEncrypted = true;
ifTlsDropped(socket, () => {

@@ -235,2 +222,3 @@ tlsClientErrorListener(socket, buildTlsError(socket, 'closed'));

socket.__timingInfo.tunnelSetupTimestamp = now();
socket.__lastHopConnectAddress = connectUrl;
server.emit('connection', socket);

@@ -253,2 +241,3 @@ });

copyTimingDetails(res.socket, res.stream);
res.stream.__lastHopConnectAddress = connectUrl;
// When layering HTTP/2 on JS streams, we have to make sure the JS stream won't autoclose

@@ -263,3 +252,3 @@ // when the other side does, because the upper HTTP/2 layers want to handle shutdown, so

}
return (0, destroyable_server_1.destroyable)(server);
return (0, destroyable_server_1.makeDestroyable)(server);
});

@@ -274,8 +263,14 @@ }

}
const SOCKET_ADDRESS_METADATA_FIELDS = [
'localAddress',
'localPort',
'remoteAddress',
'remotePort',
'__lastHopConnectAddress'
];
// Update the target socket(-ish) with the address details from the source socket,
// iff the target has no details of its own.
function copyAddressDetails(source, target) {
const fields = ['localAddress', 'localPort', 'remoteAddress', 'remotePort'];
Object.defineProperties(target, _.zipObject(fields, _.range(fields.length).map(() => ({ writable: true }))));
fields.forEach((fieldName) => {
Object.defineProperties(target, _.zipObject(SOCKET_ADDRESS_METADATA_FIELDS, _.range(SOCKET_ADDRESS_METADATA_FIELDS.length).map(() => ({ writable: true }))));
SOCKET_ADDRESS_METADATA_FIELDS.forEach((fieldName) => {
if (target[fieldName] === undefined) {

@@ -292,2 +287,35 @@ target[fieldName] = source[fieldName];

}
/**
* Takes a tls passthrough list, and reconfigures a given TLS server so that all
* matching requests are passed to the given passthrough listener, instead of
* beginning a full TLS handshake.
*/
function passThroughMatchingTls(server, passthroughList, listener) {
const hostnames = passthroughList.map(({ hostname }) => hostname);
const tlsConnectionListener = server.listeners('connection')[0];
server.removeListener('connection', tlsConnectionListener);
server.on('connection', (socket) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
try {
const helloData = yield (0, read_tls_client_hello_1.readTlsClientHello)(socket);
const [connectHostname, connectPort] = (_b = (_a = socket.__lastHopConnectAddress) === null || _a === void 0 ? void 0 : _a.split(':')) !== null && _b !== void 0 ? _b : [];
const sniHostname = helloData.serverName;
if (connectHostname && hostnames.includes(connectHostname)) {
listener(socket, connectHostname, connectPort ? parseInt(connectPort, 10) : undefined);
return; // Do not continue with TLS
}
else if (sniHostname && hostnames.includes(sniHostname)) {
listener(socket, sniHostname); // Can't guess the port - it's not included in SNI
return; // Do not continue with TLS
}
}
catch (e) {
if (!(e instanceof read_tls_client_hello_1.NonTlsError)) { // Don't even warn for non-TLS traffic
console.warn(`TLS client hello data not available for TLS connection from ${(_c = socket.remoteAddress) !== null && _c !== void 0 ? _c : 'unknown address'}: ${(_d = e.message) !== null && _d !== void 0 ? _d : e}`);
}
}
// Didn't match a passthrough hostname - continue with TLS setup
tlsConnectionListener.call(server, socket);
}));
}
//# sourceMappingURL=http-combo-server.js.map

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

import { InitiatedRequest, CompletedRequest, CompletedResponse, TlsRequest, ClientError, WebSocketMessage, WebSocketClose } from "../types";
import { InitiatedRequest, CompletedRequest, CompletedResponse, TlsHandshakeFailure, ClientError, WebSocketMessage, WebSocketClose, TlsPassthroughEvent } from "../types";
import { Mockttp, AbstractMockttp, MockttpOptions, PortRange } from "../mockttp";

@@ -48,3 +48,5 @@ import { RequestRuleData } from "../rules/requests/request-rule";

on(event: 'websocket-close', callback: (close: WebSocketClose) => void): Promise<void>;
on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise<void>;
on(event: 'tls-passthrough-opened', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
on(event: 'tls-passthrough-closed', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
on(event: 'tls-client-error', callback: (req: TlsHandshakeFailure) => void): Promise<void>;
on(event: 'client-error', callback: (error: ClientError) => void): Promise<void>;

@@ -81,2 +83,4 @@ private announceInitialRequestAsync;

private handleInvalidHttp2Request;
private outgoingPassthroughSockets;
private passthroughSocket;
}

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

const _ = require("lodash");
const net = require("net");
const url = require("url");

@@ -30,2 +31,3 @@ const tls = require("tls");

const util_1 = require("../util/util");
const socket_util_1 = require("../util/socket-util");
const request_utils_1 = require("../util/request-utils");

@@ -77,2 +79,3 @@ const buffer_utils_1 = require("../util/buffer-utils");

};
this.outgoingPassthroughSockets = new Set();
this.initialDebugSetting = this.debug;

@@ -109,3 +112,3 @@ this.httpsOptions = options.https;

http2: this.isHttp2Enabled,
}, this.app, this.announceTlsErrorAsync.bind(this));
}, this.app, this.announceTlsErrorAsync.bind(this), this.passthroughSocket.bind(this));
this.server.listen(port);

@@ -360,3 +363,3 @@ // Handle & report client request errors

}
announceAbortAsync(request) {
announceAbortAsync(request, abortError) {
return __awaiter(this, void 0, void 0, function* () {

@@ -367,3 +370,9 @@ setImmediate(() => {

timingEvents: _.clone(req.timingEvents),
tags: _.clone(req.tags)
tags: _.clone(req.tags),
error: abortError ? {
name: abortError.name,
code: abortError.code,
message: abortError.message,
stack: abortError.stack
} : undefined
}));

@@ -409,3 +418,3 @@ });

req.protocol = req.headers[':scheme'] ||
(req.socket.lastHopEncrypted ? 'https' : 'http');
(req.socket.__lastHopEncrypted ? 'https' : 'http');
req.path = req.url;

@@ -463,7 +472,7 @@ const host = req.headers[':authority'] || req.headers['host'];

let result = null;
const abort = () => {
const abort = (error) => {
if (result === null) {
result = 'aborted';
request.timingEvents.abortedTimestamp = now();
this.announceAbortAsync(request);
this.announceAbortAsync(request, error);
}

@@ -475,3 +484,3 @@ };

rawResponse.once('close', () => setImmediate(abort));
request.once('error', () => setImmediate(abort));
request.once('error', (error) => setImmediate(() => abort(error)));
this.announceInitialRequestAsync(request);

@@ -482,3 +491,3 @@ const response = (0, request_utils_1.trackResponse)(rawResponse, request.timingEvents, request.tags, { maxSize: this.maxBodySize });

console.log('Response error:', this.debug ? error : error.message);
abort();
abort(error);
});

@@ -508,3 +517,3 @@ try {

if (e instanceof request_handlers_1.AbortError) {
abort();
abort(e);
if (this.debug) {

@@ -528,3 +537,3 @@ console.error("Failed to handle request due to abort:", e);

catch (e) {
abort();
abort(e);
}

@@ -809,4 +818,43 @@ }

}
passthroughSocket(socket, host, port) {
const targetPort = port || 443;
if ((0, socket_util_1.isSocketLoop)(this.outgoingPassthroughSockets, socket)) {
// Hard to reproduce: loops can only happen if a) SNI triggers this (because tunnels
// require a repeated client request at each step) and b) the hostname points back to
// us, and c) we're running on the default port. Still good to guard against though.
console.warn(`Socket bypass loop for ${host}:${targetPort}`);
if ('resetAndDestroy' in socket) {
socket.resetAndDestroy();
}
else {
socket.destroy();
}
return;
}
if (socket.closed)
return; // Nothing to do
const eventData = (0, socket_util_1.buildSocketEventData)(socket);
eventData.id = (0, uuid_1.v4)();
eventData.hostname = host;
eventData.upstreamPort = targetPort;
setImmediate(() => this.eventEmitter.emit('tls-passthrough-opened', eventData));
const upstreamSocket = net.connect({ host, port: targetPort });
socket.pipe(upstreamSocket);
upstreamSocket.pipe(socket);
socket.on('error', () => upstreamSocket.destroy());
upstreamSocket.on('error', () => socket.destroy());
upstreamSocket.on('close', () => socket.destroy());
socket.on('close', () => {
upstreamSocket.destroy();
setImmediate(() => {
this.eventEmitter.emit('tls-passthrough-closed', Object.assign(Object.assign({}, eventData), { timingEvents: Object.assign(Object.assign({}, eventData.timingEvents), { disconnectedTimestamp: now() }) }));
});
});
upstreamSocket.once('connect', () => this.outgoingPassthroughSockets.add(upstreamSocket));
upstreamSocket.once('close', () => this.outgoingPassthroughSockets.delete(upstreamSocket));
if (this.debug)
console.log(`Passing through raw bypassed connection to ${host}:${targetPort}${!port ? ' (assumed port)' : ''}`);
}
}
exports.MockttpServer = MockttpServer;
//# sourceMappingURL=mockttp-server.js.map

@@ -48,17 +48,54 @@ /// <reference types="node" />

}
export interface TlsRequest {
export interface TlsConnectionEvent {
hostname?: string;
remoteIpAddress: string;
remotePort: number;
failureCause: 'closed' | 'reset' | 'cert-rejected' | 'no-shared-cipher' | 'unknown';
tags: string[];
timingEvents: TlsTimingEvents;
}
export interface TlsPassthroughEvent extends TlsConnectionEvent {
id: string;
upstreamPort: number;
}
export interface TlsHandshakeFailure extends TlsConnectionEvent {
failureCause: 'closed' | 'reset' | 'cert-rejected' | 'no-shared-cipher' | 'handshake-timeout' | 'unknown';
timingEvents: TlsFailureTimingEvents;
}
export interface TlsTimingEvents {
/**
* When the socket initially connected, in MS since the unix
* epoch.
*/
startTime: number;
/**
* When the socket initially connected, equivalent to startTime.
*
* High-precision floating-point monotonically increasing timestamps.
* Comparable and precise, but not related to specific current time.
*/
connectTimestamp: number;
failureTimestamp: number;
/**
* When Mockttp's handshake for this connection was completed (if there
* was one). This is not set for passed through connections.
*/
handshakeTimestamp?: number;
/**
* When the outer tunnel (e.g. a preceeding CONNECT request) was created,
* if there was one.
*/
tunnelTimestamp?: number;
/**
* When the connection was closed, if it has been closed.
*/
disconnectTimestamp?: number;
}
export interface TlsFailureTimingEvents extends TlsTimingEvents {
/**
* When the TLS connection failed. This may be due to a failed handshake
* (in which case `handshakeTimestamp` will be undefined) or due to a
* subsequent error which means the TLS connection was not usable (like
* an immediate closure due to an async certificate rejection).
*/
failureTimestamp: number;
}
export interface OngoingRequest extends Request, EventEmitter {

@@ -114,2 +151,10 @@ rawHeaders: RawHeaders;

}
export interface AbortedRequest extends InitiatedRequest {
error?: {
name?: string;
code?: string;
message?: string;
stack?: string;
};
}
export interface CompletedRequest extends Request {

@@ -116,0 +161,0 @@ body: CompletedBody;

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

try {
req.protocol = socket.lastHopEncrypted ? "https" : "http"; // Wild guess really
req.protocol = socket.__lastHopEncrypted ? "https" : "http"; // Wild guess really
// For TLS sockets, we default the hostname to the name given by SNI. Might be overridden

@@ -370,0 +370,0 @@ // by the URL or Host header later, if available.

import * as net from 'net';
import * as tls from 'tls';
import { TlsConnectionEvent } from '../types';
export declare function isLocalPortActive(interfaceIp: '::1' | '127.0.0.1', port: number): Promise<unknown>;

@@ -6,1 +8,4 @@ export declare const isLocalIPv6Available: boolean;

export declare const isSocketLoop: (outgoingSockets: net.Socket[] | Set<net.Socket>, incomingSocket: net.Socket) => boolean;
export declare const resetSocket: (socket: net.Socket) => void;
export declare function buildSocketEventData(socket: net.Socket & Partial<tls.TLSSocket>): TlsConnectionEvent;
export declare function buildSocketTimingInfo(): Required<net.Socket>['__timingInfo'];

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

Object.defineProperty(exports, "__esModule", { value: true });
exports.isSocketLoop = exports.isLocalhostAddress = exports.isLocalIPv6Available = exports.isLocalPortActive = void 0;
exports.buildSocketTimingInfo = exports.buildSocketEventData = exports.resetSocket = exports.isSocketLoop = exports.isLocalhostAddress = exports.isLocalIPv6Available = exports.isLocalPortActive = void 0;
const _ = require("lodash");
const now = require("performance-now");
const os = require("os");

@@ -77,2 +78,37 @@ const net = require("net");

exports.isSocketLoop = isSocketLoop;
const resetSocket = (socket) => {
if (!('resetAndDestroy' in socket)) {
throw new Error('Connection reset is only supported in Node v16.17+, v18.3.0+, or later');
}
socket.resetAndDestroy();
};
exports.resetSocket = resetSocket;
function buildSocketEventData(socket) {
var _a, _b, _c;
const timingInfo = socket.__timingInfo ||
((_a = socket._parent) === null || _a === void 0 ? void 0 : _a.__timingInfo) ||
buildSocketTimingInfo();
return {
hostname: socket.servername,
// These only work because of oncertcb monkeypatch above
remoteIpAddress: socket.remoteAddress || // Normal case
((_b = socket._parent) === null || _b === void 0 ? void 0 : _b.remoteAddress) || // Pre-certCB error, e.g. timeout
socket.initialRemoteAddress,
remotePort: socket.remotePort ||
((_c = socket._parent) === null || _c === void 0 ? void 0 : _c.remotePort) ||
socket.initialRemotePort,
tags: [],
timingEvents: {
startTime: timingInfo.initialSocket,
connectTimestamp: timingInfo.initialSocketTimestamp,
tunnelTimestamp: timingInfo.tunnelSetupTimestamp,
handshakeTimestamp: timingInfo.tlsConnectedTimestamp
}
};
}
exports.buildSocketEventData = buildSocketEventData;
function buildSocketTimingInfo() {
return { initialSocket: Date.now(), initialSocketTimestamp: now() };
}
exports.buildSocketTimingInfo = buildSocketTimingInfo;
//# sourceMappingURL=socket-util.js.map
/// <reference types="node" />
export declare type CAOptions = (HttpsOptions | HttpsPathOptions);
export declare type HttpsOptions = {
export declare type CAOptions = (CertDataOptions | CertPathOptions);
export interface CertDataOptions extends BaseCAOptions {
key: string;
cert: string;
keyLength?: number;
};
export declare type HttpsPathOptions = {
}
export interface CertPathOptions extends BaseCAOptions {
keyPath: string;
certPath: string;
}
export interface BaseCAOptions {
/**
* Minimum key length when generating certificates. Defaults to 2048.
*/
keyLength?: number;
};
}
export declare type PEM = string | string[] | Buffer | Buffer[];

@@ -14,0 +18,0 @@ export declare type GeneratedCertificate = {

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

const fs = require("./fs");
;
/**

@@ -92,15 +93,14 @@ * Generate a CA certificate for mocking HTTPS.

return __awaiter(this, void 0, void 0, function* () {
let httpsOptions;
if (options.key && options.cert) {
httpsOptions = options;
let certOptions;
if ('key' in options && 'cert' in options) {
certOptions = options;
}
else if (options.keyPath && options.certPath) {
let pathOptions = options;
httpsOptions = yield Promise.all([
fs.readFile(pathOptions.keyPath, 'utf8'),
fs.readFile(pathOptions.certPath, 'utf8')
else if ('keyPath' in options && 'certPath' in options) {
certOptions = yield Promise.all([
fs.readFile(options.keyPath, 'utf8'),
fs.readFile(options.certPath, 'utf8')
]).then(([keyContents, certContents]) => ({
key: keyContents,
cert: certContents,
keyLength: pathOptions.keyLength
keyLength: options.keyLength
}));

@@ -111,3 +111,3 @@ }

}
return new CA(httpsOptions.key, httpsOptions.cert, httpsOptions.keyLength || 2048);
return new CA(certOptions.key, certOptions.cert, certOptions.keyLength || 2048);
});

@@ -114,0 +114,0 @@ }

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

@@ -131,2 +131,3 @@ "exports": {

"fs-extra": "^8.1.0",
"http-proxy-agent": "^5.0.0",
"karma": "^6.3.2",

@@ -168,3 +169,3 @@ "karma-chai": "^0.1.0",

"@graphql-tools/utils": "^8.8.0",
"@httptoolkit/httpolyglot": "^2.0.1",
"@httptoolkit/httpolyglot": "^2.1.0",
"@httptoolkit/subscriptions-transport-ws": "^0.11.2",

@@ -182,2 +183,3 @@ "@httptoolkit/websocket-stream": "^6.0.1",

"cross-fetch": "^3.1.5",
"destroyable-server": "^1.0.0",
"express": "^4.14.0",

@@ -200,2 +202,3 @@ "express-graphql": "^0.11.0",

"portfinder": "1.0.28",
"read-tls-client-hello": "^1.0.0",
"socks-proxy-agent": "^7.0.0",

@@ -202,0 +205,0 @@ "typed-error": "^3.0.2",

@@ -19,3 +19,3 @@ import * as _ from 'lodash';

import { destroyable, DestroyableServer } from "../util/destroyable-server";
import { makeDestroyable, DestroyableServer } from "destroyable-server";
import { isErrorLike } from '../util/error';

@@ -124,3 +124,3 @@ import { objectAllPromise } from '../util/promise';

private app = express();
private server: DestroyableServer & http.Server | null = null;
private server: DestroyableServer<http.Server> | null = null;
private eventEmitter = new EventEmitter();

@@ -340,3 +340,3 @@

await new Promise<void>((resolve, reject) => {
this.server = destroyable(this.app.listen(listenOptions, resolve));
this.server = makeDestroyable(this.app.listen(listenOptions, resolve));

@@ -343,0 +343,0 @@ this.server.on('error', reject);

@@ -31,2 +31,4 @@ import * as _ from "lodash";

const REQUEST_ABORTED_TOPIC = 'request-aborted';
const TLS_PASSTHROUGH_OPENED_TOPIC = 'tls-passthrough-opened';
const TLS_PASSTHROUGH_CLOSED_TOPIC = 'tls-passthrough-closed';
const TLS_CLIENT_ERROR_TOPIC = 'tls-client-error';

@@ -102,4 +104,4 @@ const CLIENT_ERROR_TOPIC = 'client-error';

requestAborted: Object.assign(evt, {
// Backward compat: old clients expect this to be present. In future this can be removed
// and abort events can switch from Request to InitiatedRequest in the schema.
// Backward compat: old clients expect this to be present. In future this can be
// removed and abort events can lose the 'body' in the schema.
body: Buffer.alloc(0)

@@ -110,2 +112,14 @@ })

mockServer.on('tls-passthrough-opened', (evt) => {
pubsub.publish(TLS_PASSTHROUGH_OPENED_TOPIC, {
tlsPassthroughOpened: evt
})
});
mockServer.on('tls-passthrough-closed', (evt) => {
pubsub.publish(TLS_PASSTHROUGH_CLOSED_TOPIC, {
tlsPassthroughClosed: evt
})
});
mockServer.on('tls-client-error', (evt) => {

@@ -210,2 +224,8 @@ pubsub.publish(TLS_CLIENT_ERROR_TOPIC, {

},
tlsPassthroughOpened: {
subscribe: () => pubsub.asyncIterator(TLS_PASSTHROUGH_OPENED_TOPIC)
},
tlsPassthroughClosed: {
subscribe: () => pubsub.asyncIterator(TLS_PASSTHROUGH_CLOSED_TOPIC)
},
failedTlsRequest: {

@@ -212,0 +232,0 @@ subscribe: () => pubsub.asyncIterator(TLS_CLIENT_ERROR_TOPIC)

@@ -30,4 +30,6 @@ import gql from "graphql-tag";

webSocketClose: WebSocketClose!
requestAborted: Request!
failedTlsRequest: TlsRequest!
requestAborted: AbortedRequest!
tlsPassthroughOpened: TlsPassthroughEvent!
tlsPassthroughClosed: TlsPassthroughEvent!
failedTlsRequest: TlsHandshakeFailure!
failedClientRequest: ClientError!

@@ -59,4 +61,27 @@ }

type TlsPassthroughEvent {
id: String!
upstreamPort: Int!
hostname: String
remoteIpAddress: String!
remotePort: Int!
tags: [String!]!
timingEvents: Json!
}
type TlsHandshakeFailure {
failureCause: String!
hostname: String
remoteIpAddress: String!
remotePort: Int!
tags: [String!]!
timingEvents: Json!
}
# Old name for TlsHandshakeFailure, kept for backward compat
type TlsRequest {
failureCause: String!
hostname: String

@@ -131,2 +156,25 @@ remoteIpAddress: String!

type AbortedRequest {
id: ID!
timingEvents: Json!
tags: [String!]!
matchedRuleId: ID
protocol: String!
httpVersion: String!
method: String!
url: String!
path: String!
remoteIpAddress: String!
remotePort: Int!
hostname: String
headers: Json!
rawHeaders: Json!
body: Buffer!
error: Json
}
type Response {

@@ -133,0 +181,0 @@ id: ID!

@@ -354,4 +354,29 @@ import _ = require('lodash');

${this.schema.asOptionalField('Request', 'tags')}
${this.schema.asOptionalField('AbortedRequest', 'error')}
}
}`,
'tls-passthrough-opened': gql`subscription OnTlsPassthroughOpened {
tlsPassthroughOpened {
id
upstreamPort
hostname
remoteIpAddress
remotePort
tags
timingEvents
}
}`,
'tls-passthrough-closed': gql`subscription OnTlsPassthroughClosed {
tlsPassthroughClosed {
id
upstreamPort
hostname
remoteIpAddress
remotePort
tags
timingEvents
}
}`,
'tls-client-error': gql`subscription OnTlsClientError {

@@ -362,5 +387,5 @@ failedTlsRequest {

remoteIpAddress
${this.schema.asOptionalField('TlsRequest', 'remotePort')}
${this.schema.asOptionalField('TlsRequest', 'tags')}
${this.schema.asOptionalField('TlsRequest', 'timingEvents')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'remotePort')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'tags')}
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'timingEvents')}
}

@@ -426,2 +451,5 @@ }`,

normalizeWebSocketMessage(data);
} else if (event === 'abort') {
normalizeHttpMessage(data, event);
data.error = data.error ? JSON.parse(data.error) : undefined;
} else {

@@ -428,0 +456,0 @@ normalizeHttpMessage(data, event);

@@ -13,2 +13,6 @@ import * as _ from 'lodash';

public isTypeDefined(typeName: string): boolean {
return _.some(this.adminServerSchema.types, { name: typeName });
}
public typeHasField(typeName: string, fieldName: string): boolean {

@@ -20,4 +24,9 @@ const type: any = _.find(this.adminServerSchema.types, { name: typeName });

public asOptionalField(typeName: string, fieldName: string): string {
return (this.typeHasField(typeName, fieldName))
public asOptionalField(typeName: string | string[], fieldName: string): string {
const possibleNames = !Array.isArray(typeName) ? [typeName] : typeName;
const firstAvailableName = possibleNames.find((name) => this.isTypeDefined(name));
if (!firstAvailableName) return '';
return (this.typeHasField(firstAvailableName, fieldName))
? fieldName

@@ -24,0 +33,0 @@ : '';

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

import { Mockttp, MockttpOptions, SubscribableEvent, PortRange } from "./mockttp";
import { Mockttp, MockttpOptions, MockttpHttpsOptions, SubscribableEvent, PortRange } from "./mockttp";
import { MockttpServer } from "./server/mockttp-server";

@@ -16,2 +16,3 @@ import {

MockttpOptions,
MockttpHttpsOptions,
MockttpClientOptions,

@@ -23,2 +24,10 @@ MockttpAdminServerOptions,

// Export now-renamed types with the old aliases to provide backward compat and
// avoid unnecessary type breakage:
export type { TlsHandshakeFailure as TlsRequest } from './types';
export type {
CertDataOptions as HttpsOptions,
CertPathOptions as HttpsPathOptions
} from './util/tls';
// Export rule data builders & type definitions:

@@ -73,4 +82,4 @@ import * as matchers from './rules/matchers';

PEM,
HttpsOptions,
HttpsPathOptions
CertDataOptions,
CertPathOptions
} from './util/tls';

@@ -77,0 +86,0 @@ export type { CachedDns, DnsLookupFunction } from './util/dns';

@@ -15,3 +15,4 @@ import { stripIndent } from "common-tags";

CompletedResponse,
TlsRequest,
TlsPassthroughEvent,
TlsHandshakeFailure,
InitiatedRequest,

@@ -21,3 +22,4 @@ ClientError,

WebSocketMessage,
WebSocketClose
WebSocketClose,
AbortedRequest
} from "./types";

@@ -471,5 +473,35 @@ import type { RequestRuleData } from "./rules/requests/request-rule";

*/
on(event: 'abort', callback: (req: InitiatedRequest) => void): Promise<void>;
on(event: 'abort', callback: (req: AbortedRequest) => void): Promise<void>;
/**
* Subscribe to hear about TLS connections that are passed through the proxy without
* interception, due to the `tlsPassthrough` HTTPS option.
*
* 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 connection handling. This function
* returns a promise, and the callback is not guaranteed to be registered until
* the promise is resolved.
*
* @category Events
*/
on(event: 'tls-passthrough-opened', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
/**
* Subscribe to hear about closure of TLS connections that were passed through the
* proxy without interception, due to the `tlsPassthrough` HTTPS option.
*
* 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 connection handling. This function
* returns a promise, and the callback is not guaranteed to be registered until
* the promise is resolved.
*
* @category Events
*/
on(event: 'tls-passthrough-closed', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
/**
* Subscribe to hear about requests that start a TLS handshake, but fail to complete it.

@@ -493,3 +525,3 @@ * Not all clients report TLS errors explicitly, so this event fires for explicitly

*/
on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise<void>;
on(event: 'tls-client-error', callback: (req: TlsHandshakeFailure) => void): Promise<void>;

@@ -629,2 +661,25 @@ /**

export type MockttpHttpsOptions = CAOptions & {
/**
* The domain name that will be used in the certificate for incoming TLS
* connections which don't use SNI to request a specific domain.
*/
defaultDomain?: string;
/**
* A list of hostnames where TLS interception should always be skipped.
*
* When a TLS connection is started that references a matching hostname in its
* server name indication (SNI) extension, or which uses a matching hostname
* in a preceeding CONNECT request to create a tunnel, the connection will be
* sent raw to the upstream hostname, without handling TLS within Mockttp (i.e.
* with no TLS interception performed).
*
* Each element in the list must be an object with a 'hostname' field for the
* hostname that should be matched. In future more options may be supported
* here for additional configuration of this behaviour.
*/
tlsPassthrough?: Array<{ hostname: string }>
};
export interface MockttpOptions {

@@ -652,3 +707,3 @@ /**

*/
https?: CAOptions;
https?: MockttpHttpsOptions;

@@ -715,2 +770,4 @@ /**

| 'abort'
| 'tls-passthrough-opened'
| 'tls-passthrough-closed'
| 'tls-client-error'

@@ -717,0 +774,0 @@ | 'client-error';

@@ -137,3 +137,4 @@ import _ = require('lodash');

| CallbackResponseMessageResult
| 'close';
| 'close'
| 'reset';

@@ -539,2 +540,27 @@ /**

/**
* Whether to simulate connection errors back to the client.
*
* By default (in most cases - see below) when an upstream request fails
* outright a 502 "Bad Gateway" response is sent to the downstream client,
* explicitly indicating the failure and containing the error that caused
* the issue in the response body.
*
* Only in the case of upstream connection reset errors is a connection reset
* normally sent back downstream to existing clients (this behaviour exists
* for backward compatibility, and will change to match other error behaviour
* in a future version).
*
* When this option is set to `true`, low-level connection failures will
* always trigger a downstream connection close/reset, rather than a 502
* response.
*
* This includes DNS failures, TLS connection errors, TCP connection resets,
* etc (but not HTTP non-200 responses, which are still proxied as normal).
* This is less convenient for debugging in a testing environment or when
* using a proxy intentionally, but can be more accurate when trying to
* transparently proxy network traffic, errors and all.
*/
simulateConnectionErrors?: boolean;
/**
* A set of data to automatically transform a request. This includes properties

@@ -729,2 +755,3 @@ * to support many transformation common use cases.

lookupOptions?: PassThroughLookupOptions;
simulateConnectionErrors?: boolean;

@@ -798,2 +825,4 @@ transformRequest?: Replace<RequestTransform, {

public readonly simulateConnectionErrors: boolean;
// Used in subclass - awkwardly needs to be initialized here to ensure that its set when using a

@@ -830,2 +859,3 @@ // handler built from a definition. In future, we could improve this (compose instead of inheritance

this.proxyConfig = options.proxyConfig;
this.simulateConnectionErrors = !!options.simulateConnectionErrors;

@@ -940,2 +970,3 @@ this.clientCertificateHostMap = options.clientCertificateHostMap || {};

lookupOptions: this.lookupOptions,
simulateConnectionErrors: this.simulateConnectionErrors,
ignoreHostCertificateErrors: this.ignoreHostHttpsErrors,

@@ -1030,2 +1061,10 @@ extraCACertificates: this.extraCACertificates.map((certObject) => {

export class ResetConnectionHandlerDefinition extends Serializable implements RequestHandlerDefinition {
readonly type = 'reset-connection';
explain() {
return 'reset the connection';
}
}
export class TimeoutHandlerDefinition extends Serializable implements RequestHandlerDefinition {

@@ -1070,4 +1109,5 @@ readonly type = 'timeout';

'close-connection': CloseConnectionHandlerDefinition,
'reset-connection': ResetConnectionHandlerDefinition,
'timeout': TimeoutHandlerDefinition,
'json-rpc-response': JsonRpcResponseHandlerDefinition
}

@@ -45,4 +45,9 @@ import _ = require('lodash');

import { streamToBuffer, asBuffer } from '../../util/buffer-utils';
import { isLocalhostAddress, isLocalPortActive, isSocketLoop } from '../../util/socket-util';
import {
isLocalhostAddress,
isLocalPortActive,
isSocketLoop,
resetSocket
} from '../../util/socket-util';
import {
ClientServerChannel,

@@ -92,2 +97,3 @@ deserializeBuffer,

RequestTransform,
ResetConnectionHandlerDefinition,
ResponseTransform,

@@ -120,4 +126,13 @@ SerializedBuffer,

// This could be intentional, or an upstream server aborting the request.
export class AbortError extends TypedError { }
export class AbortError extends TypedError {
constructor(
message: string,
readonly code?: string
) {
super(message);
}
}
function isSerializedBuffer(obj: any): obj is SerializedBuffer {

@@ -190,3 +205,6 @@ return obj && obj.type === 'Buffer' && !!obj.data;

(request as any).socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
} else if (outResponse === 'reset') {
resetSocket((request as any).socket);
throw new AbortError('Connection reset intentionally by rule');
} else {

@@ -204,3 +222,5 @@ await writeResponseFromCallback(outResponse, response);

CallbackRequestMessage,
WithSerializedCallbackBuffers<CallbackResponseMessageResult> | 'close'
| WithSerializedCallbackBuffers<CallbackResponseMessageResult>
| 'close'
| 'reset'
>({ args: [

@@ -561,3 +581,7 @@ (version || -1) >= 2

socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
} else if (modifiedReq.response === 'reset') {
const socket: net.Socket = (<any> clientReq).socket;
resetSocket(socket);
throw new AbortError('Connection reset intentionally by rule');
} else {

@@ -834,3 +858,8 @@ // The callback has provided a full response: don't passthrough at all, just use it.

(clientRes as any).socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
} else if (modifiedRes === 'reset') {
// Dump the real response data and kill the client socket:
serverRes.resume();
resetSocket((clientRes as any).socket);
throw new AbortError('Connection reset intentionally by rule');
}

@@ -948,7 +977,14 @@

if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED') {
if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED' || this.simulateConnectionErrors) {
// The upstream socket closed: forcibly close the downstream stream to match
const socket: net.Socket = (clientReq as any).socket;
socket.destroy();
reject(new AbortError('Upstream connection was reset'));
if ('resetAndDestroy' in socket) {
socket.resetAndDestroy();
} else {
socket.destroy();
}
reject(new AbortError(`Upstream connection error: ${
e.message ?? e
}`, e.code));
} else {

@@ -1010,3 +1046,6 @@ e.statusCode = 502;

BeforePassthroughResponseRequest,
WithSerializedCallbackBuffers<CallbackResponseMessageResult> | 'close' | undefined
| WithSerializedCallbackBuffers<CallbackResponseMessageResult>
| 'close'
| 'reset'
| undefined
>('beforeResponse', {

@@ -1078,2 +1117,3 @@ args: [withSerializedBodyReader(res)]

lookupOptions: data.lookupOptions,
simulateConnectionErrors: !!data.simulateConnectionErrors,
ignoreHostHttpsErrors: data.ignoreHostCertificateErrors,

@@ -1092,6 +1132,33 @@ trustAdditionalCAs: data.extraCACertificates,

socket.end();
throw new AbortError('Connection closed (intentionally)');
throw new AbortError('Connection closed intentionally by rule');
}
}
export class ResetConnectionHandler extends ResetConnectionHandlerDefinition {
constructor() {
super();
if (!net.Socket.prototype.resetAndDestroy) {
throw new Error('Reset handlers are only supported in Node v16.17+, v18.3.0+, or later');
}
}
async handle(request: OngoingRequest) {
const socket: net.Socket = (<any> request).socket;
socket.resetAndDestroy();
throw new AbortError('Connection reset intentionally by rule');
}
/**
* @internal
*/
static deserialize() {
if (!net.Socket.prototype.resetAndDestroy) {
throw new Error('Reset handlers are only supported in Node v16.17+, v18.3.0+, or later');
}
return new ResetConnectionHandler();
}
}
export class TimeoutHandler extends TimeoutHandlerDefinition {

@@ -1132,4 +1199,5 @@ async handle() {

'close-connection': CloseConnectionHandler,
'reset-connection': ResetConnectionHandler,
'timeout': TimeoutHandler,
'json-rpc-response': JsonRpcResponseHandler
}

@@ -18,2 +18,3 @@ import { merge, isString, isBuffer } from "lodash";

JsonRpcResponseHandlerDefinition,
ResetConnectionHandlerDefinition,
} from "./request-handler-definitions";

@@ -379,2 +380,29 @@ import { MaybePromise } from "../../util/type-utils";

/**
* Reset connections that match this rule immediately, sending a TCP
* RST packet directly, without any status code or response, and without
* cleanly closing the TCP connection.
*
* This is only supported in Node.js versions (>=16.17, >=18.3.0, or
* later), where `net.Socket` includes the `resetAndDestroy` method.
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
* This method returns a promise that resolves with a mocked endpoint.
* Wait for the promise to confirm that the rule has taken effect
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*
* @category Responses
*/
thenResetConnection(): Promise<MockedEndpoint> {
const rule: RequestRuleData = {
...this.buildBaseRuleData(),
handler: new ResetConnectionHandlerDefinition()
};
return this.addRule(rule);
}
/**
* Hold open connections that match this rule, but never respond

@@ -381,0 +409,0 @@ * with anything at all, typically causing a timeout on the client side.

@@ -16,2 +16,3 @@ import * as _ from 'lodash';

CloseConnectionHandlerDefinition,
ResetConnectionHandlerDefinition,
TimeoutHandlerDefinition,

@@ -214,6 +215,7 @@ ForwardingOptions,

// These two work equally well for HTTP requests as websockets, but it's
// These three work equally well for HTTP requests as websockets, but it's
// useful to reexport there here for consistency.
export {
CloseConnectionHandlerDefinition,
ResetConnectionHandlerDefinition,
TimeoutHandlerDefinition

@@ -228,3 +230,4 @@ };

'close-connection': CloseConnectionHandlerDefinition,
'reset-connection': ResetConnectionHandlerDefinition,
'timeout': TimeoutHandlerDefinition
};

@@ -18,2 +18,3 @@ import * as _ from 'lodash';

CloseConnectionHandler,
ResetConnectionHandler,
TimeoutHandler

@@ -287,6 +288,6 @@ } from '../requests/request-handlers';

// lastHopEncrypted is set in http-combo-server, for requests that have explicitly
// __lastHopEncrypted is set in http-combo-server, for requests that have explicitly
// CONNECTed upstream (which may then up/downgrade from the current encryption).
if (socket.lastHopEncrypted !== undefined) {
protocol = socket.lastHopEncrypted ? 'wss' : 'ws';
if (socket.__lastHopEncrypted !== undefined) {
protocol = socket.__lastHopEncrypted ? 'wss' : 'ws';
} else {

@@ -459,6 +460,7 @@ protocol = reqMessage.connection.encrypted ? 'wss' : 'ws';

// These two work equally well for HTTP requests as websockets, but it's
// These three work equally well for HTTP requests as websockets, but it's
// useful to reexport there here for consistency.
export {
CloseConnectionHandler,
ResetConnectionHandler,
TimeoutHandler

@@ -473,3 +475,4 @@ };

'close-connection': CloseConnectionHandler,
'reset-connection': ResetConnectionHandler,
'timeout': TimeoutHandler
};

@@ -8,2 +8,3 @@ import { MockedEndpoint, Headers } from "../../types";

CloseConnectionHandlerDefinition,
ResetConnectionHandlerDefinition,
PassThroughWebSocketHandlerOptions,

@@ -227,2 +228,29 @@ RejectWebSocketHandlerDefinition,

/**
* Reset connections that match this rule immediately, sending a TCP
* RST packet directly, without accepting the socket or sending any
* other response, and without cleanly closing the TCP connection.
*
* This is only supported in Node.js versions (>=16.17, >=18.3.0, or
* later), where `net.Socket` includes the `resetAndDestroy` method.
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
* This method returns a promise that resolves with a mocked endpoint.
* Wait for the promise to confirm that the rule has taken effect
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*
* @category Responses
*/
thenResetConnection(): Promise<MockedEndpoint> {
const rule: WebSocketRuleData = {
...this.buildBaseRuleData(),
handler: new ResetConnectionHandlerDefinition()
};
return this.addRule(rule);
}
/**
* Hold open connections that match this rule, but never respond

@@ -229,0 +257,0 @@ * with anything at all, typically causing a timeout on the client side.

import _ = require('lodash');
import now = require("performance-now");
import net = require('net');

@@ -7,9 +8,11 @@ import tls = require('tls');

import * as streams from 'stream';
import { makeDestroyable, DestroyableServer } from 'destroyable-server';
import httpolyglot = require('@httptoolkit/httpolyglot');
import now = require("performance-now");
import { NonTlsError, readTlsClientHello } from 'read-tls-client-hello';
import { TlsRequest } from '../types';
import { destroyable, DestroyableServer } from '../util/destroyable-server';
import { getCA, CAOptions } from '../util/tls';
import { TlsHandshakeFailure } from '../types';
import { getCA } from '../util/tls';
import { delay } from '../util/util';
import { buildSocketTimingInfo, buildSocketEventData } from '../util/socket-util';
import { MockttpHttpsOptions } from '../mockttp';

@@ -43,3 +46,3 @@ // Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to

debug: boolean,
https: CAOptions | undefined,
https: MockttpHttpsOptions | undefined,
http2: true | false | 'fallback'

@@ -105,3 +108,5 @@ };

? 'reset'
: 'unknown'; // Something \else.
: error.code === 'ERR_TLS_HANDSHAKE_TIMEOUT'
? 'handshake-timeout'
: 'unknown'; // Something else.

@@ -113,33 +118,12 @@ if (cause === 'unknown') console.log('Unknown TLS error:', error);

function buildTimingInfo(): Required<net.Socket>['__timingInfo'] {
return { initialSocket: Date.now(), initialSocketTimestamp: now() };
}
function buildTlsError(
socket: tls.TLSSocket,
cause: TlsRequest['failureCause']
): TlsRequest {
const timingInfo = socket.__timingInfo ||
socket._parent?.__timingInfo ||
buildTimingInfo();
cause: TlsHandshakeFailure['failureCause']
): TlsHandshakeFailure {
const eventData = buildSocketEventData(socket) as TlsHandshakeFailure;
return {
failureCause: cause,
hostname: socket.servername,
// These only work because of oncertcb monkeypatch above
remoteIpAddress: socket.remoteAddress || // Normal case
socket._parent?.remoteAddress || // Pre-certCB error, e.g. timeout
socket.initialRemoteAddress!, // Recorded by certCB monkeypatch
remotePort: socket.remotePort ||
socket._parent?.remotePort ||
socket.initialRemotePort!,
tags: [],
timingEvents: {
startTime: timingInfo.initialSocket,
connectTimestamp: timingInfo.initialSocketTimestamp,
tunnelTimestamp: timingInfo.tunnelSetupTimestamp,
handshakeTimestamp: timingInfo.tlsConnectedTimestamp,
failureTimestamp: now()
}
};
eventData.failureCause = cause;
eventData.timingEvents.failureTimestamp = now();
return eventData;
}

@@ -153,4 +137,5 @@

requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void,
tlsClientErrorListener: (socket: tls.TLSSocket, req: TlsRequest) => void
): Promise<DestroyableServer & net.Server> {
tlsClientErrorListener: (socket: tls.TLSSocket, req: TlsHandshakeFailure) => void,
tlsPassthroughListener: (socket: net.Socket, address: string, port?: number) => void
): Promise<DestroyableServer<net.Server>> {
let server: net.Server;

@@ -160,6 +145,6 @@ if (!options.https) {

} else {
const ca = await getCA(options.https!);
const defaultCert = ca.generateCertificate('localhost');
const ca = await getCA(options.https);
const defaultCert = ca.generateCertificate(options.https.defaultDomain ?? 'localhost');
server = httpolyglot.createServer({
const tlsServer = tls.createServer({
key: defaultCert.key,

@@ -189,11 +174,21 @@ cert: defaultCert.cert,

}
}, requestListener);
});
if (options.https.tlsPassthrough?.length) {
passThroughMatchingTls(
tlsServer,
options.https.tlsPassthrough,
tlsPassthroughListener
);
}
server = httpolyglot.createServer(tlsServer, requestListener);
}
server.on('connection', (socket: net.Socket | http2.ServerHttp2Stream) => {
socket.__timingInfo = socket.__timingInfo || buildTimingInfo();
socket.__timingInfo = socket.__timingInfo || buildSocketTimingInfo();
// All sockets are initially marked as using unencrypted upstream connections.
// If TLS is used, this is upgraded to 'true' by secureConnection below.
socket.lastHopEncrypted = false;
socket.__lastHopEncrypted = false;

@@ -213,3 +208,3 @@ // For actual sockets, set NODELAY to avoid any buffering whilst streaming. This is

} else if (!socket.__timingInfo) {
socket.__timingInfo = buildTimingInfo();
socket.__timingInfo = buildSocketTimingInfo();
}

@@ -219,3 +214,3 @@

socket.lastHopEncrypted = true;
socket.__lastHopEncrypted = true;
ifTlsDropped(socket, () => {

@@ -266,2 +261,3 @@ tlsClientErrorListener(socket, buildTlsError(socket, 'closed'));

socket.__timingInfo!.tunnelSetupTimestamp = now();
socket.__lastHopConnectAddress = connectUrl;
server.emit('connection', socket);

@@ -287,2 +283,3 @@ });

copyTimingDetails(res.socket, res.stream);
res.stream.__lastHopConnectAddress = connectUrl;

@@ -300,3 +297,3 @@ // When layering HTTP/2 on JS streams, we have to make sure the JS stream won't autoclose

return destroyable(server);
return makeDestroyable(server);
}

@@ -313,13 +310,20 @@

const SOCKET_ADDRESS_METADATA_FIELDS = [
'localAddress',
'localPort',
'remoteAddress',
'remotePort',
'__lastHopConnectAddress'
] as const;
// Update the target socket(-ish) with the address details from the source socket,
// iff the target has no details of its own.
function copyAddressDetails(
source: SocketIsh<'localAddress' | 'localPort' | 'remoteAddress' | 'remotePort'>,
target: SocketIsh<'localAddress' | 'localPort' | 'remoteAddress' | 'remotePort'>
source: SocketIsh<typeof SOCKET_ADDRESS_METADATA_FIELDS[number]>,
target: SocketIsh<typeof SOCKET_ADDRESS_METADATA_FIELDS[number]>
) {
const fields = ['localAddress', 'localPort', 'remoteAddress', 'remotePort'] as const;
Object.defineProperties(target, _.zipObject(fields,
_.range(fields.length).map(() => ({ writable: true }))
Object.defineProperties(target, _.zipObject(SOCKET_ADDRESS_METADATA_FIELDS,
_.range(SOCKET_ADDRESS_METADATA_FIELDS.length).map(() => ({ writable: true }))
));
fields.forEach((fieldName) => {
SOCKET_ADDRESS_METADATA_FIELDS.forEach((fieldName) => {
if (target[fieldName] === undefined) {

@@ -339,2 +343,43 @@ (target as any)[fieldName] = source[fieldName];

}
}
/**
* Takes a tls passthrough list, and reconfigures a given TLS server so that all
* matching requests are passed to the given passthrough listener, instead of
* beginning a full TLS handshake.
*/
function passThroughMatchingTls(
server: tls.Server,
passthroughList: Required<MockttpHttpsOptions>['tlsPassthrough'],
listener: (socket: net.Socket, address: string, port?: number) => void
) {
const hostnames = passthroughList.map(({ hostname }) => hostname);
const tlsConnectionListener = server.listeners('connection')[0] as (socket: net.Socket) => {};
server.removeListener('connection', tlsConnectionListener);
server.on('connection', async (socket: net.Socket) => {
try {
const helloData = await readTlsClientHello(socket);
const [connectHostname, connectPort] = socket.__lastHopConnectAddress?.split(':') ?? [];
const sniHostname = helloData.serverName;
if (connectHostname && hostnames.includes(connectHostname)) {
listener(socket, connectHostname, connectPort ? parseInt(connectPort, 10) : undefined)
return; // Do not continue with TLS
} else if (sniHostname && hostnames.includes(sniHostname)) {
listener(socket, sniHostname); // Can't guess the port - it's not included in SNI
return; // Do not continue with TLS
}
} catch (e) {
if (!(e instanceof NonTlsError)) { // Don't even warn for non-TLS traffic
console.warn(`TLS client hello data not available for TLS connection from ${
socket.remoteAddress ?? 'unknown address'
}: ${(e as Error).message ?? e}`);
}
}
// Didn't match a passthrough hostname - continue with TLS setup
tlsConnectionListener.call(server, socket);
});
}

@@ -21,3 +21,3 @@ import _ = require("lodash");

CompletedResponse,
TlsRequest,
TlsHandshakeFailure,
ClientError,

@@ -27,7 +27,13 @@ TimingEvents,

WebSocketMessage,
WebSocketClose
WebSocketClose,
TlsPassthroughEvent
} from "../types";
import { CAOptions } from '../util/tls';
import { DestroyableServer } from "../util/destroyable-server";
import { Mockttp, AbstractMockttp, MockttpOptions, PortRange } from "../mockttp";
import { DestroyableServer } from "destroyable-server";
import {
Mockttp,
AbstractMockttp,
MockttpOptions,
MockttpHttpsOptions,
PortRange
} from "../mockttp";
import { RequestRule, RequestRuleData } from "../rules/requests/request-rule";

@@ -38,5 +44,6 @@ import { ServerMockedEndpoint } from "./mocked-endpoint";

import { Mutable } from "../util/type-utils";
import { isErrorLike } from "../util/error";
import { ErrorLike, isErrorLike } from "../util/error";
import { makePropertyWritable } from "../util/util";
import { buildSocketEventData, isSocketLoop } from "../util/socket-util";
import {

@@ -80,3 +87,3 @@ parseRequestBody,

private httpsOptions: CAOptions | undefined;
private httpsOptions: MockttpHttpsOptions | undefined;
private isHttp2Enabled: true | false | 'fallback';

@@ -86,3 +93,3 @@ private maxBodySize: number;

private app: connect.Server;
private server: DestroyableServer & net.Server | undefined;
private server: DestroyableServer<net.Server> | undefined;

@@ -136,3 +143,3 @@ private eventEmitter: EventEmitter;

http2: this.isHttp2Enabled,
}, this.app, this.announceTlsErrorAsync.bind(this));
}, this.app, this.announceTlsErrorAsync.bind(this), this.passthroughSocket.bind(this));

@@ -293,3 +300,5 @@ this.server!.listen(port);

public on(event: 'websocket-close', callback: (close: WebSocketClose) => void): Promise<void>;
public on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise<void>;
public on(event: 'tls-passthrough-opened', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
public on(event: 'tls-passthrough-closed', callback: (req: TlsPassthroughEvent) => void): Promise<void>;
public on(event: 'tls-client-error', callback: (req: TlsHandshakeFailure) => void): Promise<void>;
public on(event: 'client-error', callback: (error: ClientError) => void): Promise<void>;

@@ -490,3 +499,3 @@ public on(event: string, callback: (...args: any[]) => void): Promise<void> {

private async announceAbortAsync(request: OngoingRequest) {
private async announceAbortAsync(request: OngoingRequest, abortError?: ErrorLike) {
setImmediate(() => {

@@ -496,3 +505,9 @@ const req = buildInitiatedRequest(request);

timingEvents: _.clone(req.timingEvents),
tags: _.clone(req.tags)
tags: _.clone(req.tags),
error: abortError ? {
name: abortError.name,
code: abortError.code,
message: abortError.message,
stack: abortError.stack
} : undefined
}));

@@ -502,3 +517,3 @@ });

private async announceTlsErrorAsync(socket: net.Socket, request: TlsRequest) {
private async announceTlsErrorAsync(socket: net.Socket, request: TlsHandshakeFailure) {
// Ignore errors after TLS is setup, those are client errors

@@ -536,3 +551,3 @@ if (socket instanceof tls.TLSSocket && socket.tlsSetupCompleted) return;

req.protocol = req.headers[':scheme'] as string ||
(req.socket.lastHopEncrypted ? 'https' : 'http');
(req.socket.__lastHopEncrypted ? 'https' : 'http');
req.path = req.url;

@@ -596,7 +611,7 @@

let result: 'responded' | 'aborted' | null = null;
const abort = () => {
const abort = (error?: Error) => {
if (result === null) {
result = 'aborted';
request.timingEvents.abortedTimestamp = now();
this.announceAbortAsync(request);
this.announceAbortAsync(request, error);
}

@@ -608,3 +623,3 @@ }

rawResponse.once('close', () => setImmediate(abort));
request.once('error', () => setImmediate(abort));
request.once('error', (error) => setImmediate(() => abort(error)));

@@ -622,3 +637,3 @@ this.announceInitialRequestAsync(request);

console.log('Response error:', this.debug ? error : error.message);
abort();
abort(error);
});

@@ -648,3 +663,3 @@

if (e instanceof AbortError) {
abort();
abort(e);

@@ -673,3 +688,3 @@ if (this.debug) {

} catch (e) {
abort();
abort(e as Error);
}

@@ -1018,2 +1033,61 @@ }

}
private outgoingPassthroughSockets: Set<net.Socket> = new Set();
private passthroughSocket(
socket: net.Socket,
host: string,
port?: number
) {
const targetPort = port || 443;
if (isSocketLoop(this.outgoingPassthroughSockets, socket)) {
// Hard to reproduce: loops can only happen if a) SNI triggers this (because tunnels
// require a repeated client request at each step) and b) the hostname points back to
// us, and c) we're running on the default port. Still good to guard against though.
console.warn(`Socket bypass loop for ${host}:${targetPort}`);
if ('resetAndDestroy' in socket) {
socket.resetAndDestroy();
} else {
socket.destroy();
}
return;
}
if (socket.closed) return; // Nothing to do
const eventData = buildSocketEventData(socket as any) as TlsPassthroughEvent;
eventData.id = uuid();
eventData.hostname = host;
eventData.upstreamPort = targetPort;
setImmediate(() => this.eventEmitter.emit('tls-passthrough-opened', eventData));
const upstreamSocket = net.connect({ host, port: targetPort });
socket.pipe(upstreamSocket);
upstreamSocket.pipe(socket);
socket.on('error', () => upstreamSocket.destroy());
upstreamSocket.on('error', () => socket.destroy());
upstreamSocket.on('close', () => socket.destroy());
socket.on('close', () => {
upstreamSocket.destroy();
setImmediate(() => {
this.eventEmitter.emit('tls-passthrough-closed', {
...eventData,
timingEvents: {
...eventData.timingEvents,
disconnectedTimestamp: now()
}
});
});
});
upstreamSocket.once('connect', () => this.outgoingPassthroughSockets.add(upstreamSocket));
upstreamSocket.once('close', () => this.outgoingPassthroughSockets.delete(upstreamSocket));
if (this.debug) console.log(`Passing through raw bypassed connection to ${host}:${targetPort}${
!port ? ' (assumed port)' : ''
}`);
}
}

@@ -66,7 +66,6 @@ import stream = require('stream');

export interface TlsRequest {
export interface TlsConnectionEvent {
hostname?: string;
remoteIpAddress: string;
remotePort: number;
failureCause: 'closed' | 'reset' | 'cert-rejected' | 'no-shared-cipher' | 'unknown';
tags: string[];

@@ -76,13 +75,61 @@ timingEvents: TlsTimingEvents;

export interface TlsPassthroughEvent extends TlsConnectionEvent {
id: string;
upstreamPort: number;
}
export interface TlsHandshakeFailure extends TlsConnectionEvent {
failureCause:
| 'closed'
| 'reset'
| 'cert-rejected'
| 'no-shared-cipher'
| 'handshake-timeout'
| 'unknown';
timingEvents: TlsFailureTimingEvents;
}
export interface TlsTimingEvents {
startTime: number; // Ms since unix epoch
/**
* When the socket initially connected, in MS since the unix
* epoch.
*/
startTime: number;
// High-precision floating-point monotonically increasing timestamps.
// Comparable and precise, but not related to specific current time.
connectTimestamp: number; // When the socket initially connected
failureTimestamp: number; // When the error occurred
handshakeTimestamp?: number; // When the handshake completed (if it did)
tunnelTimestamp?: number; // When the outer tunnel was create (if present)
/**
* When the socket initially connected, equivalent to startTime.
*
* High-precision floating-point monotonically increasing timestamps.
* Comparable and precise, but not related to specific current time.
*/
connectTimestamp: number;
/**
* When Mockttp's handshake for this connection was completed (if there
* was one). This is not set for passed through connections.
*/
handshakeTimestamp?: number;
/**
* When the outer tunnel (e.g. a preceeding CONNECT request) was created,
* if there was one.
*/
tunnelTimestamp?: number;
/**
* When the connection was closed, if it has been closed.
*/
disconnectTimestamp?: number;
}
export interface TlsFailureTimingEvents extends TlsTimingEvents {
/**
* When the TLS connection failed. This may be due to a failed handshake
* (in which case `handshakeTimestamp` will be undefined) or due to a
* subsequent error which means the TLS connection was not usable (like
* an immediate closure due to an async certificate rejection).
*/
failureTimestamp: number;
}
// Internal representation of an ongoing HTTP request whilst it's being processed

@@ -144,2 +191,11 @@ export interface OngoingRequest extends Request, EventEmitter {

export interface AbortedRequest extends InitiatedRequest {
error?: {
name?: string;
code?: string;
message?: string;
stack?: string;
};
}
// Internal & external representation of a fully completed HTTP request

@@ -146,0 +202,0 @@ export interface CompletedRequest extends Request {

@@ -454,3 +454,3 @@ import * as _ from 'lodash';

try {
req.protocol = socket.lastHopEncrypted ? "https" : "http"; // Wild guess really
req.protocol = socket.__lastHopEncrypted ? "https" : "http"; // Wild guess really

@@ -457,0 +457,0 @@ // For TLS sockets, we default the hostname to the name given by SNI. Might be overridden

import * as _ from 'lodash';
import now = require("performance-now");
import * as os from 'os';
import * as net from 'net';
import * as tls from 'tls';
import { isNode } from './util';
import { TlsConnectionEvent } from '../types';

@@ -68,1 +71,39 @@ // Test if a local port for a given interface (IPv4/6) is currently in use

});
export const resetSocket = (socket: net.Socket) => {
if (!('resetAndDestroy' in socket)) {
throw new Error(
'Connection reset is only supported in Node v16.17+, v18.3.0+, or later'
);
}
socket.resetAndDestroy();
};
export function buildSocketEventData(socket: net.Socket & Partial<tls.TLSSocket>): TlsConnectionEvent {
const timingInfo = socket.__timingInfo ||
socket._parent?.__timingInfo ||
buildSocketTimingInfo();
return {
hostname: socket.servername,
// These only work because of oncertcb monkeypatch above
remoteIpAddress: socket.remoteAddress || // Normal case
socket._parent?.remoteAddress || // Pre-certCB error, e.g. timeout
socket.initialRemoteAddress!, // Recorded by certCB monkeypatch
remotePort: socket.remotePort ||
socket._parent?.remotePort ||
socket.initialRemotePort!,
tags: [],
timingEvents: {
startTime: timingInfo.initialSocket,
connectTimestamp: timingInfo.initialSocketTimestamp,
tunnelTimestamp: timingInfo.tunnelSetupTimestamp,
handshakeTimestamp: timingInfo.tlsConnectedTimestamp
}
};
}
export function buildSocketTimingInfo(): Required<net.Socket>['__timingInfo'] {
return { initialSocket: Date.now(), initialSocketTimestamp: now() };
}

@@ -9,13 +9,18 @@ import * as _ from 'lodash';

export type CAOptions = (HttpsOptions | HttpsPathOptions);
export type CAOptions = (CertDataOptions | CertPathOptions);
export type HttpsOptions = {
export interface CertDataOptions extends BaseCAOptions {
key: string;
cert: string;
keyLength?: number;
};
export type HttpsPathOptions = {
export interface CertPathOptions extends BaseCAOptions {
keyPath: string;
certPath: string;
}
export interface BaseCAOptions {
/**
* Minimum key length when generating certificates. Defaults to 2048.
*/
keyLength?: number;

@@ -117,15 +122,14 @@ }

export async function getCA(options: CAOptions): Promise<CA> {
let httpsOptions: HttpsOptions;
if ((<any>options).key && (<any>options).cert) {
httpsOptions = <HttpsOptions> options;
let certOptions: CertDataOptions;
if ('key' in options && 'cert' in options) {
certOptions = options;
}
else if ((<any>options).keyPath && (<any>options).certPath) {
let pathOptions = <HttpsPathOptions> options;
httpsOptions = await Promise.all([
fs.readFile(pathOptions.keyPath, 'utf8'),
fs.readFile(pathOptions.certPath, 'utf8')
else if ('keyPath' in options && 'certPath' in options) {
certOptions = await Promise.all([
fs.readFile(options.keyPath, 'utf8'),
fs.readFile(options.certPath, 'utf8')
]).then(([ keyContents, certContents ]) => ({
key: keyContents,
cert: certContents,
keyLength: pathOptions.keyLength
keyLength: options.keyLength
}));

@@ -137,3 +141,3 @@ }

return new CA(httpsOptions.key, httpsOptions.cert, httpsOptions.keyLength || 2048);
return new CA(certOptions.key, certOptions.cert, certOptions.keyLength || 2048);
}

@@ -140,0 +144,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

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

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc