Skip to content

Commit ece1b1f

Browse files
committed
fix(mitm): don’t send duplicated headers to h2
Also catch h2 push streams closing/errors
1 parent d2aa22f commit ece1b1f

File tree

6 files changed

+107
-76
lines changed

6 files changed

+107
-76
lines changed

full-client/test/emulate.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ test('should not leave stack trace markers when calling in page functions', asyn
182182
const browser = await SecretAgent.createBrowser();
183183
koaServer.get('/marker', ctx => {
184184
ctx.body = `
185-
<body></body>
185+
<body>
186186
<script type="text/javascript">
187187
function errorCheck() {
188188
const err = new Error('This is from inside');
@@ -195,6 +195,7 @@ test('should not leave stack trace markers when calling in page functions', asyn
195195
};
196196
})(document.querySelectorAll);
197197
</script>
198+
</body>
198199
`;
199200
});
200201
const url = `${koaServer.baseUrl}/marker`;

mitm/handlers/CacheHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export default class CacheHandler {
2424
const cache = this.responseCache?.get(ctx.url.href);
2525

2626
if (cache?.etag) {
27-
ctx.requestHeaders['If-None-Match'] = cache.etag;
27+
const key = this.ctx.isServerHttp2 ? 'if-none-match' : 'If-None-Match';
28+
ctx.requestHeaders[key] = cache.etag;
2829
this.didProposeCachedResource = true;
2930
}
3031
}

mitm/handlers/HeadersHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export default class HeadersHandler {
146146
}
147147
if (singleValueHttp2Headers.has(lowerKey)) {
148148
const value = ctx.requestHeaders[key];
149-
if (Array.isArray(value) && value.length) ctx.requestHeaders[key] = value[0];
149+
if (Array.isArray(value) && value.length) ctx.requestHeaders[lowerKey] = value[0];
150150
}
151151
}
152152
}

