Skip to content

Commit de493e7

Browse files
committed
Expose 'destination' on request data and avoid IPs in 'url' if possible
If you tunnel to a specific IP with a domain name specified, we'll use the domain in the url property - this is normally clearer & preferable. The IP is still available in the 'destination' property. It will still be used in the URL if there's no hostname anywhere. This also further deprecates the standalone 'hostname' field on requests, which was not really used and functioned quite ambiguously. You can now look at either the URL (best guess domain name), the destination (the actual destination IP/domain name) or the Host/:Authority header (the name the client is telling the server). In theory they should probably match all the time, but they may not. This mostly comes up when tunneling to one server, and sending a different Host header, which mostly matters for weird edge cases and testing of non-production deployments & similar.
1 parent 514109c commit de493e7

15 files changed

+287
-102
lines changed

src/admin/mockttp-schema.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const MockttpSchema = gql`
117117
rawHeaders: Json
118118
remoteIpAddress: String
119119
remotePort: Int
120+
destination: Destination
120121
}
121122
122123
type RawPassthroughEvent {
@@ -158,7 +159,9 @@ export const MockttpSchema = gql`
158159
path: String!
159160
remoteIpAddress: String
160161
remotePort: Int
161-
hostname: String
162+
163+
destination: Destination!
164+
hostname: String # Deprecated
162165
163166
headers: Json!
164167
rawHeaders: Json!
@@ -177,7 +180,9 @@ export const MockttpSchema = gql`
177180
path: String!
178181
remoteIpAddress: String
179182
remotePort: Int
180-
hostname: String
183+
184+
destination: Destination!
185+
hostname: String # Deprecated
181186
182187
headers: Json!
183188
rawHeaders: Json!
@@ -199,7 +204,9 @@ export const MockttpSchema = gql`
199204
path: String!
200205
remoteIpAddress: String
201206
remotePort: Int
202-
hostname: String
207+
208+
destination: Destination!
209+
hostname: String # Deprecated
203210
204211
headers: Json!
205212
rawHeaders: Json!
@@ -241,4 +248,9 @@ export const MockttpSchema = gql`
241248
timingEvents: Json!
242249
tags: [String!]!
243250
}
251+
252+
type Destination {
253+
hostname: String!
254+
port: Int!
255+
}
244256
`;

