Permalink
Browse files

Initial commit

  • Loading branch information...
koichik committed Feb 5, 2012
0 parents commit bbcfc56ec7a591eb63c662edb9c7dc2f9be21f8f
@@ -0,0 +1,2 @@
+/.idea
+/node_modules
@@ -0,0 +1 @@
+# node-tunnel - HTTP/HTTPS Agents for tunneling proxies
@@ -0,0 +1 @@
+module.exports = require('./lib/tunnel');
@@ -0,0 +1,201 @@
+'use strict';
+
+var net = require('net');
+var tls = require('tls');
+var http = require('http');
+var https = require('https');
+var events = require('events');
+var assert = require('assert');
+var util = require('util');
+
+
+exports.httpOverHttp = httpOverHttp;
+exports.httpOverHttps = httpOverHttps;
+exports.httpsOverHttp = httpsOverHttp;
+exports.httpsOverHttps = httpsOverHttps;
+
+
+function httpOverHttp(options) {
+ var agent = new TunnelingAgent(options);
+ agent.request = http.request;
+ return agent;
+}
+
+function httpOverHttps(options) {
+ var agent = new TunnelingAgent(options);
+ agent.request = https.request;
+ return agent;
+}
+
+function httpsOverHttp(options) {
+ var agent = new TunnelingAgent(options);
+ agent.request = http.request;
+ agent.createSocket = createSecureSocket;
+ return agent;
+}
+
+function httpsOverHttps(options) {
+ var agent = new TunnelingAgent(options);
+ agent.request = https.request;
+ agent.createSocket = createSecureSocket;
+ return agent;
+}
+
+
+function TunnelingAgent(options) {
+ var self = this;
+ self.options = options || {};
+ self.proxyOptions = self.options.proxy || {};
+ self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets;
+ self.requests = [];
+ self.sockets = [];
+
+ self.on('free', function onFree(socket, host, port) {
+ for (var i = 0, len = self.requests.length; i < len; ++i) {
+ var pending = self.requests[i];
+ if (pending.host === host && pending.port === port) {
+ // Detect the request to connect same origin server,
+ // reuse the connection.
+ self.requests.splice(i, 1);
+ pending.request.onSocket(socket);
+ return;
+ }
+ }
+ socket.destroy();
+ self.removeSocket(socket);
+ });
+}
+util.inherits(TunnelingAgent, events.EventEmitter);
+
+TunnelingAgent.prototype.addRequest = function addRequest(req, host, port) {
+ var self = this;
+
+ if (self.sockets.length >= this.maxSockets) {
+ // We are over limit so we'll add it to the queue.
+ self.requests.push({host: host, port: port, request: req});
+ return;
+ }
+
+ // If we are under maxSockets create a new one.
+ self.createSocket({host: host, port: port, request: req}, function(socket) {
+ socket.on('free', onFree);
+ socket.on('close', onCloseOrRemove);
+ socket.on('agentRemove', onCloseOrRemove);
+ req.onSocket(socket);
+
+ function onFree() {
+ self.emit('free', socket, host, port);
+ }
+
+ function onCloseOrRemove(err) {
+ self.removeSocket();
+ socket.removeListener('free', onFree);
+ socket.removeListener('close', onCloseOrRemove);
+ socket.removeListener('agentRemove', onCloseOrRemove);
+ }
+ });
+};
+
+TunnelingAgent.prototype.createSocket = function createSocket(options, cb) {
+ var self = this;
+ var placeholder = {};
+ self.sockets.push(placeholder);
+
+ var connectReq = self.request(mergeOptions({}, self.proxyOptions, {
+ method: 'CONNECT',
+ path: options.host + ':' + options.port,
+ agent: false
+ }));
+ connectReq.once('connect', onConnect);
+ connectReq.once('error', onError);
+ connectReq.end();
+
+ function onConnect(res, socket, head) {
+ connectReq.removeListener('error', onError);
+
+ if (res.statusCode === 200) {
+ assert.equal(head.length, 0);
+ self.sockets[self.sockets.indexOf(placeholder)] = socket;
+ cb(socket);
+ } else {
+ debug('tunneling socket could not be established, statusCode=%d',
+ res.statusCode);
+ var error = new Error('tunneling socket could not be established, ' +
+ 'sutatusCode=' + res.statusCode);
+ error.code = 'ECONNRESET';
+ options.request.emit('error', error);
+ self.removeSocket(placeholder);
+ }
+ }
+
+ function onError(cause) {
+ connectReq.removeListener('connect', onConnect);
+ debug('tunneling socket could not be established, cause=%s\n',
+ cause.message, cause.stack);
+ var error = new Error('tunneling socket could not be established, ' +
+ 'cause=' + cause.message);
+ error.code = 'ECONNRESET';
+ options.request.emit('error', error);
+ self.removeSocket(placeholder);
+ }
+};
+
+TunnelingAgent.prototype.removeSocket = function removeSocket(socket) {
+ var pos = this.sockets.indexOf(socket)
+ if (pos === -1) {
+ return;
+ }
+ this.sockets.splice(pos, 1);
+
+ var pending = this.requests.shift();
+ if (pending) {
+ // If we have pending requests and a socket gets closed a new one
+ // needs to be created to take over in the pool for the one that closed.
+ this.createSocket(pending, function(socket) {
+ pending.request.onSocket(socket);
+ });
+ }
+};
+
+function createSecureSocket(options, cb) {
+ var self = this;
+ TunnelingAgent.prototype.createSocket.call(self, options, function(socket) {
+ var secureSocket = tls.connect(mergeOptions({}, self.options, {
+ socket: socket
+ }));
+ cb(secureSocket);
+ });
+}
+
+
+function mergeOptions(target) {
+ for (var i = 1, len = arguments.length; i < len; ++i) {
+ var overrides = arguments[i];
+ if (typeof overrides === 'object') {
+ var keys = Object.keys(overrides);
+ for (var j = 0, keyLen = keys.length; j < keyLen; ++j) {
+ var k = keys[j];
+ if (overrides[k] !== undefined) {
+ target[k] = overrides[k];
+ }
+ }
+ }
+ }
+ return target;
+}
+
+
+var debug;
+if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
+ debug = function() {
+ var args = Array.prototype.slice.call(arguments);
+ if (typeof args[0] === 'string') {
+ args[0] = 'TUNNEL: ' + args[0];
+ } else {
+ args.unshift('TUNNEL:');
+ }
+ console.error.apply(console, args);
+ }
+} else {
+ debug = function() {};
+}
@@ -0,0 +1,29 @@
+{
+ "name": "tunnel",
+ "version": "0.0.0-pre",
+ "description": "Node HTTP/HTTPS Agents for tunneling proxies",
+ "keywords": [
+ "http",
+ "https",
+ "agent",
+ "proxy"
+ ],
+ "homepage": "http://github.com/koichik/node-tunnel/",
+ "author": "Koichi Kobayashi <koichik@improvement.jp>",
+ "main": "./index.js",
+ "licenses": {
+ "type": "The MIT License",
+ "url": "http://www.opensource.org/licenses/mit-license.php"
+ },
+ "repositories": "https://github.com/koichik/node-tunnel.git",
+ "engines": {
+ "node": ">=0.7"
+ },
+ "devDependencies": {
+ "mocha": "*",
+ "should": "*"
+ },
+ "scripts": {
+ "test": "./node_modules/mocha/bin/mocha"
+ }
+}
@@ -0,0 +1,85 @@
+var http = require('http');
+var net = require('net');
+var should = require('should');
+var tunnel = require('../index');
+
+describe('HTTP over HTTP', function() {
+ it('should finish without error', function(done) {
+ var serverPort = 3000;
+ var proxyPort = 3001;
+ var poolSize = 3;
+ var N = 10;
+ var serverConnect = 0;
+ var proxyConnect = 0;
+ var clientConnect = 0;
+ var agent;
+
+ var server = http.createServer(function(req, res) {
+ ++serverConnect;
+ res.writeHead(200);
+ res.end('Hello' + req.url);
+ });
+ server.listen(serverPort, function() {
+ var proxy = http.createServer(function(req, res) {
+ should.fail();
+ });
+ proxy.on('connect', function(req, clientSocket, head) {
+ req.method.should.equal('CONNECT');
+ req.url.should.equal('localhost:' + serverPort);
+ ++proxyConnect;
+
+ var serverSocket = net.connect(serverPort, function() {
+ clientSocket.write('HTTP/1.1 200 Connection established\r\n\r\n');
+ clientSocket.pipe(serverSocket);
+ serverSocket.write(head);
+ serverSocket.pipe(clientSocket);
+ // workaround, see joyent/node#2524
+ serverSocket.on('end', function() {
+ clientSocket.end();
+ });
+ });
+ });
+ proxy.listen(proxyPort, function() {
+ agent = tunnel.httpOverHttp({
+ maxSockets: poolSize,
+ proxy: {
+ port: proxyPort
+ }
+ });
+
+ for (var i = 0; i < N; ++i) {
+ (function(i) {
+ var req = http.get({
+ port: serverPort,
+ path: '/' + i,
+ agent: agent
+ }, function(res) {
+ res.setEncoding('utf8');
+ res.on('data', function(data) {
+ data.should.equal('Hello/' + i);
+ });
+ res.on('end', function() {
+ ++clientConnect;
+ if (clientConnect === N) {
+ proxy.close();
+ server.close();
+ }
+ });
+ });
+ })(i);
+ }
+ });
+ });
+
+ server.on('close', function() {
+ serverConnect.should.equal(N);
+ proxyConnect.should.equal(poolSize);
+ clientConnect.should.equal(N);
+
+ agent.sockets.should.have.lengthOf(0);
+ agent.requests.should.have.lengthOf(0);
+
+ done();
+ });
+ });
+});
Oops, something went wrong.

0 comments on commit bbcfc56

Please sign in to comment.