UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

297 lines (286 loc) 8.36 kB
var net = require('net'); var http = require('http'); var extend = require('extend'); var LRU = require('lru-cache'); var parseUrl = require('../util/parse-url-safe'); var getServer = require('hagent').create(function () { return net.createServer({ pauseOnConnect: true }); }, 51500); var formatHeaders = require('hparser').formatHeaders; var common = require('../util/common'); var socketProto = net.Socket.prototype; var originConnect = socketProto.connect; var SPEC_HOST_RE = /.whistle-policy(?:-(internal|capture|tunnel|connect)(?:-([A-Za-z\d]+))?)?(?:\.proto-([\w%.-]+))?$/; var lru = new LRU({ max: 5120, maxAge: 30000 }); var PROXY_ID_HEADER; var proxyIp; var proxyPort; function toNumber(x) { x = Number(x); return x >= 0 ? x : false; } function isPipeName(s) { return typeof s === 'string' && toNumber(s) === false; } function normalizeArgs(args) { if (args.length === 0) { return [{}, null]; } var arg0 = args[0]; if (Array.isArray(arg0)) { return arg0; } var options = {}; if (typeof arg0 === 'object' && arg0 !== null) { // (options[...][, cb]) options = arg0; } else if (isPipeName(arg0)) { // (path[...][, cb]) options.path = arg0; } else { // ([port][, host][...][, cb]) options.port = arg0; if (args.length > 1 && typeof args[1] === 'string') { options.host = args[1]; } } var cb = args[args.length - 1]; if (typeof cb !== 'function') { return [options, null]; } return [options, cb]; } function createSocket(opts, cb) { var done; var client; var headers = common.lowerCaseify(opts.headers); var handleCb = function (err, socket, res) { if (!done) { done = true; cb && cb(err, socket, res); } if (err && client) { client.destroy(); client = null; } }; if (opts.isHttp) { headers['x-whistle-policy'] = 'capture'; } else if (opts.isConnect) { headers['x-whistle-policy'] = 'connect'; } else if (!headers['x-whistle-policy']) { headers['x-whistle-policy'] = 'tunnel'; } if (opts.reqId && typeof opts.reqId === 'string') { headers['x-whistle-plugin-request-id'] = opts.reqId; } if (opts.isInternal) { headers[PROXY_ID_HEADER] = '1'; } if (opts.proto) { headers['x-whistle-transport-protocol'] = opts.proto; } opts.path = opts.path || common.joinIpPort(opts.host, opts.port || 80); headers.host = opts.path; var clientIp = common.removeIPV6Prefix(opts.clientIp); var clientPort = opts.clientPort; if (clientIp && net.isIP(clientIp)) { headers[common.CLIENT_IP_HEADER] = clientIp; } if (clientPort > 0 && clientPort < 65536) { headers['x-whistle-client-port'] = clientPort; } headers[common.ACK_HEADER] = '1'; client = http.request({ host: proxyIp, port: proxyPort, method: 'CONNECT', agent: false, path: opts.path, headers: headers }); client.once('connect', function (res, socket) { socket.on('error', handleCb); if (res.statusCode !== 200) { var err = new Error('Tunneling socket could not be established, statusCode=' + res.statusCode); err.statusCode = res.statusCode; socket.destroy(); return handleCb(err); } if (res.headers[common.ALLOW_ACK]) { socket.write('1'); } handleCb(null, socket, res); }); client.on('error', handleCb); client.end(); } module.exports = function (options, cb) { PROXY_ID_HEADER = options.PROXY_ID_HEADER; proxyIp = options.proxyIp; proxyPort = options.proxyPort; var wrapWsReader = options.wrapWsReader; var wrapWsWriter = options.wrapWsWriter; getServer(function (server, port) { var tempServerOpts = { host: '127.0.0.1', port: port }; server.on('connection', function (socket) { var destroyed; var sock; var destroy = function () { if (!destroyed) { destroyed = true; socket.destroy(); sock && sock.destroy(); } }; var handleProxy = function (connOpts) { createSocket(connOpts, function (err, sock) { if (err) { return destroy(); } sock.on('error', destroy); socket.pipe(sock).pipe(socket); socket.resume(); }); }; var key = `${socket.remotePort}:${port}`; var opts = lru.peek(key); if (opts) { lru.del(key); handleProxy(opts); } else { lru.set(key, handleProxy); } }); var handleTempCache = function (socket, opts) { socket.once('connect', function () { var key = `${socket.localPort}:${port}`; var handler = lru.get(key); if (typeof handler === 'function') { lru.del(key); handler(opts); } else { lru.set(key, opts); } }); return socket; }; var connect = function (opts, cb) { var socket = net.connect(tempServerOpts, cb); handleTempCache(socket, opts); socket.on('error', common.noop); return socket; }; var request = function (opts, cb) { if (typeof opts === 'string') { opts = { url: opts }; } else { opts = extend({}, opts); } opts.url = common.setProtocol(opts.url); var uri = parseUrl(opts.url); var client; if (common.isConnect(uri)) { client = connect( { host: uri.hostname, port: uri.port, headers: opts.proxyHeaders }, cb ); client.isConnect = true; client.isTunnel = true; return client; } var protocol = uri.protocol; var isHttp = protocol === 'https:' || protocol === 'http:'; var rawNames = {}; var headers = common.lowerCaseify(opts.headers, rawNames); var isWs = common.isUpgrade(uri, headers); if (!isHttp && !isWs) { return cb && cb(new Error('Bad URL')); } var isHttps = protocol === 'https:' || protocol === 'wss:'; if (isWs) { headers.connection = 'Upgrade'; headers.upgrade = headers.upgrade || 'websocket'; rawNames.connection = 'Connection'; rawNames.upgrade = 'Upgrade'; } if (!opts.reserveHost && uri.host) { headers.host = uri.host; } client = http.request( { path: opts.url || '/', method: opts.method, agent: null, createConnection: function () { return connect({ host: uri.hostname || 'localhost', port: uri.port || (isHttps ? 443 : 80), headers: opts.proxyHeaders, isHttp: true, isInternal: opts.isInternal, clientIp: opts.clientIp, clientPort: opts.clientPort }); }, headers: formatHeaders(headers, rawNames) }, cb ); if (isWs) { client.once('upgrade', function (res, socket) { socket.headers = res.headers; if (!opts.needRawData) { wrapWsReader(socket, opts.maxPayload); wrapWsWriter(socket); opts.stopPing && socket.stopPing(); } }); } client.isHttps = isHttps; client.isHttp = isHttp; client.isUpgrade = isWs; client.isWebSocket = isWs && common.isWebSocket(headers); client.on('error', common.noop); return client; }; socketProto.connect = function () { var args = normalizeArgs(arguments); var opts = args[0]; var cb = args[1]; if (!opts || !SPEC_HOST_RE.test(opts.host)) { return originConnect.apply(this, arguments); } var policy = RegExp.$1; var reqId = RegExp.$2; var proto = RegExp.$3; var host = opts.host.slice(0, -RegExp['$&'].length); originConnect.call(this, tempServerOpts, cb); handleTempCache(this, { host: host || 'localhost', port: opts.port, reqId: reqId, proto: proto, headers: opts.proxyHeaders, isInternal: policy === 'internal', isTunnel: !policy || policy === 'tunnel', isConnect: policy === 'connect', clientIp: opts.clientIp, clientPort: opts.clientPort }); return this; }; cb({ connect: connect, request: request }); }); };