mitm/handlers/HttpRequestHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as http from 'http';
22
import Log from '@secret-agent/commons/Logger';
33
import * as http2 from 'http2';
4-
import { ClientHttp2Stream } from 'http2';
4+
import { ClientHttp2Stream, Http2ServerResponse } from 'http2';
55
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
66
import HeadersHandler from './HeadersHandler';
77
import CookieHandler from './CookieHandler';
@@ -144,7 +144,7 @@ export default class HttpRequestHandler extends BaseHttpHandler {
144144

145145
for await (const chunk of serverToProxyResponse) {
146146
const data = context.cacheHandler.onResponseData(chunk as Buffer);
147-
if (data) {
147+
if (data && !(proxyToClientResponse as Http2ServerResponse).stream?.destroyed) {
148148
proxyToClientResponse.write(data, error => {
149149
if (error) this.onError('ServerToProxy.WriteResponseError', error);
150150
});

mitm/lib/MitmRequestAgent.ts

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ export default class MitmRequestAgent {
255255
const session = this.session;
256256
const sessionId = session.sessionId;
257257
log.info('Http2Client.pushReceived', { sessionId, headers, flags });
258+
stream.on('error', error => {
259+
log.warn('Http2.ProxyToServer.PushStreamError', {
260+
sessionId,
261+
error,
262+
});
263+
});
258264

259265
const pushContext = MitmRequestContext.createFromHttp2Push(parentContext, rawHeaders);
260266
this.session.trackResource(pushContext);
@@ -266,8 +272,17 @@ export default class MitmRequestAgent {
266272
}
267273

268274
pushContext.serverToProxyResponse = stream;
269-
HeadersHandler.stripHttp1HeadersForHttp2(pushContext);
270275

276+
// emit request
277+
if (!parentContext.isClientHttp2) {
278+
log.warn('Http2Client.pushReceivedWithNonH2BrowserClient', {
279+
sessionId,
280+
path: headers[':path'],
281+
});
282+
return stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
283+
}
284+
285+
HeadersHandler.stripHttp1HeadersForHttp2(pushContext);
271286
const onResponseHeaders = new Promise(resolve => {
272287
stream.once('push', (responseHeaders, responseFlags, responseRawHeaders) => {
273288
MitmRequestContext.readHttp2Response(
@@ -280,60 +295,68 @@ export default class MitmRequestAgent {
280295
});
281296
});
282297

283-
// emit request
284-
if (!parentContext.isClientHttp2) {
285-
log.warn('Http2Client.pushReceivedWithNonH2BrowserClient', {
286-
sessionId,
287-
path: headers[':path'],
288-
});
289-
return stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
290-
}
298+
if (stream.destroyed) return;
291299

292300
const clientToProxyRequest = parentContext.clientToProxyRequest as http2.Http2ServerRequest;
293301

294-
clientToProxyRequest.stream.pushStream(pushContext.requestHeaders, async (err, pushStream) => {
295-
if (err) {
296-
log.warn('Http2.PushStreamError', {
297-
sessionId,
298-
err,
302+
clientToProxyRequest.stream.pushStream(
303+
pushContext.requestHeaders,
304+
async (error, pushStream) => {
305+
if (error) {
306+
log.warn('Http2.ClientToProxy.PushStreamError', {
307+
sessionId,
308+
error,
309+
});
310+
return;
311+
}
312+
pushStream.on('error', pushError => {
313+
log.warn('Http2.ClientToProxy.PushStreamError', {
314+
sessionId,
315+
error: pushError,
316+
});
299317
});
300-
return;
301-
}
302-
303-
stream.on('headers', additional => pushStream.additionalHeaders(additional));
304-
305-
let trailers: http2.IncomingHttpHeaders;
306-
stream.once('trailers', trailerHeaders => {
307-
trailers = trailerHeaders;
308-
});
309-
310-
await onResponseHeaders;
311-
pushStream.respond(pushContext.responseHeaders, { waitForTrailers: true });
312-
pushStream.on('wantTrailers', async () => {
313-
pushContext.responseTrailers = trailers;
314-
pushStream.sendTrailers(pushContext.responseTrailers ?? {});
315-
});
316-
const cache = pushContext.cacheHandler;
317-
cache.onHttp2PushStream();
318318

319-
for await (const chunk of stream) {
320-
cache.onResponseData(chunk);
321-
pushStream.write(chunk);
322-
}
323-
if (cache.shouldServeCachedData) {
324-
pushStream.write(cache.cacheData);
325-
}
319+
stream.on('headers', additional => {
320+
if (!pushStream.destroyed) pushStream.additionalHeaders(additional);
321+
});
326322

327-
stream.end();
328-
pushStream.end();
329-
cache.onResponseEnd();
323+
let trailers: http2.IncomingHttpHeaders;
324+
stream.once('trailers', trailerHeaders => {
325+
trailers = trailerHeaders;
326+
});
330327

331-
await HeadersHandler.waitForBrowserRequest(pushContext);
332-
parentContext.requestSession.emit(
333-
'response',
334-
MitmRequestContext.toEmittedResource(pushContext),
335-
);
336-
});
328+
await onResponseHeaders;
329+
if (pushStream.destroyed || stream.destroyed) {
330+
return;
331+
}
332+
pushStream.respond(pushContext.responseHeaders, { waitForTrailers: true });
333+
pushStream.on('wantTrailers', async () => {
334+
pushContext.responseTrailers = trailers;
335+
pushStream.sendTrailers(pushContext.responseTrailers ?? {});
336+
});
337+
const cache = pushContext.cacheHandler;
338+
cache.onHttp2PushStream();
339+
340+
for await (const chunk of stream) {
341+
if (pushStream.destroyed || stream.destroyed) return;
342+
cache.onResponseData(chunk);
343+
pushStream.write(chunk);
344+
}
345+
if (cache.shouldServeCachedData) {
346+
if (!pushStream.destroyed && !stream.destroyed) pushStream.write(cache.cacheData);
347+
}
348+
349+
if (!stream.destroyed) stream.end();
350+
if (!pushStream.destroyed) pushStream.end();
351+
cache.onResponseEnd();
352+
353+
await HeadersHandler.waitForBrowserRequest(pushContext);
354+
parentContext.requestSession.emit(
355+
'response',
356+
MitmRequestContext.toEmittedResource(pushContext),
357+
);
358+
},
359+
);
337360
}
338361

339362
private getHttp2Session(origin: string) {

mitm/test/basic.test.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import http from "http";
2-
import { Helpers } from "@secret-agent/testing";
3-
import HttpProxyAgent from "http-proxy-agent";
4-
import { AddressInfo } from "net";
5-
import WebSocket from "ws";
6-
import Url from "url";
7-
import { createPromise } from "@secret-agent/commons/utils";
8-
import HttpRequestHandler from "../handlers/HttpRequestHandler";
9-
import RequestSession from "../handlers/RequestSession";
10-
import MitmServer from "../lib/MitmProxy";
11-
import HeadersHandler from "../handlers/HeadersHandler";
12-
import HttpUpgradeHandler from "../handlers/HttpUpgradeHandler";
13-
import { parseRawHeaders } from "../lib/Utils";
1+
import http from 'http';
2+
import { Helpers } from '@secret-agent/testing';
3+
import HttpProxyAgent from 'http-proxy-agent';
4+
import { AddressInfo } from 'net';
5+
import WebSocket from 'ws';
6+
import Url from 'url';
7+
import { createPromise } from '@secret-agent/commons/utils';
8+
import HttpRequestHandler from '../handlers/HttpRequestHandler';
9+
import RequestSession from '../handlers/RequestSession';
10+
import MitmServer from '../lib/MitmProxy';
11+
import HeadersHandler from '../handlers/HeadersHandler';
12+
import HttpUpgradeHandler from '../handlers/HttpUpgradeHandler';
13+
import { parseRawHeaders } from '../lib/Utils';
1414

1515
const mocks = {
1616
httpRequestHandler: {
@@ -35,14 +35,16 @@ beforeEach(() => {
3535
afterAll(Helpers.afterAll);
3636
afterEach(Helpers.afterEach);
3737

38+
let sessionCounter = 1;
39+
3840
describe('basic MitM tests', () => {
3941
it('should send request through proxy', async () => {
4042
const httpServer = await Helpers.runHttpServer();
4143
const mitmServer = await MitmServer.start();
4244
Helpers.needsClosing.push(mitmServer);
4345
const proxyHost = `http://localhost:${mitmServer.port}`;
4446

45-
const session = new RequestSession('1', 'any agent', null);
47+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
4648

4749
const proxyCredentials = session.getProxyCredentials();
4850
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
@@ -69,7 +71,7 @@ describe('basic MitM tests', () => {
6971
const proxyCredentials = session.getProxyCredentials();
7072
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
7173

72-
let rawHeaders: string[];
74+
let rawHeaders: string[] = null;
7375
const res = await Helpers.httpRequest(
7476
httpServer.url,
7577
'GET',
@@ -97,7 +99,7 @@ describe('basic MitM tests', () => {
9799
Helpers.needsClosing.push(mitmServer);
98100
const proxyHost = `http://localhost:${mitmServer.port}`;
99101

100-
const session = new RequestSession('1', 'any agent', null);
102+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
101103

102104
const proxyCredentials = session.getProxyCredentials();
103105
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
@@ -123,7 +125,11 @@ describe('basic MitM tests', () => {
123125
socket.end();
124126
});
125127

126-
const session = new RequestSession('1', 'any agent', Promise.resolve(upstreamProxyHost));
128+
const session = new RequestSession(
129+
`${(sessionCounter += 1)}`,
130+
'any agent',
131+
Promise.resolve(upstreamProxyHost),
132+
);
127133

128134
const proxyCredentials = session.getProxyCredentials();
129135

@@ -156,7 +162,7 @@ describe('basic MitM tests', () => {
156162
Helpers.needsClosing.push(mitmServer);
157163
const proxyHost = `http://localhost:${mitmServer.port}`;
158164

159-
const session = new RequestSession('1', 'any agent', null);
165+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
160166

161167
const proxyCredentials = session.getProxyCredentials();
162168
expect(mocks.httpRequestHandler.onRequest).toBeCalledTimes(0);
@@ -185,7 +191,7 @@ describe('basic MitM tests', () => {
185191
Helpers.needsClosing.push(mitmServer);
186192
const proxyHost = `http://localhost:${mitmServer.port}`;
187193

188-
const session = new RequestSession('2', 'any agent', null);
194+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
189195

190196
const proxyCredentials = session.getProxyCredentials();
191197

@@ -204,7 +210,7 @@ describe('basic MitM tests', () => {
204210
Helpers.needsClosing.push(mitmServer);
205211
const proxyHost = `http://localhost:${mitmServer.port}`;
206212

207-
const session = new RequestSession('3', 'any agent', null);
213+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
208214

209215
const proxyCredentials = session.getProxyCredentials();
210216

@@ -278,7 +284,7 @@ describe('basic MitM tests', () => {
278284
const serverMessages = [];
279285
const serverMessagePromise = createPromise();
280286
const wsServer = new WebSocket.Server({ noServer: true });
281-
const session = new RequestSession('4', 'any agent', null);
287+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
282288

283289
httpServer.server.on('upgrade', (request, socket, head) => {
284290
// ensure header is stripped
@@ -337,11 +343,11 @@ describe('basic MitM tests', () => {
337343
Helpers.needsClosing.push(mitmServer);
338344
const proxyHost = `http://localhost:${mitmServer.port}`;
339345

340-
const session = new RequestSession('1', 'any agent', null);
346+
const session = new RequestSession(`${(sessionCounter += 1)}`, 'any agent', null);
341347
session.delegate.modifyHeadersBeforeSend = jest.fn();
342348
session.registerResource({
343349
tabId: '1',
344-
browserRequestId: '1',
350+
browserRequestId: '25.123',
345351
url: `${httpServer.url}page1`,
346352
method: 'GET',
347353
resourceType: 'Document',

0 commit comments

Comments
 (0)