/** * Module dependencies. */ var net = require('net'); var url = require('url'); var http = require('http'); var assert = require('assert'); var debug = require('debug')('proxy'); // log levels debug.request = require('debug')('proxy ← ← ←'); debug.response = require('debug')('proxy → → →'); debug.proxyRequest = require('debug')('proxy ↑ ↑ ↑'); debug.proxyResponse = require('debug')('proxy ↓ ↓ ↓'); // hostname var hostname = require('os').hostname(); // proxy server version var version = require('./package.json').version; /** * Module exports. */ module.exports = setup; /** * Sets up an `http.Server` or `https.Server` instance with the necessary * "request" and "connect" event listeners in order to make the server act as an * HTTP proxy. * * @param {http.Server|https.Server} server * @param {Object} options * @api public */ function setup(server, options) { if (!server) server = http.createServer(); server.on('request', onrequest); server.on('connect', onconnect); return server; } /** * 13.5.1 End-to-end and Hop-by-hop Headers * * Hop-by-hop headers must be removed by the proxy before passing it on to the * next endpoint. Per-request basis hop-by-hop headers MUST be listed in a * Connection header, (section 14.10) to be introduced into HTTP/1.1 (or later). */ var hopByHopHeaders = [ 'Connection', 'Keep-Alive', 'Proxy-Authenticate', 'Proxy-Authorization', 'TE', 'Trailers', 'Transfer-Encoding', 'Upgrade' ]; // create a case-insensitive RegExp to match "hop by hop" headers var isHopByHop = new RegExp('^(' + hopByHopHeaders.join('|') + ')$', 'i'); /** * Iterator function for the request/response's "headers". * Invokes `fn` for "each" header entry in the request. * * @api private */ function eachHeader(obj, fn) { if (Array.isArray(obj.rawHeaders)) { // ideal scenario... >= node v0.11.x // every even entry is a "key", every odd entry is a "value" var key = null; obj.rawHeaders.forEach(function(v) { if (key === null) { key = v; } else { fn(key, v); key = null; } }); } else { // otherwise we can *only* proxy the header names as lowercase'd var headers = obj.headers; if (!headers) return; Object.keys(headers).forEach(function(key) { var value = headers[key]; if (Array.isArray(value)) { // set-cookie value.forEach(function(val) { fn(key, val); }); } else { fn(key, value); } }); } } /** * HTTP GET/POST/DELETE/PUT, etc. proxy requests. */ function onrequest(req, res) { debug.request('%s %s HTTP/%s ', req.method, req.url, req.httpVersion); var server = this; var socket = req.socket; // pause the socket during authentication so no data is lost socket.pause(); authenticate(server, req, function(err, auth) { socket.resume(); if (err) { // an error occured during login! res.writeHead(500); res.end((err.stack || err.message || err) + '\n'); return; } if (!auth) return requestAuthorization(req, res); var parsed = url.parse(req.url); // proxy the request HTTP method parsed.method = req.method; // setup outbound proxy request HTTP headers var headers = {}; var hasXForwardedFor = false; var hasVia = false; var via = '1.1 ' + hostname + ' (proxy/' + version + ')'; parsed.headers = headers; eachHeader(req, function(key, value) { debug.request('Request Header: "%s: %s"', key, value); var keyLower = key.toLowerCase(); if (!hasXForwardedFor && 'x-forwarded-for' === keyLower) { // append to existing "X-Forwarded-For" header // http://en.wikipedia.org/wiki/X-Forwarded-For hasXForwardedFor = true; value += ', ' + socket.remoteAddress; debug.proxyRequest( 'appending to existing "%s" header: "%s"', key, value ); } if (!hasVia && 'via' === keyLower) { // append to existing "Via" header hasVia = true; value += ', ' + via; debug.proxyRequest( 'appending to existing "%s" header: "%s"', key, value ); } if (isHopByHop.test(key)) { debug.proxyRequest('ignoring hop-by-hop header "%s"', key); } else { var v = headers[key]; if (Array.isArray(v)) { v.push(value); } else if (null != v) { headers[key] = [v, value]; } else { headers[key] = value; } } }); // add "X-Forwarded-For" header if it's still not here by now // http://en.wikipedia.org/wiki/X-Forwarded-For if (!hasXForwardedFor) { headers['X-Forwarded-For'] = socket.remoteAddress; debug.proxyRequest( 'adding new "X-Forwarded-For" header: "%s"', headers['X-Forwarded-For'] ); } // add "Via" header if still not set by now if (!hasVia) { headers.Via = via; debug.proxyRequest('adding new "Via" header: "%s"', headers.Via); } // custom `http.Agent` support, set `server.agent` var agent = server.agent; if (null != agent) { debug.proxyRequest( 'setting custom `http.Agent` option for proxy request: %s', agent ); parsed.agent = agent; agent = null; } if (null == parsed.port) { // default the port number if not specified, for >= node v0.11.6... // https://github.com/joyent/node/issues/6199 parsed.port = 80; } if ('http:' != parsed.protocol) { // only "http://" is supported, "https://" should use CONNECT method res.writeHead(400); res.end('Only "http:" protocol prefix is supported\n'); return; } if (server.localAddress) { parsed.localAddress = server.localAddress; } var gotResponse = false; var proxyReq = http.request(parsed); debug.proxyRequest('%s %s HTTP/1.1 ', proxyReq.method, proxyReq.path); proxyReq.on('response', function(proxyRes) { debug.proxyResponse('HTTP/1.1 %s', proxyRes.statusCode); gotResponse = true; var headers = {}; eachHeader(proxyRes, function(key, value) { debug.proxyResponse( 'Proxy Response Header: "%s: %s"', key, value ); if (isHopByHop.test(key)) { debug.response('ignoring hop-by-hop header "%s"', key); } else { var v = headers[key]; if (Array.isArray(v)) { v.push(value); } else if (null != v) { headers[key] = [v, value]; } else { headers[key] = value; } } }); debug.response('HTTP/1.1 %s', proxyRes.statusCode); res.writeHead(proxyRes.statusCode, headers); proxyRes.pipe(res); res.on('finish', onfinish); }); proxyReq.on('error', function(err) { debug.proxyResponse( 'proxy HTTP request "error" event\n%s', err.stack || err ); cleanup(); if (gotResponse) { debug.response( 'already sent a response, just destroying the socket...' ); socket.destroy(); } else if ('ENOTFOUND' == err.code) { debug.response('HTTP/1.1 404 Not Found'); res.writeHead(404); res.end(); } else { debug.response('HTTP/1.1 500 Internal Server Error'); res.writeHead(500); res.end(); } }); // if the client closes the connection prematurely, // then close the upstream socket function onclose() { debug.request( 'client socket "close" event, aborting HTTP request to "%s"', req.url ); proxyReq.abort(); cleanup(); } socket.on('close', onclose); function onfinish() { debug.response('"finish" event'); cleanup(); } function cleanup() { debug.response('cleanup'); socket.removeListener('close', onclose); res.removeListener('finish', onfinish); } req.pipe(proxyReq); }); } /** * HTTP CONNECT proxy requests. */ function onconnect(req, socket, head) { debug.request('%s %s HTTP/%s ', req.method, req.url, req.httpVersion); assert( !head || 0 == head.length, '"head" should be empty for proxy requests' ); var res; var target; var gotResponse = false; // define request socket event listeners socket.on('close', function onclientclose() { debug.request('HTTP request %s socket "close" event', req.url); }); socket.on('end', function onclientend() { debug.request('HTTP request %s socket "end" event', req.url); }); socket.on('error', function onclienterror(err) { debug.request( 'HTTP request %s socket "error" event:\n%s', req.url, err.stack || err ); }); // define target socket event listeners function ontargetclose() { debug.proxyResponse('proxy target %s "close" event', req.url); socket.destroy(); } function ontargetend() { debug.proxyResponse('proxy target %s "end" event', req.url); } function ontargeterror(err) { debug.proxyResponse( 'proxy target %s "error" event:\n%s', req.url, err.stack || err ); if (gotResponse) { debug.response( 'already sent a response, just destroying the socket...' ); socket.destroy(); } else if ('ENOTFOUND' == err.code) { debug.response('HTTP/1.1 404 Not Found'); res.writeHead(404); res.end(); } else { debug.response('HTTP/1.1 500 Internal Server Error'); res.writeHead(500); res.end(); } } function ontargetconnect() { debug.proxyResponse('proxy target %s "connect" event', req.url); debug.response('HTTP/1.1 200 Connection established'); gotResponse = true; res.removeListener('finish', onfinish); res.writeHead(200, 'Connection established'); res.flushHeaders(); // relinquish control of the `socket` from the ServerResponse instance res.detachSocket(socket); // nullify the ServerResponse object, so that it can be cleaned // up before this socket proxying is completed res = null; socket.pipe(target); target.pipe(socket); } // create the `res` instance for this request since Node.js // doesn't provide us with one :( // XXX: this is undocumented API, so it will break some day (ノಠ益ಠ)ノ彡┻━┻ res = new http.ServerResponse(req); res.shouldKeepAlive = false; res.chunkedEncoding = false; res.useChunkedEncodingByDefault = false; res.assignSocket(socket); // called for the ServerResponse's "finish" event // XXX: normally, node's "http" module has a "finish" event listener that would // take care of closing the socket once the HTTP response has completed, but // since we're making this ServerResponse instance manually, that event handler // never gets hooked up, so we must manually close the socket... function onfinish() { debug.response('response "finish" event'); res.detachSocket(socket); socket.end(); } res.once('finish', onfinish); // pause the socket during authentication so no data is lost socket.pause(); authenticate(this, req, function(err, auth) { socket.resume(); if (err) { // an error occured during login! res.writeHead(500); res.end((err.stack || err.message || err) + '\n'); return; } if (!auth) return requestAuthorization(req, res); var parts = req.url.split(':'); var host = parts[0]; var port = +parts[1]; var opts = { host: host, port: port }; debug.proxyRequest('connecting to proxy target %j', opts); target = net.connect(opts); target.on('connect', ontargetconnect); target.on('close', ontargetclose); target.on('error', ontargeterror); target.on('end', ontargetend); }); } /** * Checks `Proxy-Authorization` request headers. Same logic applied to CONNECT * requests as well as regular HTTP requests. * * @param {http.Server} server * @param {http.ServerRequest} req * @param {Function} fn callback function * @api private */ function authenticate(server, req, fn) { var hasAuthenticate = 'function' == typeof server.authenticate; if (hasAuthenticate) { debug.request('authenticating request "%s %s"', req.method, req.url); server.authenticate(req, fn); } else { // no `server.authenticate()` function, so just allow the request fn(null, true); } } /** * Sends a "407 Proxy Authentication Required" HTTP response to the `socket`. * * @api private */ function requestAuthorization(req, res) { // request Basic proxy authorization debug.response( 'requesting proxy authorization for "%s %s"', req.method, req.url ); // TODO: make "realm" and "type" (Basic) be configurable... var realm = 'proxy'; var headers = { 'Proxy-Authenticate': 'Basic realm="' + realm + '"' }; res.writeHead(407, headers); res.end(); }