Skip to content

Commit 06780b0

Browse files
committed
Use metadata from HTTP proxy auth, unwrapping proxy-* headers up front
If you authenticate to Mockttp with username 'metadata' and a JSON (raw of b64url encoded) password, that password will be used as metadata for the request. Most notably, the "tags" field will be directly attached to all request events later on. This applies to direct HTTP proxy connections (GET http://example.com) and CONNECT-tunnelled connections for both HTTP/1 & HTTP/2. This roughly matches the behaviour available for SOCKS tunnels, which already support similar metadata. Previously, proxy-connection headers were manually dropped from data during rule processing. Now both proxy-connection & proxy-authorization are preprocessed up front when sent with an absolute URL (i.e. when a client is making a non-CONNECT tunnel) so they never appear in events or traffic elsewhere.
1 parent d335a8f commit 06780b0

File tree

13 files changed

+384
-169
lines changed

13 files changed

+384
-169
lines changed

src/rules/requests/request-handlers.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -737,13 +737,6 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
737737
rawHeaders = h2HeadersToH1(rawHeaders);
738738
}
739739

740-
// Drop proxy-connection header. This is almost always intended for us, not for upstream servers,
741-
// and forwarding it causes problems (most notably, it triggers lots of weird-traffic blocks,
742-
// most notably by Cloudflare).
743-
rawHeaders = rawHeaders.filter(([key]) =>
744-
key.toLowerCase() !== 'proxy-connection'
745-
);
746-
747740
let serverReq: http.ClientRequest;
748741
return new Promise<void>((resolve, reject) => (async () => { // Wrapped to easily catch (a)sync errors
749742
serverReq = await makeRequest({

src/rules/websockets/websocket-handlers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
deserializeProxyConfig
1111
} from "../../serialization/serialization";
1212

13-
import { Headers, OngoingRequest, RawHeaders } from "../../types";
13+
import { OngoingRequest, RawHeaders } from "../../types";
1414

1515
import {
1616
CloseConnectionHandler,
@@ -19,6 +19,8 @@ import {
1919
TimeoutHandler
2020
} from '../requests/request-handlers';
2121
import { getEffectivePort } from '../../util/url';
22+
import { resetOrDestroy } from '../../util/socket-util';
23+
import { LastHopEncrypted } from '../../util/socket-extensions';
2224
import { isHttp2 } from '../../util/request-utils';
2325
import {
2426
findRawHeader,
@@ -27,7 +29,6 @@ import {
2729
pairFlatRawHeaders,
2830
rawHeadersToObjectPreservingCase
2931
} from '../../util/header-utils';
30-
import { streamToBuffer } from '../../util/buffer-utils';
3132
import { MaybePromise } from '@httptoolkit/util';
3233

3334
import { getAgent } from '../http-agents';
@@ -51,7 +52,6 @@ import {
5152
WebSocketHandlerDefinition,
5253
WsHandlerDefinitionLookup,
5354
} from './websocket-handler-definitions';
54-
import { LastHopEncrypted, resetOrDestroy } from '../../util/socket-util';
5555

5656
export interface WebSocketHandler extends WebSocketHandlerDefinition {
5757
handle(

src/server/http-combo-server.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import {
2525
getParentSocket,
2626
buildSocketTimingInfo,
2727
buildTlsSocketEventData,
28+
resetOrDestroy
29+
} from '../util/socket-util';
30+
import {
2831
SocketIsh,
2932
InitialRemoteAddress,
3033
InitialRemotePort,
@@ -34,10 +37,10 @@ import {
3437
TlsMetadata,
3538
TlsSetupCompleted,
3639
SocketMetadata,
37-
resetOrDestroy
38-
} from '../util/socket-util';
40+
} from '../util/socket-extensions';
3941
import { MockttpHttpsOptions } from '../mockttp';
4042
import { buildSocksServer, SocksServerOptions, SocksTcpAddress } from './socks-server';
43+
import { getSocketMetadataFromProxyAuth } from '../util/socket-metadata';
4144

4245
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
4346
// sockets as soon as they're available, without waiting for the handshake to fully
@@ -360,6 +363,9 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
360363
socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'utf-8', () => {
361364
socket[SocketTimingInfo]!.tunnelSetupTimestamp = now();
362365
socket[LastTunnelAddress] = connectUrl;
366+
if (req.headers['proxy-authorization']) {
367+
socket[SocketMetadata] = getSocketMetadataFromProxyAuth(socket, req.headers['proxy-authorization']);
368+
}
363369
server.emit('connection', socket);
364370
});
365371
}
@@ -378,8 +384,12 @@ export async function createComboServer(options: ComboServerOptions): Promise<De
378384

379385
// Send a 200 OK response, and start the tunnel:
380386
res.writeHead(200, {});
387+
381388
inheritSocketDetails(res.socket, res.stream);
382389
res.stream[LastTunnelAddress] = connectUrl;
390+
if (req.headers['proxy-authorization']) {
391+
res.stream[SocketMetadata] = getSocketMetadataFromProxyAuth(res.stream, req.headers['proxy-authorization']);
392+
}
383393

384394
// When layering HTTP/2 on JS streams, we have to make sure the JS stream won't autoclose
385395
// when the other side does, because the upper HTTP/2 layers want to handle shutdown, so

src/server/mockttp-server.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,17 @@ import {
5959
import {
6060
buildRawSocketEventData,
6161
buildTlsSocketEventData,
62+
isSocketLoop,
63+
resetOrDestroy
64+
} from "../util/socket-util";
65+
import {
6266
ClientErrorInProgress,
6367
LastHopEncrypted,
6468
LastTunnelAddress,
6569
TlsSetupCompleted,
66-
isSocketLoop,
67-
resetOrDestroy,
68-
getSocketMetadataTags
69-
} from "../util/socket-util";
70+
SocketMetadata
71+
} from '../util/socket-extensions';
72+
import { getSocketMetadataTags, getSocketMetadataFromProxyAuth } from '../util/socket-metadata'
7073
import {
7174
parseRequestBody,
7275
waitForCompletedRequest,
@@ -93,6 +96,7 @@ type ExtendedRawRequest = (http.IncomingMessage | http2.Http2ServerRequest) & {
9396
body?: OngoingBody;
9497
path?: string;
9598
destination?: Destination;
99+
[SocketMetadata]?: SocketMetadata;
96100
};
97101

98102
const serverPortCheckMutex = new Mutex();
@@ -600,10 +604,13 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
600604
private preprocessRequest(req: ExtendedRawRequest, type: 'request' | 'websocket'): OngoingRequest {
601605
parseRequestBody(req, { maxSize: this.maxBodySize });
602606

607+
let rawHeaders = pairFlatRawHeaders(req.rawHeaders);
608+
let socketMetadata: SocketMetadata | undefined = req.socket[SocketMetadata];
609+
603610
// Make req.url always absolute, if it isn't already, using the host header.
604611
// It might not be if this is a direct request, or if it's being transparently proxied.
605612
if (!isAbsoluteUrl(req.url!)) {
606-
req.protocol = req.headers[':scheme'] as string ||
613+
req.protocol = getHeaderValue(rawHeaders, ':scheme') ||
607614
(req.socket[LastHopEncrypted] ? 'https' : 'http');
608615
req.path = req.url;
609616

@@ -617,8 +624,8 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
617624
// If you explicitly tunnel to a hostname, that's the URL's hostname:
618625
const hostname = tunnelUrlHost
619626
// Otherwise, we infer based on headers: HTTP/2 or HTTP/1
620-
?? getHeaderValue(req.headers, ':authority')
621-
?? getHeaderValue(req.headers, 'host')
627+
?? getHeaderValue(rawHeaders, ':authority')
628+
?? getHeaderValue(rawHeaders, 'host')
622629
?? req.socket[LastTunnelAddress] // Iff we have no hostname available at all
623630
?? `localhost:${this.port}`; // If you specify literally nothing, it's a direct request
624631

@@ -637,7 +644,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
637644

638645
const absoluteUrl = `${req.protocol}://${host}${req.path}`;
639646

640-
if (!req.headers[':path']) {
647+
if (!getHeaderValue(rawHeaders, ':path')) {
641648
(req as Mutable<ExtendedRawRequest>).url = new url.URL(absoluteUrl).toString();
642649
} else {
643650
// Node's HTTP/2 compat logic maps .url to headers[':path']. We want them to
@@ -648,12 +655,27 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
648655
});
649656
}
650657
} else {
658+
// We have an absolute request. This is effectively a combined tunnel + end-server request,
659+
// so we need to handle both of those, and hide the proxy-specific bits from later logic.
651660
req.protocol = req.url!.split('://', 1)[0];
652661
req.path = getPathFromAbsoluteUrl(req.url!);
653662
req.destination = getDestination(
654663
req.protocol,
655664
req.socket[LastTunnelAddress] ?? getHostFromAbsoluteUrl(req.url!)
656665
);
666+
667+
const proxyAuthHeader = getHeaderValue(rawHeaders, 'proxy-authorization');
668+
if (proxyAuthHeader) {
669+
// Use this metadata for this request, but _only_ this request - it's not relevant
670+
// to other requests on the same socket so we don't add it to req.socket.
671+
socketMetadata = getSocketMetadataFromProxyAuth(req.socket, proxyAuthHeader);
672+
}
673+
674+
rawHeaders = rawHeaders.filter(([key]) => {
675+
const lcKey = key.toLowerCase();
676+
return lcKey !== 'proxy-connection' &&
677+
lcKey !== 'proxy-authorization';
678+
})
657679
}
658680

659681
if (type === 'websocket') {
@@ -668,7 +690,8 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
668690
}
669691

670692
const id = uuid();
671-
const tags: string[] = getSocketMetadataTags(req.socket);
693+
694+
const tags: string[] = getSocketMetadataTags(socketMetadata);
672695

673696
const timingEvents: TimingEvents = {
674697
startTime: Date.now(),
@@ -679,7 +702,6 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
679702
timingEvents.bodyReceivedTimestamp ||= now();
680703
});
681704

682-
const rawHeaders = pairFlatRawHeaders(req.rawHeaders);
683705
const headers = rawHeadersToObject(rawHeaders);
684706

685707
// Not writable for HTTP/2:
@@ -1027,7 +1049,7 @@ ${await this.suggestRule(request)}`
10271049
id: uuid(),
10281050
tags: [
10291051
`client-error:${error.code || 'UNKNOWN'}`,
1030-
...getSocketMetadataTags(socket)
1052+
...getSocketMetadataTags(socket[SocketMetadata])
10311053
],
10321054
timingEvents: { startTime: Date.now(), startTimestamp: now() } as TimingEvents
10331055
};
@@ -1120,7 +1142,7 @@ ${await this.suggestRule(request)}`
11201142
tags: [
11211143
`client-error:${error.code || 'UNKNOWN'}`,
11221144
...(isBadPreface ? ['client-error:bad-preface'] : []),
1123-
...getSocketMetadataTags(socket)
1145+
...getSocketMetadataTags(socket?.[SocketMetadata])
11241146
],
11251147
httpVersion: '2',
11261148

src/server/socks-server.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as _ from 'lodash';
22
import * as net from 'net';
3-
import { resetOrDestroy, SocketMetadata } from '../util/socket-util';
3+
4+
import { resetOrDestroy } from '../util/socket-util';
5+
import { SocketMetadata } from '../util/socket-extensions';
6+
import { getSocketMetadata } from '../util/socket-metadata';
47

58
export interface SocksServerOptions {
69
/**
@@ -259,9 +262,8 @@ async function handleCustomMetadata(socket: net.Socket) {
259262
const metadata = await readBytes(socket, length);
260263
const metadataString = metadata.toString('utf8');
261264

262-
let metadataJson: any = {};
263265
try {
264-
metadataJson = JSON.parse(metadataString);
266+
socket[SocketMetadata] = getSocketMetadata(socket[SocketMetadata], metadataString);
265267
} catch (e) {
266268
const errorData = Buffer.from(JSON.stringify({ message: 'Invalid JSON' }));
267269
const errorResponse = Buffer.alloc(4 + errorData.byteLength);
@@ -272,7 +274,7 @@ async function handleCustomMetadata(socket: net.Socket) {
272274
socket.end(errorResponse);
273275
return false;
274276
}
275-
socket[SocketMetadata] = _.merge(socket[SocketMetadata] || {}, metadataJson);
277+
276278
socket.write(Buffer.from([
277279
0x05, // Version
278280
0x00 // Success
@@ -296,15 +298,8 @@ async function handleUsernamePasswordMetadata(socket: net.Socket) {
296298
return false;
297299
}
298300

299-
let metadataJson: any = {};
300301
try {
301-
// Base64'd json always starts with 'e' (typically eyI), so we can use this fairly
302-
// reliably to detect base64 (and definitely exclude valid object JSON encoding).
303-
const decoded = password[0] === 'e'.charCodeAt(0)
304-
? Buffer.from(password.toString('utf8'), 'base64url').toString('utf8')
305-
: password.toString('utf8');
306-
307-
metadataJson = JSON.parse(decoded);
302+
socket[SocketMetadata] = getSocketMetadata(socket[SocketMetadata], password);
308303
} catch (e) {
309304
socket.end(Buffer.from([
310305
0x05,
@@ -313,7 +308,6 @@ async function handleUsernamePasswordMetadata(socket: net.Socket) {
313308
return false;
314309
}
315310

316-
socket[SocketMetadata] = _.merge(socket[SocketMetadata] || {}, metadataJson);
317311
socket.write(Buffer.from([
318312
0x05, // Version
319313
0x00 // Success

src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import stream = require('stream');
2-
import http = require('http');
3-
import { EventEmitter } from 'events';
1+
import type * as stream from 'stream';
2+
import type * as http from 'http';
3+
import type { EventEmitter } from 'events';
44

55
export const DEFAULT_ADMIN_SERVER_PORT = 45454;
66

src/util/request-utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,12 @@ import {
4040
pairFlatRawHeaders,
4141
rawHeadersToObject
4242
} from './header-utils';
43-
import { LastHopEncrypted, LastTunnelAddress } from './socket-util';
43+
import { LastHopEncrypted, LastTunnelAddress } from './socket-extensions';
4444
import { getDestination, normalizeHost } from './url';
4545

4646
export const shouldKeepAlive = (req: OngoingRequest): boolean =>
4747
req.httpVersion !== '1.0' &&
48-
req.headers['connection'] !== 'close' &&
49-
req.headers['proxy-connection'] !== 'close';
48+
req.headers['connection'] !== 'close';
5049

5150
export const writeHead = (
5251
response: http.ServerResponse | http2.Http2ServerResponse,

0 commit comments

Comments
 (0)