Browse files

Extract WebSocket classes from Faye 0.7.

  • Loading branch information...
0 parents commit 8299ff6ab0c38f9f21b80ccaa00d8d29d7067841 @jcoglan jcoglan committed Nov 24, 2011
1 .gitignore
@@ -0,0 +1 @@
+node_modules
3 .npmignore
@@ -0,0 +1,3 @@
+.git
+node_modules
+spec
69 lib/faye/websocket.js
@@ -0,0 +1,69 @@
+/**
+ * For implementation reference:
+ * http://dev.w3.org/html5/websockets/
+ * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
+ * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+ * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
+ * http://www.w3.org/TR/DOM-Level-2-Events/events.html
+ **/
+
+var Draft75Parser = require('./websocket/draft75_parser'),
+ // Draft76Parser = require('./websocket/draft75_parser'),
+ Protocol8Parser = require('./websocket/protocol8_parser'),
+ API = require('./websocket/api');
+
+var getParser = function(request) {
+ var headers = request.headers;
+ return headers['sec-websocket-version']
+ ? Protocol8Parser
+ : (headers['sec-websocket-key1'] && headers['sec-websocket-key2'])
+ ? Draft76Parser
+ : Draft75Parser;
+};
+
+var isSecureConnection = function(request) {
+ if (request.headers['x-forwarded-proto']) {
+ return request.headers['x-forwarded-proto'] === 'https';
+ } else {
+ return (request.connection && request.connection.authorized !== undefined) ||
+ (request.socket && request.socket.secure);
+ }
+};
+
+var WebSocket = function(request, socket, head) {
+ this.request = request;
+ this._stream = request.socket;
+
+ var scheme = isSecureConnection(request) ? 'wss:' : 'ws:';
+ this.url = scheme + '//' + request.headers.host + request.url;
+ this.readyState = API.CONNECTING;
+ this.bufferedAmount = 0;
+
+ var Parser = getParser(request);
+ this._parser = new Parser(this, this._stream);
+ this._parser.handshakeResponse(head);
+
+ this.readyState = API.OPEN;
+ this.version = this._parser.version;
+
+ var event = new API.Event('open');
+ event.initEvent('open', false, false);
+ this.dispatchEvent(event);
+
+ var self = this;
+
+ this._stream.addListener('data', function(data) {
+ self._parser.parse(data);
+ });
+ this._stream.addListener('close', function() {
+ self.close(1006, '', false);
+ });
+ this._stream.addListener('error', function() {});
+};
+
+var API = require('./websocket/api');
+for (var key in API) WebSocket.prototype[key] = API[key];
+
+WebSocket.Client = require('./websocket/client');
+module.exports = WebSocket;
+
94 lib/faye/websocket/api.js
@@ -0,0 +1,94 @@
+var API = {
+ CONNECTING: 0,
+ OPEN: 1,
+ CLOSING: 2,
+ CLOSED: 3,
+
+ onopen: null,
+ onmessage: null,
+ onerror: null,
+ onclose: null,
+
+ receive: function(data) {
+ if (this.readyState !== API.OPEN) return false;
+ var event = new API.Event('message');
+ event.initEvent('message', false, false);
+ event.data = data;
+ this.dispatchEvent(event);
+ },
+
+ send: function(data, type, errorType) {
+ if (this.readyState === API.CLOSED) return false;
+ return this._parser.frame(data, type, errorType);
+ },
+
+ close: function(code, reason, ack) {
+ if (this.readyState === API.CLOSING ||
+ this.readyState === API.CLOSED) return;
+
+ this.readyState = API.CLOSING;
+
+ var close = function() {
+ this.readyState = API.CLOSED;
+ this._stream.end();
+ var event = new API.Event('close', {code: code || 1000, reason: reason || ''});
+ event.initEvent('close', false, false);
+ this.dispatchEvent(event);
+ };
+
+ if (ack !== false) {
+ if (this._parser.close) this._parser.close(code, reason, close, this);
+ else close.call(this);
+ } else {
+ if (this._parser.close) this._parser.close(code, reason);
+ close.call(this);
+ }
+ },
+
+ addEventListener: function(type, listener, useCapture) {
@majek
majek added a line comment Dec 2, 2011

Any reason why not to use node built-in EventEmitter? For example, you seem to suffer from a bug similar to this.

@jcoglan
Collaborator
jcoglan added a line comment Dec 2, 2011

Because that would expose the EventEmitter interface, which I don't want. I'll take a look at that W3C spec -- the semantics seem to go against what I would expect but then that's standard behaviour for W3C ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ this.bind(type, listener);
+ },
+
+ removeEventListener: function(type, listener, useCapture) {
+ this.unbind(type, listener);
+ },
+
+ dispatchEvent: function(event) {
+ event.target = event.currentTarget = this;
+ event.eventPhase = API.Event.AT_TARGET;
+
+ this.trigger(event.type, event);
+ if (this['on' + event.type])
+ this['on' + event.type](event);
+ }
+};
+
+
+var Publisher = require('./publisher');
+for (var key in Publisher)
+ API[key] = Publisher[key];
+
+
+var Event = function(eventType, options) {
+ this.type = eventType;
+ for (var key in options)
+ this[key] = options[key];
+};
+
+Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
+ this.type = eventType;
+ this.bubbles = canBubble;
+ this.cancelable = cancelable;
+};
+
+Event.prototype.stopPropagation = function() {};
+Event.prototype.preventDefault = function() {};
+
+Event.CAPTURING_PHASE = 1;
+Event.AT_TARGET = 2;
+Event.BUBBLING_PHASE = 3;
+
+API.Event = Event;
+
+module.exports = API;
+
76 lib/faye/websocket/client.js
@@ -0,0 +1,76 @@
+var API = require('./api'),
+ net = require('net'),
+ tls = require('tls');
+
+var Protocol8Parser = require('./protocol8_parser');
+
+var Client = function(url) {
+ this.url = url;
+ this.uri = require('url').parse(url);
+
+ this.readyState = API.CONNECTING;
+ this.bufferedAmount = 0;
+
+ var secure = (this.uri.protocol === 'wss:'),
+ self = this,
+ onConnect = function() { self._onConnect() },
+
+ connection = secure
+ ? tls.connect(this.uri.port || 443, this.uri.hostname, onConnect)
+ : net.createConnection(this.uri.port || 80, this.uri.hostname);
+
+ this._parser = new Protocol8Parser(this, connection, {masking: true});
+ this._stream = connection;
+
+ if (!secure) connection.addListener('connect', onConnect);
+
+ connection.addListener('data', function(data) {
+ self._onData(data);
+ });
+ connection.addListener('close', function() {
+ self.close(1006, '', false);
+ });
+ connection.addListener('error', function() {});
+};
+
+Client.prototype._onConnect = function() {
+ this._handshake = this._parser.createHandshake(this.uri);
+ this._message = [];
+ this._handshake.requestData();
+};
+
+Client.prototype._onData = function(data) {
+ switch (this.readyState) {
+ case API.CONNECTING:
+ var bytes = this._handshake.parse(data);
+ for (var i = 0, n = bytes.length; i < n; i++)
+ this._message.push(bytes[i]);
+
+ if (!this._handshake.isComplete()) return;
+
+ if (this._handshake.isValid()) {
+ this.readyState = API.OPEN;
+ var event = new API.Event('open');
+ event.initEvent('open', false, false);
+ this.dispatchEvent(event);
+
+ this._parser.parse(this._message);
+
+ } else {
+ this.readyState = API.CLOSED;
+ var event = new API.Event('close');
+ event.initEvent('close', false, false);
+ this.dispatchEvent(event);
+ }
+ break;
+
+ case API.OPEN:
+ case API.CLOSING:
+ this._parser.parse(data);
+ }
+};
+
+for (var key in API) Client.prototype[key] = API[key];
+
+module.exports = Client;
+
68 lib/faye/websocket/draft75_parser.js
@@ -0,0 +1,68 @@
+var Draft75Parser = function(webSocket, stream) {
+ this._socket = webSocket;
+ this._stream = stream;
+ this._buffer = [];
+ this._buffering = false;
+};
+
+var instance = {
+ FRAME_START : new Buffer([0x00]),
+ FRAME_END : new Buffer([0xFF]),
+
+ version : 'draft-75',
+
+ handshakeResponse: function() {
+ var stream = this._stream;
+ try {
+ stream.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n');
+ stream.write('Upgrade: WebSocket\r\n');
+ stream.write('Connection: Upgrade\r\n');
+ stream.write('WebSocket-Origin: ' + this._socket.request.headers.origin + '\r\n');
+ stream.write('WebSocket-Location: ' + this._socket.url + '\r\n\r\n');
+ } catch (e) {
+ // socket closed while writing
+ // no handshake sent; client will stop using WebSocket
+ }
+ },
+
+ parse: function(data) {
+ for (var i = 0, n = data.length; i < n; i++)
+ this._handleChar(data[i]);
+ },
+
+ frame: function(data) {
+ var stream = this._stream;
+ try {
+ stream.write(this.FRAME_START, 'binary');
+ stream.write(new Buffer(data), 'utf8');
+ stream.write(this.FRAME_END, 'binary');
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ _handleChar: function(data) {
+ switch (data) {
+ case 0x00:
+ this._buffering = true;
+ break;
+
+ case 0xFF:
+ this._buffer = new Buffer(this._buffer);
+ this._socket.receive(this._buffer.toString('utf8', 0, this._buffer.length));
+ this._buffer = [];
+ this._buffering = false;
+ break;
+
+ default:
+ if (this._buffering) this._buffer.push(data);
+ }
+ }
+};
+
+for (var key in instance)
+ Draft75Parser.prototype[key] = instance[key];
+
+module.exports = Draft75Parser;
+
48 lib/faye/websocket/draft76_parser.js
@@ -0,0 +1,48 @@
+Faye.WebSocket.Draft76Parser = Faye.Class(Faye.WebSocket.Draft75Parser, {
+ version: 'draft-76',
+
+ handshakeResponse: function(head) {
+ var request = this._socket.request,
+ stream = this._stream,
+
+ key1 = request.headers['sec-websocket-key1'],
+ value1 = this._numberFromKey(key1) / this._spacesInKey(key1),
+
+ key2 = request.headers['sec-websocket-key2'],
+ value2 = this._numberFromKey(key2) / this._spacesInKey(key2),
+
+ MD5 = crypto.createHash('md5');
+
+ MD5.update(this._bigEndian(value1));
+ MD5.update(this._bigEndian(value2));
+ MD5.update(head.toString('binary'));
+
+ try {
+ stream.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n', 'binary');
+ stream.write('Upgrade: WebSocket\r\n', 'binary');
+ stream.write('Connection: Upgrade\r\n', 'binary');
+ stream.write('Sec-WebSocket-Origin: ' + request.headers.origin + '\r\n', 'binary');
+ stream.write('Sec-WebSocket-Location: ' + this._socket.url + '\r\n\r\n', 'binary');
+ stream.write(MD5.digest('binary'), 'binary');
+ } catch (e) {
+ // socket closed while writing
+ // no handshake sent; client will stop using WebSocket
+ }
+ },
+
+ _numberFromKey: function(key) {
+ return parseInt(key.match(/[0-9]/g).join(''), 10);
+ },
+
+ _spacesInKey: function(key) {
+ return key.match(/ /g).length;
+ },
+
+ _bigEndian: function(number) {
+ var string = '';
+ Faye.each([24,16,8,0], function(offset) {
+ string += String.fromCharCode(number >> offset & 0xFF);
+ });
+ return string;
+ }
+});
379 lib/faye/websocket/protocol8_parser.js
@@ -0,0 +1,379 @@
+var crypto = require('crypto');
+
+var Handshake = function(uri, stream) {
+ this._uri = uri;
+ this._stream = stream;
+
+ var buffer = new Buffer(16), i = 16;
+ while (i--) buffer[i] = Math.floor(Math.random() * 254);
+ this._key = buffer.toString('base64');
+
+ var SHA1 = crypto.createHash('sha1');
+ SHA1.update(this._key + Protocol8Parser.prototype.GUID);
+ this._accept = SHA1.digest('base64');
+
+ var HTTPParser = process.binding('http_parser').HTTPParser,
+ parser = new HTTPParser(HTTPParser.RESPONSE || 'response'),
+ current = null,
+ self = this;
+
+ this._nodeVersion = HTTPParser.RESPONSE ? 6 : 4;
+ this._complete = false;
+ this._headers = {};
+ this._parser = parser;
+
+ parser.onHeaderField = function(b, start, length) {
+ current = b.toString('utf8', start, start + length);
+ };
+ parser.onHeaderValue = function(b, start, length) {
+ self._headers[current] = b.toString('utf8', start, start + length);
+ };
+ parser.onHeadersComplete = function(info) {
+ self._status = info.statusCode;
+ var headers = info.headers;
+ if (!headers) return;
+ for (var i = 0, n = headers.length; i < n; i += 2)
+ self._headers[headers[i]] = headers[i+1];
+ };
+ parser.onMessageComplete = function() {
+ self._complete = true;
+ };
+};
+
+Handshake.prototype.requestData = function() {
+ var stream = this._stream, u = this._uri;
+ try {
+ stream.write('GET ' + u.pathname + (u.search || '') + ' HTTP/1.1\r\n');
+ stream.write('Host: ' + u.hostname + (u.port ? ':' + u.port : '') + '\r\n');
+ stream.write('Upgrade: websocket\r\n');
+ stream.write('Connection: Upgrade\r\n');
+ stream.write('Sec-WebSocket-Key: ' + this._key + '\r\n');
+ stream.write('Sec-WebSocket-Version: 8\r\n');
+ stream.write('\r\n');
+ } catch (e) {}
+};
+
+Handshake.prototype.parse = function(data) {
+ var consumed = this._parser.execute(data, 0, data.length),
+ offset = (this._nodeVersion < 6) ? 1 : 0;
+
+ return (consumed === data.length) ? [] : data.slice(consumed + offset);
+};
+
+Handshake.prototype.isComplete = function() {
+ return this._complete;
+};
+
+Handshake.prototype.isValid = function() {
+ if (this._status !== 101) return false;
+
+ var upgrade = this._headers.Upgrade,
+ connection = this._headers.Connection;
+
+ return upgrade && /^websocket$/i.test(upgrade) &&
+ connection && connection.split(/\s*,\s*/).indexOf('Upgrade') >= 0 &&
+ this._headers['Sec-WebSocket-Accept'] === this._accept;
+};
+
+var Protocol8Parser = function(webSocket, stream, options) {
+ this._reset();
+ this._socket = webSocket;
+ this._stream = stream;
+ this._stage = 0;
+ this._masking = options && options.masking;
+};
+
+var instance = {
+ FIN: 128,
+ MASK: 128,
+ RSV1: 64,
+ RSV2: 32,
+ RSV3: 16,
+ OPCODE: 15,
+ LENGTH: 127,
+
+ GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
+
+ OPCODES: {
+ continuation: 0,
+ text: 1,
+ binary: 2,
+ close: 8,
+ ping: 9,
+ pong: 10
+ },
+
+ ERRORS: {
+ normal_closure: 1000,
+ going_away: 1001,
+ protocol_error: 1002,
+ unacceptable: 1003,
+ encoding_error: 1007,
+ policy_violation: 1008,
+ too_large: 1009,
+ extension_error: 1010
+ },
+
+ FRAGMENTED_OPCODES: [0,1,2],
+ OPENING_OPCODES: [1,2],
+
+ ERROR_CODES: [1000,1001,1002,1003,1007,1008,1009,1010],
+
+ UTF8_MATCH: /^([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$/,
+
+ handshakeResponse: function() {
+ var secKey = this._socket.request.headers['sec-websocket-key'];
+ if (!secKey) return;
+
+ var SHA1 = crypto.createHash('sha1');
+ SHA1.update(secKey + this.GUID);
+ var accept = SHA1.digest('base64');
+
+ var stream = this._stream;
+ try {
+ stream.write('HTTP/1.1 101 Switching Protocols\r\n');
+ stream.write('Upgrade: websocket\r\n');
+ stream.write('Connection: Upgrade\r\n');
+ stream.write('Sec-WebSocket-Accept: ' + accept + '\r\n\r\n');
+ } catch (e) {
+ // socket closed while writing
+ // no handshake sent; client will stop using WebSocket
+ }
+ },
+
+ createHandshake: function() {
+ return new Handshake(this._socket.uri, this._stream);
+ },
+
+ parse: function(data) {
+ for (var i = 0, n = data.length; i < n; i++) {
+ switch (this._stage) {
+ case 0: this._parseOpcode(data[i]); break;
+ case 1: this._parseLength(data[i]); break;
+ case 2: this._parseExtendedLength(data[i]); break;
+ case 3: this._parseMask(data[i]); break;
+ case 4: this._parsePayload(data[i]); break;
+ }
+ if (this._stage === 4 && this._length === 0)
+ this._emitFrame();
+ }
+ },
+
+ frame: function(data, type, code) {
+ if (this._closed) return;
+
+ var opcode = this.OPCODES[type || (typeof data === 'string' ? 'text' : 'binary')],
+ buffer = new Buffer(data),
+ insert = code ? 2 : 0,
+ length = buffer.length + insert,
+ masked = this._masking ? this.MASK : 0,
+ stream = this._stream,
+ frame, factor, mask, i, n;
+
+ data = new Buffer(length);
+ if (code) {
+ data[0] = Math.floor(code / 256);
+ data[1] = code & 255;
+ }
+ for (i = 0, n = buffer.length; i < n; i++)
+ data[insert + i] = buffer[i];
+
+ if (length <= 125) {
+ frame = new Buffer(2);
+ frame[1] = masked | length;
+ } else if (length >= 126 && length <= 65535) {
+ frame = new Buffer(4);
+ frame[1] = masked | 126;
+ frame[2] = Math.floor(length / 256);
+ frame[3] = length & 255;
+ } else {
+ frame = new Buffer(10);
+ frame[1] = masked | 127;
+ for (var i = 0; i < 8; i++) {
+ factor = Math.pow(2, 8 * (8 - 1 - i));
+ frame[2+i] = Math.floor(length / factor) & 255;
+ }
+ }
+ frame[0] = this.FIN | opcode;
+
+ if (this._masking) {
+ mask = [1,2,3,4].map(function() { return Math.floor(Math.random() * 255) });
+ for (i = 0, n = data.length; i < n; i++)
+ data[i] = data[i] ^ mask[i % 4];
+ }
+
+ try {
+ stream.write(frame, 'binary');
+ if (mask) stream.write(new Buffer(mask), 'binary');
+ if (data.length > 0) stream.write(data, 'utf8');
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ close: function(code, reason, callback, context) {
+ if (this._closed) return;
+ if (callback) this._closingCallback = [callback, context];
+ this.frame(reason || '', 'close', code || this.ERRORS.normal_closure);
+ this._closed = true;
+ },
+
+ buffer: function(fragment) {
+ for (var i = 0, n = fragment.length; i < n; i++)
+ this._buffer.push(fragment[i]);
+ },
+
+ _parseOpcode: function(data) {
+ var rsvs = [this.RSV1, this.RSV2, this.RSV3].filter(function(rsv) {
+ return (data & rsv) === rsv;
+ }, this);
+
+ if (rsvs.length > 0) return this._socket.close(this.ERRORS.protocol_error, null, false);
+
+ this._final = (data & this.FIN) === this.FIN;
+ this._opcode = (data & this.OPCODE);
+ this._mask = [];
+ this._payload = [];
+
+ var valid = false;
+
+ for (var key in this.OPCODES) {
+ if (this.OPCODES[key] === this._opcode)
+ valid = true;
+ }
+ if (!valid) return this._socket.close(this.ERRORS.protocol_error, null, false);
+
+ if (this.FRAGMENTED_OPCODES.indexOf(this._opcode) < 0 && !this._final)
+ return this._socket.close(this.ERRORS.protocol_error, null, false);
+
+ if (this._mode && this.OPENING_OPCODES.indexOf(this._opcode) >= 0)
+ return this._socket.close(this.ERRORS.protocol_error, null, false);
+
+ this._stage = 1;
+ },
+
+ _parseLength: function(data) {
+ this._masked = (data & this.MASK) === this.MASK;
+ this._length = (data & this.LENGTH);
+
+ if (this._length >= 0 && this._length <= 125) {
+ this._stage = this._masked ? 3 : 4;
+ } else {
+ this._lengthBuffer = [];
+ this._lengthSize = (this._length === 126 ? 2 : 8);
+ this._stage = 2;
+ }
+ },
+
+ _parseExtendedLength: function(data) {
+ this._lengthBuffer.push(data);
+ if (this._lengthBuffer.length < this._lengthSize) return;
+ this._length = this._getInteger(this._lengthBuffer);
+ this._stage = this._masked ? 3 : 4;
+ },
+
+ _parseMask: function(data) {
+ this._mask.push(data);
+ if (this._mask.length < 4) return;
+ this._stage = 4;
+ },
+
+ _parsePayload: function(data) {
+ this._payload.push(data);
+ if (this._payload.length < this._length) return;
+ this._emitFrame();
+ },
+
+ _emitFrame: function() {
+ var payload = this._unmask(this._payload, this._mask),
+ opcode = this._opcode;
+
+ if (opcode === this.OPCODES.continuation) {
+ if (!this._mode) return this._socket.close(this.ERRORS.protocol_error, null, false);
+ this.buffer(payload);
+ if (this._final) {
+ var message = new Buffer(this._buffer);
+ if (this._mode === 'text') message = this._encode(message);
+ this._reset();
+ if (message !== null) this._socket.receive(message);
+ else this._socket.close(this.ERRORS.encoding_error, null, false);
+ }
+ }
+ else if (opcode === this.OPCODES.text) {
+ if (this._final) {
+ var message = this._encode(payload);
+ if (message !== null) this._socket.receive(message);
+ else this._socket.close(this.ERRORS.encoding_error, null, false);
+ } else {
+ this._mode = 'text';
+ this.buffer(payload);
+ }
+ }
+ else if (opcode === this.OPCODES.binary) {
+ if (this._final) {
+ this._socket.receive(payload);
+ } else {
+ this._mode = 'binary';
+ this.buffer(payload);
+ }
+ }
+ else if (opcode === this.OPCODES.close) {
+ var code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : null,
+ reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null;
+
+ if (!(payload.length === 0) &&
+ !(code !== null && code >= 3000 && code < 5000) &&
+ this.ERROR_CODES.indexOf(code) < 0)
+ code = this.ERRORS.protocol_error;
+
+ if (payload.length > 125 || (payload.length > 2 && !reason))
+ code = this.ERRORS.protocol_error;
+
+ this._socket.close(code, (payload.length > 2) ? reason : null, false);
+ if (this._closingCallback)
+ this._closingCallback[0].call(this._closingCallback[1]);
+ }
+ else if (opcode === this.OPCODES.ping) {
+ if (payload.length > 125) return this._socket.close(this.ERRORS.protocol_error, null, false);
+ this._socket.send(payload, 'pong');
+ }
+ this._stage = 0;
+ },
+
+ _reset: function() {
+ this._mode = null;
+ this._buffer = [];
+ },
+
+ _encode: function(buffer) {
+ try {
+ var string = buffer.toString('binary', 0, buffer.length);
+ if (!this.UTF8_MATCH.test(string)) return null;
+ } catch (e) {}
+ return buffer.toString('utf8', 0, buffer.length);
+ },
+
+ _getInteger: function(bytes) {
+ var number = 0;
+ for (var i = 0, n = bytes.length; i < n; i++)
+ number += bytes[i] << (8 * (n - 1 - i));
+ return number;
+ },
+
+ _unmask: function(payload, mask) {
+ var unmasked = new Buffer(payload.length), b;
+ for (var i = 0, n = payload.length; i < n; i++) {
+ b = payload[i];
+ if (mask.length > 0) b = b ^ mask[i % 4];
+ unmasked[i] = b;
+ }
+ return unmasked;
+ }
+};
+
+for (var key in instance)
+ Protocol8Parser.prototype[key] = instance[key];
+
+module.exports = Protocol8Parser;
+
43 lib/faye/websocket/publisher.js
@@ -0,0 +1,43 @@
+var Publisher = {
+ countListeners: function(eventType) {
+ if (!this._subscribers || !this._subscribers[eventType]) return 0;
+ return this._subscribers[eventType].length;
+ },
+
+ bind: function(eventType, listener, context) {
+ this._subscribers = this._subscribers || {};
+ var list = this._subscribers[eventType] = this._subscribers[eventType] || [];
+ list.push([listener, context]);
+ },
+
+ unbind: function(eventType, listener, context) {
+ if (!this._subscribers || !this._subscribers[eventType]) return;
+
+ if (!listener) {
+ delete this._subscribers[eventType];
+ return;
+ }
+ var list = this._subscribers[eventType],
+ i = list.length;
+
+ while (i--) {
+ if (listener !== list[i][0]) continue;
+ if (context && list[i][1] !== context) continue;
+ list.splice(i,1);
+ }
+ },
+
+ trigger: function() {
+ var args = Array.prototype.slice.call(arguments),
+ eventType = args.shift();
+
+ if (!this._subscribers || !this._subscribers[eventType]) return;
+
+ this._subscribers[eventType].forEach(function(listener) {
+ listener[0].apply(listener[1], args);
+ });
+ }
+};
+
+module.exports = Publisher;
+
24 package.json
@@ -0,0 +1,24 @@
+{ "name" : "faye-websocket"
+, "description" : "Robust general-purpose WebSocket server and client"
+, "homepage" : "http://github.com/jcoglan/faye-websocket-node"
+, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
+, "keywords" : ["websocket"]
+
+, "version" : "0.1.0"
+, "engines" : {"node": ">=0.4.0"}
+, "main" : "./lib/faye/websocket"
+, "devDependencies" : {"jsclass": ">=3.0.4"}
+
+, "bugs" : "http://github.com/jcoglan/faye-websocket-node/issues"
+
+, "licenses" : [ { "type" : "MIT"
+ , "url" : "http://www.opensource.org/licenses/mit-license.php"
+ }
+ ]
+
+, "repositories" : [ { "type" : "git"
+ , "url" : "git://github.com/jcoglan/faye-websocket-node.git"
+ }
+ ]
+}
+
117 spec/faye/websocket/client_spec.js
@@ -0,0 +1,117 @@
+var Client = require('../../../lib/faye/websocket/client')
+
+JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
+ server: function(port, callback) {
+ this._adapter = new EchoServer()
+ this._adapter.listen(port)
+ this._port = port
+ setTimeout(callback, 100)
+ },
+
+ stop: function(callback) {
+ this._adapter.stop()
+ setTimeout(callback, 100)
+ },
+
+ open_socket: function(url, callback) {
+ var done = false,
+ self = this,
+
+ resume = function(open) {
+ if (done) return
+ done = true
+ self._open = open
+ callback()
+ }
+
+ this._ws = new Client(url)
+
+ this._ws.onopen = function() { resume(true) }
+ this._ws.onclose = function() { resume(false) }
+ },
+
+ close_socket: function(callback) {
+ var self = this
+ this._ws.onclose = function() {
+ self._open = false
+ callback()
+ }
+ this._ws.close()
+ },
+
+ check_open: function(callback) {
+ this.assert( this._open )
+ callback()
+ },
+
+ check_closed: function(callback) {
+ this.assert( !this._open )
+ callback()
+ },
+
+ listen_for_message: function(callback) {
+ var self = this
+ this._ws.onmessage = function(message) { self._message = message.data }
+ callback()
+ },
+
+ send_message: function(callback) {
+ this._ws.send("I expect this to be echoed")
+ setTimeout(callback, 100)
+ },
+
+ check_response: function(callback) {
+ this.assertEqual( "I expect this to be echoed", this._message )
+ callback()
+ },
+
+ check_no_response: function(callback) {
+ this.assert( !this._message )
+ callback()
+ }
+})
+
+
+JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
+ include(WebSocketSteps)
+
+ before(function() { this.server(8000) })
+ after (function() { this.stop() })
+
+ it("can open a connection", function() { with(this) {
+ open_socket("ws://localhost:8000/bayeux")
+ check_open()
+ }})
+
+ it("can close the connection", function() { with(this) {
+ open_socket("ws://localhost:8000/bayeux")
+ close_socket()
+ check_closed()
+ }})
+
+ describe("in the OPEN state", function() { with(this) {
+ before(function() { with(this) {
+ open_socket("ws://localhost:8000/bayeux")
+ }})
+
+ it("can send and receive messages", function() { with(this) {
+ listen_for_message()
+ send_message()
+ check_response()
+ }})
+ }})
+
+ describe("in the CLOSED state", function() { with(this) {
+ before(function() { with(this) {
+ open_socket("ws://localhost:8000/bayeux")
+ close_socket()
+ }})
+
+ it("cannot send and receive messages", function() { with(this) {
+ listen_for_message()
+ send_message()
+ check_no_response()
+ }})
+ }})
+}})
+
39 spec/faye/websocket/draft75parser_spec.js
@@ -0,0 +1,39 @@
+var Draft75Parser = require('../../../lib/faye/websocket/draft75_parser')
+
+JS.ENV.Draft75ParserSpec = JS.Test.describe("Draft75Parser", function() { with(this) {
+ before(function() { with(this) {
+ this.webSocket = {dispatchEvent: function() {}}
+ this.socket = new FakeSocket
+ this.parser = new Draft75Parser(webSocket, socket)
+ }})
+
+ describe("parse", function() { with(this) {
+ it("parses text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Hello")
+ parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
+ }})
+
+ it("parses multibyte text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Apple = ")
+ parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
+ }})
+
+ it("parses fragmented frames", function() { with(this) {
+ expect(webSocket, "receive").given("Hello")
+ parser.parse([0x00, 0x48, 0x65, 0x6c])
+ parser.parse([0x6c, 0x6f, 0xff])
+ }})
+ }})
+
+ describe("frame", function() { with(this) {
+ it("returns the given string formatted as a WebSocket frame", function() { with(this) {
+ parser.frame("Hello")
+ assertEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], socket.read() )
+ }})
+
+ it("encodes multibyte characters correctly", function() { with(this) {
+ parser.frame("Apple = ")
+ assertEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], socket.read() )
+ }})
+ }})
+}})
153 spec/faye/websocket/protocol8parser_spec.js
@@ -0,0 +1,153 @@
+var Protocol8Parser = require('../../../lib/faye/websocket/protocol8_parser')
+
+JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { with(this) {
+ before(function() { with(this) {
+ this.webSocket = {dispatchEvent: function() {}}
+ this.socket = new FakeSocket
+ this.parser = new Protocol8Parser(webSocket, socket)
+ }})
+
+ define("parse", function() {
+ var bytes = [];
+ for (var i = 0, n = arguments.length; i < n; i++) bytes = bytes.concat(arguments[i])
+ this.parser.parse(new Buffer(bytes))
+ })
+
+ define("buffer", function(string) {
+ return {
+ equals: function(buffer) {
+ return buffer.toString('utf8', 0, buffer.length) === string
+ }
+ }
+ })
+
+ describe("parse", function() { with(this) {
+ define("mask", function() {
+ return this._mask = this._mask || [1,2,3,4].map(function() { return Math.floor(Math.random() * 255) })
+ })
+
+ define("maskMessage", function(bytes) {
+ var output = []
+ Array.prototype.forEach.call(bytes, function(b, i) {
+ output[i] = bytes[i] ^ this.mask()[i % 4]
+ }, this)
+ return output
+ })
+
+ it("parses unmasked text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Hello")
+ parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
+ }})
+
+ it("parses empty text frames", function() { with(this) {
+ expect(webSocket, "receive").given("")
+ parse([0x81, 0x00])
+ }})
+
+ it("parses fragmented text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Hello")
+ parse([0x01, 0x03, 0x48, 0x65, 0x6c])
+ parse([0x80, 0x02, 0x6c, 0x6f])
+ }})
+
+ it("parses masked text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Hello")
+ parse([0x81, 0x85], mask(), maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
+ }})
+
+ it("parses masked empty text frames", function() { with(this) {
+ expect(webSocket, "receive").given("")
+ parse([0x81, 0x80], mask(), maskMessage([]))
+ }})
+
+ it("parses masked fragmented text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Hello")
+ parse([0x01, 0x81], mask(), maskMessage([0x48]))
+ parse([0x80, 0x84], mask(), maskMessage([0x65, 0x6c, 0x6c, 0x6f]))
+ }})
+
+ it("closes the socket if the frame has an unrecognized opcode", function() { with(this) {
+ expect(webSocket, "close").given(1002, null, false)
+ parse([0x83, 0x00])
+ }})
+
+ it("closes the socket if a close frame is received", function() { with(this) {
+ expect(webSocket, "close").given(1000, "Hello", false)
+ parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
+ }})
+
+ it("parses unmasked multibyte text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Apple = ")
+ parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
+ }})
+
+ it("parses fragmented multibyte text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Apple = ")
+ parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
+ parse([0x80, 0x01, 0xbf])
+ }})
+
+ it("parses masked multibyte text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Apple = ")
+ parse([0x81, 0x8b], mask(), maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]))
+ }})
+
+ it("parses masked fragmented multibyte text frames", function() { with(this) {
+ expect(webSocket, "receive").given("Apple = ")
+ parse([0x01, 0x8a], mask(), maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]))
+ parse([0x80, 0x81], mask(), maskMessage([0xbf]))
+ }})
+
+ it("parses unmasked medium-length text frames", function() { with(this) {
+ expect(webSocket, "receive").given("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
+ parse([129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111])
+ }})
+
+ it("parses masked medium-length text frames", function() { with(this) {
+ expect(webSocket, "receive").given("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
+ parse([129, 254, 0, 200], mask(), maskMessage([72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111]))
+ }})
+
+ it("replies to pings with a pong", function() { with(this) {
+ expect(webSocket, "send").given(buffer("OHAI"), "pong")
+ parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
+ }})
+ }})
+
+ describe("frame", function() { with(this) {
+ it("returns the given string formatted as a WebSocket frame", function() { with(this) {
+ parser.frame("Hello")
+ assertEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], socket.read() )
+ }})
+
+ it("encodes multibyte characters correctly", function() { with(this) {
+ parser.frame("Apple = ")
+ assertEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], socket.read() )
+ }})
+
+ it("encodes medium-length strings using extra length bytes", function() { with(this) {
+ parser.frame("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
+ assertEqual( [129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111], socket.read() )
+ }})
+
+ it("encodes long strings using extra length bytes", function() { with(this) {
+ var reps = 13108, message = '', output = [0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04]
+ while (reps--) {
+ message += "Hello"
+ output = output.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
+ }
+ parser.frame(message)
+ assertEqual( output, socket.read() )
+ }})
+
+ it("encodes close frames with an error code", function() { with(this) {
+ parser.frame("Hello", "close", 1002)
+ assertEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], socket.read() )
+ }})
+
+ it("encodes pong frames", function() { with(this) {
+ parser.frame("", "pong")
+ assertEqual( [0x8a, 0x00], socket.read() )
+ }})
+ }})
+}})
53 spec/runner.js
@@ -0,0 +1,53 @@
+require('jsclass')
+var WebSocket = require('../lib/faye/websocket')
+
+
+JS.ENV.FakeSocket = function() {
+ this._fragments = []
+}
+FakeSocket.prototype.write = function(buffer, encoding) {
+ this._fragments.push([buffer, encoding])
+}
+FakeSocket.prototype.read = function() {
+ var output = []
+ this._fragments.forEach(function(buffer, i) {
+ for (var j = 0, n = buffer[0].length; j < n; j++)
+ output.push(buffer[0][j])
+ })
+ return output
+}
+FakeSocket.prototype.addListener = function() {}
+
+
+JS.ENV.EchoServer = function() {}
+EchoServer.prototype.listen = function(port) {
+ var server = require('http').createServer()
+ server.addListener('upgrade', function(request, socket, head) {
+ var ws = new WebSocket(request, socket, head)
+ ws.onmessage = function(event) {
+ ws.send(event.data)
+ }
+ })
+ this._httpServer = server
+ server.listen(port)
+}
+EchoServer.prototype.stop = function(callback, scope) {
+ this._httpServer.addListener('close', function() {
+ if (callback) callback.call(scope);
+ });
+ this._httpServer.close();
+}
+
+
+JS.Packages(function() { with(this) {
+ autoload(/.*Spec/, {from: 'spec/faye/websocket'})
+}})
+
+
+JS.require('JS.Test', function() {
+ JS.require( 'ClientSpec',
+ 'Draft75ParserSpec',
+ 'Protocol8ParserSpec',
+ JS.Test.method('autorun'))
+})
+

0 comments on commit 8299ff6

Please sign in to comment.