UNPKG

http-mitm-proxy

Version:
1,112 lines (1,032 loc) 36.7 kB
'use strict'; var async = require('async'); var net = require('net'); var http = require('http'); var https = require('https'); var util = require('util'); var fs = require('fs'); var path = require('path'); var events = require('events'); var WebSocket = require('ws'); var url = require('url'); var semaphore = require('semaphore'); var ca = require('./ca.js'); const nodeCommon = require('_http_common'); const debug = require('debug')('http-mitm-proxy'); module.exports = function() { return new Proxy(); }; module.exports.gunzip = require('./middleware/gunzip'); module.exports.wildcard = require('./middleware/wildcard'); var Proxy = function() { this.onConnectHandlers = []; this.onRequestHandlers = []; this.onRequestHeadersHandlers = []; this.onWebSocketConnectionHandlers = []; this.onWebSocketFrameHandlers = []; this.onWebSocketCloseHandlers = []; this.onWebSocketErrorHandlers = []; this.onErrorHandlers = []; this.onRequestDataHandlers = []; this.onRequestEndHandlers = []; this.onResponseHandlers = []; this.onResponseHeadersHandlers = []; this.onResponseDataHandlers = []; this.onResponseEndHandlers = []; }; module.exports.Proxy = Proxy; Proxy.prototype.listen = function(options, callback) { var self = this; this.options = options || {}; this.httpPort = options.port || 8080; this.httpHost = options.host; this.timeout = options.timeout || 0; this.keepAlive = !!options.keepAlive; this.httpAgent = typeof(options.httpAgent) !== "undefined" ? options.httpAgent : new http.Agent({ keepAlive: this.keepAlive }); this.httpsAgent = typeof(options.httpsAgent) !== "undefined" ? options.httpsAgent : new https.Agent({ keepAlive: this.keepAlive }); this.forceSNI = !!options.forceSNI; if (this.forceSNI) { debug('SNI enabled. Clients not supporting SNI may fail'); } this.httpsPort = this.forceSNI ? options.httpsPort : undefined; this.sslCaDir = options.sslCaDir || path.resolve(process.cwd(), '.http-mitm-proxy'); ca.create(this.sslCaDir, function(err, ca) { if (err) { return callback(err); } self.ca = ca; self.sslServers = {}; self.sslSemaphores = {}; self.connectRequests = {}; self.httpServer = http.createServer(); self.httpServer.timeout = self.timeout; self.httpServer.on('connect', self._onHttpServerConnect.bind(self)); self.httpServer.on('request', self._onHttpServerRequest.bind(self, false)); self.wsServer = new WebSocket.Server({ server: self.httpServer }); self.wsServer.on('error', self._onError.bind(self, 'HTTP_SERVER_ERROR', null)); self.wsServer.on('connection', (ws, req) => { ws.upgradeReq = req; self._onWebSocketServerConnect.call(self, false, ws, req); }); const listenOptions = { host: self.httpHost, port: self.httpPort }; if (self.forceSNI) { // start the single HTTPS server now self._createHttpsServer({}, function(port, httpsServer, wssServer) { debug('https server started on '+port); self.httpsServer = httpsServer; self.wssServer = wssServer; self.httpsPort = port; self.httpServer.listen(listenOptions, callback); }); } else { self.httpServer.listen(listenOptions, callback); } }); return this; }; Proxy.prototype._createHttpsServer = function (options, callback) { var httpsServer = https.createServer(options); httpsServer.timeout = this.timeout; httpsServer.on('error', this._onError.bind(this, 'HTTPS_SERVER_ERROR', null)); httpsServer.on('clientError', this._onError.bind(this, 'HTTPS_CLIENT_ERROR', null)); httpsServer.on('connect', this._onHttpServerConnect.bind(this)); httpsServer.on('request', this._onHttpServerRequest.bind(this, true)); var self = this; var wssServer = new WebSocket.Server({ server: httpsServer }); wssServer.on('connection', function(ws, req){ ws.upgradeReq = req; self._onWebSocketServerConnect.call(self, true, ws, req) }); var listenArgs = [function() { if (callback) callback(httpsServer.address().port, httpsServer, wssServer); }]; // Using listenOptions to bind the server to a particular IP if requested via options.host // port 0 to get the first available port var listenOptions = { port: 0 }; if (this.httpsPort && !options.hosts) { listenOptions.port = this.httpsPort; } if (this.httpHost) listenOptions.host = this.httpHost; listenArgs.unshift(listenOptions); httpsServer.listen.apply(httpsServer, listenArgs); }; Proxy.prototype.close = function () { var self = this; this.httpServer.close(); delete this.httpServer; if (this.httpsServer) { this.httpsServer.close(); delete this.httpsServer; delete this.wssServer; delete this.sslServers; } if (this.sslServers) { (Object.keys(this.sslServers)).forEach(function (srvName) { var server = self.sslServers[srvName].server; if (server) server.close(); delete self.sslServers[srvName]; }); } return this; }; Proxy.prototype.onError = function(fn) { this.onErrorHandlers.push(fn); return this; }; /** * Add custom handler for CONNECT method * @augments fn(req,socket,head,callback) be called on receiving CONNECT method */ Proxy.prototype.onConnect = function(fn) { this.onConnectHandlers.push(fn); return this; }; Proxy.prototype.onRequestHeaders = function(fn) { this.onRequestHeadersHandlers.push(fn); return this; }; Proxy.prototype.onRequest = function(fn) { this.onRequestHandlers.push(fn); return this; }; Proxy.prototype.onWebSocketConnection = function(fn) { this.onWebSocketConnectionHandlers.push(fn); return this; }; Proxy.prototype.onWebSocketSend = function(fn) { this.onWebSocketFrameHandlers.push(function(ctx, type, fromServer, data, flags, callback) { if (!fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(fn)); return this; }; Proxy.prototype.onWebSocketMessage = function(fn) { this.onWebSocketFrameHandlers.push(function(ctx, type, fromServer, data, flags, callback) { if (fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(fn)); return this; }; Proxy.prototype.onWebSocketFrame = function(fn) { this.onWebSocketFrameHandlers.push(fn); return this; }; Proxy.prototype.onWebSocketClose = function(fn) { this.onWebSocketCloseHandlers.push(fn); return this; }; Proxy.prototype.onWebSocketError = function(fn) { this.onWebSocketErrorHandlers.push(fn); return this; }; Proxy.prototype.onRequestData = function(fn) { this.onRequestDataHandlers.push(fn); return this; }; Proxy.prototype.onRequestEnd = function(fn) { this.onRequestEndHandlers.push(fn); return this; }; Proxy.prototype.onResponse = function(fn) { this.onResponseHandlers.push(fn); return this; }; Proxy.prototype.onResponseHeaders = function(fn) { this.onResponseHeadersHandlers.push(fn); return this; }; Proxy.prototype.onResponseData = function(fn) { this.onResponseDataHandlers.push(fn); return this; }; Proxy.prototype.onResponseEnd = function(fn) { this.onResponseEndHandlers.push(fn); return this; }; Proxy.prototype.use = function(mod) { if (mod.onError) { this.onError(mod.onError); } if (mod.onCertificateRequired) { this.onCertificateRequired = mod.onCertificateRequired; } if (mod.onCertificateMissing) { this.onCertificateMissing = mod.onCertificateMissing; } if (mod.onConnect) { this.onConnect(mod.onConnect); } if (mod.onRequest) { this.onRequest(mod.onRequest); } if (mod.onRequestHeaders) { this.onRequestHeaders(mod.onRequestHeaders); } if (mod.onRequestData) { this.onRequestData(mod.onRequestData); } if (mod.onResponse) { this.onResponse(mod.onResponse); } if (mod.onResponseHeaders) { this.onResponseHeaders(mod.onResponseHeaders); } if (mod.onResponseData) { this.onResponseData(mod.onResponseData); } if (mod.onWebSocketConnection) { this.onWebSocketConnection(mod.onWebSocketConnection); } if (mod.onWebSocketSend) { this.onWebSocketFrame(function(ctx, type, fromServer, data, flags, callback) { if (!fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(mod.onWebSocketSend)); } if (mod.onWebSocketMessage) { this.onWebSocketFrame(function(ctx, type, fromServer, data, flags, callback) { if (fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(mod.onWebSocketMessage)); } if (mod.onWebSocketFrame) { this.onWebSocketFrame(mod.onWebSocketFrame); } if (mod.onWebSocketClose) { this.onWebSocketClose(mod.onWebSocketClose); } if (mod.onWebSocketError) { this.onWebSocketError(mod.onWebSocketError); } return this; }; Proxy.prototype._onHttpServerConnect = function(req, socket, head) { var self = this; // you can forward HTTPS request directly by adding custom CONNECT method handler return async.forEach(self.onConnectHandlers, function (fn, callback) { return fn.call(self, req, socket, head, callback) }, function (err) { if (err) { return self._onError('ON_CONNECT_ERROR', null, err); } // we need first byte of data to detect if request is SSL encrypted if (!head || head.length === 0) { socket.once('data', self._onHttpServerConnectData.bind(self, req, socket)); socket.write('HTTP/1.1 200 OK\r\n'); if (self.keepAlive && req.headers['proxy-connection'] === 'keep-alive') { socket.write('Proxy-Connection: keep-alive\r\n'); socket.write('Connection: keep-alive\r\n'); } return socket.write('\r\n'); } else { self._onHttpServerConnectData(req, socket, head) } }) }; Proxy.prototype._onHttpServerConnectData = function(req, socket, head) { var self = this; socket.pause(); /* * Detect TLS from first bytes of data * Inspired from https://gist.github.com/tg-x/835636 * used heuristic: * - an incoming connection using SSLv3/TLSv1 records should start with 0x16 * - an incoming connection using SSLv2 records should start with the record size * and as the first record should not be very big we can expect 0x80 or 0x00 (the MSB is a flag) * - everything else is considered to be unencrypted */ if (head[0] == 0x16 || head[0] == 0x80 || head[0] == 0x00) { // URL is in the form 'hostname:port' var hostname = req.url.split(':', 2)[0]; var sslServer = this.sslServers[hostname]; if (sslServer) { return makeConnection(sslServer.port); } var wildcardHost = hostname.replace(/[^\.]+\./, '*.'); var sem = self.sslSemaphores[wildcardHost]; if (!sem) { sem = self.sslSemaphores[wildcardHost] = semaphore(1); } sem.take(function() { if (self.sslServers[hostname]) { process.nextTick(sem.leave.bind(sem)); return makeConnection(self.sslServers[hostname].port); } if (self.sslServers[wildcardHost]) { process.nextTick(sem.leave.bind(sem)); self.sslServers[hostname] = { port: self.sslServers[wildcardHost] }; return makeConnection(self.sslServers[hostname].port); } getHttpsServer(hostname, function(err, port) { process.nextTick(sem.leave.bind(sem)); if (err) { return self._onError('OPEN_HTTPS_SERVER_ERROR', null, err); } return makeConnection(port); }); }); } else { return makeConnection(this.httpPort); } function makeConnection(port) { // open a TCP connection to the remote host var conn = net.connect({ port: port, allowHalfOpen: true }, function() { // create a tunnel between the two hosts conn.on('finish', () => { socket.destroy(); }); var connectKey = conn.localPort + ':' + conn.remotePort; self.connectRequests[connectKey] = req; socket.pipe(conn); conn.pipe(socket); socket.emit('data', head); conn.on('end', function() { delete self.connectRequests[connectKey]; }); return socket.resume(); }); conn.on('error', function(err) { filterSocketConnReset(err, 'PROXY_TO_PROXY_SOCKET'); }); socket.on('error', function(err) { filterSocketConnReset(err, 'CLIENT_TO_PROXY_SOCKET'); }); } // Since node 0.9.9, ECONNRESET on sockets are no longer hidden function filterSocketConnReset(err, socketDescription) { if (err.errno === 'ECONNRESET') { debug('Got ECONNRESET on ' + socketDescription + ', ignoring.'); } else { self._onError(socketDescription + '_ERROR', null, err); } } function getHttpsServer(hostname, callback) { self.onCertificateRequired(hostname, function (err, files) { if (err) { return callback(err); } async.auto({ 'keyFileExists': function(callback) { return fs.exists(files.keyFile, function(exists) { return callback(null, exists); }); }, 'certFileExists': function(callback) { return fs.exists(files.certFile, function(exists) { return callback(null, exists); }); }, 'httpsOptions': ['keyFileExists', 'certFileExists', function(data, callback) { if (data.keyFileExists && data.certFileExists) { return fs.readFile(files.keyFile, function(err, keyFileData) { if (err) { return callback(err); } return fs.readFile(files.certFile, function(err, certFileData) { if (err) { return callback(err); } return callback(null, { key: keyFileData, cert: certFileData, hosts: files.hosts }); }); }); } else { var ctx = { 'hostname': hostname, 'files': files, 'data': data }; return self.onCertificateMissing(ctx, files, function(err, files) { if (err) { return callback(err); } return callback(null, { key: files.keyFileData, cert: files.certFileData, hosts: files.hosts }); }); } }] }, function(err, results) { if (err) { return callback(err); } var hosts; if (results.httpsOptions && results.httpsOptions.hosts && results.httpsOptions.hosts.length) { hosts = results.httpsOptions.hosts; if (hosts.indexOf(hostname) === -1) { hosts.push(hostname); } } else { hosts = [hostname]; } delete results.httpsOptions.hosts; if (self.forceSNI && !hostname.match(/^[\d\.]+$/)) { debug('creating SNI context for ' + hostname); hosts.forEach(function(host) { self.httpsServer.addContext(host, results.httpsOptions); self.sslServers[host] = { port : self.httpsPort }; }); return callback(null, self.httpsPort); } else { debug('starting server for ' + hostname); results.httpsOptions.hosts = hosts; self._createHttpsServer(results.httpsOptions, function(port, httpsServer, wssServer) { debug('https server started for %s on %s', hostname, port); var sslServer = { server: httpsServer, wsServer: wssServer, port: port }; hosts.forEach(function(host) { self.sslServers[hostname] = sslServer; }); return callback(null, port); }); } }); }); } }; Proxy.prototype.onCertificateRequired = function (hostname, callback) { var self = this; return callback(null, { keyFile: self.sslCaDir + '/keys/' + hostname + '.key', certFile: self.sslCaDir + '/certs/' + hostname + '.pem', hosts: [hostname] }); }; Proxy.prototype.onCertificateMissing = function (ctx, files, callback) { var hosts = files.hosts || [ctx.hostname]; this.ca.generateServerCertificateKeys(hosts, function (certPEM, privateKeyPEM) { callback(null, { certFileData: certPEM, keyFileData: privateKeyPEM, hosts: hosts }); }); return this; }; Proxy.prototype._onError = function(kind, ctx, err) { this.onErrorHandlers.forEach(function(handler) { return handler(ctx, err, kind); }); if (ctx) { ctx.onErrorHandlers.forEach(function(handler) { return handler(ctx, err, kind); }); if (ctx.proxyToClientResponse && !ctx.proxyToClientResponse.headersSent) { ctx.proxyToClientResponse.writeHead(504, 'Proxy Error'); } if (ctx.proxyToClientResponse && !ctx.proxyToClientResponse.finished) { ctx.proxyToClientResponse.end(''+kind+': '+err, 'utf8'); } } }; Proxy.prototype._onWebSocketServerConnect = function(isSSL, ws, upgradeReq) { var self = this; var ctx = { isSSL: isSSL, connectRequest: self.connectRequests[ws._socket.remotePort + ':' + ws._socket.localPort] || {}, clientToProxyWebSocket: ws, onWebSocketConnectionHandlers: [], onWebSocketFrameHandlers: [], onWebSocketCloseHandlers: [], onWebSocketErrorHandlers: [], onWebSocketConnection: function(fn) { ctx.onWebSocketConnectionHandlers.push(fn); return ctx; }, onWebSocketSend: function(fn) { ctx.onWebSocketFrameHandlers.push(function(ctx, type, fromServer, data, flags, callback) { if (!fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(fn)); return ctx; }, onWebSocketMessage: function(fn) { ctx.onWebSocketFrameHandlers.push(function(ctx, type, fromServer, data, flags, callback) { if (fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(fn)); return ctx; }, onWebSocketFrame: function(fn) { ctx.onWebSocketFrameHandlers.push(fn); return ctx; }, onWebSocketClose: function(fn) { ctx.onWebSocketCloseHandlers.push(fn); return ctx; }, onWebSocketError: function(fn) { ctx.onWebSocketErrorHandlers.push(fn); return ctx; }, use: function(mod) { if (mod.onWebSocketConnection) { ctx.onWebSocketConnection(mod.onWebSocketConnection); } if (mod.onWebSocketSend) { ctx.onWebSocketFrame(function(ctx, type, fromServer, data, flags, callback) { if (!fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(mod.onWebSocketSend)); } if (mod.onWebSocketMessage) { ctx.onWebSocketFrame(function(ctx, type, fromServer, data, flags, callback) { if (fromServer && type === 'message') return this(ctx, data, flags, callback); else callback(null, data, flags); }.bind(mod.onWebSocketMessage)); } if (mod.onWebSocketFrame) { ctx.onWebSocketFrame(mod.onWebSocketFrame); } if (mod.onWebSocketClose) { ctx.onWebSocketClose(mod.onWebSocketClose); } if (mod.onWebSocketError) { ctx.onWebSocketError(mod.onWebSocketError); } return ctx; } }; ctx.clientToProxyWebSocket.on('message', self._onWebSocketFrame.bind(self, ctx, 'message', false)); ctx.clientToProxyWebSocket.on('ping', self._onWebSocketFrame.bind(self, ctx, 'ping', false)); ctx.clientToProxyWebSocket.on('pong', self._onWebSocketFrame.bind(self, ctx, 'pong', false)); ctx.clientToProxyWebSocket.on('error', self._onWebSocketError.bind(self, ctx)); ctx.clientToProxyWebSocket.on('close', self._onWebSocketClose.bind(self, ctx, false)); ctx.clientToProxyWebSocket.pause(); var url; if (upgradeReq.url == '' || /^\//.test(upgradeReq.url)) { var hostPort = Proxy.parseHostAndPort(upgradeReq); url = (ctx.isSSL ? 'wss' : 'ws') + '://' + hostPort.host + (hostPort.port ? ':' + hostPort.port : '') + upgradeReq.url; } else { url = upgradeReq.url; } var ptosHeaders = {}; var ctopHeaders = upgradeReq.headers; for (var key in ctopHeaders) { if (key.indexOf('sec-websocket') !== 0) { ptosHeaders[key] = ctopHeaders[key]; } } ctx.proxyToServerWebSocketOptions = { url: url, agent: ctx.isSSL ? self.httpsAgent : self.httpAgent, headers: ptosHeaders }; return self._onWebSocketConnection(ctx, function(err) { if (err) { return self._onWebSocketError(ctx, err); } return makeProxyToServerWebSocket(); }); function makeProxyToServerWebSocket() { ctx.proxyToServerWebSocket = new WebSocket(ctx.proxyToServerWebSocketOptions.url, ctx.proxyToServerWebSocketOptions); ctx.proxyToServerWebSocket.on('message', self._onWebSocketFrame.bind(self, ctx, 'message', true)); ctx.proxyToServerWebSocket.on('ping', self._onWebSocketFrame.bind(self, ctx, 'ping', true)); ctx.proxyToServerWebSocket.on('pong', self._onWebSocketFrame.bind(self, ctx, 'pong', true)); ctx.proxyToServerWebSocket.on('error', self._onWebSocketError.bind(self, ctx)); ctx.proxyToServerWebSocket.on('close', self._onWebSocketClose.bind(self, ctx, true)); ctx.proxyToServerWebSocket.on('open', function() { if (ctx.clientToProxyWebSocket.readyState === WebSocket.OPEN) { ctx.clientToProxyWebSocket.resume(); } }); } }; Proxy.prototype._onHttpServerRequest = function(isSSL, clientToProxyRequest, proxyToClientResponse) { var self = this; var ctx = { isSSL: isSSL, connectRequest: self.connectRequests[clientToProxyRequest.socket.remotePort + ':' + clientToProxyRequest.socket.localPort] || {}, clientToProxyRequest: clientToProxyRequest, proxyToClientResponse: proxyToClientResponse, onRequestHandlers: [], onErrorHandlers: [], onRequestDataHandlers: [], onRequestEndHandlers: [], onResponseHandlers: [], onResponseDataHandlers: [], onResponseEndHandlers: [], requestFilters: [], responseFilters: [], onRequest: function(fn) { ctx.onRequestHandlers.push(fn); return ctx; }, onError: function(fn) { ctx.onErrorHandlers.push(fn); return ctx; }, onRequestData: function(fn) { ctx.onRequestDataHandlers.push(fn); return ctx; }, onRequestEnd: function(fn) { ctx.onRequestEndHandlers.push(fn); return ctx; }, addRequestFilter: function(filter) { ctx.requestFilters.push(filter); return ctx; }, onResponse: function(fn) { ctx.onResponseHandlers.push(fn); return ctx; }, onResponseData: function(fn) { ctx.onResponseDataHandlers.push(fn); return ctx; }, onResponseEnd: function(fn) { ctx.onResponseEndHandlers.push(fn); return ctx; }, addResponseFilter: function(filter) { ctx.responseFilters.push(filter); return ctx; }, use: function(mod) { if (mod.onError) { ctx.onError(mod.onError); } if (mod.onRequest) { ctx.onRequest(mod.onRequest); } if (mod.onRequestHeaders) { ctx.onRequestHeaders(mod.onRequestHeaders); } if (mod.onRequestData) { ctx.onRequestData(mod.onRequestData); } if (mod.onResponse) { ctx.onResponse(mod.onResponse); } if (mod.onResponseData) { ctx.onResponseData(mod.onResponseData); } return ctx; } }; ctx.clientToProxyRequest.on('error', self._onError.bind(self, 'CLIENT_TO_PROXY_REQUEST_ERROR', ctx)); ctx.proxyToClientResponse.on('error', self._onError.bind(self, 'PROXY_TO_CLIENT_RESPONSE_ERROR', ctx)); ctx.clientToProxyRequest.pause(); var hostPort = Proxy.parseHostAndPort(ctx.clientToProxyRequest, ctx.isSSL ? 443 : 80); var headers = {}; for (var h in ctx.clientToProxyRequest.headers) { // don't forward proxy- headers if (!/^proxy\-/i.test(h)) { headers[h] = ctx.clientToProxyRequest.headers[h]; } } if (this.options.forceChunkedRequest){ delete headers['content-length']; } ctx.proxyToServerRequestOptions = { method: ctx.clientToProxyRequest.method, path: ctx.clientToProxyRequest.url, host: hostPort.host, port: hostPort.port, headers: headers, agent: ctx.isSSL ? self.httpsAgent : self.httpAgent }; return self._onRequest(ctx, function(err) { if (err) { return self._onError('ON_REQUEST_ERROR', ctx, err); } return self._onRequestHeaders(ctx, function(err) { if (err) { return self._onError('ON_REQUESTHEADERS_ERROR', ctx, err); } return makeProxyToServerRequest(); }); }); function makeProxyToServerRequest() { var proto = ctx.isSSL ? https : http; ctx.proxyToServerRequest = proto.request(ctx.proxyToServerRequestOptions, proxyToServerRequestComplete); ctx.proxyToServerRequest.on('error', self._onError.bind(self, 'PROXY_TO_SERVER_REQUEST_ERROR', ctx)); ctx.requestFilters.push(new ProxyFinalRequestFilter(self, ctx)); var prevRequestPipeElem = ctx.clientToProxyRequest; ctx.requestFilters.forEach(function(filter) { filter.on('error', self._onError.bind(self, 'REQUEST_FILTER_ERROR', ctx)); prevRequestPipeElem = prevRequestPipeElem.pipe(filter); }); ctx.clientToProxyRequest.resume(); } function proxyToServerRequestComplete(serverToProxyResponse) { serverToProxyResponse.on('error', self._onError.bind(self, 'SERVER_TO_PROXY_RESPONSE_ERROR', ctx)); serverToProxyResponse.pause(); ctx.serverToProxyResponse = serverToProxyResponse; return self._onResponse(ctx, function(err) { if (err) { return self._onError('ON_RESPONSE_ERROR', ctx, err); } ctx.serverToProxyResponse.headers['transfer-encoding'] = 'chunked'; delete ctx.serverToProxyResponse.headers['content-length']; if (self.keepAlive) { if (ctx.clientToProxyRequest.headers['proxy-connection']) { ctx.serverToProxyResponse.headers['proxy-connection'] = 'keep-alive'; ctx.serverToProxyResponse.headers['connection'] = 'keep-alive'; } } else { ctx.serverToProxyResponse.headers['connection'] = 'close'; } return self._onResponseHeaders(ctx, function (err) { if (err) { return self._onError('ON_RESPONSEHEADERS_ERROR', ctx, err); } ctx.proxyToClientResponse.writeHead(ctx.serverToProxyResponse.statusCode, Proxy.filterAndCanonizeHeaders(ctx.serverToProxyResponse.headers)); ctx.responseFilters.push(new ProxyFinalResponseFilter(self, ctx)); var prevResponsePipeElem = ctx.serverToProxyResponse; ctx.responseFilters.forEach(function(filter) { filter.on('error', self._onError.bind(self, 'RESPONSE_FILTER_ERROR', ctx)); prevResponsePipeElem = prevResponsePipeElem.pipe(filter); }); return ctx.serverToProxyResponse.resume(); }); }); } }; var ProxyFinalRequestFilter = function(proxy, ctx) { events.EventEmitter.call(this); this.writable = true; this.write = function(chunk) { proxy._onRequestData(ctx, chunk, function(err, chunk) { if (err) { return proxy._onError('ON_REQUEST_DATA_ERROR', ctx, err); } if (chunk) { return ctx.proxyToServerRequest.write(chunk); } }); return true; }; this.end = function(chunk) { if (chunk) { return proxy._onRequestData(ctx, chunk, function(err, chunk) { if (err) { return proxy._onError('ON_REQUEST_DATA_ERROR', ctx, err); } return proxy._onRequestEnd(ctx, function (err) { if (err) { return proxy._onError('ON_REQUEST_END_ERROR', ctx, err); } return ctx.proxyToServerRequest.end(chunk); }); }); } else { return proxy._onRequestEnd(ctx, function (err) { if (err) { return proxy._onError('ON_REQUEST_END_ERROR', ctx, err); } return ctx.proxyToServerRequest.end(chunk || undefined); }); } }; }; util.inherits(ProxyFinalRequestFilter, events.EventEmitter); var ProxyFinalResponseFilter = function(proxy, ctx) { events.EventEmitter.call(this); this.writable = true; this.write = function(chunk) { proxy._onResponseData(ctx, chunk, function(err, chunk) { if (err) { return proxy._onError('ON_RESPONSE_DATA_ERROR', ctx, err); } if (chunk) { return ctx.proxyToClientResponse.write(chunk); } }); return true; }; this.end = function(chunk) { if (chunk) { return proxy._onResponseData(ctx, chunk, function(err, chunk) { if (err) { return proxy._onError('ON_RESPONSE_DATA_ERROR', ctx, err); } return proxy._onResponseEnd(ctx, function (err) { if (err) { return proxy._onError('ON_RESPONSE_END_ERROR', ctx, err); } return ctx.proxyToClientResponse.end(chunk || undefined); }); }); } else { return proxy._onResponseEnd(ctx, function (err) { if (err) { return proxy._onError('ON_RESPONSE_END_ERROR', ctx, err); } return ctx.proxyToClientResponse.end(chunk || undefined); }); } }; return this; }; util.inherits(ProxyFinalResponseFilter, events.EventEmitter); Proxy.prototype._onRequestHeaders = function(ctx, callback) { async.forEach(this.onRequestHeadersHandlers, function(fn, callback) { return fn(ctx, callback); }, callback); }; Proxy.prototype._onRequest = function(ctx, callback) { async.forEach(this.onRequestHandlers.concat(ctx.onRequestHandlers), function(fn, callback) { return fn(ctx, callback); }, callback); }; Proxy.prototype._onWebSocketConnection = function(ctx, callback) { async.forEach(this.onWebSocketConnectionHandlers.concat(ctx.onWebSocketConnectionHandlers), function(fn, callback) { return fn(ctx, callback); }, callback); }; Proxy.prototype._onWebSocketFrame = function(ctx, type, fromServer, data, flags) { var self = this; async.forEach(this.onWebSocketFrameHandlers.concat(ctx.onWebSocketFrameHandlers), function(fn, callback) { return fn(ctx, type, fromServer, data, flags, function(err, newData, newFlags) { if (err) { return callback(err); } data = newData; flags = newFlags; return callback(null, data, flags); }); }, function(err) { if (err) { return self._onWebSocketError(ctx, err); } var destWebSocket = fromServer ? ctx.clientToProxyWebSocket : ctx.proxyToServerWebSocket; if (destWebSocket.readyState === WebSocket.OPEN) { switch(type) { case 'message': destWebSocket.send(data, flags); break; case 'ping': destWebSocket.ping(data, flags, false); break; case 'pong': destWebSocket.pong(data, flags, false); break; } } else { self._onWebSocketError(ctx, new Error('Cannot send ' + type + ' because ' + (fromServer ? 'clientToProxy' : 'proxyToServer') + ' WebSocket connection state is not OPEN')); } }); }; Proxy.prototype._onWebSocketClose = function(ctx, closedByServer, code, message) { if (!ctx.closedByServer && !ctx.closedByClient) { ctx.closedByServer = closedByServer; ctx.closedByClient = !closedByServer; async.forEach(this.onWebSocketCloseHandlers.concat(ctx.onWebSocketCloseHandlers), function(fn, callback) { return fn(ctx, code, message, callback); }, function(err, code, message) { if (err) { return self._onWebSocketError(ctx, err); } if (ctx.clientToProxyWebSocket.readyState !== ctx.proxyToServerWebSocket.readyState) { if (ctx.clientToProxyWebSocket.readyState === WebSocket.CLOSED && ctx.proxyToServerWebSocket.readyState === WebSocket.OPEN) { ctx.proxyToServerWebSocket.close(code, message); } else if (ctx.proxyToServerWebSocket.readyState === WebSocket.CLOSED && ctx.clientToProxyWebSocket.readyState === WebSocket.OPEN) { ctx.clientToProxyWebSocket.close(code, message); } } }); } }; Proxy.prototype._onWebSocketError = function(ctx, err) { this.onWebSocketErrorHandlers.forEach(function(handler) { return handler(ctx, err); }); if (ctx) { ctx.onWebSocketErrorHandlers.forEach(function(handler) { return handler(ctx, err); }); } if (ctx.proxyToServerWebSocket && ctx.clientToProxyWebSocket.readyState !== ctx.proxyToServerWebSocket.readyState) { if (ctx.clientToProxyWebSocket.readyState === WebSocket.CLOSED && ctx.proxyToServerWebSocket.readyState === WebSocket.OPEN) { ctx.proxyToServerWebSocket.close(); } else if (ctx.proxyToServerWebSocket.readyState === WebSocket.CLOSED && ctx.clientToProxyWebSocket.readyState === WebSocket.OPEN) { ctx.clientToProxyWebSocket.close(); } } }; Proxy.prototype._onRequestData = function(ctx, chunk, callback) { var self = this; async.forEach(this.onRequestDataHandlers.concat(ctx.onRequestDataHandlers), function(fn, callback) { return fn(ctx, chunk, function(err, newChunk) { if (err) { return callback(err); } chunk = newChunk; return callback(null, newChunk); }); }, function(err) { if (err) { return self._onError('ON_REQUEST_DATA_ERROR', ctx, err); } return callback(null, chunk); }); }; Proxy.prototype._onRequestEnd = function(ctx, callback) { var self = this; async.forEach(this.onRequestEndHandlers.concat(ctx.onRequestEndHandlers), function(fn, callback) { return fn(ctx, callback); }, function(err) { if (err) { return self._onError('ON_REQUEST_END_ERROR', ctx, err); } return callback(null); }); }; Proxy.prototype._onResponse = function(ctx, callback) { async.forEach(this.onResponseHandlers.concat(ctx.onResponseHandlers), function(fn, callback) { return fn(ctx, callback); }, callback); }; Proxy.prototype._onResponseHeaders = function(ctx, callback) { async.forEach(this.onResponseHeadersHandlers, function(fn, callback) { return fn(ctx, callback); }, callback); }; Proxy.prototype._onResponseData = function(ctx, chunk, callback) { var self = this; async.forEach(this.onResponseDataHandlers.concat(ctx.onResponseDataHandlers), function(fn, callback) { return fn(ctx, chunk, function(err, newChunk) { if (err) { return callback(err); } chunk = newChunk; return callback(null, newChunk); }); }, function(err) { if (err) { return self._onError('ON_RESPONSE_DATA_ERROR', ctx, err); } return callback(null, chunk); }); }; Proxy.prototype._onResponseEnd = function(ctx, callback) { var self = this; async.forEach(this.onResponseEndHandlers.concat(ctx.onResponseEndHandlers), function(fn, callback) { return fn(ctx, callback); }, function(err) { if (err) { return self._onError('ON_RESPONSE_END_ERROR', ctx, err); } return callback(null); }); }; Proxy.parseHostAndPort = function(req, defaultPort) { var host = req.headers.host; if (!host) { return null; } var hostPort = Proxy.parseHost(host, defaultPort); // this handles paths which include the full url. This could happen if it's a proxy var m = req.url.match(/^http:\/\/([^\/]*)\/?(.*)$/); if (m) { var parsedUrl = url.parse(req.url); hostPort.host = parsedUrl.hostname; hostPort.port = parsedUrl.port; req.url = parsedUrl.path; } return hostPort; }; Proxy.parseHost = function(hostString, defaultPort) { var m = hostString.match(/^http:\/\/(.*)/); if (m) { var parsedUrl = url.parse(hostString); return { host: parsedUrl.hostname, port: parsedUrl.port }; } var hostPort = hostString.split(':'); var host = hostPort[0]; var port = hostPort.length === 2 ? +hostPort[1] : defaultPort; return { host: host, port: port }; }; Proxy.filterAndCanonizeHeaders = function(originalHeaders) { var headers = {}; for (var key in originalHeaders) { var canonizedKey = key.trim(); if (/^public\-key\-pins/i.test(canonizedKey)) { // HPKP header => filter continue; } if (!nodeCommon._checkInvalidHeaderChar(originalHeaders[key])) { headers[canonizedKey] = originalHeaders[key]; } } return headers; };