diff --git a/package.json b/package.json index 842ec7a..77a7536 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "request": "1.9.x", "vows": "0.5.x", "async": "0.1.x", - "socket.io": "0.6.17" + "socket.io": "0.9.6", + "socket.io-client": "0.9.6", + "ws": "0.4.21" }, "main": "./lib/node-http-proxy", "bin": { diff --git a/test/helpers.js b/test/helpers.js deleted file mode 100644 index b00cbc3..0000000 --- a/test/helpers.js +++ /dev/null @@ -1,407 +0,0 @@ -/* - * helpers.js: Helpers for node-http-proxy tests. - * - * (C) 2010, Charlie Robbins - * - */ - -var assert = require('assert'), - fs = require('fs'), - http = require('http'), - https = require('https'), - path = require('path'), - argv = require('optimist').argv, - request = require('request'), - vows = require('vows'), - websocket = require('../vendor/websocket'), - httpProxy = require('../lib/node-http-proxy'); - -var loadHttps = exports.loadHttps = function () { - return { - key: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-key.pem'), 'utf8'), - cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'agent2-cert.pem'), 'utf8') - }; -}; - -var parseProtocol = exports.parseProtocol = function () { - function setupProtocol (secure) { - return { - secure: secure, - protocols: { - http: secure ? 'https' : 'http', - ws: secure ? 'wss' : 'ws' - } - } - } - - return { - source: setupProtocol(argv.source === 'secure'), - target: setupProtocol(argv.target === 'secure') - }; -} - -var TestRunner = exports.TestRunner = function (options) { - options = options || {}; - this.source = options.source || {}; - this.target = options.target || {}; - this.testServers = []; - - if (this.source.secure) { - this.source.https = loadHttps(); - } - - if (this.target.secure) { - this.target.https = loadHttps(); - } -}; - -TestRunner.prototype.assertProxied = function (host, proxyPort, port, requestPath, targetPath, createProxy) { - if (!targetPath) targetPath = ""; - - var self = this, - output = "hello " + host + targetPath, - assertion = "should receive '" + output + "'"; - - var test = { - topic: function () { - var that = this, - options; - - options = { - method: 'GET', - uri: self.source.protocols.http + '://localhost:' + proxyPort, - headers: { - host: host - } - }; - - if (requestPath) options.uri += requestPath; - - function startTest () { - if (port) { - return self.startTargetServer(port, output, function () { - request(options, that.callback); - }); - } - request(options, this.callback); - } - - return createProxy ? createProxy(startTest) : startTest(); - } - }; - - test[assertion] = function (err, res, body) { - assert.isNull(err); - assert.equal(body, output); - }; - - return test; -}; - -TestRunner.prototype.assertResponseCode = function (proxyPort, statusCode, createProxy) { - var assertion = "should receive " + statusCode + " responseCode", - protocol = this.source.protocols.http; - - var test = { - topic: function () { - var that = this, options = { - method: 'GET', - uri: protocol + '://localhost:' + proxyPort, - headers: { - host: 'unknown.com' - } - }; - - if (createProxy) { - return createProxy(function () { - request(options, that.callback); - }); - } - - request(options, this.callback); - } - }; - - test[assertion] = function (err, res, body) { - assert.isNull(err); - assert.equal(res.statusCode, statusCode); - }; - - return test; -}; - -// A test helper to check and see if the http headers were set properly. -TestRunner.prototype.assertHeaders = function (proxyPort, headerName, createProxy) { - var assertion = "should receive http header \"" + headerName + "\"", - protocol = this.source.protocols.http; - - var test = { - topic: function () { - var that = this, options = { - method: 'GET', - uri: protocol + '://localhost:' + proxyPort, - headers: { - host: 'unknown.com' - } - }; - - if (createProxy) { - return createProxy(function () { - request(options, that.callback); - }); - } - - request(options, this.callback); - } - }; - - test[assertion] = function (err, res, body) { - assert.isNull(err); - assert.isNotNull(res.headers[headerName]); - }; - - return test; -}; - - -// -// WebSocketTest -// -TestRunner.prototype.webSocketTest = function (options) { - var self = this; - - this.startTargetServer(options.ports.target, 'hello websocket', function (err, target) { - var socket = options.io.listen(target); - - if (options.onListen) { - options.onListen(socket); - } - - self.startProxyServer( - options.ports.proxy, - options.ports.target, - options.host, - function (err, proxy) { - if (options.onServer) { options.onServer(proxy) } - - // - // Setup the web socket against our proxy - // - var uri = options.wsprotocol + '://' + options.host + ':' + options.ports.proxy; - var ws = new websocket.WebSocket(uri + '/socket.io/websocket/', 'borf', { - origin: options.protocol + '://' + options.host - }); - - if (options.onWsupgrade) { ws.on('wsupgrade', options.onWsupgrade) } - if (options.onMessage) { ws.on('message', options.onMessage) } - if (options.onOpen) { ws.on('open', function () { options.onOpen(ws) }) } - } - ); - }); -} - -// -// WebSocketTestWithTable -// -TestRunner.prototype.webSocketTestWithTable = function (options) { - var self = this; - - this.startTargetServer(options.ports.target, 'hello websocket', function (err, target) { - var socket = options.io.listen(target); - - if (options.onListen) { - options.onListen(socket); - } - - self.startProxyServerWithTable( - options.ports.proxy, - { router: options.router }, - function (err, proxy) { - if (options.onServer) { options.onServer(proxy) } - - // - // Setup the web socket against our proxy - // - var uri = options.wsprotocol + '://' + options.host + ':' + options.ports.proxy; - var ws = new websocket.WebSocket(uri + '/socket.io/websocket/', 'borf', { - origin: options.protocol + '://' + options.host - }); - - if (options.onWsupgrade) { ws.on('wsupgrade', options.onWsupgrade) } - if (options.onMessage) { ws.on('message', options.onMessage) } - if (options.onOpen) { ws.on('open', function () { options.onOpen(ws) }) } - } - ); - }); -} - -// -// Creates the reverse proxy server -// -TestRunner.prototype.startProxyServer = function (port, targetPort, host, callback) { - var that = this, - proxyServer = httpProxy.createServer(host, targetPort, this.getOptions()); - - proxyServer.listen(port, function () { - that.testServers.push(proxyServer); - callback(null, proxyServer); - }); -}; - -// -// Creates the reverse proxy server with a specified latency -// -TestRunner.prototype.startLatentProxyServer = function (port, targetPort, host, latency, callback) { - // - // Initialize the nodeProxy and start proxying the request - // - var that = this, - proxyServer; - - proxyServer = httpProxy.createServer(host, targetPort, function (req, res, proxy) { - var buffer = httpProxy.buffer(req); - - setTimeout(function () { - proxy.proxyRequest(req, res, buffer); - }, latency); - }, this.getOptions()); - - proxyServer.listen(port, function () { - that.testServers.push(proxyServer); - callback(); - }); -}; - -// -// Creates the reverse proxy server with a ProxyTable -// -TestRunner.prototype.startProxyServerWithTable = function (port, options, callback) { - var that = this, - proxyServer = httpProxy.createServer(merge({}, options, this.getOptions())); - - proxyServer.listen(port, function () { - that.testServers.push(proxyServer); - callback(); - }); - - return proxyServer; -}; - -// -// Creates a latent reverse proxy server using a ProxyTable -// -TestRunner.prototype.startProxyServerWithTableAndLatency = function (port, latency, options, callback) { - // - // Initialize the nodeProxy and start proxying the request - // - var that = this, - proxy = new httpProxy.RoutingProxy(merge({}, options, this.getOptions())), - proxyServer; - - var handler = function (req, res) { - var buffer = httpProxy.buffer(req); - setTimeout(function () { - proxy.proxyRequest(req, res, { - buffer: buffer - }); - }, latency); - }; - - proxyServer = this.source.https - ? https.createServer(this.source.https, handler) - : http.createServer(handler); - - proxyServer.listen(port, function () { - that.testServers.push(proxyServer); - callback(); - }); - - return proxyServer; -}; - -// -// Creates proxy server forwarding to the specified options -// -TestRunner.prototype.startProxyServerWithForwarding = function (port, targetPort, host, options, callback) { - var that = this, - proxyServer = httpProxy.createServer(targetPort, host, merge({}, options, this.getOptions())); - - proxyServer.listen(port, function () { - that.testServers.push(proxyServer); - callback(null, proxyServer); - }); -}; - -// -// Creates the 'hellonode' server -// -TestRunner.prototype.startTargetServer = function (port, output, callback) { - var that = this, - targetServer, - handler; - - handler = function (req, res) { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.write(output); - res.end(); - }; - - targetServer = this.target.https - ? https.createServer(this.target.https, handler) - : http.createServer(handler); - - targetServer.listen(port, function () { - that.testServers.push(targetServer); - callback(null, targetServer); - }); -}; - -// -// Close all of the testServers -// -TestRunner.prototype.closeServers = function () { - this.testServers.forEach(function (server) { - server.close(); - }); - - return this.testServers; -}; - -// -// Creates a new instance of the options to -// pass to `httpProxy.createServer()` -// -TestRunner.prototype.getOptions = function () { - return { - https: clone(this.source.https), - target: { - https: clone(this.target.https) - } - }; -}; - -// -// ### @private function clone (object) -// #### @object {Object} Object to clone -// Shallow clones the specified object. -// -function clone (object) { - if (!object) { return null } - - return Object.keys(object).reduce(function (obj, k) { - obj[k] = object[k]; - return obj; - }, {}); -} - -function merge (target) { - var objs = Array.prototype.slice.call(arguments, 1); - objs.forEach(function(o) { - Object.keys(o).forEach(function (attr) { - if (! o.__lookupGetter__(attr)) { - target[attr] = o[attr]; - } - }); - }); - return target; -} diff --git a/test/helpers/http.js b/test/helpers/http.js new file mode 100644 index 0000000..b2a477d --- /dev/null +++ b/test/helpers/http.js @@ -0,0 +1,124 @@ +/* + * http.js: Top level include for node-http-proxy http helpers + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +var assert = require('assert'), + http = require('http'), + url = require('url'), + async = require('async'), + helpers = require('./index'), + httpProxy = require('../../lib/node-http-proxy'); + +// +// ### function createServerPair (options, callback) +// #### @options {Object} Options to create target and proxy server. +// #### @callback {function} Continuation to respond to when complete. +// +// Creates http target and proxy servers +// +exports.createServerPair = function (options, callback) { + async.series([ + // + // 1. Create the target server + // + function createTarget(next) { + exports.createServer(options.target, next); + }, + // + // 2. Create the proxy server + // + function createTarget(next) { + exports.createProxyServer(options.proxy, next); + } + ], callback); +}; + +// +// ### function createServer (options, callback) +// #### @options {Object} Options for creatig an http server. +// #### @port {number} Port to listen on +// #### @output {string} String to write to each HTTP response +// #### @headers {Object} Headers to assert are sent by `node-http-proxy`. +// #### @callback {function} Continuation to respond to when complete. +// +// Creates a target server that the tests will proxy to. +// +exports.createServer = function (options, callback) { + http.createServer(function (req, res) { + if (options.headers) { + Object.keys(options.headers).forEach(function (key) { + assert.equal(req.headers[key], options.headers[key]); + }); + } + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write(options.output || 'hello proxy'); + res.end(); + }).listen(options.port, function () { + callback(null, this); + }); +}; + +// +// ### function createProxyServer (options, callback) +// #### @options {Object} Options for creatig an http server. +// #### @port {number} Port to listen on +// #### @latency {number} Latency of this server in milliseconds +// #### @proxy {Object} Options to pass to the HttpProxy. +// #### @routing {boolean} Enables `httpProxy.RoutingProxy` +// #### @callback {function} Continuation to respond to when complete. +// +// Creates a proxy server that the tests will request against. +// +exports.createProxyServer = function (options, callback) { + if (!options.latency) { + return httpProxy + .createServer(options.proxy) + .listen(options.port, function () { + callback(null, this); + }); + } + + var proxy = options.routing + ? new httpProxy.RoutingProxy(options.proxy) + : new httpProxy.HttpProxy(options.proxy); + + http.createServer(function (req, res) { + var buffer = httpProxy.buffer(req); + + setTimeout(function () { + // + // Setup options dynamically for `RoutingProxy.prototype.proxyRequest` + // or `HttpProxy.prototype.proxyRequest`. + // + buffer = options.routing ? { buffer: buffer } : buffer + proxy.proxyRequest(req, res, buffer); + }, options.latency); + }).listen(options.port, function () { + callback(null, this); + }); +}; + +exports.assignPortsToRoutes = function (routes) { + Object.keys(routes).forEach(function (source) { + routes[source] = routes[source].replace('{PORT}', helpers.nextPort); + }); + + return routes; +} + +exports.parseRoutes = function (options) { + var protocol = options.protocol || 'http', + routes = options.routes; + + return Object.keys(routes).map(function (source) { + return { + source: url.parse(protocol + '://' + source), + target: url.parse(protocol + '://' + routes[source]) + }; + }); +}; diff --git a/test/helpers/index.js b/test/helpers/index.js new file mode 100644 index 0000000..d98bdf4 --- /dev/null +++ b/test/helpers/index.js @@ -0,0 +1,38 @@ +/* + * index.js: Top level include for node-http-proxy helpers + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +// +// @nextPort {number} +// Returns an auto-incrementing port for tests. +// +Object.defineProperty(exports, 'nextPort', { + get: function () { + var current = this.port || 8000; + this.port = current + 1; + return current; + } +}); + +// +// @nextPortPair {Object} +// Returns an auto-incrementing pair of ports for tests. +// +Object.defineProperty(exports, 'nextPortPair', { + get: function () { + return { + target: this.nextPort, + proxy: this.nextPort + }; + } +}); + +// +// Export additional helpers for `http` and `websockets`. +// +exports.http = require('./http'); +exports.ws = require('./ws'); \ No newline at end of file diff --git a/test/helpers/ws.js b/test/helpers/ws.js new file mode 100644 index 0000000..92fe618 --- /dev/null +++ b/test/helpers/ws.js @@ -0,0 +1,48 @@ +/* + * ws.js: Top level include for node-http-proxy websocket helpers + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +var assert = require('assert'), + async = require('async'), + io = require('socket.io'), + http = require('./http'); + +// +// ### function createServerPair (options, callback) +// #### @options {Object} Options to create target and proxy server. +// #### @callback {function} Continuation to respond to when complete. +// +// Creates http target and proxy servers +// +exports.createServerPair = function (options, callback) { + async.series([ + // + // 1. Create the target server + // + function createTarget(next) { + exports.createServer(options.target, next); + }, + // + // 2. Create the proxy server + // + function createTarget(next) { + http.createProxyServer(options.proxy, next); + } + ], callback); +}; + +exports.createServer = function (options, callback) { + var server = io.listen(options.port, callback); + + server.sockets.on('connection', function (socket) { + socket.on('incoming', function (data) { + assert.equal(data, options.input); + socket.emit('outgoing', options.output); + }); + }); +}; + diff --git a/test/http/http-proxy-test.js b/test/http/http-proxy-test.js deleted file mode 100644 index d68d681..0000000 --- a/test/http/http-proxy-test.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - node-http-proxy-test.js: http proxy for node.js - - Copyright (c) 2010 Charlie Robbins, Marak Squires and Fedor Indutny - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -var assert = require('assert'), - util = require('util'), - request = require('request'), - vows = require('vows'), - helpers = require('../helpers'); - -var forwardOptions = { - forward: { - port: 8300, - host: 'localhost' - } -}; - -var badForwardOptions = { - forward: { - port: 9000, - host: 'localhost' - } -}; - -var options = helpers.parseProtocol(), - testName = [options.source.protocols.http, options.target.protocols.http].join('-to-'), - runner = new helpers.TestRunner(options); - -vows.describe('node-http-proxy/http-proxy/' + testName).addBatch({ - "When using server created by httpProxy.createServer()": { - "with no latency" : { - "and a valid target server": runner.assertProxied('localhost', 8080, 8081, false, false, function (callback) { - runner.startProxyServer(8080, 8081, 'localhost', callback); - }), - "and without a valid target server": runner.assertResponseCode(8082, 500, function (callback) { - runner.startProxyServer(8082, 9000, 'localhost', callback); - }) - }, - "with latency": { - "and a valid target server": runner.assertProxied('localhost', 8083, 8084, false, false, function (callback) { - runner.startLatentProxyServer(8083, 8084, 'localhost', 1000, callback); - }), - "and without a valid target server": runner.assertResponseCode(8085, 500, function (callback) { - runner.startLatentProxyServer(8085, 9000, 'localhost', 1000, callback); - }) - }, - "with forwarding enabled": { - topic: function () { - runner.startTargetServer(8300, 'forward proxy', this.callback); - }, - "with no latency" : { - "and a valid target server": runner.assertProxied('localhost', 8120, 8121, false, false, function (callback) { - runner.startProxyServerWithForwarding(8120, 8121, 'localhost', forwardOptions, callback); - }), - "and also a valid target server": runner.assertHeaders(8122, "x-forwarded-for", function (callback) { - runner.startProxyServerWithForwarding(8122, 8123, 'localhost', forwardOptions, callback); - }), - "and without a valid forward server": runner.assertProxied('localhost', 8124, 8125, false, false, function (callback) { - runner.startProxyServerWithForwarding(8124, 8125, 'localhost', badForwardOptions, callback); - }) - } - } - } -}).addBatch({ - "When the tests are over": { - topic: function () { - return runner.closeServers(); - }, - "the servers should clean up": function () { - assert.isTrue(true); - } - } -}).export(module); diff --git a/test/http/http-test.js b/test/http/http-test.js new file mode 100644 index 0000000..a8bfc7c --- /dev/null +++ b/test/http/http-test.js @@ -0,0 +1,57 @@ +/* + node-http-proxy-test.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Marak Squires and Fedor Indutny + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +var assert = require('assert'), + fs = require('fs'), + path = require('path'), + async = require('async'), + request = require('request'), + vows = require('vows'), + macros = require('../macros'), + helpers = require('../helpers/index'); + +var routeFile = path.join(__dirname, 'config.json'); + +vows.describe('node-http-proxy/http').addBatch({ + "With a valid target server": { + "and no latency": { + "and no headers": macros.http.assertProxied(), + "and headers": macros.http.assertProxied({ + request: { headers: { host: 'unknown.com' } } + }), + "and forwarding enabled": macros.http.assertForwardProxied() + }, + "and latency": macros.http.assertProxied({ + latency: 2000 + }) + }, + "With a no valid target server": { + "and no latency": macros.http.assertInvalidProxy(), + "and latency": macros.http.assertInvalidProxy({ + latency: 2000 + }) + } +}).export(module); \ No newline at end of file diff --git a/test/http/routing-proxy-test.js b/test/http/routing-proxy-test.js deleted file mode 100644 index 54a25d0..0000000 --- a/test/http/routing-proxy-test.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * proxy-table-test.js: Tests for the ProxyTable object. - * - * (C) 2010, Charlie Robbins - * - */ - -var assert = require('assert'), - fs = require('fs'), - path = require('path'), - util = require('util'), - argv = require('optimist').argv, - request = require('request'), - vows = require('vows'), - helpers = require('../helpers'); - -var options = helpers.parseProtocol(), - testName = [options.source.protocols.http, options.target.protocols.http].join('-to-'), - runner = new helpers.TestRunner(options), - routeFile = path.join(__dirname, 'config.json'); - -var fileOptions = { - router: { - "foo.com": "127.0.0.1:8101", - "bar.com": "127.0.0.1:8102" - } -}; - -var defaultOptions = { - router: { - "foo.com": "127.0.0.1:8091", - "bar.com": "127.0.0.1:8092", - "baz.com/taco": "127.0.0.1:8098", - "pizza.com/taco/muffins": "127.0.0.1:8099", - "blah.com/me": "127.0.0.1:8088/remapped", - "bleh.com/remap/this": "127.0.0.1:8087/remap/remapped", - "test.com/double/tap": "127.0.0.1:8086/remap", - } -}; - -var hostnameOptions = { - hostnameOnly: true, - router: { - "foo.com": "127.0.0.1:8091", - "bar.com": "127.0.0.1:8092" - } -}; - -vows.describe('node-http-proxy/routing-proxy/' + testName).addBatch({ - "When using server created by httpProxy.createServer()": { - "when passed a routing table": { - "and routing by RegExp": { - topic: function () { - this.server = runner.startProxyServerWithTable(8090, defaultOptions, this.callback); - }, - "an incoming request to foo.com": runner.assertProxied('foo.com', 8090, 8091), - "an incoming request to bar.com": runner.assertProxied('bar.com', 8090, 8092), - "an incoming request to baz.com/taco": runner.assertProxied('baz.com', 8090, 8098, "/taco", "/"), - "an incoming request to pizza.com/taco/muffins": runner.assertProxied('pizza.com', 8090, 8099, "/taco/muffins", "/"), - "an incoming request to blah.com/me/fun": runner.assertProxied('blah.com', 8090, 8088, "/me/fun", "/remapped/fun"), - "an incoming request to bleh.com/remap/this": runner.assertProxied('bleh.com', 8090, 8087, "/remap/this", "/remap/remapped"), - "an incoming request to test.com/double/tap/double/tap": runner.assertProxied('test.com', 8090, 8086, "/double/tap/double/tap", "/remap/double/tap"), - "an incoming request to unknown.com": runner.assertResponseCode(8090, 404) - }, - "and routing by Hostname": { - topic: function () { - this.server = runner.startProxyServerWithTable(8093, hostnameOptions, this.callback); - }, - "an incoming request to foo.com": runner.assertProxied('foo.com', 8093, 8094), - "an incoming request to bar.com": runner.assertProxied('bar.com', 8093, 8095), - "an incoming request to unknown.com": runner.assertResponseCode(8093, 404) - } - }, - "when passed a routing file": { - topic: function () { - fs.writeFileSync(routeFile, JSON.stringify(fileOptions)); - this.server = runner.startProxyServerWithTable(8100, { - router: routeFile - }, this.callback); - }, - "an incoming request to foo.com": runner.assertProxied('foo.com', 8100, 8101), - "an incoming request to bar.com": runner.assertProxied('bar.com', 8100, 8102), - "an incoming request to unknown.com": runner.assertResponseCode(8100, 404), - "an incoming request to dynamic.com": { - "after the file has been modified": { - topic: function () { - var that = this, - data = fs.readFileSync(routeFile), - config = JSON.parse(data); - - config.router['dynamic.com'] = "127.0.0.1:8103"; - fs.writeFileSync(routeFile, JSON.stringify(config)); - - this.server.on('routes', function () { - runner.startTargetServer(8103, 'hello dynamic.com', function () { - request({ - method: 'GET', - uri: options.source.protocols.http + '://localhost:8100', - headers: { - host: 'dynamic.com' - } - }, that.callback); - }); - }); - }, - "should receive 'hello dynamic.com'": function (err, res, body) { - assert.equal(body, 'hello dynamic.com'); - } - } - } - } - } -}).addBatch({ - "When using an instance of ProxyTable combined with HttpProxy directly": { - topic: function () { - this.server = runner.startProxyServerWithTableAndLatency(8110, 100, { - router: { - 'foo.com': 'localhost:8111', - 'bar.com': 'localhost:8112' - } - }, this.callback); - }, - "an incoming request to foo.com": runner.assertProxied('foo.com', 8110, 8111), - "an incoming request to bar.com": runner.assertProxied('bar.com', 8110, 8112), - "an incoming request to unknown.com": runner.assertResponseCode(8110, 404) - } -}).addBatch({ - "When the tests are over": { - topic: function () { - //fs.unlinkSync(routeFile); - return runner.closeServers(); - }, - "the servers should clean up": function () { - assert.isTrue(true); - } - } -}).export(module); diff --git a/test/http/routing-table-test.js b/test/http/routing-table-test.js new file mode 100644 index 0000000..432100f --- /dev/null +++ b/test/http/routing-table-test.js @@ -0,0 +1,88 @@ +/* + * routing-table-test.js: Tests for the proxying using the ProxyTable object. + * + * (C) 2010, Charlie Robbins + * + */ + +var assert = require('assert'), + fs = require('fs'), + path = require('path'), + async = require('async'), + request = require('request'), + vows = require('vows'), + macros = require('../macros'), + helpers = require('../helpers/index'); + +var routeFile = path.join(__dirname, 'config.json'); + +vows.describe('node-http-proxy/http/routing-table').addBatch({ + "With a routing table": { + "with latency": macros.http.assertProxiedToRoutes({ + latency: 2000, + routes: { + "icanhaz.com": "127.0.0.1:{PORT}", + "latency.com": "127.0.0.1:{PORT}" + } + }), + "using RegExp": macros.http.assertProxiedToRoutes({ + routes: { + "foo.com": "127.0.0.1:{PORT}", + "bar.com": "127.0.0.1:{PORT}", + "baz.com/taco": "127.0.0.1:{PORT}", + "pizza.com/taco/muffins": "127.0.0.1:{PORT}", + "blah.com/me": "127.0.0.1:{PORT}/remapped", + "bleh.com/remap/this": "127.0.0.1:{PORT}/remap/remapped", + "test.com/double/tap": "127.0.0.1:{PORT}/remap" + } + }), + "using hostnameOnly": macros.http.assertProxiedToRoutes({ + hostnameOnly: true, + routes: { + "foo.com": "127.0.0.1:{PORT}", + "bar.com": "127.0.0.1:{PORT}" + } + }), + "using a routing file": macros.http.assertProxiedToRoutes({ + filename: routeFile, + routes: { + "foo.com": "127.0.0.1:{PORT}", + "bar.com": "127.0.0.1:{PORT}" + } + }, { + "after the file has been modified": { + topic: function () { + var config = JSON.parse(fs.readFileSync(routeFile, 'utf8')), + port = helpers.nextPort, + that = this; + + config.router['dynamic.com'] = "127.0.0.1:" + port; + fs.writeFileSync(routeFile, JSON.stringify(config)); + + async.parallel([ + function waitForRoutes(next) { + that.proxyServer.on('routes', next); + }, + async.apply( + helpers.http.createServer, + { + port: port, + output: 'hello from dynamic.com' + } + ) + ], function () { + request({ + uri: 'http://localhost:' + that.port, + headers: { + host: 'dynamic.com' + } + }, that.callback); + }); + }, + "should receive 'hello from dynamic.com'": function (err, res, body) { + assert.equal(body, 'hello from dynamic.com'); + } + } + }) + } +}).export(module); diff --git a/test/macros/http.js b/test/macros/http.js new file mode 100644 index 0000000..7ce3338 --- /dev/null +++ b/test/macros/http.js @@ -0,0 +1,310 @@ +/* + * http.js: Macros for proxying HTTP requests + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +var assert = require('assert'), + fs = require('fs'), + async = require('async'), + request = require('request'), + helpers = require('../helpers/index'); + +// +// ### function assertRequest (options) +// #### @options {Object} Options for this request assertion. +// #### @request {Object} Options to use for `request`. +// #### @assert {Object} Test assertions against the response. +// +// Makes a request using `options.request` and then asserts the response +// and body against anything in `options.assert`. +// +exports.assertRequest = function (options) { + return { + topic: function () { + // + // Now make the HTTP request and assert. + // + request(options.request, this.callback); + }, + "should succeed": function (err, res, body) { + if (options.assert.body) { + assert.equal(body, options.assert.body); + } + + if (options.assert.statusCode) { + assert.equal(res.statusCode, options.assert.statusCode); + } + } + }; +}; + +// +// ### function assertProxied (options) +// #### @options {Object} Options for this test +// #### @latency {number} Latency in milliseconds for the proxy server. +// #### @ports {Object} Ports for the request (target, proxy). +// #### @output {string} Output to assert from. +// #### @forward {Object} Options for forward proxying. +// +// Creates a complete end-to-end test for requesting against an +// http proxy. +// +exports.assertProxied = function (options) { + options = options || {}; + + var ports = options.ports || helpers.nextPortPair, + output = options.output || 'hello world from ' + ports.target, + req = options.request || {}; + + req.uri = req.uri || 'http://localhost:' + ports.proxy; + + return { + topic: function () { + // + // Create a target server and a proxy server + // using the `options` supplied. + // + helpers.http.createServerPair({ + target: { + output: output, + port: ports.target, + headers: req.headers + }, + proxy: { + latency: options.latency, + port: ports.proxy, + proxy: { + forward: options.forward, + target: { + host: 'localhost', + port: ports.target + } + } + } + }, this.callback); + }, + "the proxy request": exports.assertRequest({ + request: req, + assert: { + body: output + } + }) + }; +}; + +// +// ### function assertInvalidProxy (options) +// #### @options {Object} Options for this test +// #### @latency {number} Latency in milliseconds for the proxy server +// #### @ports {Object} Ports for the request (target, proxy) +// +// Creates a complete end-to-end test for requesting against an +// http proxy with no target server. +// +exports.assertInvalidProxy = function (options) { + options = options || {}; + + var ports = options.ports || helpers.nextPortPair, + req = options.request || {}; + + req.uri = req.uri || 'http://localhost:' + ports.proxy; + + return { + topic: function () { + // + // Only create the proxy server, simulating a reverse-proxy + // to an invalid location. + // + helpers.http.createProxyServer({ + latency: options.latency, + port: ports.proxy, + proxy: { + target: { + host: 'localhost', + port: ports.target + } + } + }, this.callback); + }, + "the proxy request": exports.assertRequest({ + request: req, + assert: { + statusCode: 500 + } + }) + }; +}; + +// +// ### function assertForwardProxied (options) +// #### @options {Object} Options for this test. +// +// Creates a complete end-to-end test for requesting against an +// http proxy with both a valid and invalid forward target. +// +exports.assertForwardProxied = function (options) { + var forwardPort = helpers.nextPort; + + return { + topic: function () { + helpers.http.createServer({ + output: 'hello from forward', + port: forwardPort + }, this.callback) + }, + "and a valid forward target": exports.assertProxied({ + forward: { + port: forwardPort, + host: 'localhost' + } + }), + "and an invalid forward target": exports.assertProxied({ + forward: { + port: 9898, + host: 'localhost' + } + }) + }; +}; + +// +// ### function assertProxiedtoRoutes (options, nested) +// #### @options {Object} Options for this ProxyTable-based test +// #### @routes {Object|string} Routes to use for the proxy. +// #### @hostnameOnly {boolean} Enables hostnameOnly routing. +// #### @nested {Object} Nested vows to add to the returned context. +// +// Creates a complete end-to-end test for requesting against an +// http proxy using `options.routes`: +// +// 1. Creates target servers for all routes in `options.routes.` +// 2. Creates a proxy server. +// 3. Ensure requests to the proxy server for all route targets +// returns the unique expected output. +// +exports.assertProxiedToRoutes = function (options, nested) { + // + // Assign dynamic ports to the routes to use. + // + options.routes = helpers.http.assignPortsToRoutes(options.routes); + + // + // Parse locations from routes for making assertion requests. + // + var locations = helpers.http.parseRoutes(options), + port = helpers.nextPort, + context, + proxy; + + if (options.filename) { + // + // If we've been passed a filename write the routes to it + // and setup the proxy options to use that file. + // + fs.writeFileSync(options.filename, JSON.stringify({ router: options.routes })); + proxy = { router: options.filename }; + } + else { + // + // Otherwise just use the routes themselves. + // + proxy = { + hostnameOnly: options.hostnameOnly, + router: options.routes + }; + } + + // + // Create the test context which creates all target + // servers for all routes and a proxy server. + // + context = { + topic: function () { + var that = this; + + async.waterfall([ + // + // 1. Create all the target servers + // + async.apply( + async.forEach, + locations, + function createRouteTarget(location, next) { + helpers.http.createServer({ + port: location.target.port, + output: 'hello from ' + location.source.href + }, next); + } + ), + // + // 2. Create the proxy server + // + async.apply( + helpers.http.createProxyServer, + { + port: port, + latency: options.latency, + routing: true, + proxy: proxy + } + ) + ], function (_, server) { + // + // 3. Set the proxy server for later use + // + that.proxyServer = server; + that.callback(); + }); + + // + // 4. Assign the port to the context for later use + // + this.port = port; + }, + // + // Add an extra assertion to a route which + // should respond with 404 + // + "a request to unknown.com": exports.assertRequest({ + assert: { statusCode: 404 }, + request: { + uri: 'http://localhost:' + port, + headers: { + host: 'unknown.com' + } + } + }) + }; + + // + // Add test assertions for each of the route locations. + // + locations.forEach(function (location) { + context[location.source.href] = exports.assertRequest({ + request: { + uri: 'http://localhost:' + port + location.source.path, + headers: { + host: location.source.hostname + } + }, + assert: { + body: 'hello from ' + location.source.href + } + }); + }); + + // + // If there are any nested vows to add to the context + // add them before returning the full context. + // + if (nested) { + Object.keys(nested).forEach(function (key) { + context[key] = nested[key]; + }); + } + + return context; +}; \ No newline at end of file diff --git a/test/macros/index.js b/test/macros/index.js new file mode 100644 index 0000000..72d3249 --- /dev/null +++ b/test/macros/index.js @@ -0,0 +1,10 @@ +/* + * index.js: Top level include for node-http-proxy macros + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +exports.http = require('./http'); +exports.ws = require('./ws'); \ No newline at end of file diff --git a/test/macros/ws.js b/test/macros/ws.js new file mode 100644 index 0000000..92c2b61 --- /dev/null +++ b/test/macros/ws.js @@ -0,0 +1,62 @@ +/* + * ws.js: Macros for proxying Websocket requests + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +var assert = require('assert'), + io = require('socket.io-client'), + helpers = require('../helpers/index'); + +// +// ### function assertProxied (options) +// #### @options {Object} Options for this test +// #### @latency {number} Latency in milliseconds for the proxy server. +// #### @ports {Object} Ports for the request (target, proxy). +// #### @input {string} Input to assert sent to the target ws server. +// #### @output {string} Output to assert from the taget ws server. +// +// Creates a complete end-to-end test for requesting against an +// http proxy. +// +exports.assertProxied = function (options) { + options = options || {}; + + var ports = options.ports || helpers.nextPortPair, + input = options.input || 'hello world to ' + ports.target, + output = options.output || 'hello world from ' + ports.target; + + return { + topic: function () { + helpers.ws.createServerPair({ + target: { + input: input, + output: output, + port: ports.target + }, + proxy: { + latency: options.latency, + port: ports.proxy, + proxy: { + target: { + host: 'localhost', + port: ports.target + } + } + } + }, this.callback); + }, + "the proxy WebSocket": { + topic: function () { + var socket = io.connect('http://localhost:' + ports.proxy); + socket.on('outgoing', this.callback.bind(this, null)); + socket.emit('incoming', input); + }, + "should send input and receive output": function (_, data) { + assert.equal(data, output); + } + } + }; +}; \ No newline at end of file diff --git a/test/websocket/websocket-proxy-test.js b/test/websocket/websocket-proxy-test.js deleted file mode 100644 index 2f23e77..0000000 --- a/test/websocket/websocket-proxy-test.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - node-http-proxy-test.js: http proxy for node.js - - Copyright (c) 2010 Charlie Robbins, Marak Squires and Fedor Indutny - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -var util = require('util'), - assert = require('assert'), - argv = require('optimist').argv, - colors = require('colors'), - request = require('request'), - vows = require('vows'), - websocket = require('../../vendor/websocket'), - helpers = require('../helpers'); - -try { - var utils = require('socket.io/lib/socket.io/utils'), - io = require('socket.io'); -} -catch (ex) { - console.error('Socket.io is required for this example:'); - console.error('npm ' + 'install'.green + ' socket.io@0.6.17'.magenta); - process.exit(1); -} - -var options = helpers.parseProtocol(), - testName = [options.source.protocols.ws, options.target.protocols.ws].join('-to-'), - runner = new helpers.TestRunner(options); - -vows.describe('node-http-proxy/http-proxy/' + testName).addBatch({ - "When using server created by httpProxy.createServer()": { - "with no latency" : { - "when an inbound message is sent from a WebSocket client": { - topic: function () { - var that = this - headers = {}; - - runner.webSocketTest({ - io: io, - host: 'localhost', - wsprotocol: options.source.protocols.ws, - protocol: options.source.protocols.http, - ports: { - target: 8130, - proxy: 8131 - }, - onListen: function (socket) { - socket.on('connection', function (client) { - client.on('message', function (msg) { - that.callback(null, msg, headers); - }); - }); - }, - onWsupgrade: function (req, res) { - headers.request = req; - headers.response = res.headers; - }, - onOpen: function (ws) { - ws.send(utils.encode('from client')); - } - }); - }, - "the target server should receive the message": function (err, msg, headers) { - assert.equal(msg, 'from client'); - }, - "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { - assert.isString(headers.response['sec-websocket-location']); - assert.isTrue(headers.response['sec-websocket-location'].indexOf(options.source.protocols.ws) !== -1); - assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); - } - }, - "when an inbound message is sent from a WebSocket client with event listeners": { - topic: function () { - var that = this - headers = {}; - - runner.webSocketTest({ - io: io, - host: 'localhost', - wsprotocol: options.source.protocols.ws, - protocol: options.source.protocols.http, - ports: { - target: 8132, - proxy: 8133 - }, - onServer: function (server) { - server.proxy.on('websocket:incoming', function (req, socket, head, data) { - that.callback(null, data); - }); - }, - onOpen: function (ws) { - ws.send(utils.encode('from client')); - } - }); - }, - "should raise the `websocket:incoming` event": function (ign, data) { - assert.equal(utils.decode(data.toString().replace('\u0000', '')), 'from client'); - }, - }, - "when an outbound message is sent from the target server": { - topic: function () { - var that = this, - headers = {}; - - runner.webSocketTest({ - io: io, - host: 'localhost', - wsprotocol: options.source.protocols.ws, - protocol: options.source.protocols.http, - ports: { - target: 8134, - proxy: 8135 - }, - onListen: function (socket) { - socket.on('connection', function (client) { - socket.broadcast('from server'); - }); - }, - onWsupgrade: function (req, res) { - headers.request = req; - headers.response = res.headers; - }, - onMessage: function (msg) { - msg = utils.decode(msg); - if (!/\d+/.test(msg)) { - that.callback(null, msg, headers); - } - } - }); - }, - "the client should receive the message": function (err, msg, headers) { - assert.equal(msg, 'from server'); - }, - "the origin and sec-websocket-origin headers should match": function (err, msg, headers) { - assert.isString(headers.response['sec-websocket-location']); - assert.isTrue(headers.response['sec-websocket-location'].indexOf(options.source.protocols.ws) !== -1); - assert.equal(headers.request.Origin, headers.response['sec-websocket-origin']); - } - } - } - } -}).addBatch({ - "When the tests are over": { - topic: function () { - return runner.closeServers(); - }, - "the servers should clean up": function () { - assert.isTrue(true); - } - } -}).export(module); diff --git a/test/websocket/websocket-routing-proxy-test.js b/test/ws/routing-table-test.js similarity index 100% rename from test/websocket/websocket-routing-proxy-test.js rename to test/ws/routing-table-test.js diff --git a/test/ws/socket.io-test.js b/test/ws/socket.io-test.js new file mode 100644 index 0000000..e7510e5 --- /dev/null +++ b/test/ws/socket.io-test.js @@ -0,0 +1,20 @@ +/* + * socket.io-test.js: Test for proxying `socket.io` requests. + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ + +var vows = require('vows'), + macros = require('../macros'), + helpers = require('../helpers/index'); + +vows.describe('node-http-proxy/ws').addBatch({ + "With a valid target server": { + "and no latency": macros.ws.assertProxied(), + // "and latency": macros.websocket.assertProxied({ + // latency: 2000 + // }) + } +}).export(module); \ No newline at end of file diff --git a/test/ws/ws-test.js b/test/ws/ws-test.js new file mode 100644 index 0000000..5325980 --- /dev/null +++ b/test/ws/ws-test.js @@ -0,0 +1,7 @@ +/* + * ws-test.js: Tests for proxying raw Websocket requests. + * + * (C) 2010 Nodejitsu Inc. + * MIT LICENCE + * + */ \ No newline at end of file diff --git a/vendor/websocket.js b/vendor/websocket.js deleted file mode 100644 index b8c3b92..0000000 --- a/vendor/websocket.js +++ /dev/null @@ -1,636 +0,0 @@ -/* - * Copyright (c) 2010, Peter Griess - * https://github.com/pgriess/node-websocket-client - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * * Neither the name of node-websocket-client nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -var assert = require('assert'); -var buffer = require('buffer'); -var crypto = require('crypto'); -var events = require('events'); -var http = require('http'); -var https = require('https'); -var net = require('net'); -var urllib = require('url'); -var util = require('util'); - -var FRAME_NO = 0; -var FRAME_LO = 1; -var FRAME_HI = 2; - -// Values for readyState as per the W3C spec -var CONNECTING = 0; -var OPEN = 1; -var CLOSING = 2; -var CLOSED = 3; - -var debugLevel = parseInt(process.env.NODE_DEBUG, 16); -var debug = (debugLevel & 0x4) ? - function() { util.error.apply(this, arguments); } : - function() { }; - -// Generate a Sec-WebSocket-* value -var createSecretKey = function() { - // How many spaces will we be inserting? - var numSpaces = 1 + Math.floor(Math.random() * 12); - assert.ok(1 <= numSpaces && numSpaces <= 12); - - // What is the numerical value of our key? - var keyVal = (Math.floor( - Math.random() * (4294967295 / numSpaces) - ) * numSpaces); - - // Our string starts with a string representation of our key - var s = keyVal.toString(); - - // Insert 'numChars' worth of noise in the character ranges - // [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters) - var numChars = 1 + Math.floor(Math.random() * 12); - assert.ok(1 <= numChars && numChars <= 12); - - for (var i = 0; i < numChars; i++) { - var pos = Math.floor(Math.random() * s.length + 1); - - var c = Math.floor(Math.random() * (14 + 68)); - c = (c <= 14) ? - String.fromCharCode(c + 0x21) : - String.fromCharCode((c - 14) + 0x3a); - - s = s.substring(0, pos) + c + s.substring(pos, s.length); - } - - // We shoudln't have any spaces in our value until we insert them - assert.equal(s.indexOf(' '), -1); - - // Insert 'numSpaces' worth of spaces - for (var i = 0; i < numSpaces; i++) { - var pos = Math.floor(Math.random() * (s.length - 1)) + 1; - s = s.substring(0, pos) + ' ' + s.substring(pos, s.length); - } - - assert.notEqual(s.charAt(0), ' '); - assert.notEqual(s.charAt(s.length), ' '); - - return s; -}; - -// Generate a challenge sequence -var createChallenge = function() { - var c = ''; - for (var i = 0; i < 8; i++) { - c += String.fromCharCode(Math.floor(Math.random() * 255)); - } - - return c; -}; - -// Get the value of a secret key string -// -// This strips non-digit values and divides the result by the number of -// spaces found. -var secretKeyValue = function(sk) { - var ns = 0; - var v = 0; - - for (var i = 0; i < sk.length; i++) { - var cc = sk.charCodeAt(i); - - if (cc == 0x20) { - ns++; - } else if (0x30 <= cc && cc <= 0x39) { - v = v * 10 + cc - 0x30; - } - } - - return Math.floor(v / ns); -} - -// Get the to-be-hashed value of a secret key string -// -// This takes the result of secretKeyValue() and encodes it in a big-endian -// byte string -var secretKeyHashValue = function(sk) { - var skv = secretKeyValue(sk); - - var hv = ''; - hv += String.fromCharCode((skv >> 24) & 0xff); - hv += String.fromCharCode((skv >> 16) & 0xff); - hv += String.fromCharCode((skv >> 8) & 0xff); - hv += String.fromCharCode((skv >> 0) & 0xff); - - return hv; -}; - -// Compute the secret key signature based on two secret key strings and some -// handshaking data. -var computeSecretKeySignature = function(s1, s2, hs) { - assert.equal(hs.length, 8); - - var hash = crypto.createHash('md5'); - - hash.update(secretKeyHashValue(s1)); - hash.update(secretKeyHashValue(s2)); - hash.update(hs); - - return hash.digest('binary'); -}; - -// Return a hex representation of the given binary string; used for debugging -var str2hex = function(str) { - var hexChars = [ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f' - ]; - - var out = ''; - for (var i = 0; i < str.length; i++) { - var c = str.charCodeAt(i); - out += hexChars[(c & 0xf0) >>> 4]; - out += hexChars[c & 0x0f]; - out += ' '; - } - - return out.trim(); -}; - -// Set a constant on the given object -var setConstant = function(obj, name, value) { - Object.defineProperty(obj, name, { - get : function() { - return value; - } - }); -}; - -// WebSocket object -// -// This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/ -// -// N.B. Arguments are parsed in the anonymous function at the bottom of the -// constructor. -var WebSocket = function(url, proto, opts) { - events.EventEmitter.call(this); - - // Retain a reference to our object - var self = this; - - // State of our end of the connection - var readyState = CONNECTING; - - // Whether or not the server has sent a close handshake - var serverClosed = false; - - // Our underlying net.Stream instance - var stream = undefined; - - opts = opts || { - origin : 'http://www.example.com' - }; - - // Frame parsing functions - // - // These read data from the given buffer starting at the given offset, - // looking for the end of the current frame. If found, the current frame is - // emitted and the function returns. Only a single frame is processed at a - // time. - // - // The number of bytes read to complete a frame is returned, which the - // caller is to use to advance along its buffer. If 0 is returned, no - // completed frame bytes were found, and the caller should probably enqueue - // the buffer as a continuation of the current message. If a complete frame - // is read, the function is responsible for resting 'frameType'. - - // Framing data - var frameType = FRAME_NO; - var bufs = []; - var bufsBytes = 0; - - // Frame-parsing functions - var frameFuncs = [ - // FRAME_NO - function(buf, off) { - if (buf[off] & 0x80) { - frameType = FRAME_HI; - } else { - frameType = FRAME_LO; - } - - return 1; - }, - - // FRAME_LO - function(buf, off) { - debug('frame_lo(' + util.inspect(buf) + ', ' + off + ')'); - - // Find the first instance of 0xff, our terminating byte - for (var i = off; i < buf.length && buf[i] != 0xff; i++) - ; - - // We didn't find a terminating byte - if (i >= buf.length) { - return 0; - } - - // We found a terminating byte; collect all bytes into a single buffer - // and emit it - var mb = null; - if (bufs.length == 0) { - mb = buf.slice(off, i); - } else { - mb = new buffer.Buffer(bufsBytes + i); - - var mbOff = 0; - bufs.forEach(function(b) { - b.copy(mb, mbOff, 0, b.length); - mbOff += b.length; - }); - - assert.equal(mbOff, bufsBytes); - - // Don't call Buffer.copy() if we're coping 0 bytes. Rather - // than being a no-op, this will trigger a range violation on - // the destination. - if (i > 0) { - buf.copy(mb, mbOff, off, i); - } - - // We consumed all of the buffers that we'd been saving; clear - // things out - bufs = []; - bufsBytes = 0; - } - - process.nextTick(function() { - var b = mb; - return function() { - var m = b.toString('utf8'); - - self.emit('data', b); - self.emit('message', m); // wss compat - - if (self.onmessage) { - self.onmessage({data: m}); - } - }; - }()); - - frameType = FRAME_NO; - return i - off + 1; - }, - - // FRAME_HI - function(buf, off) { - debug('frame_hi(' + util.inspect(buf) + ', ' + off + ')'); - - if (buf[off] !== 0) { - throw new Error('High-byte framing not supported.'); - } - - serverClosed = true; - return 1; - } - ]; - - // Handle data coming from our socket - var dataListener = function(buf) { - if (buf.length <= 0 || serverClosed) { - return; - } - - debug('dataListener(' + util.inspect(buf) + ')'); - - var off = 0; - var consumed = 0; - - do { - if (frameType < 0 || frameFuncs.length <= frameType) { - throw new Error('Unexpected frame type: ' + frameType); - } - - assert.equal(bufs.length === 0, bufsBytes === 0); - assert.ok(off < buf.length); - - consumed = frameFuncs[frameType](buf, off); - off += consumed; - } while (!serverClosed && consumed > 0 && off < buf.length); - - if (serverClosed) { - serverCloseHandler(); - } - - if (consumed == 0) { - bufs.push(buf.slice(off, buf.length)); - bufsBytes += buf.length - off; - } - }; - - // Handle incoming file descriptors - var fdListener = function(fd) { - self.emit('fd', fd); - }; - - // Handle errors from any source (HTTP client, stream, etc) - var errorListener = function(e) { - process.nextTick(function() { - self.emit('wserror', e); - - if (self.onerror) { - self.onerror(e); - } - }); - }; - - // Finish the closing process; destroy the socket and tell the application - // that we've closed. - var finishClose = function() { - readyState = CLOSED; - - if (stream) { - stream.end(); - stream.destroy(); - stream = undefined; - } - - process.nextTick(function() { - self.emit('close'); - if (self.onclose) { - self.onclose(); - } - }); - }; - - // Send a close frame to the server - var sendClose = function() { - assert.equal(OPEN, readyState); - - readyState = CLOSING; - stream.write('\xff\x00', 'binary'); - }; - - // Handle a close packet sent from the server - var serverCloseHandler = function() { - assert.ok(serverClosed); - assert.ok(readyState === OPEN || readyState === CLOSING); - - bufs = []; - bufsBytes = 0; - - // Handle state transitions asynchronously so that we don't change - // readyState before the application has had a chance to process data - // events which are already in the delivery pipeline. For example, a - // 'data' event could be delivered with a readyState of CLOSING if we - // received both frames in the same packet. - process.nextTick(function() { - if (readyState === OPEN) { - sendClose(); - } - - finishClose(); - }); - }; - - // External API - self.close = function(timeout) { - if (readyState === CONNECTING) { - // If we're still in the process of connecting, the server is not - // in a position to understand our close frame. Just nuke the - // connection and call it a day. - finishClose(); - } else if (readyState === OPEN) { - sendClose(); - - if (timeout) { - setTimeout(finishClose, timeout * 1000); - } - } - }; - - self.send = function(str, fd) { - if (readyState != OPEN) { - return; - } - - stream.write('\x00', 'binary'); - stream.write(str, 'utf8', fd); - stream.write('\xff', 'binary'); - }; - - // wss compat - self.write = self.send; - - setConstant(self, 'url', url); - - Object.defineProperty(self, 'readyState', { - get : function() { - return readyState; - } - }); - - // Connect and perform handshaking with the server - (function() { - // Parse constructor arguments - if (!url) { - throw new Error('Url and must be specified.'); - } - - // Secrets used for handshaking - var key1 = createSecretKey(); - var key2 = createSecretKey(); - var challenge = createChallenge(); - - debug( - 'key1=\'' + str2hex(key1) + '\'; ' + - 'key2=\'' + str2hex(key2) + '\'; ' + - 'challenge=\'' + str2hex(challenge) + '\'' - ); - - var httpHeaders = { - 'Connection' : 'Upgrade', - 'Upgrade' : 'WebSocket', - 'Sec-WebSocket-Key1' : key1, - 'Sec-WebSocket-Key2' : key2 - }; - if (opts.origin) { - httpHeaders['Origin'] = opts.origin; - } - if (proto) { - httpHeaders['Sec-WebSocket-Protocol'] = proto; - } - - var httpPath = '/'; - - // Create the HTTP client that we'll use for handshaking. We'll cannabalize - // its socket via the 'upgrade' event and leave it to rot. - // - // N.B. The ws+unix:// scheme makes use of the implementation detail - // that http.Client passes its constructor arguments through, - // un-inspected to net.Stream.connect(). The latter accepts a - // string as its first argument to connect to a UNIX socket. - var protocol, agent, port, u = urllib.parse(url); - if (u.protocol === 'ws:' || u.protocol === 'wss:') { - protocol = u.protocol === 'ws:' ? http : https; - port = u.protocol === 'ws:' ? 80 : 443; - agent = u.protocol === new protocol.Agent({ - host: u.hostname, - port: u.port || port - }); - - httpPath = (u.pathname || '/') + (u.search || ''); - httpHeaders.Host = u.hostname + (u.port ? (":" + u.port) : ""); - } - else if (urlScheme === 'ws+unix') { - throw new Error('ws+unix is not implemented'); - // var sockPath = url.substring('ws+unix://'.length, url.length); - // httpClient = http.createClient(sockPath); - // httpHeaders.Host = 'localhost'; - } - else { - throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.'); - } - - var httpReq = protocol.request({ - host: u.hostname, - method: 'GET', - agent: agent, - port: u.port, - path: httpPath, - headers: httpHeaders - }); - - httpReq.on('error', function (e) { - errorListener(e); - }); - - httpReq.on('upgrade', (function() { - var data = undefined; - - return function(res, s, head) { - stream = s; - - // - // Emit the `wsupgrade` event to inspect the raw - // arguments returned from the websocket request. - // - self.emit('wsupgrade', httpHeaders, res, s, head); - - stream.on('data', function(d) { - if (d.length <= 0) { - return; - } - - if (!data) { - data = d; - } else { - var data2 = new buffer.Buffer(data.length + d.length); - - data.copy(data2, 0, 0, data.length); - d.copy(data2, data.length, 0, d.length); - - data = data2; - } - - if (data.length >= 16) { - var expected = computeSecretKeySignature(key1, key2, challenge); - var actual = data.slice(0, 16).toString('binary'); - - // Handshaking fails; we're donezo - if (actual != expected) { - debug( - 'expected=\'' + str2hex(expected) + '\'; ' + - 'actual=\'' + str2hex(actual) + '\'' - ); - - process.nextTick(function() { - // N.B. Emit 'wserror' here, as 'error' is a reserved word in the - // EventEmitter world, and gets thrown. - self.emit( - 'wserror', - new Error('Invalid handshake from server:' + - 'expected \'' + str2hex(expected) + '\', ' + - 'actual \'' + str2hex(actual) + '\'' - ) - ); - - if (self.onerror) { - self.onerror(); - } - - finishClose(); - }); - } - - // - // Un-register our data handler and add the one to be used - // for the normal, non-handshaking case. If we have extra - // data left over, manually fire off the handler on - // whatever remains. - // - stream.removeAllListeners('data'); - stream.on('data', dataListener); - - readyState = OPEN; - - process.nextTick(function() { - self.emit('open'); - - if (self.onopen) { - self.onopen(); - } - }); - - // Consume any leftover data - if (data.length > 16) { - stream.emit('data', data.slice(16, data.length)); - } - } - }); - stream.on('fd', fdListener); - stream.on('error', errorListener); - stream.on('close', function() { - errorListener(new Error('Stream closed unexpectedly.')); - }); - - stream.emit('data', head); - }; - })()); - - httpReq.write(challenge, 'binary'); - httpReq.end(); - })(); -}; -util.inherits(WebSocket, events.EventEmitter); -exports.WebSocket = WebSocket; - -// Add some constants to the WebSocket object -setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING); -setConstant(WebSocket.prototype, 'OPEN', OPEN); -setConstant(WebSocket.prototype, 'CLOSING', CLOSING); -setConstant(WebSocket.prototype, 'CLOSED', CLOSED); - -// vim:ts=4 sw=4 et \ No newline at end of file