Skip to content

Commit c891838

Browse files
committed
Add raw-passthrough-data events to monitor tunnel contents
1 parent 6139a07 commit c891838

File tree

7 files changed

+152
-7
lines changed

7 files changed

+152
-7
lines changed

src/admin/mockttp-admin-model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const TLS_CLIENT_ERROR_TOPIC = 'tls-client-error';
3434
const CLIENT_ERROR_TOPIC = 'client-error';
3535
const RAW_PASSTHROUGH_OPENED_TOPIC = 'raw-passthrough-opened';
3636
const RAW_PASSTHROUGH_CLOSED_TOPIC = 'raw-passthrough-closed';
37+
const RAW_PASSTHROUGH_DATA_TOPIC = 'raw-passthrough-data';
3738
const RULE_EVENT_TOPIC = 'rule-event';
3839

3940
async function buildMockedEndpointData(endpoint: ServerMockedEndpoint): Promise<MockedEndpointData> {
@@ -146,6 +147,12 @@ export function buildAdminServerModel(
146147
})
147148
});
148149

150+
mockServer.on('raw-passthrough-data', (evt) => {
151+
pubsub.publish(RAW_PASSTHROUGH_DATA_TOPIC, {
152+
rawPassthroughData: evt
153+
})
154+
});
155+
149156
mockServer.on('rule-event', (evt) => {
150157
pubsub.publish(RULE_EVENT_TOPIC, {
151158
ruleEvent: evt
@@ -257,6 +264,9 @@ export function buildAdminServerModel(
257264
rawPassthroughClosed: {
258265
subscribe: () => pubsub.asyncIterator(RAW_PASSTHROUGH_CLOSED_TOPIC)
259266
},
267+
rawPassthroughData: {
268+
subscribe: () => pubsub.asyncIterator(RAW_PASSTHROUGH_DATA_TOPIC)
269+
},
260270
ruleEvent: {
261271
subscribe: () => pubsub.asyncIterator(RULE_EVENT_TOPIC)
262272
}

src/admin/mockttp-schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const MockttpSchema = gql`
3333
failedTlsRequest: TlsHandshakeFailure!
3434
rawPassthroughOpened: RawPassthroughEvent!
3535
rawPassthroughClosed: RawPassthroughEvent!
36+
rawPassthroughData: RawPassthroughDataEvent!
3637
failedClientRequest: ClientError!
3738
ruleEvent: RuleEvent!
3839
}
@@ -130,6 +131,13 @@ export const MockttpSchema = gql`
130131
timingEvents: Json!
131132
}
132133
134+
type RawPassthroughDataEvent {
135+
id: String!
136+
direction: String!
137+
content: Buffer!
138+
eventTimestamp: Float!
139+
}
140+
133141
type RuleEvent {
134142
requestId: ID!
135143
ruleId: ID!

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,14 @@ export class MockttpAdminRequestBuilder {
481481
timingEvents
482482
}
483483
}`,
484+
'raw-passthrough-data': gql`subscription OnRawPassthroughData {
485+
rawPassthroughData {
486+
id
487+
direction
488+
content
489+
eventTimestamp
490+
}
491+
}`,
484492
'rule-event': gql`subscription OnRuleEvent {
485493
ruleEvent {
486494
requestId
@@ -510,6 +518,8 @@ export class MockttpAdminRequestBuilder {
510518
}
511519
} else if (event === 'websocket-message-received' || event === 'websocket-message-sent') {
512520
normalizeWebSocketMessage(data);
521+
} else if (event === 'raw-passthrough-data') {
522+
data.content = Buffer.from(data.content, 'base64');
513523
} else if (event === 'abort') {
514524
normalizeHttpMessage(data, event);
515525
data.error = data.error ? JSON.parse(data.error) : undefined;

src/mockttp.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
WebSocketClose,
2222
AbortedRequest,
2323
RuleEvent,
24-
RawPassthroughEvent
24+
RawPassthroughEvent,
25+
RawPassthroughDataEvent
2526
} from "./types";
2627
import type { RequestRuleData } from "./rules/requests/request-rule";
2728
import type { WebSocketRuleData } from "./rules/websockets/websocket-rule";
@@ -570,6 +571,15 @@ export interface Mockttp {
570571
*/
571572
on(event: 'raw-passthrough-closed', callback: (req: RawPassthroughEvent) => void): Promise<void>;
572573

574+
/**
575+
* Subscribe to hear about each chunk of data that is passed through the raw passthrough
576+
* non-intercepted tunnels, due to the `passthrough` option. See `raw-passthrough-opened`
577+
* for more details.
578+
*
579+
* @category Events
580+
*/
581+
on(event: 'raw-passthrough-data', callback: (req: RawPassthroughDataEvent) => void): Promise<void>;
582+
573583
/**
574584
* Some rules may emit events with metadata about request processing. For example,
575585
* passthrough rules may emit events about upstream server interactions.
@@ -884,6 +894,7 @@ export type SubscribableEvent =
884894
| 'client-error'
885895
| 'raw-passthrough-opened'
886896
| 'raw-passthrough-closed'
897+
| 'raw-passthrough-data'
887898
| 'rule-event';
888899

889900
/**

src/server/mockttp-server.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
TlsPassthroughEvent,
3030
RuleEvent,
3131
RawTrailers,
32-
RawPassthroughEvent
32+
RawPassthroughEvent,
33+
RawPassthroughDataEvent
3334
} from "../types";
3435
import { DestroyableServer } from "destroyable-server";
3536
import {
@@ -325,6 +326,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
325326
public on(event: 'client-error', callback: (error: ClientError) => void): Promise<void>;
326327
public on(event: 'raw-passthrough-opened', callback: (req: RawPassthroughEvent) => void): Promise<void>;
327328
public on(event: 'raw-passthrough-closed', callback: (req: RawPassthroughEvent) => void): Promise<void>;
329+
public on(event: 'raw-passthrough-data', callback: (req: RawPassthroughDataEvent) => void): Promise<void>;
328330
public on<T = unknown>(event: 'rule-event', callback: (event: RuleEvent<T>) => void): Promise<void>;
329331
public on(event: string, callback: (...args: any[]) => void): Promise<void> {
330332
this.eventEmitter.on(event, callback);
@@ -1135,6 +1137,29 @@ ${await this.suggestRule(request)}`
11351137
socket.pipe(upstreamSocket);
11361138
upstreamSocket.pipe(socket);
11371139

1140+
if (type === 'raw') {
1141+
socket.on('data', (data) => {
1142+
setImmediate(() => {
1143+
this.eventEmitter.emit('raw-passthrough-data', {
1144+
id: eventData.id,
1145+
direction: 'received',
1146+
content: data,
1147+
eventTimestamp: now()
1148+
} satisfies RawPassthroughDataEvent);
1149+
});
1150+
});
1151+
upstreamSocket.on('data', (data) => {
1152+
setImmediate(() => {
1153+
this.eventEmitter.emit('raw-passthrough-data', {
1154+
id: eventData.id,
1155+
direction: 'sent',
1156+
content: data,
1157+
eventTimestamp: now()
1158+
} satisfies RawPassthroughDataEvent);
1159+
});
1160+
});
1161+
}
1162+
11381163
socket.on('error', () => upstreamSocket.destroy());
11391164
upstreamSocket.on('error', () => socket.destroy());
11401165
upstreamSocket.on('close', () => socket.destroy());

src/types.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,32 @@ export interface RawPassthroughEvent {
130130
timingEvents: ConnectionTimingEvents;
131131
}
132132

133+
export interface RawPassthroughDataEvent {
134+
/**
135+
* The id of the passthrough tunnel.
136+
*/
137+
id: string;
138+
139+
/**
140+
* The direction of the message, from the downstream perspective (received from the client,
141+
* or sent back to the client).
142+
*/
143+
direction: 'sent' | 'received';
144+
145+
/**
146+
* The contents of the message as a raw buffer.
147+
*/
148+
content: Uint8Array;
149+
150+
/**
151+
* A high-precision floating-point monotonically increasing timestamp.
152+
* Comparable and precise, but not related to specific current time.
153+
*
154+
* To link this to the current time, compare it to `timingEvents.startTime`.
155+
*/
156+
eventTimestamp: number;
157+
}
158+
133159
export interface ConnectionTimingEvents {
134160
/**
135161
* When the socket initially connected, in MS since the unix

test/integration/subscriptions/raw-passthrough-events.spec.ts

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import * as net from 'net';
22
import { expect } from "chai";
33

4-
import { getAdminServer, getLocal, getRemote } from "../../..";
4+
import { getAdminServer, getLocal, getRemote, RawPassthroughDataEvent, RawPassthroughEvent } from "../../..";
55
import {
66
sendRawRequest,
77
openSocksSocket,
88
makeDestroyable,
99
nodeOnly,
10-
delay
10+
delay,
11+
getDeferred
1112
} from "../../test-utils";
1213

1314
nodeOnly(() => {
@@ -21,7 +22,7 @@ nodeOnly(() => {
2122
// Simple TCP echo server:
2223
let remoteServer = makeDestroyable(net.createServer((socket) => {
2324
socket.on('data', (data) => {
24-
socket.end(data);
25+
socket.write(data);
2526
});
2627
}));
2728
let remotePort!: number;
@@ -65,6 +66,50 @@ nodeOnly(() => {
6566
expect(openEvent.upstreamPort).to.equal(remotePort);
6667
});
6768

69+
it("should expose sent & received data", async () => {
70+
const openDeferred = getDeferred<RawPassthroughEvent>();
71+
let dataEvents = [] as RawPassthroughDataEvent[];
72+
73+
await server.on('raw-passthrough-opened', (e) => openDeferred.resolve(e));
74+
await server.on('raw-passthrough-data', (e) => dataEvents.push(e));
75+
76+
const socksSocket = await openSocksSocket(server, 'localhost', remotePort);
77+
78+
socksSocket.write('hello');
79+
80+
const openEvent = await openDeferred;
81+
await delay(10);
82+
83+
expect(dataEvents.length).to.equal(2);
84+
const [firstDataEvent, secondDataEvent] = dataEvents;
85+
dataEvents = [];
86+
87+
expect(firstDataEvent.id).to.equal(openEvent.id);
88+
expect(firstDataEvent.direction).to.equal('received');
89+
expect(firstDataEvent.content.toString()).to.equal('hello');
90+
91+
expect(secondDataEvent.id).to.equal(openEvent.id);
92+
expect(secondDataEvent.direction).to.equal('sent');
93+
expect(secondDataEvent.content.toString()).to.equal('hello');
94+
expect(secondDataEvent.eventTimestamp).to.be.greaterThan(firstDataEvent.eventTimestamp);
95+
96+
socksSocket.write('world');
97+
await delay(10);
98+
99+
expect(dataEvents.length).to.equal(2);
100+
const [thirdDataEvent, fourthDataEvent] = dataEvents;
101+
102+
expect(thirdDataEvent.id).to.equal(openEvent.id);
103+
expect(thirdDataEvent.direction).to.equal('received');
104+
expect(thirdDataEvent.content.toString()).to.equal('world');
105+
expect(thirdDataEvent.eventTimestamp).to.be.greaterThan(secondDataEvent.eventTimestamp);
106+
107+
expect(fourthDataEvent.id).to.equal(openEvent.id);
108+
expect(fourthDataEvent.direction).to.equal('sent');
109+
expect(fourthDataEvent.content.toString()).to.equal('world');
110+
expect(fourthDataEvent.eventTimestamp).to.be.greaterThan(thirdDataEvent.eventTimestamp);
111+
});
112+
68113
describe("with a remote client", () => {
69114
const adminServer = getAdminServer();
70115
const remoteClient = getRemote({
@@ -84,6 +129,7 @@ nodeOnly(() => {
84129
it("should fire for raw sockets that are passed through SOCKS", async () => {
85130
const events: any[] = [];
86131
await remoteClient.on('raw-passthrough-opened', (e) => events.push(e));
132+
await remoteClient.on('raw-passthrough-data', (e) => events.push(e));
87133
await remoteClient.on('raw-passthrough-closed', (e) => events.push(e));
88134

89135
const socksSocket = await openSocksSocket(remoteClient, 'localhost', remotePort);
@@ -92,12 +138,21 @@ nodeOnly(() => {
92138

93139
await delay(10);
94140

95-
expect(events.length).to.equal(2);
96-
const [openEvent, closeEvent] = events;
141+
expect(events.length).to.equal(4);
142+
const [openEvent, receivedEvent, sentEvent, closeEvent] = events;
143+
expect(receivedEvent.id).to.equal(openEvent.id);
144+
expect(sentEvent.id).to.equal(openEvent.id);
97145
expect(openEvent.id).to.equal(closeEvent.id);
98146

99147
expect(openEvent.upstreamHost).to.equal('localhost');
100148
expect(openEvent.upstreamPort).to.equal(remotePort);
149+
150+
expect(receivedEvent.content.toString()).to.equal('123456789');
151+
expect(receivedEvent.direction).to.equal('received');
152+
expect(receivedEvent.eventTimestamp).to.be.greaterThan(openEvent.timingEvents.connectTimestamp);
153+
expect(sentEvent.content.toString()).to.equal('123456789');
154+
expect(sentEvent.direction).to.equal('sent');
155+
expect(sentEvent.eventTimestamp).to.be.greaterThan(receivedEvent.eventTimestamp);
101156
});
102157
});
103158

0 commit comments

Comments
 (0)