@dev-swarup/http-mitm-proxy 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@dev-swarup/http-mitm-proxy",
3
+ "version": "0.9.6",
4
+ "description": "HTTP Man In The Middle (MITM) Proxy. This is a fork of Joe Ferners' library node-http-mitm-proxy. Its first release was identical to the master version of the original library, commit 66ac0f5d3298f66b731f90ebf1e9b430fa5d76eb. I decided to publish a scoped version of this library to npm, since I needed the codebase in npm. It is only intended for my own library cypress-ntlm-auth. Use at you own risk!",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "test": "mocha --exit"
9
+ },
10
+ "repository": "https://github.com/dev-swarup/node-http-mitm-proxy",
11
+ "bugs": {
12
+ "url": "https://github.com/dev-swarup/node-http-mitm-proxy/issues"
13
+ },
14
+ "bin": {
15
+ "http-mitm-proxy": "./bin/mitm-proxy.js"
16
+ },
17
+ "keywords": [
18
+ "mitm",
19
+ "http",
20
+ "https",
21
+ "ssl",
22
+ "websocket",
23
+ "proxy"
24
+ ],
25
+ "author": "Swarup Banerjee <dev-swarup@hotmail.com>",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=14"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^9.19.0",
32
+ "globals": "^15.14.0",
33
+ "mocha": "^11.1.0",
34
+ "request": "^2.88.2"
35
+ },
36
+ "dependencies": {
37
+ "async": "^3.2.0",
38
+ "debug": "^4.3.2",
39
+ "mkdirp": "^3.0.1",
40
+ "node-forge": "^1.3.1",
41
+ "semaphore": "^1.1.0",
42
+ "ws": "^8.5.0",
43
+ "yargs": "^17.4.0"
44
+ }
45
+ }
@@ -0,0 +1,555 @@
1
+ var util = require('util');
2
+ var assert = require('assert');
3
+ var crypto = require('crypto');
4
+ var zlib = require('zlib');
5
+ var httpRequest = require('./http.client');
6
+ var fs = require('fs');
7
+ var http = require('http');
8
+ var net = require('net');
9
+ var path = require('path');
10
+ var WebSocket = require('ws');
11
+ var Proxy = require('../');
12
+ const TunnelAgent = require('./tunnel.agent');
13
+
14
+ var filePathA = __dirname + '/wwwA';
15
+ var filePathB = __dirname + '/wwwB';
16
+ var testPortA = 40005;
17
+ var testPortB = 40006;
18
+ var testProxyPort = 40010;
19
+ var testWSPort = 40007;
20
+
21
+ var sendStaticFile = function (root, req, res) {
22
+ const filePath = path.join(root, req.url);
23
+ if (!fs.existsSync(filePath)) {
24
+ console.error('no file ', filePath);
25
+ res.writeHead(404);
26
+ return res.end();
27
+ }
28
+ var body = fs.readFileSync(filePath, 'utf8');
29
+ res.writeHead(200, { 'content-length': body.length });
30
+ res.write(body);
31
+ res.end();
32
+ };
33
+
34
+ ['127.0.0.1', '::1', 'localhost'].forEach((testHost) => {
35
+ var testHostForUrl = testHost === '::1' ? '[::1]' : testHost;
36
+ var testUrlA = 'http://' + testHostForUrl + ':' + testPortA;
37
+ var testUrlB = 'http://' + testHostForUrl + ':' + testPortB;
38
+
39
+ var getHttp = function (url, cb) {
40
+ httpRequest(url, null, null, function (err, resp, body) {
41
+ cb(err, resp, body);
42
+ });
43
+ };
44
+
45
+ var proxyHttp = function (url, keepAlive, cb) {
46
+ httpRequest(
47
+ url,
48
+ {
49
+ agent: new TunnelAgent(
50
+ {
51
+ ca: fs.readFileSync(__dirname + '/../.http-mitm-proxy/certs/ca.pem'),
52
+ keepAlive: keepAlive,
53
+ proxy: {
54
+ host: testHost,
55
+ port: testProxyPort,
56
+ },
57
+ },
58
+ false,
59
+ url.indexOf('https:') === 0
60
+ ),
61
+ },
62
+ null,
63
+ function (err, resp, body) {
64
+ cb(err, resp, body);
65
+ }
66
+ );
67
+ };
68
+
69
+ var countString = function (str, substr, cb) {
70
+ var pos = str.indexOf(substr);
71
+ var len = substr.length;
72
+ var count = 0;
73
+ if (pos > -1) {
74
+ var offSet = len;
75
+ while (pos !== -1) {
76
+ count++;
77
+ offSet = pos + len;
78
+ pos = str.indexOf(substr, offSet);
79
+ }
80
+ }
81
+ cb(count);
82
+ };
83
+
84
+ describe('proxy on ' + testHostForUrl, function () {
85
+ this.timeout(30000);
86
+ var srvA = null;
87
+ var srvB = null;
88
+ var proxy = null;
89
+ var testHashes = {};
90
+ var testFiles = ['1024.bin'];
91
+ var wss = null;
92
+
93
+ before(function (done) {
94
+ testFiles.forEach(function (val) {
95
+ testHashes[val] = crypto
96
+ .createHash('sha256')
97
+ .update(fs.readFileSync(__dirname + '/www/' + val, 'utf8'), 'utf8')
98
+ .digest()
99
+ .toString();
100
+ });
101
+ srvA = http.createServer(function (req, res) {
102
+ req
103
+ .addListener('end', function () {
104
+ sendStaticFile(filePathA, req, res);
105
+ })
106
+ .resume();
107
+ });
108
+ srvA.listen(testPortA, testHost, () => {
109
+ srvB = http.createServer(function (req, res) {
110
+ req
111
+ .addListener('end', function () {
112
+ sendStaticFile(filePathB, req, res);
113
+ })
114
+ .resume();
115
+ });
116
+ srvB.listen(testPortB, testHost, () => {
117
+ wss = new WebSocket.Server(
118
+ {
119
+ port: testWSPort,
120
+ },
121
+ done
122
+ );
123
+ wss.on('connection', function (ws) {
124
+ // just reply with the same message
125
+ ws.on('message', function (data, isBinary) {
126
+ if (!isBinary && data.toString() === 'send ping') {
127
+ ws.ping('send ping');
128
+ } else {
129
+ ws.send(data, { binary: isBinary });
130
+ }
131
+ });
132
+
133
+ ws.on('ping', function (data) {
134
+ ws.pong(data);
135
+ });
136
+ });
137
+ });
138
+ });
139
+ });
140
+
141
+ beforeEach(function (done) {
142
+ proxy = new Proxy();
143
+ proxy.listen({ port: testProxyPort, host: testHost }, done);
144
+ proxy.onError(function (ctx, err, errorKind) {
145
+ var url = ctx && ctx.clientToProxyRequest ? ctx.clientToProxyRequest.url : '';
146
+ console.log('proxy error: ' + errorKind + ' on ' + url + ':', err);
147
+ });
148
+ });
149
+
150
+ afterEach(function () {
151
+ proxy.close();
152
+ proxy = null;
153
+ });
154
+
155
+ after(function () {
156
+ srvA.close();
157
+ srvA = null;
158
+ srvB.close();
159
+ srvB = null;
160
+ wss.close();
161
+ wss = null;
162
+ });
163
+
164
+ describe('ca server', function () {
165
+ it('should generate a root CA file', function (done) {
166
+ fs.access(__dirname + '/../.http-mitm-proxy/certs/ca.pem', function (err) {
167
+ var rtv = null;
168
+ if (err) {
169
+ rtv = __dirname + '/../.http-mitm-proxy/certs/ca.pem ' + err;
170
+ } else {
171
+ rtv = true;
172
+ }
173
+ assert.equal(true, rtv, 'Can access the CA cert');
174
+ done();
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('http server', function () {
180
+ describe('get a 1024 byte file', function () {
181
+ it('a', function (done) {
182
+ getHttp(testUrlA + '/1024.bin', function (err, resp, body) {
183
+ if (err) return done(new Error(err));
184
+ var len = 0;
185
+ if (body.hasOwnProperty('length')) len = body.length;
186
+ assert.equal(1024, len, 'body length is 1024');
187
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString(), 'sha256 hash matches');
188
+ done();
189
+ });
190
+ });
191
+ it('b', function (done) {
192
+ getHttp(testUrlB + '/1024.bin', function (err, resp, body) {
193
+ if (err) return done(new Error(err));
194
+ var len = 0;
195
+ if (body.hasOwnProperty('length')) len = body.length;
196
+ assert.equal(1024, len, 'body length is 1024');
197
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString(), 'sha256 hash matches');
198
+ done();
199
+ });
200
+ });
201
+ });
202
+ });
203
+
204
+ describe('proxy server', function () {
205
+ this.timeout(5000);
206
+
207
+ it('should handle socket errors in connect', function (done) {
208
+ // If a socket disconnects during the CONNECT process, the resulting
209
+ // error should be handled and shouldn't cause the proxy server to fail.
210
+ const socket = net.createConnection(testProxyPort, testHost, function () {
211
+ socket.write('CONNECT ' + testHostForUrl + ':' + testPortA + '\r\n\r\n');
212
+ socket.destroy();
213
+ });
214
+ socket.on('close', function () {
215
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
216
+ if (err) {
217
+ return done(new Error(err));
218
+ }
219
+ var len = 0;
220
+ if (body.hasOwnProperty('length')) {
221
+ len = body.length;
222
+ }
223
+ assert.equal(1024, len);
224
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString());
225
+ done();
226
+ });
227
+ });
228
+ });
229
+
230
+ describe('proxy a 1024 byte file', function () {
231
+ it('a', function (done) {
232
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
233
+ if (err) return done(new Error(err));
234
+ var len = 0;
235
+ if (body.hasOwnProperty('length')) len = body.length;
236
+ assert.equal(1024, len);
237
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString());
238
+ done();
239
+ });
240
+ });
241
+ it('b', function (done) {
242
+ proxyHttp(testUrlB + '/1024.bin', false, function (err, resp, body) {
243
+ if (err) return done(new Error(err));
244
+ var len = 0;
245
+ if (body.hasOwnProperty('length')) len = body.length;
246
+ assert.equal(1024, len);
247
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString());
248
+ done();
249
+ });
250
+ });
251
+ });
252
+ describe('ssl', function () {
253
+ it('proxys to google.com using local ca file', function (done) {
254
+ proxyHttp('https://www.google.com/', false, function (err, resp, body) {
255
+ if (err) return done(new Error(err));
256
+ assert.equal(200, resp.statusCode, '200 Status code from Google.');
257
+ done();
258
+ });
259
+ }).timeout(15000);
260
+ });
261
+
262
+ describe('proxy a 1024 byte file with keepAlive', function () {
263
+ it('a', function (done) {
264
+ proxyHttp(testUrlA + '/1024.bin', true, function (err, resp, body) {
265
+ if (err) return done(new Error(err));
266
+ var len = 0;
267
+ if (body.hasOwnProperty('length')) len = body.length;
268
+ assert.equal(1024, len);
269
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString());
270
+ done();
271
+ });
272
+ });
273
+ it('b', function (done) {
274
+ proxyHttp(testUrlB + '/1024.bin', true, function (err, resp, body) {
275
+ if (err) return done(new Error(err));
276
+ var len = 0;
277
+ if (body.hasOwnProperty('length')) len = body.length;
278
+ assert.equal(1024, len);
279
+ assert.equal(testHashes['1024.bin'], crypto.createHash('sha256').update(body, 'utf8').digest().toString());
280
+ done();
281
+ });
282
+ });
283
+ });
284
+ describe('ssl with keepAlive', function () {
285
+ it('proxys to google.com using local ca file', function (done) {
286
+ proxyHttp('https://www.google.com/', true, function (err, resp, body) {
287
+ if (err) return done(new Error(err));
288
+ assert.equal(200, resp.statusCode, '200 Status code from Google.');
289
+ done();
290
+ });
291
+ }).timeout(15000);
292
+ });
293
+
294
+ describe('host match', function () {
295
+ it('proxy and modify AAA 5 times if hostA', function (done) {
296
+ proxy.onRequest(function (ctx, callback) {
297
+ var testHostNameA = testHostForUrl + ':' + testPortA;
298
+ if (ctx.clientToProxyRequest.headers.host === testHostNameA) {
299
+ var chunks = [];
300
+ ctx.onResponseData(function (ctx, chunk, callback) {
301
+ chunks.push(chunk);
302
+ return callback(null, null);
303
+ });
304
+ ctx.onResponseEnd(function (ctx, callback) {
305
+ var body = Buffer.concat(chunks).toString();
306
+ for (var i = 0; i < 5; i++) {
307
+ var off = i * 10;
308
+ body = body.substr(0, off) + 'AAA' + body.substr(off + 3);
309
+ }
310
+ ctx.proxyToClientResponse.write(body);
311
+ return callback();
312
+ });
313
+ }
314
+ return callback();
315
+ });
316
+
317
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
318
+ if (err) return done(new Error(err));
319
+ var len = 0;
320
+ if (body.hasOwnProperty('length')) len = body.length;
321
+ assert.equal(1024, len);
322
+ countString(body, 'AAA', function (count) {
323
+ assert.equal(5, count);
324
+ proxyHttp(testUrlB + '/1024.bin', false, function (errB, respB, bodyB) {
325
+ if (errB) console.log('errB: ' + errB.toString());
326
+ var lenB = 0;
327
+ if (bodyB.hasOwnProperty('length')) lenB = bodyB.length;
328
+ assert.equal(1024, lenB);
329
+ countString(bodyB, 'AAA', function (countB) {
330
+ assert.equal(0, countB);
331
+ done();
332
+ });
333
+ });
334
+ });
335
+ });
336
+ });
337
+ });
338
+
339
+ describe('chunked transfer', function () {
340
+ it('should not change transfer encoding when no content modification is active', function (done) {
341
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
342
+ if (err) return done(new Error(err));
343
+ var len = 0;
344
+ if (body.hasOwnProperty('length')) len = body.length;
345
+ assert.equal(1024, len);
346
+ assert.equal(null, resp.headers['transfer-encoding']);
347
+ assert.equal(1024, resp.headers['content-length']);
348
+ done();
349
+ });
350
+ });
351
+
352
+ it('should use chunked transfer encoding when global onResponseData is active', function (done) {
353
+ proxy.onResponseData(function (ctx, chunk, callback) {
354
+ callback(null, chunk);
355
+ });
356
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
357
+ if (err) return done(new Error(err));
358
+ var len = 0;
359
+ if (body.hasOwnProperty('length')) len = body.length;
360
+ assert.equal(1024, len);
361
+ assert.equal('chunked', resp.headers['transfer-encoding']);
362
+ assert.equal(null, resp.headers['content-length']);
363
+ done();
364
+ });
365
+ });
366
+
367
+ it('should use chunked transfer encoding when context onResponseData is active', function (done) {
368
+ proxy.onResponse(function (ctx, callback) {
369
+ ctx.onResponseData(function (ctx, chunk, callback) {
370
+ callback(null, chunk);
371
+ });
372
+ callback(null);
373
+ });
374
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
375
+ if (err) return done(new Error(err));
376
+ var len = 0;
377
+ if (body.hasOwnProperty('length')) len = body.length;
378
+ assert.equal(1024, len);
379
+ assert.equal('chunked', resp.headers['transfer-encoding']);
380
+ assert.equal(null, resp.headers['content-length']);
381
+ done();
382
+ });
383
+ });
384
+
385
+ it('should use chunked transfer encoding when context ResponseFilter is active', function (done) {
386
+ proxy.onResponse(function (ctx, callback) {
387
+ ctx.addResponseFilter(zlib.createGzip());
388
+ callback(null);
389
+ });
390
+ proxyHttp(testUrlA + '/1024.bin', false, function (err, resp, body) {
391
+ if (err) return done(new Error(err));
392
+ var len = 0;
393
+ if (body.hasOwnProperty('length')) len = body.length;
394
+ assert.equal(true, len < 1024); // Compressed body
395
+ assert.equal('chunked', resp.headers['transfer-encoding']);
396
+ assert.equal(null, resp.headers['content-length']);
397
+ done();
398
+ });
399
+ });
400
+ });
401
+ });
402
+
403
+ describe('websocket server', function () {
404
+ this.timeout(2000);
405
+
406
+ it('send + receive message without proxy', function (done) {
407
+ var ws = new WebSocket('ws://' + testHostForUrl + ':' + testWSPort);
408
+ var testMessage = 'does the websocket server reply?';
409
+ ws.on('open', function () {
410
+ ws.on('message', function (dataBuf, isBinary) {
411
+ const data = isBinary ? dataBuf : dataBuf.toString();
412
+ assert.equal(data, testMessage);
413
+ ws.close();
414
+ done();
415
+ });
416
+ ws.send(testMessage, { binary: false });
417
+ });
418
+ });
419
+
420
+ it('send + receive message through proxy', function (done) {
421
+ var ws = new WebSocket('ws://' + testHostForUrl + ':' + testProxyPort, {
422
+ headers: {
423
+ Host: testHostForUrl + ':' + testWSPort,
424
+ },
425
+ });
426
+ var testMessage = 'does websocket proxying work?';
427
+ ws.on('open', function () {
428
+ ws.on('message', function (dataBuf, isBinary) {
429
+ assert.ok(!isBinary);
430
+ assert.equal(dataBuf.toString(), testMessage);
431
+ ws.close();
432
+ done();
433
+ });
434
+ ws.send(testMessage, { binary: false });
435
+ });
436
+ });
437
+
438
+ it('send + receive binary message through proxy', function (done) {
439
+ var ws = new WebSocket('ws://' + testHostForUrl + ':' + testProxyPort, {
440
+ headers: {
441
+ Host: testHostForUrl + ':' + testWSPort,
442
+ },
443
+ });
444
+ var testMessage = 'does websocket binary proxying work?';
445
+ ws.on('open', function () {
446
+ ws.on('message', function (dataBuf, isBinary) {
447
+ assert.ok(isBinary);
448
+ assert.equal(dataBuf.toString(), testMessage);
449
+ ws.close();
450
+ done();
451
+ });
452
+ ws.send(Buffer.from(testMessage, 'utf-8'), { binary: true });
453
+ });
454
+ });
455
+
456
+ it('send ping + receive pong through proxy', function (done) {
457
+ var ws = new WebSocket('ws://' + testHostForUrl + ':' + testProxyPort, {
458
+ headers: {
459
+ Host: testHostForUrl + ':' + testWSPort,
460
+ },
461
+ });
462
+ var testMessage = 'does websocket client ping/server pong proxying work?';
463
+ ws.on('open', function () {
464
+ ws.on('pong', function (dataBuf) {
465
+ assert.equal(dataBuf.toString(), testMessage);
466
+ ws.close();
467
+ done();
468
+ });
469
+ ws.ping(testMessage);
470
+ });
471
+ });
472
+
473
+ it('send + receive pong through proxy', function (done) {
474
+ var ws = new WebSocket('ws://' + testHostForUrl + ':' + testProxyPort, {
475
+ headers: {
476
+ Host: testHostForUrl + ':' + testWSPort,
477
+ },
478
+ });
479
+ var testMessage = 'send ping';
480
+ ws.on('open', function () {
481
+ ws.on('ping', function (dataBuf) {
482
+ assert.equal(dataBuf.toString(), testMessage);
483
+ ws.close();
484
+ done();
485
+ });
486
+ ws.send(testMessage, { binary: false });
487
+ });
488
+ });
489
+
490
+ it('websocket callbacks get called', function (done) {
491
+ var stats = {
492
+ connection: false,
493
+ frame: false,
494
+ send: false,
495
+ message: false,
496
+ close: false,
497
+ };
498
+
499
+ proxy.onWebSocketConnection(function (ctx, callback) {
500
+ stats.connection = true;
501
+ return callback();
502
+ });
503
+ proxy.onWebSocketFrame(function (ctx, type, fromServer, message, flags, callback) {
504
+ stats.frame = true;
505
+ message = rewrittenMessage;
506
+ return callback(null, message, flags);
507
+ });
508
+ proxy.onWebSocketSend(function (ctx, message, flags, callback) {
509
+ stats.send = true;
510
+ return callback(null, message, flags);
511
+ });
512
+ proxy.onWebSocketMessage(function (ctx, message, flags, callback) {
513
+ stats.message = true;
514
+ return callback(null, message, flags);
515
+ });
516
+ proxy.onWebSocketClose(function (ctx, code, message, callback) {
517
+ stats.close = true;
518
+ callback(null, code, message);
519
+ });
520
+
521
+ var ws = new WebSocket('ws://' + testHostForUrl + ':' + testProxyPort, {
522
+ headers: {
523
+ host: testHostForUrl + ':' + testWSPort,
524
+ },
525
+ });
526
+ var testMessage = 'does rewriting messages work?';
527
+ var rewrittenMessage = 'rewriting messages does work!';
528
+ ws.on('open', function () {
529
+ ws.on('message', function (dataBuf, isBinary) {
530
+ const data = isBinary ? dataBuf : dataBuf.toString();
531
+ assert.equal(data, rewrittenMessage);
532
+ ws.close();
533
+ });
534
+ ws.on('close', function () {
535
+ setTimeout(() => {
536
+ assert(stats.connection);
537
+ assert(stats.frame);
538
+ assert(stats.send);
539
+ assert(stats.message);
540
+ if (!stats.close) {
541
+ setTimeout(() => {
542
+ assert(stats.close);
543
+ done();
544
+ }, 500);
545
+ } else {
546
+ done();
547
+ }
548
+ }, 0);
549
+ });
550
+ ws.send(testMessage, { binary: false });
551
+ });
552
+ });
553
+ });
554
+ });
555
+ });
@@ -0,0 +1,37 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+
4
+ module.exports = function httpRequest(url, options, body, cb) {
5
+ var proto = url.indexOf('http:') === 0 ? http : https;
6
+ const request = proto.request(url, options, (response) => {
7
+ if (response.statusCode >= 400) {
8
+ request.destroy(new Error());
9
+ return cb(new Error('Non success status code'), response, null);
10
+ }
11
+
12
+ const chunks = [];
13
+ response.on('data', (chunk) => {
14
+ chunks.push(chunk);
15
+ });
16
+
17
+ response.once('end', () => {
18
+ const buffer = Buffer.concat(chunks);
19
+ return cb(null, response, buffer.toString());
20
+ });
21
+
22
+ response.once('error', (err) => {
23
+ return cb(err, response, null);
24
+ });
25
+ });
26
+
27
+ request.once('error', (err) => {
28
+ return cb(err, null, null);
29
+ });
30
+ if (body) {
31
+ const bodyStr = JSON.stringify(body);
32
+ request.setHeader('content-type', 'application/json; charset=utf-8');
33
+ request.setHeader('content-length', bodyStr.length);
34
+ request.write(bodyStr);
35
+ }
36
+ request.end();
37
+ };