Skip to content

Commit ad9e42f

Browse files
committed
Add support for tagging request metadata via SOCKS auth
1 parent c891838 commit ad9e42f

File tree

9 files changed

+665
-249
lines changed

9 files changed

+665
-249
lines changed

src/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type { MaybePromise } from '@httptoolkit/util';
22

33
import { Mockttp, MockttpOptions, MockttpHttpsOptions, SubscribableEvent, PortRange } from "./mockttp";
44
import { MockttpServer } from "./server/mockttp-server";
5+
import { SocksServerOptions } from "./server/socks-server";
56
import {
67
MockttpClient,
78
MockttpClientOptions
@@ -19,7 +20,8 @@ export type {
1920
MockttpClientOptions,
2021
MockttpAdminServerOptions,
2122
SubscribableEvent,
22-
PortRange
23+
PortRange,
24+
SocksServerOptions
2325
};
2426

2527
// Export now-renamed types with the old aliases to provide backward compat and

src/mockttp.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from "./types";
2727
import type { RequestRuleData } from "./rules/requests/request-rule";
2828
import type { WebSocketRuleData } from "./rules/websockets/websocket-rule";
29+
import type { SocksServerOptions } from "./server/socks-server";
2930

3031
export type PortRange = { startPort: number, endPort: number };
3132

@@ -811,12 +812,15 @@ export interface MockttpOptions {
811812

812813
/**
813814
* Should the server accept incoming SOCKS connections? Defaults to false.
814-
* If set to true, the server will listen for incoming SOCKS connections
815-
* on the same port as the HTTP server, unwrap received connections, and
816-
* handle them like any other incoming TCP connection (intercepting HTTP(S)
817-
* from within the SOCKS connection as normal).
815+
*
816+
* If set to true or if detailed options are provided, the server will listen
817+
* for incoming SOCKS connections on the same port as the HTTP server, unwrap
818+
* received connections, and handle them like any other incoming TCP connection
819+
* (intercepting HTTP(S) from within the SOCKS connection as normal).
820+
*
821+
* The only supported option for now is `authMethods`.
818822
*/
819-
socks?: boolean;
823+
socks?: boolean | SocksServerOptions;
820824

821825
/**
822826
* An array of rules for traffic that should be passed through the proxy

src/rules/requests/request-handlers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1121,7 +1121,10 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
11211121

11221122
options.emitEventCallback('passthrough-abort', {
11231123
downstreamAborted: !!(serverReq?.aborted),
1124-
tags: buildUpstreamErrorTags(e),
1124+
tags: [
1125+
...clientReq.tags,
1126+
buildUpstreamErrorTags(e)
1127+
],
11251128
error: {
11261129
name: e.name,
11271130
code: e.code,

src/server/http-combo-server.ts

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ import {
3333
TlsMetadata,
3434
TlsSetupCompleted,
3535
getAddressAndPort,
36-
resetOrDestroy
36+
resetOrDestroy,
37+
SocketMetadata
3738
} from '../util/socket-util';
3839
import { MockttpHttpsOptions } from '../mockttp';
39-
import { buildSocksServer, SocksTcpAddress } from './socks-server';
40+
import { buildSocksServer, SocksServerOptions, SocksTcpAddress } from './socks-server';
4041

4142
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
4243
// sockets as soon as they're available, without waiting for the handshake to fully
@@ -145,7 +146,7 @@ export interface ComboServerOptions {
145146
debug: boolean;
146147
https: MockttpHttpsOptions | undefined;
147148
http2: boolean | 'fallback';
148-
socks: boolean;
149+
socks: boolean | SocksServerOptions;
149150
passthroughUnknownProtocols: boolean;
150151

151152
requestListener: (req: http.IncomingMessage, res: http.ServerResponse) => void;
@@ -225,7 +226,7 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
225226
}
226227

227228
if (options.socks) {
228-
socksServer = buildSocksServer();
229+
socksServer = buildSocksServer(options.socks === true ? {} : options.socks);
229230
socksServer.on('socks-tcp-connect', (socket: net.Socket, address: SocksTcpAddress) => {
230231
const addressString =
231232
address.type === 'ipv4'
@@ -291,8 +292,7 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
291292
if (parentSocket) {
292293
// Sometimes wrapper TLS sockets created by the HTTP/2 server don't include the
293294
// underlying socket details, so it's better to make sure we copy them up.
294-
copyAddressDetails(parentSocket, socket);
295-
copyTimingDetails(parentSocket, socket);
295+
inheritSocketDetails(parentSocket, socket);
296296
// With TLS metadata, we only propagate directly from parent sockets, not through
297297
// CONNECT etc - we only want it if the final hop is TLS, previous values don't matter.
298298
socket[TlsMetadata] ??= parentSocket[TlsMetadata];
@@ -371,8 +371,7 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
371371

372372
// Send a 200 OK response, and start the tunnel:
373373
res.writeHead(200, {});
374-
copyAddressDetails(res.socket, res.stream);
375-
copyTimingDetails(res.socket, res.stream);
374+
inheritSocketDetails(res.socket, res.stream);
376375
res.stream[LastTunnelAddress] = connectUrl;
377376

378377
// When layering HTTP/2 on JS streams, we have to make sure the JS stream won't autoclose
@@ -390,39 +389,37 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
390389
}
391390

392391

393-
const SOCKET_ADDRESS_METADATA_FIELDS = [
392+
const SOCKET_METADATA = [
394393
'localAddress',
395394
'localPort',
396395
'remoteAddress',
397396
'remotePort',
397+
SocketTimingInfo,
398+
SocketMetadata,
398399
LastTunnelAddress
399400
] as const;
400401

401-
// Update the target socket(-ish) with the address details from the source socket,
402-
// iff the target has no details of its own.
403-
function copyAddressDetails(
404-
source: SocketIsh<typeof SOCKET_ADDRESS_METADATA_FIELDS[number]>,
405-
target: SocketIsh<typeof SOCKET_ADDRESS_METADATA_FIELDS[number]>
402+
function inheritSocketDetails(
403+
source: SocketIsh<typeof SOCKET_METADATA[number]>,
404+
target: SocketIsh<typeof SOCKET_METADATA[number]>
406405
) {
406+
// Update the target socket(-ish) with the assorted metadata from the source socket,
407+
// iff the target has no details of its own.
408+
409+
// Make sure all properties are writable - HTTP/2 streams notably try to block this.
407410
Object.defineProperties(target, _.zipObject(
408-
SOCKET_ADDRESS_METADATA_FIELDS,
409-
_.range(SOCKET_ADDRESS_METADATA_FIELDS.length).map(() => ({ writable: true }))
411+
SOCKET_METADATA,
412+
_.range(SOCKET_METADATA.length).map(() => ({ writable: true }))
410413
) as PropertyDescriptorMap);
411414

412-
SOCKET_ADDRESS_METADATA_FIELDS.forEach((fieldName) => {
415+
for (let fieldName of SOCKET_METADATA) {
413416
if (target[fieldName] === undefined) {
414-
(target as any)[fieldName] = source[fieldName];
417+
if (typeof source[fieldName] === 'object') {
418+
(target as any)[fieldName] = _.cloneDeep(source[fieldName]);
419+
} else {
420+
(target as any)[fieldName] = source[fieldName];
421+
}
415422
}
416-
});
417-
}
418-
419-
function copyTimingDetails<T extends SocketIsh<typeof SocketTimingInfo>>(
420-
source: SocketIsh<typeof SocketTimingInfo>,
421-
target: T
422-
): asserts target is T & { [SocketTimingInfo]: Required<net.Socket>[typeof SocketTimingInfo] } {
423-
if (!target[SocketTimingInfo]) {
424-
// Clone timing info, don't copy it - child sockets get their own independent timing stats
425-
target[SocketTimingInfo] = Object.assign({}, source[SocketTimingInfo]);
426423
}
427424
}
428425

src/server/mockttp-server.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ import {
5656
LastTunnelAddress,
5757
TlsSetupCompleted,
5858
isSocketLoop,
59-
resetOrDestroy
59+
resetOrDestroy,
60+
SocketMetadata,
61+
getSocketMetadataTags
6062
} from "../util/socket-util";
6163
import {
6264
parseRequestBody,
@@ -77,6 +79,7 @@ import {
7779
import { AbortError } from "../rules/requests/request-handlers";
7880
import { WebSocketRuleData, WebSocketRule } from "../rules/websockets/websocket-rule";
7981
import { RejectWebSocketHandler, WebSocketHandler } from "../rules/websockets/websocket-handlers";
82+
import { SocksServerOptions } from "./socks-server";
8083

8184
type ExtendedRawRequest = (http.IncomingMessage | http2.Http2ServerRequest) & {
8285
protocol?: string;
@@ -99,7 +102,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
99102

100103
private httpsOptions: MockttpHttpsOptions | undefined;
101104
private isHttp2Enabled: boolean | 'fallback';
102-
private socksEnabled: boolean;
105+
private socksOptions: boolean | SocksServerOptions;
103106
private passthroughUnknownProtocols: boolean;
104107
private maxBodySize: number;
105108

@@ -119,7 +122,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
119122

120123
this.httpsOptions = options.https;
121124
this.isHttp2Enabled = options.http2 ?? 'fallback';
122-
this.socksEnabled = options.socks ?? false;
125+
this.socksOptions = options.socks ?? false;
123126
this.passthroughUnknownProtocols = options.passthrough?.includes('unknown-protocol') ?? false;
124127
this.maxBodySize = options.maxBodySize ?? Infinity;
125128
this.eventEmitter = new EventEmitter();
@@ -146,7 +149,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
146149
debug: this.debug,
147150
https: this.httpsOptions,
148151
http2: this.isHttp2Enabled,
149-
socks: this.socksEnabled,
152+
socks: this.socksOptions,
150153
passthroughUnknownProtocols: this.passthroughUnknownProtocols,
151154

152155
requestListener: this.app,
@@ -629,7 +632,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
629632
}
630633

631634
const id = uuid();
632-
const tags: string[] = [];
635+
const tags: string[] = getSocketMetadataTags(req.socket);
633636

634637
const timingEvents: TimingEvents = {
635638
startTime: Date.now(),
@@ -986,7 +989,10 @@ ${await this.suggestRule(request)}`
986989

987990
const commonParams = {
988991
id: uuid(),
989-
tags: [`client-error:${error.code || 'UNKNOWN'}`],
992+
tags: [
993+
`client-error:${error.code || 'UNKNOWN'}`,
994+
...getSocketMetadataTags(socket)
995+
],
990996
timingEvents: { startTime: Date.now(), startTimestamp: now() } as TimingEvents
991997
};
992998

@@ -1076,7 +1082,8 @@ ${await this.suggestRule(request)}`
10761082
id: uuid(),
10771083
tags: [
10781084
`client-error:${error.code || 'UNKNOWN'}`,
1079-
...(isBadPreface ? ['client-error:bad-preface'] : [])
1085+
...(isBadPreface ? ['client-error:bad-preface'] : []),
1086+
...getSocketMetadataTags(socket)
10801087
],
10811088
httpVersion: '2',
10821089

0 commit comments

Comments
 (0)