Comparing version 3.5.1 to 3.6.0
@@ -19,2 +19,6 @@ // There's a few places where we attach extra data to some node objects during | ||
// Extra metadata attached to a TLS socket, taken from the client hello and | ||
// preceeding tunneling steps. | ||
__tlsMetadata?: {}; // Can't ref Mockttp real type here | ||
// Normally only defined on TLSSocket, but useful to explicitly include here | ||
@@ -21,0 +25,0 @@ // Undefined on plain HTTP, 'true' on TLSSocket. |
@@ -71,2 +71,3 @@ "use strict"; | ||
timingEvents: Json! | ||
tlsMetadata: Json! | ||
} | ||
@@ -82,2 +83,3 @@ | ||
timingEvents: Json! | ||
tlsMetadata: Json! | ||
} | ||
@@ -84,0 +86,0 @@ |
@@ -46,2 +46,12 @@ "use strict"; | ||
message.tags = []; | ||
if (event === null || event === void 0 ? void 0 : event.startsWith('tls-')) { | ||
// TLS passthrough & error events should have raw JSON socket metadata: | ||
if (message.tlsMetadata) { | ||
message.tlsMetadata = JSON.parse(message.tlsMetadata); | ||
} | ||
else { | ||
// For old servers, just use empty metadata: | ||
message.tlsMetadata = {}; | ||
} | ||
} | ||
} | ||
@@ -335,2 +345,3 @@ function normalizeWebSocketMessage(message) { | ||
timingEvents | ||
${this.schema.asOptionalField('TlsPassthroughEvent', 'tlsMetadata')} | ||
} | ||
@@ -348,2 +359,3 @@ }`, | ||
timingEvents | ||
${this.schema.asOptionalField('TlsPassthroughEvent', 'tlsMetadata')} | ||
} | ||
@@ -359,2 +371,3 @@ }`, | ||
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'timingEvents')} | ||
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'tlsMetadata')} | ||
} | ||
@@ -361,0 +374,0 @@ }`, |
@@ -150,5 +150,3 @@ "use strict"; | ||
}); | ||
if ((_b = options.https.tlsPassthrough) === null || _b === void 0 ? void 0 : _b.length) { | ||
passThroughMatchingTls(tlsServer, options.https.tlsPassthrough, tlsPassthroughListener); | ||
} | ||
analyzeAndMaybePassThroughTls(tlsServer, (_b = options.https.tlsPassthrough) !== null && _b !== void 0 ? _b : [], tlsPassthroughListener); | ||
server = httpolyglot.createServer(tlsServer, requestListener); | ||
@@ -167,2 +165,3 @@ } | ||
server.on('secureConnection', (socket) => { | ||
var _a; | ||
const parentSocket = (0, socket_util_1.getParentSocket)(socket); | ||
@@ -174,2 +173,5 @@ if (parentSocket) { | ||
copyTimingDetails(parentSocket, socket); | ||
// With TLS metadata, we only propagate directly from parent sockets, not through | ||
// CONNECT etc - we only want it if the final hop is TLS, previous values don't matter. | ||
(_a = socket.__tlsMetadata) !== null && _a !== void 0 ? _a : (socket.__tlsMetadata = parentSocket.__tlsMetadata); | ||
} | ||
@@ -274,7 +276,7 @@ else if (!socket.__timingInfo) { | ||
/** | ||
* 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. | ||
* Takes a tls passthrough list (may be empty), and reconfigures a given TLS server so that all | ||
* client hellos are parsed, matching requests are passed to the given passthrough listener (without | ||
* continuing setup) and client hello metadata is attached to all sockets. | ||
*/ | ||
function passThroughMatchingTls(server, passthroughList, listener) { | ||
function analyzeAndMaybePassThroughTls(server, passthroughList, passthroughListener) { | ||
const hostnames = passthroughList.map(({ hostname }) => hostname); | ||
@@ -289,8 +291,16 @@ const tlsConnectionListener = server.listeners('connection')[0]; | ||
const sniHostname = helloData.serverName; | ||
socket.__tlsMetadata = { | ||
sniHostname, | ||
connectHostname, | ||
connectPort, | ||
clientAlpn: helloData.alpnProtocols, | ||
ja3Fingerprint: (0, read_tls_client_hello_1.calculateJa3FromFingerprintData)(helloData.fingerprintData) | ||
}; | ||
if (connectHostname && hostnames.includes(connectHostname)) { | ||
listener(socket, connectHostname, connectPort ? parseInt(connectPort, 10) : undefined); | ||
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined; | ||
passthroughListener(socket, connectHostname, upstreamPort); | ||
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 | ||
passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI | ||
return; // Do not continue with TLS | ||
@@ -297,0 +307,0 @@ } |
@@ -54,3 +54,11 @@ /// <reference types="node" /> | ||
timingEvents: TlsTimingEvents; | ||
tlsMetadata: TlsSocketMetadata; | ||
} | ||
export interface TlsSocketMetadata { | ||
sniHostname?: string; | ||
connectHostname?: string; | ||
connectPort?: string; | ||
clientAlpn?: string[]; | ||
ja3Fingerprint?: string; | ||
} | ||
export interface TlsPassthroughEvent extends TlsConnectionEvent { | ||
@@ -57,0 +65,0 @@ id: string; |
@@ -156,14 +156,18 @@ "use strict"; | ||
function buildSocketEventData(socket) { | ||
var _a, _b, _c; | ||
var _a, _b, _c, _d; | ||
const timingInfo = socket.__timingInfo || | ||
((_a = socket._parent) === null || _a === void 0 ? void 0 : _a.__timingInfo) || | ||
buildSocketTimingInfo(); | ||
// Attached in passThroughMatchingTls TLS sniffing logic in http-combo-server: | ||
const tlsMetadata = socket.__tlsMetadata || | ||
((_b = socket._parent) === null || _b === void 0 ? void 0 : _b.__tlsMetadata) || | ||
{}; | ||
return { | ||
hostname: socket.servername, | ||
// These only work because of oncertcb monkeypatch above | ||
// These only work because of oncertcb monkeypatch in http-combo-server: | ||
remoteIpAddress: socket.remoteAddress || // Normal case | ||
((_b = socket._parent) === null || _b === void 0 ? void 0 : _b.remoteAddress) || // Pre-certCB error, e.g. timeout | ||
((_c = socket._parent) === null || _c === void 0 ? void 0 : _c.remoteAddress) || // Pre-certCB error, e.g. timeout | ||
socket.initialRemoteAddress, | ||
remotePort: socket.remotePort || | ||
((_c = socket._parent) === null || _c === void 0 ? void 0 : _c.remotePort) || | ||
((_d = socket._parent) === null || _d === void 0 ? void 0 : _d.remotePort) || | ||
socket.initialRemotePort, | ||
@@ -176,3 +180,4 @@ tags: [], | ||
handshakeTimestamp: timingInfo.tlsConnectedTimestamp | ||
} | ||
}, | ||
tlsMetadata | ||
}; | ||
@@ -179,0 +184,0 @@ } |
@@ -16,2 +16,17 @@ /// <reference types="node" /> | ||
keyLength?: number; | ||
/** | ||
* The countryName that will be used in the certificate for incoming TLS | ||
* connections. | ||
*/ | ||
countryName?: string; | ||
/** | ||
* The localityName that will be used in the certificate for incoming TLS | ||
* connections. | ||
*/ | ||
localityName?: string; | ||
/** | ||
* The organizationName that will be used in the certificate for incoming TLS | ||
* connections. | ||
*/ | ||
organizationName?: string; | ||
} | ||
@@ -47,5 +62,6 @@ export declare type PEM = string | string[] | Buffer | Buffer[]; | ||
private caKey; | ||
private options; | ||
private certCache; | ||
constructor(caKey: PEM, caCert: PEM, keyLength: number); | ||
constructor(options: CertDataOptions); | ||
generateCertificate(domain: string): GeneratedCertificate; | ||
} |
@@ -100,7 +100,3 @@ "use strict"; | ||
fs.readFile(options.certPath, 'utf8') | ||
]).then(([keyContents, certContents]) => ({ | ||
key: keyContents, | ||
cert: certContents, | ||
keyLength: options.keyLength | ||
})); | ||
]).then(([keyContents, certContents]) => (Object.assign(Object.assign({}, _.omit(options, ['keyPath', 'certPath'])), { key: keyContents, cert: certContents }))); | ||
} | ||
@@ -110,3 +106,3 @@ else { | ||
} | ||
return new CA(certOptions.key, certOptions.cert, certOptions.keyLength || 2048); | ||
return new CA(certOptions); | ||
}); | ||
@@ -122,6 +118,8 @@ } | ||
class CA { | ||
constructor(caKey, caCert, keyLength) { | ||
this.caKey = pki.privateKeyFromPem(caKey.toString('utf8')); | ||
this.caCert = pki.certificateFromPem(caCert.toString('utf8')); | ||
constructor(options) { | ||
this.caKey = pki.privateKeyFromPem(options.key.toString()); | ||
this.caCert = pki.certificateFromPem(options.cert.toString()); | ||
this.certCache = {}; | ||
this.options = options !== null && options !== void 0 ? options : {}; | ||
const keyLength = options.keyLength || 2048; | ||
if (!KEY_PAIR || KEY_PAIR.length < keyLength) { | ||
@@ -133,2 +131,3 @@ // If we have no key, or not a long enough one, generate one. | ||
generateCertificate(domain) { | ||
var _a, _b, _c, _d, _e, _f; | ||
// TODO: Expire domains from the cache? Based on their actual expiry? | ||
@@ -164,5 +163,5 @@ if (this.certCache[domain]) | ||
: [{ name: 'commonName', value: domain }]), | ||
{ name: 'countryName', value: 'XX' }, | ||
{ name: 'localityName', value: 'Unknown' }, | ||
{ name: 'organizationName', value: 'Mockttp Cert - DO NOT TRUST' } | ||
{ name: 'countryName', value: (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.countryName) !== null && _b !== void 0 ? _b : 'XX' }, | ||
{ name: 'localityName', value: (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.localityName) !== null && _d !== void 0 ? _d : 'Unknown' }, | ||
{ name: 'organizationName', value: (_f = (_e = this.options) === null || _e === void 0 ? void 0 : _e.organizationName) !== null && _f !== void 0 ? _f : 'Mockttp Cert - DO NOT TRUST' } | ||
]); | ||
@@ -169,0 +168,0 @@ cert.setIssuer(this.caCert.subject.attributes); |
{ | ||
"name": "mockttp", | ||
"version": "3.5.1", | ||
"version": "3.6.0", | ||
"description": "Mock HTTP server for testing HTTP clients and stubbing webservices", | ||
@@ -5,0 +5,0 @@ "exports": { |
@@ -69,2 +69,3 @@ import gql from "graphql-tag"; | ||
timingEvents: Json! | ||
tlsMetadata: Json! | ||
} | ||
@@ -80,2 +81,3 @@ | ||
timingEvents: Json! | ||
tlsMetadata: Json! | ||
} | ||
@@ -82,0 +84,0 @@ |
@@ -10,4 +10,2 @@ import _ = require('lodash'); | ||
import type { Serialized } from '../serialization/serialization'; | ||
import { AdminQuery } from './admin-query'; | ||
@@ -51,2 +49,12 @@ import { SchemaIntrospector } from './schema-introspection'; | ||
if (!message.tags) message.tags = []; | ||
if (event?.startsWith('tls-')) { | ||
// TLS passthrough & error events should have raw JSON socket metadata: | ||
if (message.tlsMetadata) { | ||
message.tlsMetadata = JSON.parse(message.tlsMetadata); | ||
} else { | ||
// For old servers, just use empty metadata: | ||
message.tlsMetadata = {}; | ||
} | ||
} | ||
} | ||
@@ -369,2 +377,3 @@ | ||
timingEvents | ||
${this.schema.asOptionalField('TlsPassthroughEvent', 'tlsMetadata')} | ||
} | ||
@@ -382,2 +391,3 @@ }`, | ||
timingEvents | ||
${this.schema.asOptionalField('TlsPassthroughEvent', 'tlsMetadata')} | ||
} | ||
@@ -393,2 +403,3 @@ }`, | ||
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'timingEvents')} | ||
${this.schema.asOptionalField(['TlsHandshakeFailure', 'TlsRequest'], 'tlsMetadata')} | ||
} | ||
@@ -395,0 +406,0 @@ }`, |
@@ -10,3 +10,7 @@ import _ = require('lodash'); | ||
import httpolyglot = require('@httptoolkit/httpolyglot'); | ||
import { NonTlsError, readTlsClientHello } from 'read-tls-client-hello'; | ||
import { | ||
calculateJa3FromFingerprintData, | ||
NonTlsError, | ||
readTlsClientHello | ||
} from 'read-tls-client-hello'; | ||
@@ -175,9 +179,7 @@ import { TlsHandshakeFailure } from '../types'; | ||
if (options.https.tlsPassthrough?.length) { | ||
passThroughMatchingTls( | ||
tlsServer, | ||
options.https.tlsPassthrough, | ||
tlsPassthroughListener | ||
); | ||
} | ||
analyzeAndMaybePassThroughTls( | ||
tlsServer, | ||
options.https.tlsPassthrough ?? [], | ||
tlsPassthroughListener | ||
); | ||
@@ -206,2 +208,5 @@ server = httpolyglot.createServer(tlsServer, requestListener); | ||
copyTimingDetails(parentSocket, socket); | ||
// With TLS metadata, we only propagate directly from parent sockets, not through | ||
// CONNECT etc - we only want it if the final hop is TLS, previous values don't matter. | ||
socket.__tlsMetadata ??= parentSocket.__tlsMetadata; | ||
} else if (!socket.__timingInfo) { | ||
@@ -334,10 +339,10 @@ socket.__timingInfo = buildSocketTimingInfo(); | ||
/** | ||
* 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. | ||
* Takes a tls passthrough list (may be empty), and reconfigures a given TLS server so that all | ||
* client hellos are parsed, matching requests are passed to the given passthrough listener (without | ||
* continuing setup) and client hello metadata is attached to all sockets. | ||
*/ | ||
function passThroughMatchingTls( | ||
function analyzeAndMaybePassThroughTls( | ||
server: tls.Server, | ||
passthroughList: Required<MockttpHttpsOptions>['tlsPassthrough'], | ||
listener: (socket: net.Socket, address: string, port?: number) => void | ||
passthroughListener: (socket: net.Socket, address: string, port?: number) => void | ||
) { | ||
@@ -355,7 +360,16 @@ const hostnames = passthroughList.map(({ hostname }) => hostname); | ||
socket.__tlsMetadata = { | ||
sniHostname, | ||
connectHostname, | ||
connectPort, | ||
clientAlpn: helloData.alpnProtocols, | ||
ja3Fingerprint: calculateJa3FromFingerprintData(helloData.fingerprintData) | ||
}; | ||
if (connectHostname && hostnames.includes(connectHostname)) { | ||
listener(socket, connectHostname, connectPort ? parseInt(connectPort, 10) : undefined) | ||
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined; | ||
passthroughListener(socket, connectHostname, upstreamPort); | ||
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 | ||
passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI | ||
return; // Do not continue with TLS | ||
@@ -362,0 +376,0 @@ } |
@@ -72,4 +72,13 @@ import stream = require('stream'); | ||
timingEvents: TlsTimingEvents; | ||
tlsMetadata: TlsSocketMetadata; | ||
} | ||
export interface TlsSocketMetadata { | ||
sniHostname?: string; | ||
connectHostname?: string; | ||
connectPort?: string; | ||
clientAlpn?: string[]; | ||
ja3Fingerprint?: string; | ||
} | ||
export interface TlsPassthroughEvent extends TlsConnectionEvent { | ||
@@ -76,0 +85,0 @@ id: string; |
@@ -161,5 +161,10 @@ import * as _ from 'lodash'; | ||
// Attached in passThroughMatchingTls TLS sniffing logic in http-combo-server: | ||
const tlsMetadata = socket.__tlsMetadata || | ||
socket._parent?.__tlsMetadata || | ||
{}; | ||
return { | ||
hostname: socket.servername, | ||
// These only work because of oncertcb monkeypatch above | ||
// These only work because of oncertcb monkeypatch in http-combo-server: | ||
remoteIpAddress: socket.remoteAddress || // Normal case | ||
@@ -177,3 +182,4 @@ socket._parent?.remoteAddress || // Pre-certCB error, e.g. timeout | ||
handshakeTimestamp: timingInfo.tlsConnectedTimestamp | ||
} | ||
}, | ||
tlsMetadata | ||
}; | ||
@@ -180,0 +186,0 @@ } |
@@ -26,2 +26,20 @@ import * as _ from 'lodash'; | ||
keyLength?: number; | ||
/** | ||
* The countryName that will be used in the certificate for incoming TLS | ||
* connections. | ||
*/ | ||
countryName?: string; | ||
/** | ||
* The localityName that will be used in the certificate for incoming TLS | ||
* connections. | ||
*/ | ||
localityName?: string; | ||
/** | ||
* The organizationName that will be used in the certificate for incoming TLS | ||
* connections. | ||
*/ | ||
organizationName?: string; | ||
} | ||
@@ -131,5 +149,5 @@ | ||
]).then(([ keyContents, certContents ]) => ({ | ||
..._.omit(options, ['keyPath', 'certPath']), | ||
key: keyContents, | ||
cert: certContents, | ||
keyLength: options.keyLength | ||
cert: certContents | ||
})); | ||
@@ -141,3 +159,3 @@ } | ||
return new CA(certOptions.key, certOptions.cert, certOptions.keyLength || 2048); | ||
return new CA(certOptions); | ||
} | ||
@@ -159,14 +177,14 @@ | ||
private caKey: forge.pki.PrivateKey; | ||
private options: CertDataOptions; | ||
private certCache: { [domain: string]: GeneratedCertificate }; | ||
constructor( | ||
caKey: PEM, | ||
caCert: PEM, | ||
keyLength: number | ||
) { | ||
this.caKey = pki.privateKeyFromPem(caKey.toString('utf8')); | ||
this.caCert = pki.certificateFromPem(caCert.toString('utf8')); | ||
constructor(options: CertDataOptions) { | ||
this.caKey = pki.privateKeyFromPem(options.key.toString()); | ||
this.caCert = pki.certificateFromPem(options.cert.toString()); | ||
this.certCache = {}; | ||
this.options = options ?? {}; | ||
const keyLength = options.keyLength || 2048; | ||
if (!KEY_PAIR || KEY_PAIR.length < keyLength) { | ||
@@ -221,5 +239,5 @@ // If we have no key, or not a long enough one, generate one. | ||
), | ||
{ name: 'countryName', value: 'XX' }, // ISO-3166-1 alpha-2 'unknown country' code | ||
{ name: 'localityName', value: 'Unknown' }, | ||
{ name: 'organizationName', value: 'Mockttp Cert - DO NOT TRUST' } | ||
{ name: 'countryName', value: this.options?.countryName ?? 'XX' }, // ISO-3166-1 alpha-2 'unknown country' code | ||
{ name: 'localityName', value: this.options?.localityName ?? 'Unknown' }, | ||
{ name: 'organizationName', value: this.options?.organizationName ?? 'Mockttp Cert - DO NOT TRUST' } | ||
]); | ||
@@ -226,0 +244,0 @@ cert.setIssuer(this.caCert.subject.attributes); |
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1393580
24851