diff --git a/examples/basic-proxy.js b/examples/basic-proxy.js new file mode 100644 index 0000000..394c5e5 --- /dev/null +++ b/examples/basic-proxy.js @@ -0,0 +1,47 @@ +/* + demo.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires. + + 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'), + colors = require('colors') + http = require('http'), + httpProxy = require('./../lib/node-http-proxy'); + +// +// Basic Http Proxy Server +// +httpProxy.createServer(9000, 'localhost').listen(8000); + +// +// Target Http Server +// +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); + +util.puts('http proxy server'.blue + ' started '.green.bold + 'on port '.blue + '8000'.yellow); +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); diff --git a/demo.js b/examples/forward-proxy.js similarity index 55% rename from demo.js rename to examples/forward-proxy.js index d3c17db..cdc9d10 100644 --- a/demo.js +++ b/examples/forward-proxy.js @@ -27,47 +27,10 @@ var util = require('util'), colors = require('colors') http = require('http'), - httpProxy = require('./lib/node-http-proxy'); - -// ascii art from http://github.com/marak/asciimo -var welcome = '\ -# # ##### ##### ##### ##### ##### #### # # # # \n\ -# # # # # # # # # # # # # # # # \n\ -###### # # # # ##### # # # # # # ## # \n\ -# # # # ##### ##### ##### # # ## # \n\ -# # # # # # # # # # # # # \n\ -# # # # # # # # #### # # # \n'; -util.puts(welcome.rainbow.bold); - -// -// Basic Http Proxy Server -// -httpProxy.createServer(9000, 'localhost').listen(8000); -util.puts('http proxy server'.blue + ' started '.green.bold + 'on port '.blue + '8000'.yellow); - -// -// Http Proxy Server with Proxy Table -// -httpProxy.createServer({ - router: { - 'localhost': 'localhost:9000' - } -}).listen(8001); -util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8001 '.yellow + 'with proxy table'.magenta.underline) - -// -// Http Proxy Server with Latency -// -httpProxy.createServer(function (req, res, proxy) { - var buffer = proxy.buffer(req); - setTimeout(function() { - proxy.proxyRequest(req, res, 9000, 'localhost', buffer); - }, 200) -}).listen(8002); -util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8002 '.yellow + 'with latency'.magenta.underline); - -// + httpProxy = require('./../lib/node-http-proxy'); + // +// Setup proxy server with forwarding // httpProxy.createServer(9000, 'localhost', { forward: { @@ -75,19 +38,6 @@ httpProxy.createServer(9000, 'localhost', { host: 'localhost' } }).listen(8003); -util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with forward proxy'.magenta.underline) - -// -// Http Server with proxyRequest Handler and Latency -// -var standAloneProxy = new httpProxy.HttpProxy(); -http.createServer(function (req, res) { - var buffer = standAloneProxy.buffer(req); - setTimeout(function() { - proxy.proxyRequest(req, res, 9000, 'localhost', buffer); - }, 200); -}).listen(8004); -util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8004 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta); // // Target Http Server @@ -97,7 +47,6 @@ http.createServer(function (req, res) { res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); res.end(); }).listen(9000); -util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); // // Target Http Forwarding Server @@ -108,4 +57,7 @@ http.createServer(function (req, res) { res.write('request successfully forwarded to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); res.end(); }).listen(9001); + +util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8003 '.yellow + 'with forward proxy'.magenta.underline) +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); util.puts('http forward server '.blue + 'started '.green.bold + 'on port '.blue + '9001 '.yellow); diff --git a/examples/latent-proxy.js b/examples/latent-proxy.js new file mode 100644 index 0000000..ee7bf67 --- /dev/null +++ b/examples/latent-proxy.js @@ -0,0 +1,52 @@ +/* + demo.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires. + + 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'), + colors = require('colors') + http = require('http'), + httpProxy = require('./../lib/node-http-proxy'); + +// +// Http Proxy Server with Latency +// +httpProxy.createServer(function (req, res, proxy) { + var buffer = proxy.buffer(req); + setTimeout(function() { + proxy.proxyRequest(req, res, 9000, 'localhost', buffer); + }, 200) +}).listen(8002); + +// +// Target Http Server +// +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); + +util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8002 '.yellow + 'with latency'.magenta.underline); +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); \ No newline at end of file diff --git a/examples/proxy-table.js b/examples/proxy-table.js new file mode 100644 index 0000000..7934995 --- /dev/null +++ b/examples/proxy-table.js @@ -0,0 +1,51 @@ +/* + demo.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires. + + 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'), + colors = require('colors') + http = require('http'), + httpProxy = require('./../lib/node-http-proxy'); + +// +// Http Proxy Server with Proxy Table +// +httpProxy.createServer({ + router: { + 'localhost': 'localhost:9000' + } +}).listen(8001); + +// +// Target Http Server +// +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); + +util.puts('http proxy server '.blue + 'started '.green.bold + 'on port '.blue + '8001 '.yellow + 'with proxy table'.magenta.underline) +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); diff --git a/examples/standalone-proxy.js b/examples/standalone-proxy.js new file mode 100644 index 0000000..06e98c8 --- /dev/null +++ b/examples/standalone-proxy.js @@ -0,0 +1,53 @@ +/* + demo.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires. + + 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'), + colors = require('colors') + http = require('http'), + httpProxy = require('./../lib/node-http-proxy'); + +// +// Http Server with proxyRequest Handler and Latency +// +var proxy = new httpProxy.HttpProxy(); +http.createServer(function (req, res) { + var buffer = proxy.buffer(req); + setTimeout(function() { + proxy.proxyRequest(req, res, 9000, 'localhost', buffer); + }, 200); +}).listen(8004); + +// +// Target Http Server +// +http.createServer(function (req, res) { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2)); + res.end(); +}).listen(9000); + +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '8004 '.yellow + 'with proxyRequest handler'.cyan.underline + ' and latency'.magenta); +util.puts('http server '.blue + 'started '.green.bold + 'on port '.blue + '9000 '.yellow); \ No newline at end of file diff --git a/examples/web-socket-proxy.js b/examples/web-socket-proxy.js new file mode 100644 index 0000000..e9a59ef --- /dev/null +++ b/examples/web-socket-proxy.js @@ -0,0 +1,74 @@ +/* + demo.js: http proxy for node.js + + Copyright (c) 2010 Charlie Robbins, Mikeal Rogers, Fedor Indutny, & Marak Squires. + + 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 sys = require('sys'), + http = require('http'), + websocket = require('./websocket'), + utils = require('socket.io/lib/socket.io/utils'), + io = require('socket.io'), + httpProxy = require('./../lib/node-http-proxy'); + +// +// Create the target HTTP server +// +var server = http.createServer(function (req, res) { + res.writeHead(200); + res.end(); +}); +server.listen(8080); + +// +// Setup socket.io on the target HTTP server +// +var socket = io.listen(server); +socket.on('connection', function (client) { + sys.debug('Got websocket connection'); + + client.on('message', function (msg) { + sys.debug('Got message from client: ' + msg); + }); + + socket.broadcast('from server'); +}); + +// +// Create a proxy server with node-http-proxy +// +var proxy = httpProxy.createServer(8080, 'localhost'); +proxy.listen(8081); + +// +// Setup the web socket against our proxy +// +var ws = new websocket.WebSocket('ws://localhost:8081/socket.io/websocket/', 'borf'); + +ws.on('open', function () { + ws.send(utils.encode('from client')); +}); + +ws.on('message', function (msg) { + sys.debug('Got message: ' + utils.decode(msg)); +}); diff --git a/examples/websocket.js b/examples/websocket.js new file mode 100644 index 0000000..82c0297 --- /dev/null +++ b/examples/websocket.js @@ -0,0 +1,631 @@ +/* + * 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 net = require('net'); +var urllib = require('url'); +var sys = require('sys'); + +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() { sys.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(); +}; + +// Get the scheme for a URL, undefined if none is found +var getUrlScheme = function(url) { + var i = url.indexOf(':'); + if (i == -1) { + return undefined; + } + + return url.substring(0, i); +}; + +// 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(' + sys.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(' + sys.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(' + sys.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 httpClient = undefined; + switch (getUrlScheme(url)) { + case 'ws': + var u = urllib.parse(url); + httpClient = http.createClient(u.port || 80, u.hostname); + httpPath = (u.pathname || '/') + (u.search || ''); + httpHeaders.Host = u.hostname + (u.port ? (":" + u.port) : ""); + break; + + case 'ws+unix': + var sockPath = url.substring('ws+unix://'.length, url.length); + httpClient = http.createClient(sockPath); + httpHeaders.Host = 'localhost'; + break; + + default: + throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.'); + } + + httpClient.on('upgrade', (function() { + var data = undefined; + + return function(req, s, head) { + stream = s; + + 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. + // + // XXX: This is lame. We should only remove the listeners + // that we added. + httpClient.removeAllListeners('upgrade'); + 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); + }; + })()); + httpClient.on('error', function(e) { + httpClient.end(); + errorListener(e); + }); + + var httpReq = httpClient.request(httpPath, httpHeaders); + + httpReq.write(challenge, 'binary'); + httpReq.end(); + })(); +}; +sys.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