src/client/mockttp-admin-request-builder.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ export class MockttpAdminRequestBuilder {
233233
path
234234
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')}
235235
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')}
236-
hostname
236+
237+
${this.schema.asOptionalField('InitiatedRequest', 'destination', 'destination { hostname, port }')}
237238
238239
${this.schema.typeHasField('InitiatedRequest', 'rawHeaders')
239240
? 'rawHeaders'
@@ -254,7 +255,8 @@ export class MockttpAdminRequestBuilder {
254255
path
255256
${this.schema.asOptionalField('Request', 'remoteIpAddress')}
256257
${this.schema.asOptionalField('Request', 'remotePort')}
257-
hostname
258+
259+
${this.schema.asOptionalField('Request', 'destination', 'destination { hostname, port }')}
258260
259261
${this.schema.typeHasField('Request', 'rawHeaders')
260262
? 'rawHeaders'
@@ -297,7 +299,8 @@ export class MockttpAdminRequestBuilder {
297299
path
298300
remoteIpAddress
299301
remotePort
300-
hostname
302+
303+
${this.schema.asOptionalField('Request', 'destination', 'destination { hostname, port }')}
301304
302305
rawHeaders
303306
body
@@ -359,12 +362,13 @@ export class MockttpAdminRequestBuilder {
359362
}`,
360363
abort: gql`subscription OnAbort {
361364
requestAborted {
362-
id,
363-
protocol,
364-
method,
365-
url,
366-
path,
367-
hostname,
365+
id
366+
protocol
367+
method
368+
url
369+
path
370+
371+
${this.schema.asOptionalField('AbortedRequest', 'destination', 'destination { hostname, port }')}
368372
369373
${this.schema.typeHasField('Request', 'rawHeaders')
370374
? 'rawHeaders'
@@ -437,6 +441,7 @@ export class MockttpAdminRequestBuilder {
437441
438442
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}
439443
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}
444+
${this.schema.asOptionalField('ClientErrorRequest', 'destination', 'destination { hostname, port }')}
440445
}
441446
response {
442447
id

src/client/schema-introspection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export class SchemaIntrospector {
2020
return !!_.find(type.fields, { name: fieldName });
2121
}
2222

23-
public asOptionalField(typeName: string | string[], fieldName: string): string {
23+
public asOptionalField(typeName: string | string[], fieldName: string, specifier: string = fieldName): string {
2424
const possibleNames = !Array.isArray(typeName) ? [typeName] : typeName;
2525

2626
const firstAvailableName = possibleNames.find((name) => this.isTypeDefined(name));
2727
if (!firstAvailableName) return '';
2828

2929
return (this.typeHasField(firstAvailableName, fieldName))
30-
? fieldName
30+
? specifier
3131
: '';
3232
}
3333

src/rules/requests/request-handlers.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from "../../types";
2222

2323
import { MaybePromise, ErrorLike, isErrorLike } from '@httptoolkit/util';
24-
import { isAbsoluteUrl, getEffectivePort } from '../../util/url';
24+
import { isAbsoluteUrl, getEffectivePort, getDestination } from '../../util/url';
2525
import {
2626
waitForCompletedRequest,
2727
buildBodyReader,
@@ -413,8 +413,10 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
413413
dropDefaultHeaders(clientRes);
414414

415415
// Capture raw request data:
416-
let { method, url: reqUrl, rawHeaders } = clientReq as OngoingRequest;
417-
let { protocol, hostname, port, path } = url.parse(reqUrl);
416+
let { method, url: reqUrl, rawHeaders, destination } = clientReq as OngoingRequest;
417+
let { protocol, path } = url.parse(reqUrl);
418+
let hostname: string | null = destination.hostname;
419+
let port: string | null = destination.port.toString();
418420

419421
// Check if this request is a request loop:
420422
if (isSocketLoop(this.outgoingSockets, (clientReq as any).socket)) {
@@ -938,6 +940,10 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
938940
protocol: protocol?.replace(':', '') ?? '',
939941
method: method,
940942
url: reqUrl,
943+
destination: {
944+
hostname: hostname || 'localhost',
945+
port: effectivePort
946+
},
941947
path: path ?? '',
942948
headers: reqHeader,
943949
rawHeaders: rawHeaders,

src/server/http-combo-server.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
} from 'read-tls-client-hello';
1818
import { URLPattern } from "urlpattern-polyfill";
1919

20-
import { TlsHandshakeFailure } from '../types';
20+
import { Destination, TlsHandshakeFailure } from '../types';
2121
import { getCA } from '../util/tls';
2222
import { shouldPassThrough } from '../util/server-utils';
23+
import { getDestination } from '../util/url';
2324
import {
2425
getParentSocket,
2526
buildSocketTimingInfo,
@@ -32,9 +33,8 @@ import {
3233
LastHopEncrypted,
3334
TlsMetadata,
3435
TlsSetupCompleted,
35-
getAddressAndPort,
36-
resetOrDestroy,
37-
SocketMetadata
36+
SocketMetadata,
37+
resetOrDestroy
3838
} from '../util/socket-util';
3939
import { MockttpHttpsOptions } from '../mockttp';
4040
import { buildSocksServer, SocksServerOptions, SocksTcpAddress } from './socks-server';
@@ -151,8 +151,8 @@ export interface ComboServerOptions {
151151

152152
requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void;
153153
tlsClientErrorListener: (socket: tls.TLSSocket, req: TlsHandshakeFailure) => void;
154-
tlsPassthroughListener: (socket: net.Socket, address: string, port?: number) => void;
155-
rawPassthroughListener: (socket: net.Socket, address: string, port?: number) => void;
154+
tlsPassthroughListener: (socket: net.Socket, hostname: string, port?: number) => void;
155+
rawPassthroughListener: (socket: net.Socket, hostname: string, port?: number) => void;
156156
};
157157

158158
// The low-level server that handles all the sockets & TLS. The server will correctly call the
@@ -249,19 +249,26 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
249249

250250
if (options.passthroughUnknownProtocols) {
251251
unknownProtocolServer = net.createServer((socket) => {
252-
const destination = socket[LastTunnelAddress];
253-
if (!destination) {
254-
server.emit('clientError', new Error('Unknown protocol without destination'), socket);
255-
return;
256-
}
252+
const tunnelAddress = socket[LastTunnelAddress];
257253

258-
const [host, port] = getAddressAndPort(destination);
259-
if (!port) { // Both CONNECT & SOCKS require a port, so this shouldn't happen
260-
server.emit('clientError', new Error('Unknown protocol without destination port'), socket);
261-
return;
262-
}
254+
try {
255+
if (!tunnelAddress) {
256+
server.emit('clientError', new Error('Unknown protocol without destination'), socket);
257+
return;
258+
}
263259

264-
options.rawPassthroughListener(socket, host, port);
260+
if (!tunnelAddress.includes(':')) {
261+
// Both CONNECT & SOCKS require a port, so this shouldn't happen
262+
server.emit('clientError', new Error('Unknown protocol without destination port'), socket);
263+
return;
264+
}
265+
266+
const { hostname, port } = getDestination('unknown', tunnelAddress); // Has port, so no protocol required
267+
options.rawPassthroughListener(socket, hostname, port);
268+
} catch (e) {
269+
console.error('Unknown protocol server error', e);
270+
resetOrDestroy(socket);
271+
}
265272
});
266273
}
267274

@@ -432,7 +439,7 @@ function analyzeAndMaybePassThroughTls(
432439
server: tls.Server,
433440
passthroughList: Required<MockttpHttpsOptions>['tlsPassthrough'] | undefined,
434441
interceptOnlyList: Required<MockttpHttpsOptions>['tlsInterceptOnly'] | undefined,
435-
passthroughListener: (socket: net.Socket, address: string, port?: number) => void
442+
passthroughListener: (socket: net.Socket, hostname: string, port?: number) => void
436443
) {
437444
if (passthroughList && interceptOnlyList){
438445
throw new Error('Cannot use both tlsPassthrough and tlsInterceptOnly options at the same time.');
@@ -450,23 +457,22 @@ function analyzeAndMaybePassThroughTls(
450457

451458
// SNI is a good clue for where the request is headed, but an explicit proxy address (via
452459
// CONNECT or SOCKS) is even better. Note that this may be a hostname or IPv4/6 address:
453-
let upstreamHostname: string | undefined;
454-
let upstreamPort: number | undefined;
460+
let upstreamDestination: Destination | undefined;
455461
if (socket[LastTunnelAddress]) {
456-
([upstreamHostname, upstreamPort] = getAddressAndPort(socket[LastTunnelAddress]));
462+
upstreamDestination = getDestination('https', socket[LastTunnelAddress]);
457463
}
458464

459465
socket[TlsMetadata] = {
460466
sniHostname,
461-
connectHostname: upstreamHostname,
462-
connectPort: upstreamPort?.toString(),
467+
connectHostname: upstreamDestination?.hostname,
468+
connectPort: upstreamDestination?.port.toString(),
463469
clientAlpn: helloData.alpnProtocols,
464470
ja3Fingerprint: calculateJa3FromFingerprintData(helloData.fingerprintData),
465471
ja4Fingerprint: calculateJa4FromHelloData(helloData)
466472
};
467473

468-
if (shouldPassThrough(upstreamHostname, passThroughPatterns, interceptOnlyPatterns)) {
469-
passthroughListener(socket, upstreamHostname, upstreamPort);
474+
if (shouldPassThrough(upstreamDestination?.hostname, passThroughPatterns, interceptOnlyPatterns)) {
475+
passthroughListener(socket, upstreamDestination.hostname, upstreamDestination.port);
470476
return; // Do not continue with TLS
471477
} else if (shouldPassThrough(sniHostname, passThroughPatterns, interceptOnlyPatterns)) {
472478
passthroughListener(socket, sniHostname!); // Can't guess the port - not included in SNI

0 commit comments

Comments
 (0)