Comparing version 0.21.1 to 0.21.2
@@ -6,3 +6,3 @@ // There's a few places where we attach extra data to some node objects during | ||
import * as net from 'net'; | ||
import * as stream from 'stream'; | ||
import * as streams from 'stream'; | ||
@@ -29,4 +29,15 @@ interface Socket { | ||
// Internal reference to a parent socket, e.g. for TLS wrappers | ||
// Our recordings of various timestamps, used for monitoring & | ||
// performance analysis later on | ||
__timingInfo?: { | ||
initialSocket?: number; // Initial raw socket time | ||
tunnelSetup?: number; // Latest CONNECT completion, if any | ||
tlsConnected?: number; // Latest TLS handshake completion, if any | ||
}; | ||
// Internal reference to the parent socket, available on TLS sockets | ||
_parent?: Socket; | ||
// Internal reference to the underlying stream, available on _stream_wrap | ||
stream?: streams.Duplex & Partial<net.Socket>; | ||
} | ||
@@ -33,0 +44,0 @@ } |
@@ -444,3 +444,3 @@ "use strict"; | ||
!_.includes(this.ignoreHostCertificateErrors, hostWithPort); | ||
// Use a client cert if it's listed for the host+port or whole hostname | ||
// Use a client cert if it's listed for the host+port or whole hostname | ||
const clientCert = this.clientCertificateHostMap[hostWithPort] || | ||
@@ -540,5 +540,14 @@ this.clientCertificateHostMap[hostname] || | ||
}); | ||
clientRes.writeHead(serverStatusCode, serverStatusMessage || clientRes.statusMessage); | ||
try { | ||
clientRes.writeHead(serverStatusCode, serverStatusMessage || clientRes.statusMessage); | ||
} | ||
catch (e) { | ||
serverReq.abort(); | ||
reject(e); | ||
} | ||
if (resBodyOverride) { | ||
// Return the override data to the client: | ||
clientRes.end(resBodyOverride); | ||
// Dump the real response data: | ||
serverRes.resume(); | ||
resolve(); | ||
@@ -548,3 +557,3 @@ } | ||
serverRes.pipe(clientRes); | ||
serverRes.once('end', resolve); | ||
clientRes.once('finish', resolve); | ||
} | ||
@@ -572,4 +581,10 @@ })); | ||
} | ||
clientReq.once('abort', () => serverReq.abort()); | ||
clientRes.once('close', () => serverReq.abort()); | ||
// If the downstream connection aborts, before the response has been completed, | ||
// we also abort the upstream connection. Important to avoid unnecessary connections, | ||
// and to correctly proxy client connection behaviour to the upstream server. | ||
function abortUpstream() { | ||
serverReq.abort(); | ||
} | ||
clientReq.on('aborted', abortUpstream); | ||
clientRes.once('finish', () => clientReq.removeListener('aborted', abortUpstream)); | ||
serverReq.once('error', (e) => { | ||
@@ -576,0 +591,0 @@ if (serverReq.aborted) |
@@ -15,3 +15,2 @@ "use strict"; | ||
const tls = require("tls"); | ||
const SocketWrapper = require("_stream_wrap"); | ||
const httpolyglot = require("@httptoolkit/httpolyglot"); | ||
@@ -46,8 +45,15 @@ const destroyable_server_1 = require("../util/destroyable-server"); | ||
socket.once('data', resolve); | ||
// If you keep it open, you probably trust the TLS connection | ||
// (some clients open unused connections for connection pools etc) | ||
util_1.delay(5000).then(resolve); | ||
// If you silently close it very quicky, you probably don't trust us | ||
socket.once('close', reject); | ||
socket.once('end', reject); | ||
// Some clients open later-unused TLS connections for connection pools, preconnect, etc. | ||
// Even if these are shut later on, that doesn't mean they're are rejected connections. | ||
// To differentiate the two cases, we consider connections OK after waiting 10x longer | ||
// than the initial TLS handshake for an unhappy disconnection. | ||
const timing = socket.__timingInfo || {}; | ||
const tlsSetupDuration = timing.tlsConnected - (timing.tunnelSetup || timing.initialSocket); | ||
const maxTlsRejectionTime = (tlsSetupDuration !== NaN && tlsSetupDuration !== 0) | ||
? tlsSetupDuration * 10 | ||
: 5000; | ||
util_1.delay(maxTlsRejectionTime).then(resolve); | ||
}) | ||
@@ -126,2 +132,3 @@ .then(() => { | ||
server.on('connection', (socket) => { | ||
socket.__timingInfo = socket.__timingInfo || { initialSocket: Date.now() }; | ||
// All sockets are initially marked as using unencrypted upstream connections. | ||
@@ -132,7 +139,13 @@ // If TLS is used, this is upgraded to 'true' by secureConnection below. | ||
server.on('secureConnection', (socket) => { | ||
if (socket._parent) { | ||
const parentSocket = getParentSocket(socket); | ||
if (parentSocket) { | ||
// Sometimes wrapper TLS sockets created by the HTTP/2 server don't include the | ||
// underlying port details, so it's better to make sure we copy them up. | ||
copyAddressDetails(socket._parent, socket); | ||
// underlying socket details, so it's better to make sure we copy them up. | ||
copyAddressDetails(parentSocket, socket); | ||
copyTimingDetails(parentSocket, socket); | ||
} | ||
else if (!socket.__timingInfo) { | ||
socket.__timingInfo = { initialSocket: Date.now() }; | ||
} | ||
socket.__timingInfo.tlsConnected = Date.now(); | ||
socket.lastHopEncrypted = true; | ||
@@ -189,6 +202,4 @@ ifTlsDropped(socket, () => { | ||
socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'utf-8', () => { | ||
// Required here to avoid https://github.com/nodejs/node/issues/29902 | ||
const socketWrapper = new SocketWrapper(socket); | ||
copyAddressDetails(socket, socketWrapper); | ||
server.emit('connection', socketWrapper); | ||
socket.__timingInfo.tunnelSetup = Date.now(); | ||
server.emit('connection', socket); | ||
}); | ||
@@ -209,2 +220,3 @@ } | ||
copyAddressDetails(res.socket, res.stream); | ||
copyTimingDetails(res.socket, res.stream); | ||
server.emit('connection', res.stream); | ||
@@ -216,2 +228,8 @@ } | ||
exports.createComboServer = createComboServer; | ||
function getParentSocket(socket) { | ||
if (socket._parent) | ||
return socket._parent; // TLS wrapper | ||
else | ||
return socket.stream; // SocketWrapper | ||
} | ||
// Update the target socket(-ish) with the address details from the source socket, | ||
@@ -228,2 +246,8 @@ // iff the target has no details of its own. | ||
} | ||
function copyTimingDetails(source, target) { | ||
if (!target.__timingInfo) { | ||
// Clone timing info, don't copy it - child sockets get their own independent timing stats | ||
target.__timingInfo = Object.assign({}, source.__timingInfo); | ||
} | ||
} | ||
//# sourceMappingURL=http-combo-server.js.map |
@@ -9,3 +9,3 @@ /// <reference types="node" /> | ||
constructor(debug: boolean, ignoreHostCertificateErrors: string[]); | ||
handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void>; | ||
handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer): void; | ||
private connectUpstream; | ||
@@ -12,0 +12,0 @@ private pipeWebSocket; |
@@ -30,29 +30,27 @@ "use strict"; | ||
handleUpgrade(req, socket, head) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let { protocol: requestedProtocol, hostname, port, path } = url.parse(req.url); | ||
if (this.debug) | ||
console.log(`Handling upgrade for ${req.url}`); | ||
const transparentProxy = !hostname; | ||
if (transparentProxy) { | ||
const hostHeader = req.headers.host; | ||
[hostname, port] = hostHeader.split(':'); | ||
// upstreamEncryption is set in http-combo-server, for requests that have explicitly | ||
// CONNECTed upstream (which may then up/downgrade from the current encryption). | ||
let protocol; | ||
if (socket.lastHopEncrypted !== undefined) { | ||
protocol = socket.lastHopEncrypted ? 'wss' : 'ws'; | ||
} | ||
else { | ||
protocol = req.connection.encrypted ? 'wss' : 'ws'; | ||
} | ||
this.connectUpstream(`${protocol}://${hostname}${port ? ':' + port : ''}${path}`, req, socket, head); | ||
let { protocol: requestedProtocol, hostname, port, path } = url.parse(req.url); | ||
if (this.debug) | ||
console.log(`Handling upgrade for ${req.url}`); | ||
const transparentProxy = !hostname; | ||
if (transparentProxy) { | ||
const hostHeader = req.headers.host; | ||
[hostname, port] = hostHeader.split(':'); | ||
// upstreamEncryption is set in http-combo-server, for requests that have explicitly | ||
// CONNECTed upstream (which may then up/downgrade from the current encryption). | ||
let protocol; | ||
if (socket.lastHopEncrypted !== undefined) { | ||
protocol = socket.lastHopEncrypted ? 'wss' : 'ws'; | ||
} | ||
else { | ||
// Connect directly according to the specified URL | ||
const protocol = requestedProtocol.replace('http', 'ws'); | ||
this.connectUpstream(`${protocol}//${hostname}${port ? ':' + port : ''}${path}`, req, socket, head); | ||
protocol = req.connection.encrypted ? 'wss' : 'ws'; | ||
} | ||
}); | ||
this.connectUpstream(`${protocol}://${hostname}${port ? ':' + port : ''}${path}`, req, socket, head); | ||
} | ||
else { | ||
// Connect directly according to the specified URL | ||
const protocol = requestedProtocol.replace('http', 'ws'); | ||
this.connectUpstream(`${protocol}//${hostname}${port ? ':' + port : ''}${path}`, req, socket, head); | ||
} | ||
} | ||
connectUpstream(wsUrl, req, socket, head) { | ||
connectUpstream(wsUrl, req, incomingSocket, head) { | ||
if (this.debug) | ||
@@ -66,3 +64,5 @@ console.log(`Connecting to upstream websocket at ${wsUrl}`); | ||
rejectUnauthorized: checkServerCertificate, | ||
maxPayload: 0 | ||
maxPayload: 0, | ||
headers: _.omitBy(req.headers, (_v, headerName) => headerName.toLowerCase().startsWith('sec-websocket') || | ||
headerName.toLowerCase() === 'connection') // Simplify to string - doesn't matter though, only used by http module anyway | ||
}); | ||
@@ -72,3 +72,4 @@ upstreamSocket.once('open', () => { | ||
console.log(`Websocket connected to ${wsUrl}`); | ||
this.wsServer.handleUpgrade(req, socket, head, (ws) => { | ||
// Presumably the below adds an error handler. But what about before we get here? | ||
this.wsServer.handleUpgrade(req, incomingSocket, head, (ws) => { | ||
ws.upstreamSocket = upstreamSocket; | ||
@@ -81,3 +82,3 @@ this.wsServer.emit('connection', ws); | ||
console.log(`Unexpected websocket response from ${wsUrl}: ${res.statusCode}`); | ||
this.mirrorRejection(socket, res); | ||
this.mirrorRejection(incomingSocket, res); | ||
}); | ||
@@ -87,4 +88,5 @@ // If there's some other error, we just kill the socket: | ||
console.warn(e); | ||
socket.end(); | ||
incomingSocket.end(); | ||
}); | ||
incomingSocket.once('error', () => upstreamSocket.close(1011)); // Internal error | ||
} | ||
@@ -91,0 +93,0 @@ pipeWebSocket(inSocket, outSocket) { |
{ | ||
"name": "mockttp", | ||
"version": "0.21.1", | ||
"version": "0.21.2", | ||
"description": "Mock HTTP server for testing HTTP clients and stubbing webservices", | ||
@@ -5,0 +5,0 @@ "main": "dist/main.js", |
@@ -670,4 +670,4 @@ /** | ||
// Use a client cert if it's listed for the host+port or whole hostname | ||
const clientCert =this.clientCertificateHostMap[hostWithPort] || | ||
// Use a client cert if it's listed for the host+port or whole hostname | ||
const clientCert = this.clientCertificateHostMap[hostWithPort] || | ||
this.clientCertificateHostMap[hostname!] || | ||
@@ -783,13 +783,22 @@ {}; | ||
clientRes.writeHead( | ||
serverStatusCode, | ||
serverStatusMessage || clientRes.statusMessage | ||
); | ||
try { | ||
clientRes.writeHead( | ||
serverStatusCode, | ||
serverStatusMessage || clientRes.statusMessage | ||
); | ||
} catch (e) { | ||
serverReq.abort(); | ||
reject(e); | ||
} | ||
if (resBodyOverride) { | ||
// Return the override data to the client: | ||
clientRes.end(resBodyOverride); | ||
// Dump the real response data: | ||
serverRes.resume(); | ||
resolve(); | ||
} else { | ||
serverRes.pipe(clientRes); | ||
serverRes.once('end', resolve); | ||
clientRes.once('finish', resolve); | ||
} | ||
@@ -820,4 +829,10 @@ }); | ||
clientReq.once('abort', () => serverReq.abort()); | ||
clientRes.once('close', () => serverReq.abort()); | ||
// If the downstream connection aborts, before the response has been completed, | ||
// we also abort the upstream connection. Important to avoid unnecessary connections, | ||
// and to correctly proxy client connection behaviour to the upstream server. | ||
function abortUpstream() { | ||
serverReq.abort(); | ||
} | ||
clientReq.on('aborted', abortUpstream); | ||
clientRes.once('finish', () => clientReq.removeListener('aborted', abortUpstream)); | ||
@@ -824,0 +839,0 @@ serverReq.once('error', (e: any) => { |
@@ -49,9 +49,16 @@ import _ = require('lodash'); | ||
// If you keep it open, you probably trust the TLS connection | ||
// (some clients open unused connections for connection pools etc) | ||
delay(5000).then(resolve); | ||
// If you silently close it very quicky, you probably don't trust us | ||
socket.once('close', reject); | ||
socket.once('end', reject); | ||
// Some clients open later-unused TLS connections for connection pools, preconnect, etc. | ||
// Even if these are shut later on, that doesn't mean they're are rejected connections. | ||
// To differentiate the two cases, we consider connections OK after waiting 10x longer | ||
// than the initial TLS handshake for an unhappy disconnection. | ||
const timing = socket.__timingInfo || {}; | ||
const tlsSetupDuration = timing.tlsConnected! - (timing.tunnelSetup! || timing.initialSocket!); | ||
const maxTlsRejectionTime = (tlsSetupDuration !== NaN && tlsSetupDuration !== 0) | ||
? tlsSetupDuration * 10 | ||
: 5000; | ||
delay(maxTlsRejectionTime).then(resolve); | ||
}) | ||
@@ -136,2 +143,4 @@ .then(() => { | ||
server.on('connection', (socket: net.Socket) => { | ||
socket.__timingInfo = socket.__timingInfo || { initialSocket: Date.now() }; | ||
// All sockets are initially marked as using unencrypted upstream connections. | ||
@@ -143,8 +152,14 @@ // If TLS is used, this is upgraded to 'true' by secureConnection below. | ||
server.on('secureConnection', (socket: tls.TLSSocket) => { | ||
if ((socket as { _parent?: net.Socket })._parent) { | ||
const parentSocket = getParentSocket(socket); | ||
if (parentSocket) { | ||
// Sometimes wrapper TLS sockets created by the HTTP/2 server don't include the | ||
// underlying port details, so it's better to make sure we copy them up. | ||
copyAddressDetails((socket as any)._parent, socket); | ||
// underlying socket details, so it's better to make sure we copy them up. | ||
copyAddressDetails(parentSocket, socket); | ||
copyTimingDetails(parentSocket, socket); | ||
} else if (!socket.__timingInfo) { | ||
socket.__timingInfo = { initialSocket: Date.now() }; | ||
} | ||
socket.__timingInfo!.tlsConnected = Date.now(); | ||
socket.lastHopEncrypted = true; | ||
@@ -208,6 +223,4 @@ ifTlsDropped(socket, () => { | ||
socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'utf-8', () => { | ||
// Required here to avoid https://github.com/nodejs/node/issues/29902 | ||
const socketWrapper = new SocketWrapper(socket); | ||
copyAddressDetails(socket, socketWrapper); | ||
server.emit('connection', socketWrapper); | ||
socket.__timingInfo!.tunnelSetup = Date.now(); | ||
server.emit('connection', socket); | ||
}); | ||
@@ -231,2 +244,4 @@ } | ||
copyAddressDetails(res.socket, res.stream); | ||
copyTimingDetails(res.socket, res.stream); | ||
server.emit('connection', res.stream); | ||
@@ -238,8 +253,15 @@ } | ||
function getParentSocket(socket: net.Socket) { | ||
if (socket._parent) return socket._parent; // TLS wrapper | ||
else return socket.stream; // SocketWrapper | ||
} | ||
type SocketIsh<MinProps extends keyof net.Socket> = | ||
streams.Duplex & Partial<Pick<net.Socket, MinProps>>; | ||
// 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: net.Socket, | ||
target: streams.Duplex & | ||
Partial<Pick<net.Socket, 'localAddress' | 'localPort' | 'remoteAddress' | 'remotePort'>> | ||
source: SocketIsh<'localAddress' | 'localPort' | 'remoteAddress' | 'remotePort'>, | ||
target: SocketIsh<'localAddress' | 'localPort' | 'remoteAddress' | 'remotePort'> | ||
) { | ||
@@ -255,2 +277,12 @@ const fields = ['localAddress', 'localPort', 'remoteAddress', 'remotePort'] as const; | ||
}); | ||
} | ||
function copyTimingDetails<T extends SocketIsh<'__timingInfo'>>( | ||
source: SocketIsh<'__timingInfo'>, | ||
target: T | ||
): asserts target is T & { __timingInfo: Required<net.Socket>['__timingInfo'] } { | ||
if (!target.__timingInfo) { | ||
// Clone timing info, don't copy it - child sockets get their own independent timing stats | ||
target.__timingInfo = Object.assign({}, source.__timingInfo); | ||
} | ||
} |
@@ -29,3 +29,3 @@ import * as _ from 'lodash'; | ||
async handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { | ||
handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { | ||
let { protocol: requestedProtocol, hostname, port, path } = url.parse(req.url!); | ||
@@ -58,3 +58,3 @@ | ||
private connectUpstream(wsUrl: string, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { | ||
private connectUpstream(wsUrl: string, req: http.IncomingMessage, incomingSocket: net.Socket, head: Buffer) { | ||
if (this.debug) console.log(`Connecting to upstream websocket at ${wsUrl}`); | ||
@@ -69,3 +69,7 @@ | ||
rejectUnauthorized: checkServerCertificate, | ||
maxPayload: 0 | ||
maxPayload: 0, | ||
headers: _.omitBy(req.headers, (_v, headerName) => | ||
headerName.toLowerCase().startsWith('sec-websocket') || | ||
headerName.toLowerCase() === 'connection' | ||
) as { [key: string]: string } // Simplify to string - doesn't matter though, only used by http module anyway | ||
}); | ||
@@ -75,3 +79,4 @@ | ||
if (this.debug) console.log(`Websocket connected to ${wsUrl}`); | ||
this.wsServer.handleUpgrade(req, socket, head, (ws) => { | ||
// Presumably the below adds an error handler. But what about before we get here? | ||
this.wsServer.handleUpgrade(req, incomingSocket, head, (ws) => { | ||
(<InterceptedWebSocket> ws).upstreamSocket = upstreamSocket; | ||
@@ -85,3 +90,3 @@ this.wsServer.emit('connection', ws); | ||
console.log(`Unexpected websocket response from ${wsUrl}: ${res.statusCode}`); | ||
this.mirrorRejection(socket, res); | ||
this.mirrorRejection(incomingSocket, res); | ||
}); | ||
@@ -92,4 +97,6 @@ | ||
console.warn(e); | ||
socket.end(); | ||
incomingSocket.end(); | ||
}); | ||
incomingSocket.once('error', () => upstreamSocket.close(1011)); // Internal error | ||
} | ||
@@ -96,0 +103,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
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
689518
12559