From 9d2c84edfda092c693810a550e3ce2d86cf88003 Mon Sep 17 00:00:00 2001 From: Ben Polidore Date: Sat, 24 Dec 2011 14:42:52 -0500 Subject: [PATCH 1/2] This branch does not implement a websocket. It just implements a hook.io bridge to websockets using socket.io-client. I had to use a branch that is pending a pull request with learnboost, but hopefully they will accept it soon, and this can be the standard. --- .gitmodules | 3 + 3rd/socket.io-client | 1 + README.md | 18 +- bin/ws | 2 +- lib/websocketModule.js | 599 ----------------------------------------- lib/ws.js | 13 +- 6 files changed, 23 insertions(+), 613 deletions(-) create mode 100644 .gitmodules create mode 160000 3rd/socket.io-client delete mode 100644 lib/websocketModule.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e74e87c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "3rd/socket.io-client"] + path = 3rd/socket.io-client + url = https://github.com/kazuyukitanimura/socket.io-client.git diff --git a/3rd/socket.io-client b/3rd/socket.io-client new file mode 160000 index 0000000..e69fc78 --- /dev/null +++ b/3rd/socket.io-client @@ -0,0 +1 @@ +Subproject commit e69fc7811197506120ee0cc97a9095eb64aa6b4b diff --git a/README.md b/README.md index 2b45fe0..3ab0222 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -# Node.js Compatibility -There is some issues with the code from Peter, and with the new improvements on node.js 0.6 this lib __don't work__ correctly.Work fine only with node.js 0.4 +# Changes -Please be sure of use node.js 0.4 (recommended 0.4.12) before use this lib. +I made this fork to reuse socket.io-client. It wasn't as simple as I'd hoped because LearnBoost hasn't updated the official branch with WSS/HTTPS functionality. For this reason, I am referencing the [kazuyukitanimura](https://github.com/kazuyukitanimura/socket.io-client) branch of socket.io-client. -## hook.io-ws +# hook.io-ws Hook listening messages on *WebSocket* client connection. Based on [Peter Griess](https://github.com/pgriess/node-websocket-client) work. [WebSocket Nodejs Client](https://github.com/pgriess/node-websocket-client) @@ -18,7 +17,12 @@ Based on [Peter Griess](https://github.com/pgriess/node-websocket-client) work. ## Installation - npm install hook.io-ws + +I don't want to pollute npm since this is a branch, so for now, here is how you get it to work: + +- clone this repo +- cd into it +- npm install 3rd/socket.io-client ## Usage hookio-ws @@ -34,7 +38,7 @@ var WebSocketHook = require('hook.io-ws').HookSocket; var mtgoxTest = new WebSocketHook({ name: 'mtgox', - url:'ws://websocket.mtgox.com/mtgox', + url:'https://socketio.mtgox.com/mtgox', debug:false }); @@ -61,7 +65,7 @@ var MtgoxHook = exports.MtgoxHook = function(options){ "name": "mtgox-ws-hook", "type": "ws", // Doing this. Hook.io will look for hook.io-ws "debug": "false", - "url": "ws://websocket.mtgox.com/mtgox" + "url":'https://socketio.mtgox.com/mtgox', }], function(err){ // spawn ready self.on('*::websocket::message',function(message){ diff --git a/bin/ws b/bin/ws index 4d79a05..0033792 100755 --- a/bin/ws +++ b/bin/ws @@ -4,7 +4,7 @@ var WebSocketHook = require('../lib/ws').HookSocket; var mtgoxTest = new WebSocketHook({ name: 'mtgox', - url:'ws://websocket.mtgox.com/mtgox', + url:'https://socketio.mtgox.com/mtgox', debug:false }); diff --git a/lib/websocketModule.js b/lib/websocketModule.js deleted file mode 100644 index a015708..0000000 --- a/lib/websocketModule.js +++ /dev/null @@ -1,599 +0,0 @@ -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 diff --git a/lib/ws.js b/lib/ws.js index 003ec6f..febd087 100644 --- a/lib/ws.js +++ b/lib/ws.js @@ -3,28 +3,29 @@ */ var Hook = require('hook.io').Hook, util = require('util'), - WebSocket = require('./websocketModule').WebSocket; + io = require('socket.io-client') var HookSocket = exports.HookSocket = function(options){ Hook.call(this, options); var self = this; self.on('hook::ready', function(){ - var ws = new WebSocket(options.url); + var ws = io.connect(options.url); - ws.on('open', function(){ + ws.on('connect', function(){ self.emit('websocket::connected'); }); ws.on('message', function(msg){ - self.emit('websocket::message',JSON.parse(msg)); + console.log(msg); + self.emit('websocket::message',msg); }); - ws.on('wserror', function(e){ + ws.on('connect_failed', function(e){ self.emit('websocket::error', e); }); - ws.on('close', function(){ + ws.on('disconnect', function(){ self.emit('websocket::close'); }); }); From 97f48c190bd7e3fbe67974a6e42dc809a5cd173b Mon Sep 17 00:00:00 2001 From: Ben Polidore Date: Sat, 24 Dec 2011 15:07:35 -0500 Subject: [PATCH 2/2] removed debugging console log --- lib/ws.js | 1 - package.json | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/ws.js b/lib/ws.js index febd087..f5414ec 100644 --- a/lib/ws.js +++ b/lib/ws.js @@ -17,7 +17,6 @@ var HookSocket = exports.HookSocket = function(options){ }); ws.on('message', function(msg){ - console.log(msg); self.emit('websocket::message',msg); }); diff --git a/package.json b/package.json index adf3aa0..f5c886c 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ }, "main": "lib/ws", "engines": { - "node": "0.4" + "node": "0.6" }, "dependencies": { - "hook.io": "0.7.x" + "hook.io": ">0.7.x" }, "devDependencies": {} }