Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

unexpected-mitm

Package Overview
Dependencies
Maintainers
1
Versions
108
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

unexpected-mitm - npm Package Compare versions

Comparing version 1.4.1 to 2.0.0

374

lib/unexpectedMitm.js

@@ -1,18 +0,313 @@

/* global setImmediate */
/* global setImmediate, after */
var messy = require('messy'),
createMitm = require('mitm-papandreou'),
_ = require('underscore'),
http = require('http'),
urlModule = require('url');
https = require('https'),
fs = require('fs'),
urlModule = require('url'),
memoizeSync = require('memoizesync'),
callsite = require('callsite'),
detectIndent = require('detect-indent'),
passError = require('passerror');
function formatHeaderObj(headerObj) {
var result = {};
Object.keys(headerObj).forEach(function (headerName) {
result[messy.formatHeaderName(headerName)] = headerObj[headerName];
});
return result;
}
function isTextualContentType(contentType) {
if (typeof contentType === 'string') {
contentType = contentType.toLowerCase().trim().replace(/\s*;.*$/, '');
return (
/^text\//.test(contentType) ||
/^application\/(json|javascript)$/.test(contentType) ||
/^application\/xml/.test(contentType) ||
/^application\/x-www-form-urlencoded\b/.test(contentType) ||
/\+xml$/.test(contentType)
);
}
return false;
}
function bufferCanBeInterpretedAsUtf8(buffer) {
// Hack: Since Buffer.prototype.toString('utf-8') is very forgiving, convert the buffer to a string
// with percent-encoded octets, then see if decodeURIComponent accepts it.
try {
decodeURIComponent(Array.prototype.map.call(buffer, function (octet) {
return '%' + (octet < 16 ? '0' : '') + octet.toString(16);
}).join(''));
} catch (e) {
return false;
}
return true;
}
function trimMessage(message) {
if (typeof message.body !== 'undefined') {
if (message.body.length === 0) {
delete message.body;
} else if (isTextualContentType(message.headers['Content-Type']) && bufferCanBeInterpretedAsUtf8(message.body)) {
message.body = message.body.toString('utf-8');
}
if (/^application\/json(?:;|$)/.test(message.headers['Content-Type']) && /^\s*[[{]/.test(message.body)) {
try {
message.body = JSON.parse(message.body);
if (message.headers['Content-Type'] === 'application/json') {
delete message.headers['Content-Type'];
}
} catch (e) {}
}
}
if (message.headers) {
delete message.headers['Content-Length'];
delete message.headers['Transfer-Encoding'];
delete message.headers['Connection'];
delete message.headers['Date'];
if (Object.keys(message.headers).length === 0) {
delete message.headers;
}
}
if (message.url && message.method) {
message.url = message.method + ' ' + message.url;
delete message.method;
}
if (Object.keys(message).length === 1) {
if (typeof message.url === 'string') {
return message.url;
}
if (typeof message.statusCode === 'number') {
return message.statusCode;
}
}
return message;
}
function bufferStream(buffer, cb) {
var chunks = [];
buffer.on('data', function (chunk) {
chunks.push(chunk);
}).on('end', function () {
cb(null, Buffer.concat(chunks));
}).on('error', cb);
}
function trimRecordedExchange(recordedExchange) {
return {
request: trimMessage(recordedExchange.request),
response: trimMessage(recordedExchange.response)
};
}
function createSerializedRequestHandler(onRequest) {
var activeRequest = false,
requestQueue = [];
function processNextRequest() {
while (requestQueue.length > 0 && !activeRequest) {
activeRequest = true;
var reqAndRes = requestQueue.shift(),
req = reqAndRes[0],
res = reqAndRes[1],
resEnd = res.end;
res.end = function () {
resEnd.apply(this, arguments);
activeRequest = false;
setImmediate(processNextRequest);
};
onRequest(req, res);
}
}
return function (req, res) {
requestQueue.push([req, res]);
processNextRequest();
};
}
module.exports = {
name: 'unexpected-mitm',
installInto: function (expect) {
var expectForRendering = expect.clone();
expectForRendering.addType({
name: 'Buffer',
base: 'Buffer',
hexDumpWidth: Infinity // Prevents Buffer instances > 16 bytes from being truncated
});
function stringify(obj, indentationWidth) {
expectForRendering.output.indentationWidth = indentationWidth;
return expectForRendering.inspect(obj, Infinity).toString('text');
}
var injectionsBySourceFileName = {},
getSourceText = memoizeSync(function (sourceFileName) {
return fs.readFileSync(sourceFileName, 'utf-8');
});
function injectRecordedExchanges(sourceFileName, recordedExchanges, pos) {
var sourceText = getSourceText(sourceFileName),
// FIXME: Does not support tabs:
indentationWidth = 4,
detectedIndent = detectIndent(sourceText);
if (detectedIndent) {
indentationWidth = detectedIndent.amount;
}
while (pos > 0 && sourceText.charAt(pos - 1) !== '\n') {
pos -= 1;
}
var searchRegExp = /([ ]*)(.*)(['"])with http recorded\3,/g;
searchRegExp.lastIndex = pos;
// NB: Return value of replace not used:
var matchSearchRegExp = searchRegExp.exec(sourceText);
if (matchSearchRegExp) {
var lineIndentation = matchSearchRegExp[1],
before = matchSearchRegExp[2],
quote = matchSearchRegExp[3];
(injectionsBySourceFileName[sourceFileName] = injectionsBySourceFileName[sourceFileName] || []).push({
pos: matchSearchRegExp.index,
length: matchSearchRegExp[0].length,
replacement: lineIndentation + before + quote + 'with http mocked out' + quote + ', ' + stringify(recordedExchanges, indentationWidth).replace(/\n^/mg, '\n' + lineIndentation) + ','
});
}
}
function applyInjections() {
Object.keys(injectionsBySourceFileName).forEach(function (sourceFileName) {
var injections = injectionsBySourceFileName[sourceFileName],
sourceText = getSourceText(sourceFileName),
offset = 0;
injections.sort(function (a, b) {
return a.pos - b.pos;
}).forEach(function (injection) {
var pos = injection.pos + offset;
sourceText = sourceText.substr(0, pos) + injection.replacement + sourceText.substr(pos + injection.length);
offset += injection.replacement.length - injection.length;
});
fs.writeFileSync(sourceFileName, sourceText, 'utf-8');
});
}
var afterBlockRegistered = false;
expect
.addAssertion('with http recorded', function (expect, subject) {
var stack = callsite(),
cb = this.args.pop(),
mitm = createMitm(),
callbackCalled = false,
recordedExchanges = [];
if (!afterBlockRegistered) {
after(applyInjections);
afterBlockRegistered = true;
}
function cleanUp() {
mitm.disable();
}
function handleError(err) {
if (!callbackCalled) {
callbackCalled = true;
cleanUp();
cb(err);
}
}
var bypassNextConnect = false;
mitm.on('connect', function (socket, opts) {
if (bypassNextConnect) {
socket.bypass();
bypassNextConnect = false;
}
}).on('request', createSerializedRequestHandler(function (req, res) {
var recordedExchange = {
request: {
url: req.method + ' ' + req.url,
headers: formatHeaderObj(req.headers)
},
response: {}
};
recordedExchanges.push(recordedExchange);
bufferStream(req, passError(handleError, function (body) {
recordedExchange.request.body = body;
bypassNextConnect = true;
var matchHostHeader = req.headers.host && req.headers.host.match(/^([^:]*)(?::(\d+))?/),
host,
port;
// https://github.com/moll/node-mitm/issues/14
if (matchHostHeader) {
if (matchHostHeader[1]) {
host = matchHostHeader[1];
}
if (matchHostHeader[2]) {
port = parseInt(matchHostHeader[2], 10);
}
}
if (!host) {
return handleError(new Error('unexpected-mitm recording mode: Could not determine the host name from Host header: ' + req.headers.host));
}
(req.socket.encrypted ? https : http).request({
method: req.method,
host: host,
port: port || (req.socket.encrypted ? 443 : 80),
headers: req.headers,
path: req.url
}).on('response', function (response) {
recordedExchange.response.headers = formatHeaderObj(response.headers);
bufferStream(response, passError(handleError, function (body) {
recordedExchange.response.body = body;
setImmediate(function () {
Object.keys(response.headers).forEach(function (headerName) {
res.setHeader(headerName, response.headers[headerName]);
});
res.end(recordedExchange.response.body);
});
}));
}).on('error', function (err) {
recordedExchange.response = err;
}).end(recordedExchange.request.body);
}));
}));
this.args.push(function (err) {
var args = arguments;
if (!callbackCalled) {
callbackCalled = true;
cleanUp();
setImmediate(function () {
recordedExchanges = recordedExchanges.map(trimRecordedExchange);
if (recordedExchanges.length === 1) {
recordedExchanges = recordedExchanges[0];
}
// Find the first call site that has mocha's "test" property:
var containingCallsite = stack.filter(function (parentCallsite) {
return parentCallsite.receiver.test;
}).shift();
var fileName = containingCallsite && containingCallsite.getFileName();
if (fileName) {
injectRecordedExchanges(fileName, recordedExchanges, containingCallsite.pos);
}
return cb.apply(this, args);
});
}
});
try {
this.shift(expect, subject, 0);
} finally {
this.args.pop(); // Prevent the wrapped callback from being inspected when the assertion fails.
}
})
.addAssertion('with http mocked out', function (expect, subject, requestDescriptions) { // ...
var cb = this.args.pop();
var that = this,
cb = this.args.pop();
this.errorMode = 'nested';
expect(cb, 'to be a function'); // We need a cb
this.errorMode = 'default';
var mitm = require('mitm-papandreou')(),
var mitm = createMitm(),
callbackCalled = false;

@@ -44,3 +339,6 @@

mitm.on('request', function (req, res) {
mitm.on('request', createSerializedRequestHandler(function (req, res) {
if (callbackCalled) {
return;
}

@@ -80,8 +378,5 @@ var noMoreMockedOutRequests = nextRequestDescriptionIndex >= requestDescriptions.length,

mockResponseBodyIsReady = false;
var mockResponseBodyChunks = [];
mockResponse.body.on('data', function (chunk) {
mockResponseBodyChunks.push(chunk);
}).on('end', function () {
mockResponse.body = Buffer.concat(mockResponseBodyChunks);
}).on('error', handleError);
bufferStream(mockResponse.body, passError(handleError, function (body) {
mockResponse.body = body;
}));
}

@@ -135,10 +430,14 @@ }

httpConversation.exchanges.push(httpExchange);
var httpConversationSnapshot = new messy.HttpConversation({
exchanges: [].concat(httpConversation.exchanges)
});
if (expectedRequestProperties) {
httpConversationSatisfySpec.exchanges.push({request: expectedRequestProperties || {}});
}
var requestBodyChunks = [];
req.on('data', function (chunk) {
requestBodyChunks.push(chunk);
}).on('end', function () {
actualRequest.body = Buffer.concat(requestBodyChunks);
var httpConversationSatisfySpecSnapshot = { exchanges: [].concat(httpConversationSatisfySpec.exchanges) };
bufferStream(req, passError(handleError, function (body) {
if (callbackCalled) {
return;
}
actualRequest.body = body;
function assertAndDeliverMockResponse() {

@@ -153,20 +452,25 @@ if (mockResponse) {

}
try {
expect(httpConversation, 'to satisfy', httpConversationSatisfySpec);
} catch (e) {
return handleError(e);
}
setImmediate(function () {
try {
that.errorMode = 'default';
expect(httpConversationSnapshot, 'to satisfy', httpConversationSatisfySpecSnapshot);
} catch (e) {
that.errorMode = 'nested';
return handleError(e);
}
that.errorMode = 'nested';
if (mockResponse) {
res.statusCode = mockResponse.statusCode;
mockResponse.headers.getNames().forEach(function (headerName) {
mockResponse.headers.getAll(headerName).forEach(function (value) {
res.setHeader(headerName, value);
if (mockResponse) {
res.statusCode = mockResponse.statusCode;
mockResponse.headers.getNames().forEach(function (headerName) {
mockResponse.headers.getAll(headerName).forEach(function (value) {
res.setHeader(headerName, value);
});
});
});
if (typeof mockResponse.body !== 'undefined') {
res.write(mockResponse.body);
if (typeof mockResponse.body !== 'undefined') {
res.write(mockResponse.body);
}
}
}
res.end();
res.end();
});
}

@@ -181,4 +485,4 @@

}
}).on('error', handleError);
});
}));
}));

@@ -185,0 +489,0 @@ this.args.push(function (err) {

{
"name": "unexpected-mitm",
"version": "1.4.1",
"version": "2.0.0",
"description": "Unexpected plugin for the mitm library",

@@ -22,7 +22,11 @@ "main": "lib/unexpectedMitm.js",

"dependencies": {
"callsite": "1.0.0",
"detect-indent": "3.0.0",
"memoizesync": "0.5.0",
"messy": "4.1.1",
"mitm-papandreou": "1.0.3-patch1",
"mocha": "2.1.0",
"passerror": "1.1.0",
"underscore": "1.7.0"
}
}

@@ -297,3 +297,3 @@ /*global describe, it, __dirname*/

it('should produce an error if the test issues more requests than has been mocked', function (done) {
it('should produce an error if the test issues more requests than have been mocked', function (done) {
expect('http://www.google.com/foo', 'with http mocked out', [], 'to yield response', 200, function (err) {

@@ -332,2 +332,22 @@ expect(err, 'to be an', Error);

});
it('should record', function (done) {
expect({
url: 'POST http://www.google.com/',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'foo=bar'
}, 'with http recorded', 'to yield response', 200, done);
});
it('should record some more', function (done) {
expect({
url: 'DELETE http://www.google.com/',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'foo=bar'
}, 'with http recorded', 'to yield response', 200, done);
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc