UNPKG

mitm-papandreou

Version:

Intercept and mock outgoing network TCP connections and HTTP requests for testing. Intercepts and gives you a Net.Socket, Http.IncomingMessage and Http.ServerResponse to test and respond with. Useful when testing code that hits remote servers.

783 lines (647 loc) 26.7 kB
var _ = require("underscore") var Sinon = require("sinon") var Net = require("net") var Tls = require("tls") var Http = require("http") var Https = require("https") var Semver = require("semver") var Transform = require("stream").Transform var IncomingMessage = Http.IncomingMessage var ServerResponse = Http.ServerResponse var ClientRequest = Http.ClientRequest var EventEmitter = require("events").EventEmitter var Mitm = require("..") var NODE_0_10 = Semver.satisfies(process.version, ">= 0.10 < 0.11") var newBuffer = Buffer.from || function(d, enc) { return new Buffer(d, enc) } describe("Mitm", function() { beforeEach(function() { Mitm.passthrough = false }) it("must return an instance of Mitm when called as a function", function() { var mitm = Mitm() mitm.must.be.an.instanceof(Mitm) mitm.disable() }) function mustConnect(module) { describe("as connect", function() { it("must return an instance of Net.Socket", function() { var socket = module.connect({host: "foo", port: 80}) socket.must.be.an.instanceof(Net.Socket) }) it("must return an instance of Net.Socket given port", function() { module.connect(80).must.be.an.instanceof(Net.Socket) }) it("must return an instance of Net.Socket given port and host", function() { module.connect(80, "10.0.0.1").must.be.an.instanceof(Net.Socket) }) it("must emit connect on Mitm", function() { var onConnect = Sinon.spy() this.mitm.on("connect", onConnect) var opts = {host: "foo"} var socket = module.connect(opts) onConnect.callCount.must.equal(1) onConnect.firstCall.args[0].must.equal(socket) onConnect.firstCall.args[1].must.equal(opts) }) it("must emit connect on Mitm with options object given host and port", function() { var onConnect = Sinon.spy() this.mitm.on("connect", onConnect) var socket = module.connect(9, "127.0.0.1") onConnect.callCount.must.equal(1) onConnect.firstCall.args[0].must.equal(socket) onConnect.firstCall.args[1].must.eql({host: "127.0.0.1", port: 9}) }) it("must emit connection on Mitm", function() { var onConnection = Sinon.spy() this.mitm.on("connection", onConnection) var opts = {host: "foo"} var socket = module.connect(opts) onConnection.callCount.must.equal(1) onConnection.firstCall.args[0].must.be.an.instanceof(Net.Socket) onConnection.firstCall.args[0].must.not.equal(socket) onConnection.firstCall.args[1].must.equal(opts) }) it("must emit connect on socket in next ticks", function(done) { var socket = module.connect({host: "foo"}) socket.on("connect", done.bind(null, null)) }) it("must call back on connect given callback", function(done) { module.connect({host: "foo"}, done.bind(null, null)) }) it("must call back on connect given port and callback", function(done) { module.connect(80, done.bind(null, null)) }) // This was a bug found on Apr 26, 2014 where the host argument was taken // to be the callback because arguments weren't normalized to an options // object. it("must call back on connect given port, host and callback", function(done) { module.connect(80, "localhost", done.bind(null, null)) }) // The "close" event broke on Node v12.16.3 as the // InternalSocket.prototype.close method didn't call back if // the WritableStream had already been closed. it("must emit close on socket if ended immediately", function(done) { this.mitm.on("connection", function(socket) { socket.end() }) var socket = module.connect({host: "foo"}) socket.on("close", done.bind(null, null)) }) it("must emit close on socket if ended in next tick", function(done) { this.mitm.on("connection", function(socket) { process.nextTick(socket.end.bind(socket)) }) var socket = module.connect({host: "foo"}) socket.on("close", done.bind(null, null)) }) it("must intercept 127.0.0.1", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = module.connect({host: "127.0.0.1"}) server.write("Hello") client.setEncoding("utf8") client.on("data", function(data) { data.must.equal("Hello") }) client.on("data", done.bind(null, null)) }) describe("when bypassed", function() { beforeEach(function() { this.sinon = Sinon.sandbox.create() }) afterEach(function() { this.sinon.restore() }) it("must not intercept", function(done) { this.mitm.on("connect", function(client) { client.bypass() }) module.connect({host: "127.0.0.1", port: 9}).on("error", function(err) { err.must.be.an.instanceof(Error) err.message.must.include("ECONNREFUSED") done() }) }) it("must call original module.connect", function() { this.mitm.disable() var connect = this.sinon.spy(module, "connect") var mitm = Mitm() mitm.on("connect", function(client) { client.bypass() }) try { module.connect({host: "127.0.0.1", port: 9}).on("error", noop) connect.callCount.must.equal(1) connect.firstCall.args[0].must.eql({host: "127.0.0.1", port: 9}) } // Working around Mocha's context bug(s) and poor design decision // with a manual `finally`. finally { mitm.disable() } }) it("must not call back twice on connect given callback", function(done) { this.mitm.on("connect", function(client) { client.bypass() }) var onConnect = Sinon.spy() var client = module.connect({host: "127.0.0.1", port: 9}, onConnect) client.on("error", process.nextTick.bind(null, function() { onConnect.callCount.must.equal(0) done() })) }) it("must not emit connection", function() { this.mitm.on("connect", function(client) { client.bypass() }) var onConnection = Sinon.spy() this.mitm.on("connection", onConnection) module.connect({host: "127.0.0.1", port: 9}).on("error", noop) onConnection.callCount.must.equal(0) }) }) }) } describe("Net.connect", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) mustConnect(Net) if (!NODE_0_10) it("must not return an instance of Tls.TLSSocket", function() { var client = Net.connect({host: "foo", port: 80}) client.must.not.be.an.instanceof(Tls.TLSSocket) }) it("must not set the encrypted property", function() { Net.connect({host: "foo"}).must.not.have.property("encrypted") }) it("must not set the authorized property", function() { Net.connect({host: "foo"}).must.not.have.property("authorized") }) it("must not emit secureConnect on client", function(done) { var client = Net.connect({host: "foo"}) // Let Mocha raise an error when done called twice. client.on("secureConnect", done.bind(null, null)) done() }) it("must not emit secureConnect on server", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) Net.connect({host: "foo"}) // Let Mocha raise an error when done called twice. server.on("secureConnect", done.bind(null, null)) done() }) describe("Socket", function() { describe(".prototype.write", function() { it("must write to client from server", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) server.write("Hello ☺️") client.setEncoding("utf8") client.on("data", function(data) { data.must.equal("Hello ☺️") }) client.on("data", done.bind(null, null)) }) it("must write to client from server in the next tick", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) var ticked = false client.once("data", function() { ticked.must.be.true(); done() }) server.write("Hello") ticked = true }) it("must write to server from client", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write("Hello ☺️") server.setEncoding("utf8") process.nextTick(function() { server.read().must.equal("Hello ☺️") }) process.nextTick(done) }) it("must write to server from client in the next tick", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) var ticked = false server.once("data", function() { ticked.must.be.true(); done() }) client.write("Hello") ticked = true }) // Writing binary strings was introduced in Node v0.11.14. // The test still passes for Node v0.10 and newer v0.11s, so let it be. it("must write to server from client given binary", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write("Hello", "binary") server.setEncoding("binary") process.nextTick(function() { server.read().must.equal("Hello") }) process.nextTick(done) }) // Writing latin1 strings was introduced in v6.4. // https://github.com/nodejs/node/commit/28071a130e2137bd14d0762a25f0ad83b7a28259 if (Semver.satisfies(process.version, ">= 6.4")) it("must write to server from client given latin1", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write("Hello", "latin1") server.setEncoding("latin1") process.nextTick(function() { server.read().must.equal("Hello") }) process.nextTick(done) }) it("must write to server from client given a buffer", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write(newBuffer("Hello", "binary")) process.nextTick(function() { assertBuffers(server.read(), newBuffer("Hello", "binary")) done() }) }) it("must write to server from client given a UTF-8 string", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write("Hello", "utf8") process.nextTick(function() { assertBuffers(server.read(), newBuffer("Hello", "binary")) done() }) }) it("must write to server from client given a ASCII string", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write("Hello", "ascii") process.nextTick(function() { assertBuffers(server.read(), newBuffer("Hello", "binary")) done() }) }) it("must write to server from client given a UCS-2 string", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) client.write("Hello", "ucs2") process.nextTick(function() { assertBuffers( server.read(), newBuffer("H\u0000e\u0000l\u0000l\u0000o\u0000", "binary") ) done() }) }) }) describe(".prototype.end", function() { it("must emit end when closed on server", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) server.end() client.on("end", done) }) }) describe(".prototype.ref", function() { it("must allow calling on client", function() { Net.connect({host: "foo"}).ref() }) it("must allow calling on server", function() { var server; this.mitm.on("connection", function(s) { server = s }) Net.connect({host: "foo"}) server.ref() }) }) describe(".prototype.unref", function() { it("must allow calling on client", function() { Net.connect({host: "foo"}).unref() }) it("must allow calling on server", function() { var server; this.mitm.on("connection", function(s) { server = s }) Net.connect({host: "foo"}) server.unref() }) }) describe(".prototype.pipe", function() { // To confirm https://github.com/moll/node-mitm/issues/47 won't become // an issue. it("must allow piping to itself", function(done) { this.mitm.on("connection", function(server) { server.pipe(new Upcase).pipe(server) }) var client = Net.connect({host: "foo"}) client.write("Hello") client.setEncoding("utf8") client.on("data", function(data) { data.must.equal("HELLO") }) client.on("data", done.bind(null, null)) }) }) // Bug report from Io.js v3 days: // https://github.com/moll/node-mitm/issues/26 describe(".prototype.destroy", function() { it("must emit end when destroyed on server", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) var client = Net.connect({host: "foo"}) server.destroy() client.on("end", done) }) }) }) }) describe("Net.createConnection", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) it("must be equal to Net.connect", function() { Net.createConnection.must.equal(Net.connect) }) }) describe("Tls.connect", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) mustConnect(Tls) if (!NODE_0_10) it("must return an instance of Tls.TLSSocket", function() { Tls.connect({host: "foo", port: 80}).must.be.an.instanceof(Tls.TLSSocket) }) if (!NODE_0_10) it("must return an instance of Tls.TLSSocket given port", function() { Tls.connect(80).must.be.an.instanceof(Tls.TLSSocket) }) if (!NODE_0_10) it("must return an instance of Tls.TLSSocket given port and host", function() { Tls.connect(80, "10.0.0.1").must.be.an.instanceof(Tls.TLSSocket) }) it("must emit secureConnect in next ticks", function(done) { var socket = Tls.connect({host: "foo"}) socket.on("secureConnect", done.bind(null, null)) }) it("must emit secureConnect after connect in next ticks", function(done) { var socket = Tls.connect({host: "foo"}) socket.on("connect", function() { socket.on("secureConnect", done.bind(null, null)) }) }) it("must not emit secureConnect on server", function(done) { var server; this.mitm.on("connection", function(s) { server = s }) Tls.connect({host: "foo"}) // Let Mocha raise an error when done called twice. server.on("secureConnect", done.bind(null, null)) done() }) it("must call back on secureConnect", function(done) { var connected = false var client = Tls.connect({host: "foo"}, function() { connected.must.be.true() done() }) client.on("connect", function() { connected = true }) }) it("must set encrypted true", function() { Tls.connect({host: "foo"}).encrypted.must.be.true() }) it("must set authorized true", function() { Tls.connect({host: "foo"}).authorized.must.be.true() }) }) function mustRequest(request) { describe("as request", function() { it("must return ClientRequest", function() { request({host: "foo"}).must.be.an.instanceof(ClientRequest) }) it("must emit connect on Mitm", function() { var onConnect = Sinon.spy() this.mitm.on("connect", onConnect) request({host: "foo"}) onConnect.callCount.must.equal(1) }) it("must emit connect on Mitm after multiple connections", function() { var onConnect = Sinon.spy() this.mitm.on("connect", onConnect) request({host: "foo"}) request({host: "foo"}) request({host: "foo"}) onConnect.callCount.must.equal(3) }) it("must emit connection on Mitm", function() { var onConnection = Sinon.spy() this.mitm.on("connection", onConnection) request({host: "foo"}) onConnection.callCount.must.equal(1) }) it("must emit connection on Mitm after multiple connections", function() { var onConnection = Sinon.spy() this.mitm.on("connection", onConnection) request({host: "foo"}) request({host: "foo"}) request({host: "foo"}) onConnection.callCount.must.equal(3) }) it("must emit request on Mitm", function(done) { var client = request({host: "foo"}) client.end() this.mitm.on("request", function(req, res) { req.must.be.an.instanceof(IncomingMessage) req.must.not.equal(client) res.must.be.an.instanceof(ServerResponse) done() }) }) it("must provide the client socket and options via _mitm", function(done) { var clientSocket; var connectOpts; this.mitm.on("connect", function (socket, opts) { clientSocket = socket connectOpts = opts clientSocket.must.be.an.instanceof(Object) connectOpts.host.must.equal("foo") }).on("request", function(req, res) { req.connection._mitm.must.be.an.instanceof(Object) req.connection._mitm.opts.must.be(connectOpts) req.connection._mitm.client.must.be(clientSocket) done() }) var client = request({host: "foo"}) client.end() }) it("must emit request on Mitm after multiple requests", function(done) { request({host: "foo"}).end() request({host: "foo"}).end() request({host: "foo"}).end() this.mitm.on("request", _.after(3, done.bind(null, null))) }) it("must emit socket on request in next ticks", function(done) { var client = request({host: "foo"}) client.on("socket", done.bind(null, null)) }) // https://github.com/moll/node-mitm/pull/25 it("must emit connect after socket event", function(done) { var client = request({host: "foo"}) client.on("socket", function(socket) { socket.on("connect", done.bind(null, null)) }) }) describe("when bypassed", function() { it("must not intercept", function(done) { this.mitm.on("connect", function(client) { client.bypass() }) request({host: "127.0.0.1"}).on("error", function(err) { err.must.be.an.instanceof(Error) err.message.must.include("ECONNREFUSED") done() }) }) it("must not emit request", function(done) { this.mitm.on("connect", function(client) { client.bypass() }) var onRequest = Sinon.spy() this.mitm.on("request", onRequest) request({host: "127.0.0.1"}).on("error", function(_err) { onRequest.callCount.must.equal(0) done() }) }) }) }) } describe("via Http.request", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) mustRequest(Http.request) }) describe("via Https.request", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) mustRequest(Https.request) // https://github.com/moll/node-mitm/pull/25 it("must emit secureConnect after socket event", function(done) { var client = Https.request({host: "foo"}) client.on("socket", function(socket) { socket.on("secureConnect", done.bind(null, null)) }) }) }) describe("via Http.Agent", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) mustRequest(function(opts) { return Http.request(_.extend({agent: new Http.Agent}, opts)) }) it("must support keep-alive", function(done) { var client = Http.request({ host: "foo", agent: new Http.Agent({keepAlive: true}) }) client.end() this.mitm.on("request", function(_req, res) { res.setHeader("Connection", "keep-alive") res.end() }) // Just waiting for response is too early to trigger: // TypeError: socket._handle.getAsyncId is not a function in _http_client. client.on("response", function(res) { res.on("data", noop) res.on("end", done) }) }) }) describe("via Https.Agent", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) mustRequest(function(opts) { return Https.request(_.extend({agent: new Https.Agent}, opts)) }) }) describe("IncomingMessage", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) it("must have URL", function(done) { Http.request({host: "foo", path: "/foo"}).end() this.mitm.on("request", function(req) { req.url.must.equal("/foo") done() }) }) it("must have headers", function(done) { var req = Http.request({host: "foo"}) req.setHeader("Content-Type", "application/json") req.end() this.mitm.on("request", function(req) { req.headers["content-type"].must.equal("application/json") done() }) }) it("must have body", function(done) { var client = Http.request({host: "foo", method: "POST"}) client.write("Hello") this.mitm.on("request", function(req, _res) { req.setEncoding("utf8") req.on("data", function(data) { data.must.equal("Hello"); done() }) }) }) it("must have a reference to the ServerResponse", function(done) { Http.request({host: "foo", method: "POST"}).end() this.mitm.on("request", function(req, res) { req.res.must.equal(res) }) this.mitm.on("request", done.bind(null, null)) }) }) describe("ServerResponse", function() { beforeEach(function() { this.mitm = Mitm() }) afterEach(function() { this.mitm.disable() }) it("must respond with status, headers and body", function(done) { this.mitm.on("request", function(_req, res) { res.statusCode = 442 res.setHeader("Content-Type", "application/json") res.end("Hi!") }) Http.request({host: "foo"}).on("response", function(res) { res.statusCode.must.equal(442) res.headers["content-type"].must.equal("application/json") res.setEncoding("utf8") res.once("data", function(data) { data.must.equal("Hi!"); done() }) }).end() }) it("must have a reference to the IncomingMessage", function(done) { Http.request({host: "foo", method: "POST"}).end() this.mitm.on("request", function(req, res) { res.req.must.equal(req) }) this.mitm.on("request", done.bind(null, null)) }) describe(".prototype.write", function() { it("must make clientRequest emit response", function(done) { var req = Http.request({host: "foo"}) req.end() this.mitm.on("request", function(_req, res) { res.write("Test") }) req.on("response", done.bind(null, null)) }) // Under Node v0.10 it's the writeQueueSize that's checked to see if // the callback can be called. it("must call given callback", function(done) { Http.request({host: "foo"}).end() this.mitm.on("request", function(_req, res) { res.write("Test", done) }) }) }) describe(".prototype.end", function() { it("must make ClientRequest emit response", function(done) { var client = Http.request({host: "foo"}) client.end() this.mitm.on("request", function(_req, res) { res.end() }) client.on("response", done.bind(null, null)) }) // In an app of mine Node v0.11.7 did not emit the end event, but // v0.11.11 did. I'll investigate properly if this becomes a problem in // later Node versions. it("must make IncomingMessage emit end", function(done) { var client = Http.request({host: "foo"}) client.end() this.mitm.on("request", function(_req, res) { res.end() }) client.on("response", function(res) { res.on("data", noop) res.on("end", done) }) }) }) }) _.each({ on: EventEmitter.prototype.on, once: EventEmitter.prototype.once, off: EventEmitter.prototype.removeListener, addListener: EventEmitter.prototype.addListener, removeListener: EventEmitter.prototype.removeListener, emit: EventEmitter.prototype.emit }, function(to, from) { describe(".prototype." + from, function() { it("must be an alias to EventEmitter.prototype", function() { Mitm.prototype.must.have.property(from, to) Mitm.prototype[from].must.be.a.function() }) }) }) }) function Upcase() { Transform.call(this, arguments) } Upcase.prototype = Object.create(Transform.prototype, { constructor: {value: Upcase, configurable: true, writeable: true} }) Upcase.prototype._transform = function(chunk, _enc, done) { done(null, String(chunk).toUpperCase()) } function assertBuffers(a, b) { if (a.equals) a.equals(b).must.be.true() else a.toString("utf8").must.equal(b.toString("utf8")) } function noop() {}