├── .gitignore ├── CODE_OF_CONDUCT.md ├── lib └── websocket │ ├── driver │ ├── hybi │ │ ├── frame.js │ │ └── message.js │ ├── headers.js │ ├── stream_reader.js │ ├── proxy.js │ ├── draft75.js │ ├── draft76.js │ ├── server.js │ ├── client.js │ ├── base.js │ └── hybi.js │ ├── driver.js │ ├── http_parser.js │ └── streams.js ├── LICENSE.md ├── examples ├── net_server.js └── net_client.js ├── .github └── workflows │ └── test.yml ├── spec ├── runner.js └── websocket │ └── driver │ ├── draft75_spec.js │ ├── draft75_examples.js │ ├── draft76_spec.js │ ├── client_spec.js │ └── hybi_spec.js ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All projects under the [Faye](https://github.com/faye) umbrella are covered by 4 | the [Code of Conduct](https://github.com/faye/code-of-conduct). 5 | -------------------------------------------------------------------------------- /lib/websocket/driver/hybi/frame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Frame = function() {}; 4 | 5 | var instance = { 6 | final: false, 7 | rsv1: false, 8 | rsv2: false, 9 | rsv3: false, 10 | opcode: null, 11 | masked: false, 12 | maskingKey: null, 13 | lengthBytes: 1, 14 | length: 0, 15 | payload: null 16 | }; 17 | 18 | for (var key in instance) 19 | Frame.prototype[key] = instance[key]; 20 | 21 | module.exports = Frame; 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2010-2020 James Coglan 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /examples/net_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net'), 4 | websocket = require('..'), 5 | deflate = require('permessage-deflate'); 6 | 7 | var server = net.createServer(function(connection) { 8 | var driver = websocket.server(); 9 | driver.addExtension(deflate); 10 | 11 | driver.on('connect', function() { 12 | if (websocket.isWebSocket(driver)) driver.start(); 13 | }); 14 | 15 | driver.on('close', function() { connection.end() }); 16 | connection.on('error', function() {}); 17 | 18 | connection.pipe(driver.io); 19 | driver.io.pipe(connection); 20 | 21 | driver.messages.pipe(driver.messages); 22 | }); 23 | 24 | server.listen(process.argv[2]); 25 | -------------------------------------------------------------------------------- /examples/net_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net'), 4 | url = require('url'), 5 | websocket = require('..'), 6 | deflate = require('permessage-deflate'); 7 | 8 | var DEFAULT_PORTS = { 'ws:': 80, 'wss:': 443 }; 9 | 10 | var uri = url.parse(process.argv[2]), 11 | port = uri.port || DEFAULT_PORTS[uri.protocol], 12 | conn = net.connect({ host: uri.hostname, port: port }); 13 | 14 | var driver = websocket.client(uri.href); 15 | driver.addExtension(deflate); 16 | 17 | driver.on('open', function() { 18 | driver.text('Hello, world'); 19 | }); 20 | 21 | driver.on('message', function(event) { 22 | console.log(['message', event.data]); 23 | }); 24 | 25 | driver.on('close', function(event) { 26 | console.log(['close', event.code, event.reason]); 27 | conn.end(); 28 | }); 29 | 30 | conn.pipe(driver.io); 31 | driver.io.pipe(conn); 32 | 33 | driver.start(); 34 | -------------------------------------------------------------------------------- /lib/websocket/driver/hybi/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer; 4 | 5 | var Message = function() { 6 | this.rsv1 = false; 7 | this.rsv2 = false; 8 | this.rsv3 = false; 9 | this.opcode = null; 10 | this.length = 0; 11 | this._chunks = []; 12 | }; 13 | 14 | var instance = { 15 | read: function() { 16 | return this.data = this.data || Buffer.concat(this._chunks, this.length); 17 | }, 18 | 19 | pushFrame: function(frame) { 20 | this.rsv1 = this.rsv1 || frame.rsv1; 21 | this.rsv2 = this.rsv2 || frame.rsv2; 22 | this.rsv3 = this.rsv3 || frame.rsv3; 23 | 24 | if (this.opcode === null) this.opcode = frame.opcode; 25 | 26 | this._chunks.push(frame.payload); 27 | this.length += frame.length; 28 | } 29 | }; 30 | 31 | for (var key in instance) 32 | Message.prototype[key] = instance[key]; 33 | 34 | module.exports = Message; 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - push 3 | - pull_request 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | node: 11 | - '0.8' 12 | - '0.10' 13 | - '0.12' 14 | - '4' 15 | - '6' 16 | - '8' 17 | - '10' 18 | - '12' 19 | - '14' 20 | - '16' 21 | - '18' 22 | - '20' 23 | name: node.js v${{ matrix.node }} 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node }} 31 | 32 | - if: matrix.node == '0.8' 33 | run: npm conf set strict-ssl false 34 | 35 | - run: node --version 36 | - run: npm install 37 | 38 | - run: npm install 'nopt@5' 39 | - run: rm -rf node_modules/jstest/node_modules/nopt 40 | 41 | - run: npm test 42 | -------------------------------------------------------------------------------- /lib/websocket/driver/headers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Headers = function() { 4 | this.clear(); 5 | }; 6 | 7 | Headers.prototype.ALLOWED_DUPLICATES = ['set-cookie', 'set-cookie2', 'warning', 'www-authenticate']; 8 | 9 | Headers.prototype.clear = function() { 10 | this._sent = {}; 11 | this._lines = []; 12 | }; 13 | 14 | Headers.prototype.set = function(name, value) { 15 | if (value === undefined) return; 16 | 17 | name = this._strip(name); 18 | value = this._strip(value); 19 | 20 | var key = name.toLowerCase(); 21 | if (!this._sent.hasOwnProperty(key) || this.ALLOWED_DUPLICATES.indexOf(key) >= 0) { 22 | this._sent[key] = true; 23 | this._lines.push(name + ': ' + value + '\r\n'); 24 | } 25 | }; 26 | 27 | Headers.prototype.toString = function() { 28 | return this._lines.join(''); 29 | }; 30 | 31 | Headers.prototype._strip = function(string) { 32 | return string.toString().replace(/^ */, '').replace(/ *$/, ''); 33 | }; 34 | 35 | module.exports = Headers; 36 | -------------------------------------------------------------------------------- /lib/websocket/driver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Protocol references: 4 | // 5 | // * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 6 | // * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 7 | // * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 8 | 9 | var Base = require('./driver/base'), 10 | Client = require('./driver/client'), 11 | Server = require('./driver/server'); 12 | 13 | var Driver = { 14 | client: function(url, options) { 15 | options = options || {}; 16 | if (options.masking === undefined) options.masking = true; 17 | return new Client(url, options); 18 | }, 19 | 20 | server: function(options) { 21 | options = options || {}; 22 | if (options.requireMasking === undefined) options.requireMasking = true; 23 | return new Server(options); 24 | }, 25 | 26 | http: function() { 27 | return Server.http.apply(Server, arguments); 28 | }, 29 | 30 | isSecureRequest: function(request) { 31 | return Server.isSecureRequest(request); 32 | }, 33 | 34 | isWebSocket: function(request) { 35 | return Base.isWebSocket(request); 36 | }, 37 | 38 | validateOptions: function(options, validKeys) { 39 | Base.validateOptions(options, validKeys); 40 | } 41 | }; 42 | 43 | module.exports = Driver; 44 | -------------------------------------------------------------------------------- /spec/runner.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('safe-buffer').Buffer, 2 | test = require('jstest').Test, 3 | Stream = require('stream').Stream, 4 | util = require('util') 5 | 6 | var BufferMatcher = function(data) { 7 | this._data = Buffer.from(data) 8 | } 9 | BufferMatcher.prototype.equals = function(other) { 10 | if (this._data.length !== other.length) return false; 11 | for (var i = 0, n = other.length; i < n; i++) { 12 | if (other[i] !== this._data[i]) return false; 13 | } 14 | return true; 15 | } 16 | 17 | var Collector = function() { 18 | this.bytes = [] 19 | this.writable = true 20 | } 21 | util.inherits(Collector, Stream) 22 | Collector.prototype.write = function(buffer) { 23 | this.bytes = [] 24 | for (var i = 0, n = buffer.length; i < n; i++) { 25 | this.bytes[i] = buffer[i] 26 | } 27 | return true 28 | } 29 | 30 | test.Unit.TestCase.include({ 31 | buffer: function(data) { 32 | return new BufferMatcher(data) 33 | }, 34 | collector: function() { 35 | return this._collector = this._collector || new Collector() 36 | } 37 | }) 38 | 39 | require('./websocket/driver/draft75_examples') 40 | require('./websocket/driver/draft75_spec') 41 | require('./websocket/driver/draft76_spec') 42 | require('./websocket/driver/hybi_spec') 43 | require('./websocket/driver/client_spec') 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-driver", 3 | "description": "WebSocket protocol handler with pluggable I/O", 4 | "homepage": "https://github.com/faye/websocket-driver-node", 5 | "author": "James Coglan (http://jcoglan.com/)", 6 | "keywords": [ 7 | "websocket" 8 | ], 9 | "license": "Apache-2.0", 10 | "version": "0.7.4", 11 | "engines": { 12 | "node": ">=0.8.0" 13 | }, 14 | "files": [ 15 | "lib" 16 | ], 17 | "main": "./lib/websocket/driver", 18 | "dependencies": { 19 | "http-parser-js": ">=0.5.1", 20 | "safe-buffer": ">=5.1.0", 21 | "websocket-extensions": ">=0.1.1" 22 | }, 23 | "devDependencies": { 24 | "jstest": "*", 25 | "permessage-deflate": "*" 26 | }, 27 | "scripts": { 28 | "test": "jstest spec/runner.js" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/faye/websocket-driver-node.git" 33 | }, 34 | "bugs": "https://github.com/faye/websocket-driver-node/issues" 35 | } 36 | -------------------------------------------------------------------------------- /lib/websocket/driver/stream_reader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer; 4 | 5 | var StreamReader = function() { 6 | this._queue = []; 7 | this._queueSize = 0; 8 | this._offset = 0; 9 | }; 10 | 11 | StreamReader.prototype.put = function(buffer) { 12 | if (!buffer || buffer.length === 0) return; 13 | if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer); 14 | this._queue.push(buffer); 15 | this._queueSize += buffer.length; 16 | }; 17 | 18 | StreamReader.prototype.read = function(length) { 19 | if (length > this._queueSize) return null; 20 | if (length === 0) return Buffer.alloc(0); 21 | 22 | this._queueSize -= length; 23 | 24 | var queue = this._queue, 25 | remain = length, 26 | first = queue[0], 27 | buffers, buffer; 28 | 29 | if (first.length >= length) { 30 | if (first.length === length) { 31 | return queue.shift(); 32 | } else { 33 | buffer = first.slice(0, length); 34 | queue[0] = first.slice(length); 35 | return buffer; 36 | } 37 | } 38 | 39 | for (var i = 0, n = queue.length; i < n; i++) { 40 | if (remain < queue[i].length) break; 41 | remain -= queue[i].length; 42 | } 43 | buffers = queue.splice(0, i); 44 | 45 | if (remain > 0 && queue.length > 0) { 46 | buffers.push(queue[0].slice(0, remain)); 47 | queue[0] = queue[0].slice(remain); 48 | } 49 | return Buffer.concat(buffers, length); 50 | }; 51 | 52 | StreamReader.prototype.eachByte = function(callback, context) { 53 | var buffer, n, index; 54 | 55 | while (this._queue.length > 0) { 56 | buffer = this._queue[0]; 57 | n = buffer.length; 58 | 59 | while (this._offset < n) { 60 | index = this._offset; 61 | this._offset += 1; 62 | callback.call(context, buffer[index]); 63 | } 64 | this._offset = 0; 65 | this._queue.shift(); 66 | } 67 | }; 68 | 69 | module.exports = StreamReader; 70 | -------------------------------------------------------------------------------- /lib/websocket/driver/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer, 4 | Stream = require('stream').Stream, 5 | url = require('url'), 6 | util = require('util'), 7 | Base = require('./base'), 8 | Headers = require('./headers'), 9 | HttpParser = require('../http_parser'); 10 | 11 | var PORTS = { 'ws:': 80, 'wss:': 443 }; 12 | 13 | var Proxy = function(client, origin, options) { 14 | this._client = client; 15 | this._http = new HttpParser('response'); 16 | this._origin = (typeof client.url === 'object') ? client.url : url.parse(client.url); 17 | this._url = (typeof origin === 'object') ? origin : url.parse(origin); 18 | this._options = options || {}; 19 | this._state = 0; 20 | 21 | this.readable = this.writable = true; 22 | this._paused = false; 23 | 24 | this._headers = new Headers(); 25 | this._headers.set('Host', this._origin.host); 26 | this._headers.set('Connection', 'keep-alive'); 27 | this._headers.set('Proxy-Connection', 'keep-alive'); 28 | 29 | var auth = this._url.auth && Buffer.from(this._url.auth, 'utf8').toString('base64'); 30 | if (auth) this._headers.set('Proxy-Authorization', 'Basic ' + auth); 31 | }; 32 | util.inherits(Proxy, Stream); 33 | 34 | var instance = { 35 | setHeader: function(name, value) { 36 | if (this._state !== 0) return false; 37 | this._headers.set(name, value); 38 | return true; 39 | }, 40 | 41 | start: function() { 42 | if (this._state !== 0) return false; 43 | this._state = 1; 44 | 45 | var origin = this._origin, 46 | port = origin.port || PORTS[origin.protocol], 47 | start = 'CONNECT ' + origin.hostname + ':' + port + ' HTTP/1.1'; 48 | 49 | var headers = [start, this._headers.toString(), '']; 50 | 51 | this.emit('data', Buffer.from(headers.join('\r\n'), 'utf8')); 52 | return true; 53 | }, 54 | 55 | pause: function() { 56 | this._paused = true; 57 | }, 58 | 59 | resume: function() { 60 | this._paused = false; 61 | this.emit('drain'); 62 | }, 63 | 64 | write: function(chunk) { 65 | if (!this.writable) return false; 66 | 67 | this._http.parse(chunk); 68 | if (!this._http.isComplete()) return !this._paused; 69 | 70 | this.statusCode = this._http.statusCode; 71 | this.headers = this._http.headers; 72 | 73 | if (this.statusCode === 200) { 74 | this.emit('connect', new Base.ConnectEvent()); 75 | } else { 76 | var message = "Can't establish a connection to the server at " + this._origin.href; 77 | this.emit('error', new Error(message)); 78 | } 79 | this.end(); 80 | return !this._paused; 81 | }, 82 | 83 | end: function(chunk) { 84 | if (!this.writable) return; 85 | if (chunk !== undefined) this.write(chunk); 86 | this.readable = this.writable = false; 87 | this.emit('close'); 88 | this.emit('end'); 89 | }, 90 | 91 | destroy: function() { 92 | this.end(); 93 | } 94 | }; 95 | 96 | for (var key in instance) 97 | Proxy.prototype[key] = instance[key]; 98 | 99 | module.exports = Proxy; 100 | -------------------------------------------------------------------------------- /lib/websocket/driver/draft75.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer, 4 | Base = require('./base'), 5 | util = require('util'); 6 | 7 | var Draft75 = function(request, url, options) { 8 | Base.apply(this, arguments); 9 | this._stage = 0; 10 | this.version = 'hixie-75'; 11 | 12 | this._headers.set('Upgrade', 'WebSocket'); 13 | this._headers.set('Connection', 'Upgrade'); 14 | this._headers.set('WebSocket-Origin', this._request.headers.origin); 15 | this._headers.set('WebSocket-Location', this.url); 16 | }; 17 | util.inherits(Draft75, Base); 18 | 19 | var instance = { 20 | close: function() { 21 | if (this.readyState === 3) return false; 22 | this.readyState = 3; 23 | this.emit('close', new Base.CloseEvent(null, null)); 24 | return true; 25 | }, 26 | 27 | parse: function(chunk) { 28 | if (this.readyState > 1) return; 29 | 30 | this._reader.put(chunk); 31 | 32 | this._reader.eachByte(function(octet) { 33 | var message; 34 | 35 | switch (this._stage) { 36 | case -1: 37 | this._body.push(octet); 38 | this._sendHandshakeBody(); 39 | break; 40 | 41 | case 0: 42 | this._parseLeadingByte(octet); 43 | break; 44 | 45 | case 1: 46 | this._length = (octet & 0x7F) + 128 * this._length; 47 | 48 | if (this._closing && this._length === 0) { 49 | return this.close(); 50 | } 51 | else if ((octet & 0x80) !== 0x80) { 52 | if (this._length === 0) { 53 | this._stage = 0; 54 | } 55 | else { 56 | this._skipped = 0; 57 | this._stage = 2; 58 | } 59 | } 60 | break; 61 | 62 | case 2: 63 | if (octet === 0xFF) { 64 | this._stage = 0; 65 | message = Buffer.from(this._buffer).toString('utf8', 0, this._buffer.length); 66 | this.emit('message', new Base.MessageEvent(message)); 67 | } 68 | else { 69 | if (this._length) { 70 | this._skipped += 1; 71 | if (this._skipped === this._length) 72 | this._stage = 0; 73 | } else { 74 | this._buffer.push(octet); 75 | if (this._buffer.length > this._maxLength) return this.close(); 76 | } 77 | } 78 | break; 79 | } 80 | }, this); 81 | }, 82 | 83 | frame: function(buffer) { 84 | if (this.readyState === 0) return this._queue([buffer]); 85 | if (this.readyState > 1) return false; 86 | 87 | if (typeof buffer !== 'string') buffer = buffer.toString(); 88 | 89 | var length = Buffer.byteLength(buffer), 90 | frame = Buffer.allocUnsafe(length + 2); 91 | 92 | frame[0] = 0x00; 93 | frame.write(buffer, 1); 94 | frame[frame.length - 1] = 0xFF; 95 | 96 | this._write(frame); 97 | return true; 98 | }, 99 | 100 | _handshakeResponse: function() { 101 | var start = 'HTTP/1.1 101 Web Socket Protocol Handshake', 102 | headers = [start, this._headers.toString(), '']; 103 | 104 | return Buffer.from(headers.join('\r\n'), 'utf8'); 105 | }, 106 | 107 | _parseLeadingByte: function(octet) { 108 | if ((octet & 0x80) === 0x80) { 109 | this._length = 0; 110 | this._stage = 1; 111 | } else { 112 | delete this._length; 113 | delete this._skipped; 114 | this._buffer = []; 115 | this._stage = 2; 116 | } 117 | } 118 | }; 119 | 120 | for (var key in instance) 121 | Draft75.prototype[key] = instance[key]; 122 | 123 | module.exports = Draft75; 124 | -------------------------------------------------------------------------------- /lib/websocket/driver/draft76.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer, 4 | Base = require('./base'), 5 | Draft75 = require('./draft75'), 6 | crypto = require('crypto'), 7 | util = require('util'); 8 | 9 | 10 | var numberFromKey = function(key) { 11 | return parseInt((key.match(/[0-9]/g) || []).join(''), 10); 12 | }; 13 | 14 | var spacesInKey = function(key) { 15 | return (key.match(/ /g) || []).length; 16 | }; 17 | 18 | 19 | var Draft76 = function(request, url, options) { 20 | Draft75.apply(this, arguments); 21 | this._stage = -1; 22 | this._body = []; 23 | this.version = 'hixie-76'; 24 | 25 | this._headers.clear(); 26 | 27 | this._headers.set('Upgrade', 'WebSocket'); 28 | this._headers.set('Connection', 'Upgrade'); 29 | this._headers.set('Sec-WebSocket-Origin', this._request.headers.origin); 30 | this._headers.set('Sec-WebSocket-Location', this.url); 31 | }; 32 | util.inherits(Draft76, Draft75); 33 | 34 | var instance = { 35 | BODY_SIZE: 8, 36 | 37 | start: function() { 38 | if (!Draft75.prototype.start.call(this)) return false; 39 | this._started = true; 40 | this._sendHandshakeBody(); 41 | return true; 42 | }, 43 | 44 | close: function() { 45 | if (this.readyState === 3) return false; 46 | if (this.readyState === 1) this._write(Buffer.from([0xFF, 0x00])); 47 | this.readyState = 3; 48 | this.emit('close', new Base.CloseEvent(null, null)); 49 | return true; 50 | }, 51 | 52 | _handshakeResponse: function() { 53 | var headers = this._request.headers, 54 | key1 = headers['sec-websocket-key1'], 55 | key2 = headers['sec-websocket-key2']; 56 | 57 | if (!key1) throw new Error('Missing required header: Sec-WebSocket-Key1'); 58 | if (!key2) throw new Error('Missing required header: Sec-WebSocket-Key2'); 59 | 60 | var number1 = numberFromKey(key1), 61 | spaces1 = spacesInKey(key1), 62 | 63 | number2 = numberFromKey(key2), 64 | spaces2 = spacesInKey(key2); 65 | 66 | if (number1 % spaces1 !== 0 || number2 % spaces2 !== 0) 67 | throw new Error('Client sent invalid Sec-WebSocket-Key headers'); 68 | 69 | this._keyValues = [number1 / spaces1, number2 / spaces2]; 70 | 71 | var start = 'HTTP/1.1 101 WebSocket Protocol Handshake', 72 | headers = [start, this._headers.toString(), '']; 73 | 74 | return Buffer.from(headers.join('\r\n'), 'binary'); 75 | }, 76 | 77 | _handshakeSignature: function() { 78 | if (this._body.length < this.BODY_SIZE) return null; 79 | 80 | var md5 = crypto.createHash('md5'), 81 | buffer = Buffer.allocUnsafe(8 + this.BODY_SIZE); 82 | 83 | buffer.writeUInt32BE(this._keyValues[0], 0); 84 | buffer.writeUInt32BE(this._keyValues[1], 4); 85 | Buffer.from(this._body).copy(buffer, 8, 0, this.BODY_SIZE); 86 | 87 | md5.update(buffer); 88 | return Buffer.from(md5.digest('binary'), 'binary'); 89 | }, 90 | 91 | _sendHandshakeBody: function() { 92 | if (!this._started) return; 93 | var signature = this._handshakeSignature(); 94 | if (!signature) return; 95 | 96 | this._write(signature); 97 | this._stage = 0; 98 | this._open(); 99 | 100 | if (this._body.length > this.BODY_SIZE) 101 | this.parse(this._body.slice(this.BODY_SIZE)); 102 | }, 103 | 104 | _parseLeadingByte: function(octet) { 105 | if (octet !== 0xFF) 106 | return Draft75.prototype._parseLeadingByte.call(this, octet); 107 | 108 | this._closing = true; 109 | this._length = 0; 110 | this._stage = 1; 111 | } 112 | }; 113 | 114 | for (var key in instance) 115 | Draft76.prototype[key] = instance[key]; 116 | 117 | module.exports = Draft76; 118 | -------------------------------------------------------------------------------- /spec/websocket/driver/draft75_spec.js: -------------------------------------------------------------------------------- 1 | var Draft75 = require("../../../lib/websocket/driver/draft75"), 2 | test = require('jstest').Test 3 | 4 | test.describe("Draft75", function() { with(this) { 5 | define("request", function() { 6 | return this._request = this._request || { 7 | method: "GET", 8 | headers: { 9 | "connection": "Upgrade", 10 | "upgrade": "WebSocket", 11 | "origin": "http://www.example.com" 12 | } 13 | } 14 | }) 15 | 16 | define("options", function() { 17 | return this._options = this._options || { masking: false } 18 | }) 19 | 20 | define("driver", function() { 21 | if (this._driver) return this._driver 22 | this._driver = new Draft75(this.request(), "ws://www.example.com/socket", this.options()) 23 | var self = this 24 | this._driver.on('open', function(e) { self.open = true }) 25 | this._driver.on('message', function(e) { self.message += e.data }) 26 | this._driver.on('close', function(e) { self.close = true }) 27 | this._driver.io.pipe(this.collector()) 28 | return this._driver 29 | }) 30 | 31 | before(function() { 32 | this.open = this.close = false 33 | this.message = "" 34 | }) 35 | 36 | describe("in the connecting state", function() { with(this) { 37 | it("starts in the connecting state", function() { with(this) { 38 | assertEqual( "connecting", driver().getState() ) 39 | }}) 40 | 41 | describe("start", function() { with(this) { 42 | it("writes the handshake response to the socket", function() { with(this) { 43 | expect(driver().io, "emit").given("data", buffer( 44 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + 45 | "Upgrade: WebSocket\r\n" + 46 | "Connection: Upgrade\r\n" + 47 | "WebSocket-Origin: http://www.example.com\r\n" + 48 | "WebSocket-Location: ws://www.example.com/socket\r\n" + 49 | "\r\n")) 50 | driver().start() 51 | }}) 52 | 53 | it("returns true", function() { with(this) { 54 | assertEqual( true, driver().start() ) 55 | }}) 56 | 57 | it("triggers the onopen event", function() { with(this) { 58 | driver().start() 59 | assertEqual( true, open ) 60 | }}) 61 | 62 | it("changes the state to open", function() { with(this) { 63 | driver().start() 64 | assertEqual( "open", driver().getState() ) 65 | }}) 66 | 67 | it("sets the protocol version", function() { with(this) { 68 | driver().start() 69 | assertEqual( "hixie-75", driver().version ) 70 | }}) 71 | }}) 72 | 73 | describe("frame", function() { with(this) { 74 | it("does not write to the socket", function() { with(this) { 75 | expect(driver().io, "emit").exactly(0) 76 | driver().frame("Hello, world") 77 | }}) 78 | 79 | it("returns true", function() { with(this) { 80 | assertEqual( true, driver().frame("whatever") ) 81 | }}) 82 | 83 | it("queues the frames until the handshake has been sent", function() { with(this) { 84 | expect(driver().io, "emit").given("data", buffer( 85 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + 86 | "Upgrade: WebSocket\r\n" + 87 | "Connection: Upgrade\r\n" + 88 | "WebSocket-Origin: http://www.example.com\r\n" + 89 | "WebSocket-Location: ws://www.example.com/socket\r\n" + 90 | "\r\n")) 91 | expect(driver().io, "emit").given("data", buffer([0x00, 0x48, 0x69, 0xff])) 92 | 93 | driver().frame("Hi") 94 | driver().start() 95 | }}) 96 | }}) 97 | }}) 98 | 99 | itShouldBehaveLike("draft-75 protocol") 100 | }}) 101 | -------------------------------------------------------------------------------- /lib/websocket/driver/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'), 4 | HttpParser = require('../http_parser'), 5 | Base = require('./base'), 6 | Draft75 = require('./draft75'), 7 | Draft76 = require('./draft76'), 8 | Hybi = require('./hybi'); 9 | 10 | var Server = function(options) { 11 | Base.call(this, null, null, options); 12 | this._http = new HttpParser('request'); 13 | }; 14 | util.inherits(Server, Base); 15 | 16 | var instance = { 17 | EVENTS: ['open', 'message', 'error', 'close', 'ping', 'pong'], 18 | 19 | _bindEventListeners: function() { 20 | this.messages.on('error', function() {}); 21 | this.on('error', function() {}); 22 | }, 23 | 24 | parse: function(chunk) { 25 | if (this._delegate) return this._delegate.parse(chunk); 26 | 27 | this._http.parse(chunk); 28 | if (!this._http.isComplete()) return; 29 | 30 | this.method = this._http.method; 31 | this.url = this._http.url; 32 | this.headers = this._http.headers; 33 | this.body = this._http.body; 34 | 35 | var self = this; 36 | this._delegate = Server.http(this, this._options); 37 | this._delegate.messages = this.messages; 38 | this._delegate.io = this.io; 39 | this._open(); 40 | 41 | this.EVENTS.forEach(function(event) { 42 | this._delegate.on(event, function(e) { self.emit(event, e) }); 43 | }, this); 44 | 45 | this.protocol = this._delegate.protocol; 46 | this.version = this._delegate.version; 47 | 48 | this.parse(this._http.body); 49 | this.emit('connect', new Base.ConnectEvent()); 50 | }, 51 | 52 | _open: function() { 53 | this.__queue.forEach(function(msg) { 54 | this._delegate[msg[0]].apply(this._delegate, msg[1]); 55 | }, this); 56 | this.__queue = []; 57 | } 58 | }; 59 | 60 | ['addExtension', 'setHeader', 'start', 'frame', 'text', 'binary', 'ping', 'close'].forEach(function(method) { 61 | instance[method] = function() { 62 | if (this._delegate) { 63 | return this._delegate[method].apply(this._delegate, arguments); 64 | } else { 65 | this.__queue.push([method, arguments]); 66 | return true; 67 | } 68 | }; 69 | }); 70 | 71 | for (var key in instance) 72 | Server.prototype[key] = instance[key]; 73 | 74 | Server.isSecureRequest = function(request) { 75 | if (request.connection && request.connection.authorized !== undefined) return true; 76 | if (request.socket && request.socket.secure) return true; 77 | 78 | var headers = request.headers; 79 | if (!headers) return false; 80 | if (headers['https'] === 'on') return true; 81 | if (headers['x-forwarded-ssl'] === 'on') return true; 82 | if (headers['x-forwarded-scheme'] === 'https') return true; 83 | if (headers['x-forwarded-proto'] === 'https') return true; 84 | 85 | return false; 86 | }; 87 | 88 | Server.determineUrl = function(request) { 89 | var scheme = this.isSecureRequest(request) ? 'wss:' : 'ws:'; 90 | return scheme + '//' + request.headers.host + request.url; 91 | }; 92 | 93 | Server.http = function(request, options) { 94 | options = options || {}; 95 | if (options.requireMasking === undefined) options.requireMasking = true; 96 | 97 | var headers = request.headers, 98 | version = headers['sec-websocket-version'], 99 | key = headers['sec-websocket-key'], 100 | key1 = headers['sec-websocket-key1'], 101 | key2 = headers['sec-websocket-key2'], 102 | url = this.determineUrl(request); 103 | 104 | if (version || key) 105 | return new Hybi(request, url, options); 106 | else if (key1 || key2) 107 | return new Draft76(request, url, options); 108 | else 109 | return new Draft75(request, url, options); 110 | }; 111 | 112 | module.exports = Server; 113 | -------------------------------------------------------------------------------- /lib/websocket/http_parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var NodeHTTPParser = require('http-parser-js').HTTPParser, 4 | Buffer = require('safe-buffer').Buffer; 5 | 6 | var TYPES = { 7 | request: NodeHTTPParser.REQUEST || 'request', 8 | response: NodeHTTPParser.RESPONSE || 'response' 9 | }; 10 | 11 | var HttpParser = function(type) { 12 | this._type = type; 13 | this._parser = new NodeHTTPParser(TYPES[type]); 14 | this._complete = false; 15 | this.headers = {}; 16 | 17 | var current = null, 18 | self = this; 19 | 20 | this._parser.onHeaderField = function(b, start, length) { 21 | current = b.toString('utf8', start, start + length).toLowerCase(); 22 | }; 23 | 24 | this._parser.onHeaderValue = function(b, start, length) { 25 | var value = b.toString('utf8', start, start + length); 26 | 27 | if (self.headers.hasOwnProperty(current)) 28 | self.headers[current] += ', ' + value; 29 | else 30 | self.headers[current] = value; 31 | }; 32 | 33 | this._parser.onHeadersComplete = this._parser[NodeHTTPParser.kOnHeadersComplete] = 34 | function(majorVersion, minorVersion, headers, method, pathname, statusCode) { 35 | var info = arguments[0]; 36 | 37 | if (typeof info === 'object') { 38 | method = info.method; 39 | pathname = info.url; 40 | statusCode = info.statusCode; 41 | headers = info.headers; 42 | } 43 | 44 | self.method = (typeof method === 'number') ? HttpParser.METHODS[method] : method; 45 | self.statusCode = statusCode; 46 | self.url = pathname; 47 | 48 | if (!headers) return; 49 | 50 | for (var i = 0, n = headers.length, key, value; i < n; i += 2) { 51 | key = headers[i].toLowerCase(); 52 | value = headers[i+1]; 53 | if (self.headers.hasOwnProperty(key)) 54 | self.headers[key] += ', ' + value; 55 | else 56 | self.headers[key] = value; 57 | } 58 | 59 | self._complete = true; 60 | }; 61 | }; 62 | 63 | HttpParser.METHODS = { 64 | 0: 'DELETE', 65 | 1: 'GET', 66 | 2: 'HEAD', 67 | 3: 'POST', 68 | 4: 'PUT', 69 | 5: 'CONNECT', 70 | 6: 'OPTIONS', 71 | 7: 'TRACE', 72 | 8: 'COPY', 73 | 9: 'LOCK', 74 | 10: 'MKCOL', 75 | 11: 'MOVE', 76 | 12: 'PROPFIND', 77 | 13: 'PROPPATCH', 78 | 14: 'SEARCH', 79 | 15: 'UNLOCK', 80 | 16: 'BIND', 81 | 17: 'REBIND', 82 | 18: 'UNBIND', 83 | 19: 'ACL', 84 | 20: 'REPORT', 85 | 21: 'MKACTIVITY', 86 | 22: 'CHECKOUT', 87 | 23: 'MERGE', 88 | 24: 'M-SEARCH', 89 | 25: 'NOTIFY', 90 | 26: 'SUBSCRIBE', 91 | 27: 'UNSUBSCRIBE', 92 | 28: 'PATCH', 93 | 29: 'PURGE', 94 | 30: 'MKCALENDAR', 95 | 31: 'LINK', 96 | 32: 'UNLINK' 97 | }; 98 | 99 | var VERSION = process.version 100 | ? process.version.match(/[0-9]+/g).map(function(n) { return parseInt(n, 10) }) 101 | : []; 102 | 103 | if (VERSION[0] === 0 && VERSION[1] === 12) { 104 | HttpParser.METHODS[16] = 'REPORT'; 105 | HttpParser.METHODS[17] = 'MKACTIVITY'; 106 | HttpParser.METHODS[18] = 'CHECKOUT'; 107 | HttpParser.METHODS[19] = 'MERGE'; 108 | HttpParser.METHODS[20] = 'M-SEARCH'; 109 | HttpParser.METHODS[21] = 'NOTIFY'; 110 | HttpParser.METHODS[22] = 'SUBSCRIBE'; 111 | HttpParser.METHODS[23] = 'UNSUBSCRIBE'; 112 | HttpParser.METHODS[24] = 'PATCH'; 113 | HttpParser.METHODS[25] = 'PURGE'; 114 | } 115 | 116 | HttpParser.prototype.isComplete = function() { 117 | return this._complete; 118 | }; 119 | 120 | HttpParser.prototype.parse = function(chunk) { 121 | var consumed = this._parser.execute(chunk, 0, chunk.length); 122 | 123 | if (typeof consumed !== 'number') { 124 | this.error = consumed; 125 | this._complete = true; 126 | return; 127 | } 128 | 129 | if (this._complete) 130 | this.body = (consumed < chunk.length) 131 | ? chunk.slice(consumed) 132 | : Buffer.alloc(0); 133 | }; 134 | 135 | module.exports = HttpParser; 136 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.7.4 / 2020-05-22 2 | 3 | - Avoid crashing if `process.version` does not contain any digits 4 | - Emit `ping` and `pong` events from the `Server` driver 5 | - Require http-parser-js >=0.5.1 which fixes the bug we addressed in 0.7.3 6 | 7 | ### 0.7.3 / 2019-06-13 8 | 9 | - Cap version of http-parser-js below 0.4.11, which introduced a bug that 10 | prevents us from handling messages that are part of the same input buffer as 11 | the handshake response if chunked encoding is specified 12 | 13 | ### 0.7.2 / 2019-06-13 14 | 15 | (This version was pulled due to an error when publishing) 16 | 17 | ### 0.7.1 / 2019-06-10 18 | 19 | - Catch any exceptions produced while generating a handshake response and send a 20 | `400 Bad Request` response to the client 21 | - Pick the RFC-6455 protocol version if the request contains any of the headers 22 | used by that version 23 | - Use the `Buffer.alloc()` and `Buffer.from()` functions instead of the unsafe 24 | `Buffer()` constructor 25 | - Handle errors encountered while handling malformed draft-76 requests 26 | - Change license from MIT to Apache 2.0 27 | 28 | ### 0.7.0 / 2017-09-11 29 | 30 | - Add `ping` and `pong` to the set of events users can listen to 31 | - Replace the bindings to Node's HTTP parser with `http-parser-js` 32 | 33 | ### 0.6.5 / 2016-05-20 34 | 35 | - Don't mutate buffers passed in by the application when masking 36 | 37 | ### 0.6.4 / 2016-01-07 38 | 39 | - If a number is given as input for a frame payload, send it as a string 40 | 41 | ### 0.6.3 / 2015-11-06 42 | 43 | - Reject draft-76 handshakes if their Sec-WebSocket-Key headers are invalid 44 | - Throw a more helpful error if a client is created with an invalid URL 45 | 46 | ### 0.6.2 / 2015-07-18 47 | 48 | - When the peer sends a close frame with no error code, emit 1000 49 | 50 | ### 0.6.1 / 2015-07-13 51 | 52 | - Use the `buffer.{read,write}UInt{16,32}BE` methods for reading/writing numbers 53 | to buffers rather than including duplicate logic for this 54 | 55 | ### 0.6.0 / 2015-07-08 56 | 57 | - Allow the parser to recover cleanly if event listeners raise an error 58 | - Add a `pong` method for sending unsolicited pong frames 59 | 60 | ### 0.5.4 / 2015-03-29 61 | 62 | - Don't emit extra close frames if we receive a close frame after we already 63 | sent one 64 | - Fail the connection when the driver receives an invalid 65 | `Sec-WebSocket-Extensions` header 66 | 67 | ### 0.5.3 / 2015-02-22 68 | 69 | - Don't treat incoming data as WebSocket frames if a client driver is closed 70 | before receiving the server handshake 71 | 72 | ### 0.5.2 / 2015-02-19 73 | 74 | - Fix compatibility with the HTTP parser on io.js 75 | - Use `websocket-extensions` to make sure messages and close frames are kept in 76 | order 77 | - Don't emit multiple `error` events 78 | 79 | ### 0.5.1 / 2014-12-18 80 | 81 | - Don't allow drivers to be created with unrecognized options 82 | 83 | ### 0.5.0 / 2014-12-13 84 | 85 | - Support protocol extensions via the websocket-extensions module 86 | 87 | ### 0.4.0 / 2014-11-08 88 | 89 | - Support connection via HTTP proxies using `CONNECT` 90 | 91 | ### 0.3.6 / 2014-10-04 92 | 93 | - It is now possible to call `close()` before `start()` and close the driver 94 | 95 | ### 0.3.5 / 2014-07-06 96 | 97 | - Don't hold references to frame buffers after a message has been emitted 98 | - Make sure that `protocol` and `version` are exposed properly by the TCP driver 99 | 100 | ### 0.3.4 / 2014-05-08 101 | 102 | - Don't hold memory-leaking references to I/O buffers after they have been 103 | parsed 104 | 105 | ### 0.3.3 / 2014-04-24 106 | 107 | - Correct the draft-76 status line reason phrase 108 | 109 | ### 0.3.2 / 2013-12-29 110 | 111 | - Expand `maxLength` to cover sequences of continuation frames and 112 | `draft-{75,76}` 113 | - Decrease default maximum frame buffer size to 64MB 114 | - Stop parsing when the protocol enters a failure mode, to save CPU cycles 115 | 116 | ### 0.3.1 / 2013-12-03 117 | 118 | - Add a `maxLength` option to limit allowed frame size 119 | - Don't pre-allocate a message buffer until the whole frame has arrived 120 | - Fix compatibility with Node v0.11 `HTTPParser` 121 | 122 | ### 0.3.0 / 2013-09-09 123 | 124 | - Support client URLs with Basic Auth credentials 125 | 126 | ### 0.2.2 / 2013-07-05 127 | 128 | - No functional changes, just updates to package.json 129 | 130 | ### 0.2.1 / 2013-05-17 131 | 132 | - Export the isSecureRequest() method since faye-websocket relies on it 133 | - Queue sent messages in the client's initial state 134 | 135 | ### 0.2.0 / 2013-05-12 136 | 137 | - Add API for setting and reading headers 138 | - Add Driver.server() method for getting a driver for TCP servers 139 | 140 | ### 0.1.0 / 2013-05-04 141 | 142 | - First stable release 143 | -------------------------------------------------------------------------------- /lib/websocket/driver/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer, 4 | crypto = require('crypto'), 5 | url = require('url'), 6 | util = require('util'), 7 | HttpParser = require('../http_parser'), 8 | Base = require('./base'), 9 | Hybi = require('./hybi'), 10 | Proxy = require('./proxy'); 11 | 12 | var Client = function(_url, options) { 13 | this.version = 'hybi-' + Hybi.VERSION; 14 | Hybi.call(this, null, _url, options); 15 | 16 | this.readyState = -1; 17 | this._key = Client.generateKey(); 18 | this._accept = Hybi.generateAccept(this._key); 19 | this._http = new HttpParser('response'); 20 | 21 | var uri = url.parse(this.url), 22 | auth = uri.auth && Buffer.from(uri.auth, 'utf8').toString('base64'); 23 | 24 | if (this.VALID_PROTOCOLS.indexOf(uri.protocol) < 0) 25 | throw new Error(this.url + ' is not a valid WebSocket URL'); 26 | 27 | this._pathname = (uri.pathname || '/') + (uri.search || ''); 28 | 29 | this._headers.set('Host', uri.host); 30 | this._headers.set('Upgrade', 'websocket'); 31 | this._headers.set('Connection', 'Upgrade'); 32 | this._headers.set('Sec-WebSocket-Key', this._key); 33 | this._headers.set('Sec-WebSocket-Version', Hybi.VERSION); 34 | 35 | if (this._protocols.length > 0) 36 | this._headers.set('Sec-WebSocket-Protocol', this._protocols.join(', ')); 37 | 38 | if (auth) 39 | this._headers.set('Authorization', 'Basic ' + auth); 40 | }; 41 | util.inherits(Client, Hybi); 42 | 43 | Client.generateKey = function() { 44 | return crypto.randomBytes(16).toString('base64'); 45 | }; 46 | 47 | var instance = { 48 | VALID_PROTOCOLS: ['ws:', 'wss:'], 49 | 50 | proxy: function(origin, options) { 51 | return new Proxy(this, origin, options); 52 | }, 53 | 54 | start: function() { 55 | if (this.readyState !== -1) return false; 56 | this._write(this._handshakeRequest()); 57 | this.readyState = 0; 58 | return true; 59 | }, 60 | 61 | parse: function(chunk) { 62 | if (this.readyState === 3) return; 63 | if (this.readyState > 0) return Hybi.prototype.parse.call(this, chunk); 64 | 65 | this._http.parse(chunk); 66 | if (!this._http.isComplete()) return; 67 | 68 | this._validateHandshake(); 69 | if (this.readyState === 3) return; 70 | 71 | this._open(); 72 | this.parse(this._http.body); 73 | }, 74 | 75 | _handshakeRequest: function() { 76 | var extensions = this._extensions.generateOffer(); 77 | if (extensions) 78 | this._headers.set('Sec-WebSocket-Extensions', extensions); 79 | 80 | var start = 'GET ' + this._pathname + ' HTTP/1.1', 81 | headers = [start, this._headers.toString(), '']; 82 | 83 | return Buffer.from(headers.join('\r\n'), 'utf8'); 84 | }, 85 | 86 | _failHandshake: function(message) { 87 | message = 'Error during WebSocket handshake: ' + message; 88 | this.readyState = 3; 89 | this.emit('error', new Error(message)); 90 | this.emit('close', new Base.CloseEvent(this.ERRORS.protocol_error, message)); 91 | }, 92 | 93 | _validateHandshake: function() { 94 | this.statusCode = this._http.statusCode; 95 | this.headers = this._http.headers; 96 | 97 | if (this._http.error) 98 | return this._failHandshake(this._http.error.message); 99 | 100 | if (this._http.statusCode !== 101) 101 | return this._failHandshake('Unexpected response code: ' + this._http.statusCode); 102 | 103 | var headers = this._http.headers, 104 | upgrade = headers['upgrade'] || '', 105 | connection = headers['connection'] || '', 106 | accept = headers['sec-websocket-accept'] || '', 107 | protocol = headers['sec-websocket-protocol'] || ''; 108 | 109 | if (upgrade === '') 110 | return this._failHandshake("'Upgrade' header is missing"); 111 | if (upgrade.toLowerCase() !== 'websocket') 112 | return this._failHandshake("'Upgrade' header value is not 'WebSocket'"); 113 | 114 | if (connection === '') 115 | return this._failHandshake("'Connection' header is missing"); 116 | if (connection.toLowerCase() !== 'upgrade') 117 | return this._failHandshake("'Connection' header value is not 'Upgrade'"); 118 | 119 | if (accept !== this._accept) 120 | return this._failHandshake('Sec-WebSocket-Accept mismatch'); 121 | 122 | this.protocol = null; 123 | 124 | if (protocol !== '') { 125 | if (this._protocols.indexOf(protocol) < 0) 126 | return this._failHandshake('Sec-WebSocket-Protocol mismatch'); 127 | else 128 | this.protocol = protocol; 129 | } 130 | 131 | try { 132 | this._extensions.activate(this.headers['sec-websocket-extensions']); 133 | } catch (e) { 134 | return this._failHandshake(e.message); 135 | } 136 | } 137 | }; 138 | 139 | for (var key in instance) 140 | Client.prototype[key] = instance[key]; 141 | 142 | module.exports = Client; 143 | -------------------------------------------------------------------------------- /lib/websocket/streams.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | 5 | Streams in a WebSocket connection 6 | --------------------------------- 7 | 8 | We model a WebSocket as two duplex streams: one stream is for the wire protocol 9 | over an I/O socket, and the other is for incoming/outgoing messages. 10 | 11 | 12 | +----------+ +---------+ +----------+ 13 | [1] write(chunk) -->| ~~~~~~~~ +----->| parse() +----->| ~~~~~~~~ +--> emit('data') [2] 14 | | | +----+----+ | | 15 | | | | | | 16 | | IO | | [5] | Messages | 17 | | | V | | 18 | | | +---------+ | | 19 | [4] emit('data') <--+ ~~~~~~~~ |<-----+ frame() |<-----+ ~~~~~~~~ |<-- write(chunk) [3] 20 | +----------+ +---------+ +----------+ 21 | 22 | 23 | Message transfer in each direction is simple: IO receives a byte stream [1] and 24 | sends this stream for parsing. The parser will periodically emit a complete 25 | message text on the Messages stream [2]. Similarly, when messages are written 26 | to the Messages stream [3], they are framed using the WebSocket wire format and 27 | emitted via IO [4]. 28 | 29 | There is a feedback loop via [5] since some input from [1] will be things like 30 | ping, pong and close frames. In these cases the protocol responds by emitting 31 | responses directly back to [4] rather than emitting messages via [2]. 32 | 33 | For the purposes of flow control, we consider the sources of each Readable 34 | stream to be as follows: 35 | 36 | * [2] receives input from [1] 37 | * [4] receives input from [1] and [3] 38 | 39 | The classes below express the relationships described above without prescribing 40 | anything about how parse() and frame() work, other than assuming they emit 41 | 'data' events to the IO and Messages streams. They will work with any protocol 42 | driver having these two methods. 43 | **/ 44 | 45 | 46 | var Stream = require('stream').Stream, 47 | util = require('util'); 48 | 49 | 50 | var IO = function(driver) { 51 | this.readable = this.writable = true; 52 | this._paused = false; 53 | this._driver = driver; 54 | }; 55 | util.inherits(IO, Stream); 56 | 57 | // The IO pause() and resume() methods will be called when the socket we are 58 | // piping to gets backed up and drains. Since IO output [4] comes from IO input 59 | // [1] and Messages input [3], we need to tell both of those to return false 60 | // from write() when this stream is paused. 61 | 62 | IO.prototype.pause = function() { 63 | this._paused = true; 64 | this._driver.messages._paused = true; 65 | }; 66 | 67 | IO.prototype.resume = function() { 68 | this._paused = false; 69 | this.emit('drain'); 70 | 71 | var messages = this._driver.messages; 72 | messages._paused = false; 73 | messages.emit('drain'); 74 | }; 75 | 76 | // When we receive input from a socket, send it to the parser and tell the 77 | // source whether to back off. 78 | IO.prototype.write = function(chunk) { 79 | if (!this.writable) return false; 80 | this._driver.parse(chunk); 81 | return !this._paused; 82 | }; 83 | 84 | // The IO end() method will be called when the socket piping into it emits 85 | // 'close' or 'end', i.e. the socket is closed. In this situation the Messages 86 | // stream will not emit any more data so we emit 'end'. 87 | IO.prototype.end = function(chunk) { 88 | if (!this.writable) return; 89 | if (chunk !== undefined) this.write(chunk); 90 | this.writable = false; 91 | 92 | var messages = this._driver.messages; 93 | if (messages.readable) { 94 | messages.readable = messages.writable = false; 95 | messages.emit('end'); 96 | } 97 | }; 98 | 99 | IO.prototype.destroy = function() { 100 | this.end(); 101 | }; 102 | 103 | 104 | var Messages = function(driver) { 105 | this.readable = this.writable = true; 106 | this._paused = false; 107 | this._driver = driver; 108 | }; 109 | util.inherits(Messages, Stream); 110 | 111 | // The Messages pause() and resume() methods will be called when the app that's 112 | // processing the messages gets backed up and drains. If we're emitting 113 | // messages too fast we should tell the source to slow down. Message output [2] 114 | // comes from IO input [1]. 115 | 116 | Messages.prototype.pause = function() { 117 | this._driver.io._paused = true; 118 | }; 119 | 120 | Messages.prototype.resume = function() { 121 | this._driver.io._paused = false; 122 | this._driver.io.emit('drain'); 123 | }; 124 | 125 | // When we receive messages from the user, send them to the formatter and tell 126 | // the source whether to back off. 127 | Messages.prototype.write = function(message) { 128 | if (!this.writable) return false; 129 | if (typeof message === 'string') this._driver.text(message); 130 | else this._driver.binary(message); 131 | return !this._paused; 132 | }; 133 | 134 | // The Messages end() method will be called when a stream piping into it emits 135 | // 'end'. Many streams may be piped into the WebSocket and one of them ending 136 | // does not mean the whole socket is done, so just process the input and move 137 | // on leaving the socket open. 138 | Messages.prototype.end = function(message) { 139 | if (message !== undefined) this.write(message); 140 | }; 141 | 142 | Messages.prototype.destroy = function() {}; 143 | 144 | 145 | exports.IO = IO; 146 | exports.Messages = Messages; 147 | -------------------------------------------------------------------------------- /lib/websocket/driver/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer, 4 | Emitter = require('events').EventEmitter, 5 | util = require('util'), 6 | streams = require('../streams'), 7 | Headers = require('./headers'), 8 | Reader = require('./stream_reader'); 9 | 10 | var Base = function(request, url, options) { 11 | Emitter.call(this); 12 | Base.validateOptions(options || {}, ['maxLength', 'masking', 'requireMasking', 'protocols']); 13 | 14 | this._request = request; 15 | this._reader = new Reader(); 16 | this._options = options || {}; 17 | this._maxLength = this._options.maxLength || this.MAX_LENGTH; 18 | this._headers = new Headers(); 19 | this.__queue = []; 20 | this.readyState = 0; 21 | this.url = url; 22 | 23 | this.io = new streams.IO(this); 24 | this.messages = new streams.Messages(this); 25 | this._bindEventListeners(); 26 | }; 27 | util.inherits(Base, Emitter); 28 | 29 | Base.isWebSocket = function(request) { 30 | var connection = request.headers.connection || '', 31 | upgrade = request.headers.upgrade || ''; 32 | 33 | return request.method === 'GET' && 34 | connection.toLowerCase().split(/ *, */).indexOf('upgrade') >= 0 && 35 | upgrade.toLowerCase() === 'websocket'; 36 | }; 37 | 38 | Base.validateOptions = function(options, validKeys) { 39 | for (var key in options) { 40 | if (validKeys.indexOf(key) < 0) 41 | throw new Error('Unrecognized option: ' + key); 42 | } 43 | }; 44 | 45 | var instance = { 46 | // This is 64MB, small enough for an average VPS to handle without 47 | // crashing from process out of memory 48 | MAX_LENGTH: 0x3ffffff, 49 | 50 | STATES: ['connecting', 'open', 'closing', 'closed'], 51 | 52 | _bindEventListeners: function() { 53 | var self = this; 54 | 55 | // Protocol errors are informational and do not have to be handled 56 | this.messages.on('error', function() {}); 57 | 58 | this.on('message', function(event) { 59 | var messages = self.messages; 60 | if (messages.readable) messages.emit('data', event.data); 61 | }); 62 | 63 | this.on('error', function(error) { 64 | var messages = self.messages; 65 | if (messages.readable) messages.emit('error', error); 66 | }); 67 | 68 | this.on('close', function() { 69 | var messages = self.messages; 70 | if (!messages.readable) return; 71 | messages.readable = messages.writable = false; 72 | messages.emit('end'); 73 | }); 74 | }, 75 | 76 | getState: function() { 77 | return this.STATES[this.readyState] || null; 78 | }, 79 | 80 | addExtension: function(extension) { 81 | return false; 82 | }, 83 | 84 | setHeader: function(name, value) { 85 | if (this.readyState > 0) return false; 86 | this._headers.set(name, value); 87 | return true; 88 | }, 89 | 90 | start: function() { 91 | if (this.readyState !== 0) return false; 92 | 93 | if (!Base.isWebSocket(this._request)) 94 | return this._failHandshake(new Error('Not a WebSocket request')); 95 | 96 | var response; 97 | 98 | try { 99 | response = this._handshakeResponse(); 100 | } catch (error) { 101 | return this._failHandshake(error); 102 | } 103 | 104 | this._write(response); 105 | if (this._stage !== -1) this._open(); 106 | return true; 107 | }, 108 | 109 | _failHandshake: function(error) { 110 | var headers = new Headers(); 111 | headers.set('Content-Type', 'text/plain'); 112 | headers.set('Content-Length', Buffer.byteLength(error.message, 'utf8')); 113 | 114 | headers = ['HTTP/1.1 400 Bad Request', headers.toString(), error.message]; 115 | this._write(Buffer.from(headers.join('\r\n'), 'utf8')); 116 | this._fail('protocol_error', error.message); 117 | 118 | return false; 119 | }, 120 | 121 | text: function(message) { 122 | return this.frame(message); 123 | }, 124 | 125 | binary: function(message) { 126 | return false; 127 | }, 128 | 129 | ping: function() { 130 | return false; 131 | }, 132 | 133 | pong: function() { 134 | return false; 135 | }, 136 | 137 | close: function(reason, code) { 138 | if (this.readyState !== 1) return false; 139 | this.readyState = 3; 140 | this.emit('close', new Base.CloseEvent(null, null)); 141 | return true; 142 | }, 143 | 144 | _open: function() { 145 | this.readyState = 1; 146 | this.__queue.forEach(function(args) { this.frame.apply(this, args) }, this); 147 | this.__queue = []; 148 | this.emit('open', new Base.OpenEvent()); 149 | }, 150 | 151 | _queue: function(message) { 152 | this.__queue.push(message); 153 | return true; 154 | }, 155 | 156 | _write: function(chunk) { 157 | var io = this.io; 158 | if (io.readable) io.emit('data', chunk); 159 | }, 160 | 161 | _fail: function(type, message) { 162 | this.readyState = 2; 163 | this.emit('error', new Error(message)); 164 | this.close(); 165 | } 166 | }; 167 | 168 | for (var key in instance) 169 | Base.prototype[key] = instance[key]; 170 | 171 | 172 | Base.ConnectEvent = function() {}; 173 | 174 | Base.OpenEvent = function() {}; 175 | 176 | Base.CloseEvent = function(code, reason) { 177 | this.code = code; 178 | this.reason = reason; 179 | }; 180 | 181 | Base.MessageEvent = function(data) { 182 | this.data = data; 183 | }; 184 | 185 | Base.PingEvent = function(data) { 186 | this.data = data; 187 | }; 188 | 189 | Base.PongEvent = function(data) { 190 | this.data = data; 191 | }; 192 | 193 | module.exports = Base; 194 | -------------------------------------------------------------------------------- /spec/websocket/driver/draft75_examples.js: -------------------------------------------------------------------------------- 1 | var test = require('jstest').Test 2 | 3 | test.describe("draft-75", function() { with(this) { 4 | sharedExamplesFor("draft-75 protocol", function() { with(this) { 5 | describe("in the open state", function() { with(this) { 6 | before(function() { this.driver().start() }) 7 | 8 | describe("parse", function() { with(this) { 9 | it("parses text frames", function() { with(this) { 10 | driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]) 11 | assertEqual( "Hello", message ) 12 | }}) 13 | 14 | it("parses multiple frames from the same packet", function() { with(this) { 15 | driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]) 16 | assertEqual( "HelloHello", message ) 17 | }}) 18 | 19 | it("parses text frames beginning 0x00-0x7F", function() { with(this) { 20 | driver().parse([0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]) 21 | assertEqual( "Hello", message ) 22 | }}) 23 | 24 | it("ignores frames with a length header", function() { with(this) { 25 | driver().parse([0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]) 26 | assertEqual( "Hello", message ) 27 | }}) 28 | 29 | it("parses multibyte text frames", function() { with(this) { 30 | driver().parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]) 31 | assertEqual( "Apple = ", message ) 32 | }}) 33 | 34 | it("parses frames received in several packets", function() { with(this) { 35 | driver().parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65]) 36 | driver().parse([0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff]) 37 | assertEqual( "Apple = ", message ) 38 | }}) 39 | 40 | it("parses fragmented frames", function() { with(this) { 41 | driver().parse([0x00, 0x48, 0x65, 0x6c]) 42 | driver().parse([0x6c, 0x6f, 0xff]) 43 | assertEqual( "Hello", message ) 44 | }}) 45 | 46 | describe("when a message listener throws an error", function() { with(this) { 47 | before(function() { with(this) { 48 | this.messages = [] 49 | 50 | driver().on("message", function(msg) { 51 | messages.push(msg.data) 52 | throw new Error("an error") 53 | }) 54 | }}) 55 | 56 | it("is not trapped by the parser", function() { with(this) { 57 | var buffer = [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff] 58 | assertThrows(Error, function() { driver().parse(buffer) }) 59 | }}) 60 | 61 | it("parses text frames without dropping input", function() { with(this) { 62 | try { driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x57]) } catch (e) {} 63 | try { driver().parse([0x6f, 0x72, 0x6c, 0x64, 0xff]) } catch (e) {} 64 | assertEqual( ["Hello", "World"], messages ) 65 | }}) 66 | }}) 67 | }}) 68 | 69 | describe("frame", function() { with(this) { 70 | it("formats the given string as a WebSocket frame", function() { with(this) { 71 | driver().frame("Hello") 72 | assertEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], collector().bytes ) 73 | }}) 74 | 75 | it("encodes multibyte characters correctly", function() { with(this) { 76 | driver().frame("Apple = ") 77 | assertEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], collector().bytes ) 78 | }}) 79 | 80 | it("converts numbers to strings", function() { with(this) { 81 | driver().frame(50) 82 | assertEqual( [0x00, 0x35, 0x30, 0xff], collector().bytes ) 83 | }}) 84 | 85 | it("returns true", function() { with(this) { 86 | assertEqual( true, driver().frame("lol") ) 87 | }}) 88 | }}) 89 | 90 | describe("ping", function() { with(this) { 91 | it("does not write to the socket", function() { with(this) { 92 | expect(driver().io, "emit").exactly(0) 93 | driver().ping() 94 | }}) 95 | 96 | it("returns false", function() { with(this) { 97 | assertEqual( false, driver().ping() ) 98 | }}) 99 | }}) 100 | 101 | describe("close", function() { with(this) { 102 | it("triggers the onclose event", function() { with(this) { 103 | driver().close() 104 | assertEqual( true, close ) 105 | }}) 106 | 107 | it("returns true", function() { with(this) { 108 | assertEqual( true, driver().close() ) 109 | }}) 110 | 111 | it("changes the state to closed", function() { with(this) { 112 | driver().close() 113 | assertEqual( "closed", driver().getState() ) 114 | }}) 115 | }}) 116 | }}) 117 | 118 | describe("in the closed state", function() { with(this) { 119 | before(function() { 120 | this.driver().start() 121 | this.driver().close() 122 | }) 123 | 124 | describe("close", function() { with(this) { 125 | it("does not write to the socket", function() { with(this) { 126 | expect(driver().io, "emit").exactly(0) 127 | driver().close() 128 | }}) 129 | 130 | it("returns false", function() { with(this) { 131 | assertEqual( false, driver().close() ) 132 | }}) 133 | 134 | it("leaves the protocol in the closed state", function() { with(this) { 135 | driver().close() 136 | assertEqual( "closed", driver().getState() ) 137 | }}) 138 | }}) 139 | }}) 140 | }}) 141 | }}) 142 | -------------------------------------------------------------------------------- /spec/websocket/driver/draft76_spec.js: -------------------------------------------------------------------------------- 1 | var Draft76 = require("../../../lib/websocket/driver/draft76"), 2 | Buffer = require('safe-buffer').Buffer, 3 | test = require('jstest').Test 4 | 5 | test.describe("Draft76", function() { with(this) { 6 | BODY = Buffer.from([0x91, 0x25, 0x3e, 0xd3, 0xa9, 0xe7, 0x6a, 0x88]) 7 | 8 | define("body", function() { 9 | return BODY 10 | }) 11 | 12 | define("response", function() { 13 | return [0xb4, 0x9c, 0x6e, 0x40, 0x53, 0x04, 0x04, 0x26, 0xe5, 0x1b, 0xbf, 0x6c, 0xb7, 0x9f, 0x1d, 0xf9] 14 | }) 15 | 16 | define("request", function() { 17 | return this._request = this._request || { 18 | method: "GET", 19 | headers: { 20 | "connection": "Upgrade", 21 | "upgrade": "WebSocket", 22 | "origin": "http://www.example.com", 23 | "sec-websocket-key1": "1 38 wZ3f9 23O0 3l 0r", 24 | "sec-websocket-key2": "27 0E 6 2 1665:< ;U 1H" 25 | } 26 | } 27 | }) 28 | 29 | define("options", function() { 30 | return this._options = this._options || { masking: false } 31 | }) 32 | 33 | define("driver", function() { 34 | if (this._driver) return this._driver 35 | this._driver = new Draft76(this.request(), "ws://www.example.com/socket", this.options()) 36 | var self = this 37 | this._driver.on('open', function(e) { self.open = true }) 38 | this._driver.on('message', function(e) { self.message += e.data }) 39 | this._driver.on('error', function(e) { self.error = e }) 40 | this._driver.on('close', function(e) { self.close = true }) 41 | this._driver.io.pipe(this.collector()) 42 | this._driver.io.write(this.body()) 43 | return this._driver 44 | }) 45 | 46 | before(function() { 47 | this.open = this.close = false 48 | this.message = "" 49 | }) 50 | 51 | describe("in the connecting state", function() { with(this) { 52 | it("starts in the connecting state", function() { with(this) { 53 | assertEqual( "connecting", driver().getState() ) 54 | }}) 55 | 56 | describe("start", function() { with(this) { 57 | it("writes the handshake response to the socket", function() { with(this) { 58 | expect(driver().io, "emit").given("data", buffer( 59 | "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + 60 | "Upgrade: WebSocket\r\n" + 61 | "Connection: Upgrade\r\n" + 62 | "Sec-WebSocket-Origin: http://www.example.com\r\n" + 63 | "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" + 64 | "\r\n")) 65 | expect(driver().io, "emit").given("data", buffer(response())) 66 | driver().start() 67 | }}) 68 | 69 | it("returns true", function() { with(this) { 70 | assertEqual( true, driver().start() ) 71 | }}) 72 | 73 | it("triggers the onopen event", function() { with(this) { 74 | driver().start() 75 | assertEqual( true, open ) 76 | }}) 77 | 78 | it("changes the state to open", function() { with(this) { 79 | driver().start() 80 | assertEqual( "open", driver().getState() ) 81 | }}) 82 | 83 | it("sets the protocol version", function() { with(this) { 84 | driver().start() 85 | assertEqual( "hixie-76", driver().version ) 86 | }}) 87 | 88 | describe("with an invalid key header", function() { with(this) { 89 | before(function() { with(this) { 90 | request().headers["sec-websocket-key1"] = "2 L785 8o% s9Sy9@V. 4<1P5" 91 | }}) 92 | 93 | it("writes a handshake error response", function() { with(this) { 94 | expect(driver().io, "emit").given("data", buffer( 95 | "HTTP/1.1 400 Bad Request\r\n" + 96 | "Content-Type: text/plain\r\n" + 97 | "Content-Length: 45\r\n" + 98 | "\r\n" + 99 | "Client sent invalid Sec-WebSocket-Key headers")) 100 | driver().start() 101 | }}) 102 | 103 | it("does not trigger the onopen event", function() { with(this) { 104 | driver().start() 105 | assertEqual( false, open ) 106 | }}) 107 | 108 | it("triggers the onerror event", function() { with(this) { 109 | driver().start() 110 | assertEqual( "Client sent invalid Sec-WebSocket-Key headers", error.message ) 111 | }}) 112 | 113 | it("triggers the onclose event", function() { with(this) { 114 | driver().start() 115 | assertEqual( true, close ) 116 | }}) 117 | 118 | it("changes the state to closed", function() { with(this) { 119 | driver().start() 120 | assertEqual( "closed", driver().getState() ) 121 | }}) 122 | }}) 123 | }}) 124 | 125 | describe("frame", function() { with(this) { 126 | it("does not write to the socket", function() { with(this) { 127 | expect(driver().io, "emit").exactly(0) 128 | driver().frame("Hello, world") 129 | }}) 130 | 131 | it("returns true", function() { with(this) { 132 | assertEqual( true, driver().frame("whatever") ) 133 | }}) 134 | 135 | it("queues the frames until the handshake has been sent", function() { with(this) { 136 | expect(driver().io, "emit").given("data", buffer( 137 | "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + 138 | "Upgrade: WebSocket\r\n" + 139 | "Connection: Upgrade\r\n" + 140 | "Sec-WebSocket-Origin: http://www.example.com\r\n" + 141 | "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" + 142 | "\r\n")) 143 | expect(driver().io, "emit").given("data", buffer(response())) 144 | expect(driver().io, "emit").given("data", buffer([0x00, 72, 105, 0xff])) 145 | 146 | driver().frame("Hi") 147 | driver().start() 148 | }}) 149 | }}) 150 | 151 | describe("with no request body", function() { with(this) { 152 | define("body", function() { 153 | return Buffer.alloc(0) 154 | }) 155 | 156 | it("writes the handshake response with no body", function() { with(this) { 157 | expect(driver().io, "emit").given("data", buffer( 158 | "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + 159 | "Upgrade: WebSocket\r\n" + 160 | "Connection: Upgrade\r\n" + 161 | "Sec-WebSocket-Origin: http://www.example.com\r\n" + 162 | "Sec-WebSocket-Location: ws://www.example.com/socket\r\n" + 163 | "\r\n")) 164 | driver().start() 165 | }}) 166 | 167 | it("does not trigger the onopen event", function() { with(this) { 168 | driver().start() 169 | assertEqual( false, open ) 170 | }}) 171 | 172 | it("leaves the protocol in the connecting state", function() { with(this) { 173 | driver().start() 174 | assertEqual( "connecting", driver().getState() ) 175 | }}) 176 | 177 | describe("when the request body is received", function() { with(this) { 178 | before(function() { this.driver().start() }) 179 | 180 | it("sends the response body", function() { with(this) { 181 | expect(driver().io, "emit").given("data", buffer(response())) 182 | driver().parse(BODY) 183 | }}) 184 | 185 | it("triggers the onopen event", function() { with(this) { 186 | driver().parse(BODY) 187 | assertEqual( true, open ) 188 | }}) 189 | 190 | it("changes the state to open", function() { with(this) { 191 | driver().parse(BODY) 192 | assertEqual( "open", driver().getState() ) 193 | }}) 194 | 195 | it("sends any frames queued before the handshake was complete", function() { with(this) { 196 | expect(driver().io, "emit").given("data", buffer(response())) 197 | expect(driver().io, "emit").given("data", buffer([0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xff])) 198 | driver().frame("hello") 199 | driver().parse(BODY) 200 | }}) 201 | }}) 202 | }}) 203 | }}) 204 | 205 | itShouldBehaveLike("draft-75 protocol") 206 | 207 | describe("in the open state", function() { with(this) { 208 | before(function() { this.driver().start() }) 209 | 210 | describe("parse", function() { with(this) { 211 | it("closes the socket if a close frame is received", function() { with(this) { 212 | driver().parse([0xff, 0x00]) 213 | assertEqual( true, close ) 214 | assertEqual( "closed", driver().getState() ) 215 | }}) 216 | }}) 217 | 218 | describe("close", function() { with(this) { 219 | it("writes a close message to the socket", function() { with(this) { 220 | expect(driver().io, "emit").given("data", buffer([0xff, 0x00])) 221 | driver().close() 222 | }}) 223 | }}) 224 | }}) 225 | }}) 226 | -------------------------------------------------------------------------------- /spec/websocket/driver/client_spec.js: -------------------------------------------------------------------------------- 1 | var Client = require("../../../lib/websocket/driver/client"), 2 | Buffer = require('safe-buffer').Buffer, 3 | test = require('jstest').Test 4 | 5 | test.describe("Client", function() { with(this) { 6 | define("options", function() { 7 | return this._options = this._options || { protocols: this.protocols() } 8 | }) 9 | 10 | define("protocols", function() { 11 | null 12 | }) 13 | 14 | define("url", function() { 15 | return "ws://www.example.com/socket" 16 | }) 17 | 18 | define("driver", function() { 19 | if (this._driver) return this._driver 20 | this._driver = new Client(this.url(), this.options()) 21 | var self = this 22 | this._driver.on('open', function(e) { self.open = true }) 23 | this._driver.on('message', function(e) { self.message += e.data }) 24 | this._driver.on('error', function(e) { self.error = e }) 25 | this._driver.on('close', function(e) { self.close = [e.code, e.reason] }) 26 | var collector = this.collector() 27 | this._driver.io.on("data", function(d) { collector.write(d) }) 28 | return this._driver 29 | }) 30 | 31 | define("key", function() { 32 | return "2vBVWg4Qyk3ZoM/5d3QD9Q==" 33 | }) 34 | 35 | define("response", function() { 36 | return "HTTP/1.1 101 Switching Protocols\r\n" + 37 | "Upgrade: websocket\r\n" + 38 | "Connection: Upgrade\r\n" + 39 | "Sec-WebSocket-Accept: QV3I5XUXU2CdhtjixE7QCkCcMZM=\r\n" + 40 | "\r\n" 41 | }) 42 | 43 | before(function() { 44 | this.stub(Client, "generateKey").returns(this.key()) 45 | this.open = this.error = this.close = false 46 | this.message = "" 47 | }) 48 | 49 | describe("in the beginning state", function() { with(this) { 50 | it("starts in no state", function() { with(this) { 51 | assertEqual( null, driver().getState() ) 52 | }}) 53 | 54 | describe("close", function() { with(this) { 55 | it("changes the state to closed", function() { with(this) { 56 | driver().close() 57 | assertEqual( "closed", driver().getState() ) 58 | assertEqual( [1000, ''], close ) 59 | }}) 60 | }}) 61 | 62 | describe("start", function() { with(this) { 63 | it("writes the handshake request to the socket", function() { with(this) { 64 | expect(driver().io, "emit").given("data", buffer( 65 | "GET /socket HTTP/1.1\r\n" + 66 | "Host: www.example.com\r\n" + 67 | "Upgrade: websocket\r\n" + 68 | "Connection: Upgrade\r\n" + 69 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 70 | "Sec-WebSocket-Version: 13\r\n" + 71 | "\r\n")) 72 | driver().start() 73 | }}) 74 | 75 | it("returns true", function() { with(this) { 76 | assertEqual( true, driver().start() ) 77 | }}) 78 | 79 | describe("with subprotocols", function() { with(this) { 80 | define("protocols", function() { return ["foo", "bar", "xmpp"] }) 81 | 82 | it("writes the handshake with Sec-WebSocket-Protocol", function() { with(this) { 83 | expect(driver().io, "emit").given("data", buffer( 84 | "GET /socket HTTP/1.1\r\n" + 85 | "Host: www.example.com\r\n" + 86 | "Upgrade: websocket\r\n" + 87 | "Connection: Upgrade\r\n" + 88 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 89 | "Sec-WebSocket-Version: 13\r\n" + 90 | "Sec-WebSocket-Protocol: foo, bar, xmpp\r\n" + 91 | "\r\n")) 92 | driver().start() 93 | }}) 94 | }}) 95 | 96 | describe("with basic auth", function() { with(this) { 97 | define("url", function() { return "ws://user:pass@www.example.com/socket" }) 98 | 99 | it("writes the handshake with Authorization", function() { with(this) { 100 | expect(driver().io, "emit").given("data", buffer( 101 | "GET /socket HTTP/1.1\r\n" + 102 | "Host: www.example.com\r\n" + 103 | "Upgrade: websocket\r\n" + 104 | "Connection: Upgrade\r\n" + 105 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 106 | "Sec-WebSocket-Version: 13\r\n" + 107 | "Authorization: Basic dXNlcjpwYXNz\r\n" + 108 | "\r\n")) 109 | driver().start() 110 | }}) 111 | }}) 112 | 113 | describe("with an invalid URL", function() { with(this) { 114 | define("url", function() { return "stream.wikimedia.org/rc" }) 115 | 116 | it("throws an error", function() { with(this) { 117 | var message 118 | try { driver() } catch (e) { message = e.message } 119 | assertEqual( "stream.wikimedia.org/rc is not a valid WebSocket URL", message ) 120 | }}) 121 | }}) 122 | 123 | describe("with custom headers", function() { with(this) { 124 | before(function() { with(this) { 125 | driver().setHeader("User-Agent", "Chrome") 126 | }}) 127 | 128 | it("writes the handshake with custom headers", function() { with(this) { 129 | expect(driver().io, "emit").given("data", buffer( 130 | "GET /socket HTTP/1.1\r\n" + 131 | "Host: www.example.com\r\n" + 132 | "Upgrade: websocket\r\n" + 133 | "Connection: Upgrade\r\n" + 134 | "Sec-WebSocket-Key: 2vBVWg4Qyk3ZoM/5d3QD9Q==\r\n" + 135 | "Sec-WebSocket-Version: 13\r\n" + 136 | "User-Agent: Chrome\r\n" + 137 | "\r\n")) 138 | driver().start() 139 | }}) 140 | }}) 141 | 142 | it("changes the state to connecting", function() { with(this) { 143 | driver().start() 144 | assertEqual( "connecting", driver().getState() ) 145 | }}) 146 | }}) 147 | }}) 148 | 149 | describe("using a proxy", function() { with(this) { 150 | it("sends a CONNECT request", function() { with(this) { 151 | var proxy = driver().proxy("http://proxy.example.com") 152 | expect(proxy, "emit").given("data", buffer( 153 | "CONNECT www.example.com:80 HTTP/1.1\r\n" + 154 | "Host: www.example.com\r\n" + 155 | "Connection: keep-alive\r\n" + 156 | "Proxy-Connection: keep-alive\r\n" + 157 | "\r\n")) 158 | proxy.start() 159 | }}) 160 | 161 | it("sends an authenticated CONNECT request", function() { with(this) { 162 | var proxy = driver().proxy("http://user:pass@proxy.example.com") 163 | expect(proxy, "emit").given("data", buffer( 164 | "CONNECT www.example.com:80 HTTP/1.1\r\n" + 165 | "Host: www.example.com\r\n" + 166 | "Connection: keep-alive\r\n" + 167 | "Proxy-Connection: keep-alive\r\n" + 168 | "Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" + 169 | "\r\n")) 170 | proxy.start() 171 | }}) 172 | 173 | it("sends a CONNECT request with custom headers", function() { with(this) { 174 | var proxy = driver().proxy("http://user:pass@proxy.example.com") 175 | proxy.setHeader("User-Agent", "Chrome") 176 | expect(proxy, "emit").given("data", buffer( 177 | "CONNECT www.example.com:80 HTTP/1.1\r\n" + 178 | "Host: www.example.com\r\n" + 179 | "Connection: keep-alive\r\n" + 180 | "Proxy-Connection: keep-alive\r\n" + 181 | "Proxy-Authorization: Basic dXNlcjpwYXNz\r\n" + 182 | "User-Agent: Chrome\r\n" + 183 | "\r\n")) 184 | proxy.start() 185 | }}) 186 | 187 | describe("receiving a response", function() { with(this) { 188 | before(function() { with(this) { 189 | this.proxy = driver().proxy("http://proxy.example.com") 190 | }}) 191 | 192 | it("returns true when the response is written", function() { with(this) { 193 | // this prevents downstream connections suddenly closing for no reason 194 | assertEqual( true, proxy.write(Buffer.from("HTTP/1.1 200 OK\r\n\r\n")) ) 195 | }}) 196 | 197 | it("emits a 'connect' event when the proxy connects", function() { with(this) { 198 | expect(proxy, "emit").given("connect", anything()) 199 | expect(proxy, "emit").given("close") 200 | expect(proxy, "emit").given("end") 201 | proxy.write(Buffer.from("HTTP/1.1 200 OK\r\n\r\n")) 202 | }}) 203 | 204 | it("emits an 'error' event if the proxy does not connect", function() { with(this) { 205 | expect(proxy, "emit").given("error", objectIncluding({ message: "Can't establish a connection to the server at ws://www.example.com/socket" })) 206 | expect(proxy, "emit").given("close") 207 | expect(proxy, "emit").given("end") 208 | proxy.write(Buffer.from("HTTP/1.1 403 Forbidden\r\n\r\n")) 209 | }}) 210 | }}) 211 | }}) 212 | 213 | describe("in the connecting state", function() { with(this) { 214 | before(function() { this.driver().start() }) 215 | 216 | describe("with a valid response", function() { with(this) { 217 | before(function() { this.driver().parse(Buffer.from(this.response())) }) 218 | 219 | it("changes the state to open", function() { with(this) { 220 | assertEqual( true, open ) 221 | assertEqual( false, close ) 222 | assertEqual( "open", driver().getState() ) 223 | }}) 224 | 225 | it("makes the response status available", function() { with(this) { 226 | assertEqual( 101, driver().statusCode ) 227 | }}) 228 | 229 | it("makes the response headers available", function() { with(this) { 230 | assertEqual( "websocket", driver().headers.upgrade ) 231 | }}) 232 | }}) 233 | 234 | describe("with a valid response followed by a frame", function() { with(this) { 235 | before(function() { with(this) { 236 | var resp = Buffer.alloc(response().length + 4) 237 | Buffer.from(response()).copy(resp) 238 | Buffer.from([0x81, 0x02, 72, 105]).copy(resp, resp.length - 4) 239 | driver().parse(resp) 240 | }}) 241 | 242 | it("changes the state to open", function() { with(this) { 243 | assertEqual( true, open ) 244 | assertEqual( false, close ) 245 | assertEqual( "open", driver().getState() ) 246 | }}) 247 | 248 | it("parses the frame", function() { with(this) { 249 | assertEqual( "Hi", message ) 250 | }}) 251 | }}) 252 | 253 | describe("with a bad status line", function() { with(this) { 254 | before(function() { 255 | var resp = this.response().replace(/101/g, "4") 256 | this.driver().parse(Buffer.from(resp)) 257 | }) 258 | 259 | it("changes the state to closed", function() { with(this) { 260 | assertEqual( false, open ) 261 | assertEqual( "Error during WebSocket handshake: Parse Error", error.message ) 262 | assertEqual( [1002, "Error during WebSocket handshake: Parse Error"], close ) 263 | assertEqual( "closed", driver().getState() ) 264 | }}) 265 | }}) 266 | 267 | describe("with a bad Upgrade header", function() { with(this) { 268 | before(function() { 269 | var resp = this.response().replace(/websocket/g, "wrong") 270 | this.driver().parse(Buffer.from(resp)) 271 | }) 272 | 273 | it("changes the state to closed", function() { with(this) { 274 | assertEqual( false, open ) 275 | assertEqual( "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'", error.message ) 276 | assertEqual( [1002, "Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'"], close ) 277 | assertEqual( "closed", driver().getState() ) 278 | }}) 279 | }}) 280 | 281 | describe("with a bad Accept header", function() { with(this) { 282 | before(function() { 283 | var resp = this.response().replace(/QV3/g, "wrong") 284 | this.driver().parse(Buffer.from(resp)) 285 | }) 286 | 287 | it("changes the state to closed", function() { with(this) { 288 | assertEqual( false, open ) 289 | assertEqual( "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch", error.message ) 290 | assertEqual( [1002, "Error during WebSocket handshake: Sec-WebSocket-Accept mismatch"], close ) 291 | assertEqual( "closed", driver().getState() ) 292 | }}) 293 | }}) 294 | 295 | describe("with valid subprotocols", function() { with(this) { 296 | define("protocols", function() { return ["foo", "xmpp"] }) 297 | 298 | before(function() { 299 | var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: xmpp\r\n\r\n") 300 | this.driver().parse(Buffer.from(resp)) 301 | }) 302 | 303 | it("changs the state to open", function() { with(this) { 304 | assertEqual( true, open ) 305 | assertEqual( false, close ) 306 | assertEqual( "open", driver().getState() ) 307 | }}) 308 | 309 | it("selects the subprotocol", function() { with(this) { 310 | assertEqual( "xmpp", driver().protocol ) 311 | }}) 312 | }}) 313 | 314 | describe("with invalid subprotocols", function() { with(this) { 315 | define("protocols", function() { return ["foo", "xmpp"] }) 316 | 317 | before(function() { 318 | var resp = this.response().replace(/\r\n\r\n/, "\r\nSec-WebSocket-Protocol: irc\r\n\r\n") 319 | this.driver().parse(Buffer.from(resp)) 320 | }) 321 | 322 | it("changs the state to closed", function() { with(this) { 323 | assertEqual( false, open ) 324 | assertEqual( "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch", error.message ) 325 | assertEqual( [1002, "Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch"], close ) 326 | assertEqual( "closed", driver().getState() ) 327 | }}) 328 | 329 | it("selects no subprotocol", function() { with(this) { 330 | assertEqual( null, driver().protocol ) 331 | }}) 332 | }}) 333 | }}) 334 | }}) 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # websocket-driver 2 | 3 | This module provides a complete implementation of the WebSocket protocols that 4 | can be hooked up to any I/O stream. It aims to simplify things by decoupling the 5 | protocol details from the I/O layer, such that users only need to implement code 6 | to stream data in and out of it without needing to know anything about how the 7 | protocol actually works. Think of it as a complete WebSocket system with 8 | pluggable I/O. 9 | 10 | Due to this design, you get a lot of things for free. In particular, if you hook 11 | this module up to some I/O object, it will do all of this for you: 12 | 13 | - Select the correct server-side driver to talk to the client 14 | - Generate and send both server- and client-side handshakes 15 | - Recognize when the handshake phase completes and the WS protocol begins 16 | - Negotiate subprotocol selection based on `Sec-WebSocket-Protocol` 17 | - Negotiate and use extensions via the 18 | [websocket-extensions](https://github.com/faye/websocket-extensions-node) 19 | module 20 | - Buffer sent messages until the handshake process is finished 21 | - Deal with proxies that defer delivery of the draft-76 handshake body 22 | - Notify you when the socket is open and closed and when messages arrive 23 | - Recombine fragmented messages 24 | - Dispatch text, binary, ping, pong and close frames 25 | - Manage the socket-closing handshake process 26 | - Automatically reply to ping frames with a matching pong 27 | - Apply masking to messages sent by the client 28 | 29 | This library was originally extracted from the [Faye](http://faye.jcoglan.com) 30 | project but now aims to provide simple WebSocket support for any Node-based 31 | project. 32 | 33 | 34 | ## Installation 35 | 36 | ``` 37 | $ npm install websocket-driver 38 | ``` 39 | 40 | 41 | ## Usage 42 | 43 | This module provides protocol drivers that have the same interface on the server 44 | and on the client. A WebSocket driver is an object with two duplex streams 45 | attached; one for incoming/outgoing messages and one for managing the wire 46 | protocol over an I/O stream. The full API is described below. 47 | 48 | 49 | ### Server-side with HTTP 50 | 51 | A Node webserver emits a special event for 'upgrade' requests, and this is where 52 | you should handle WebSockets. You first check whether the request is a 53 | WebSocket, and if so you can create a driver and attach the request's I/O stream 54 | to it. 55 | 56 | ```js 57 | var http = require('http'), 58 | websocket = require('websocket-driver'); 59 | 60 | var server = http.createServer(); 61 | 62 | server.on('upgrade', function(request, socket, body) { 63 | if (!websocket.isWebSocket(request)) return; 64 | 65 | var driver = websocket.http(request); 66 | 67 | driver.io.write(body); 68 | socket.pipe(driver.io).pipe(socket); 69 | 70 | driver.messages.on('data', function(message) { 71 | console.log('Got a message', message); 72 | }); 73 | 74 | driver.start(); 75 | }); 76 | ``` 77 | 78 | Note the line `driver.io.write(body)` - you must pass the `body` buffer to the 79 | socket driver in order to make certain versions of the protocol work. 80 | 81 | 82 | ### Server-side with TCP 83 | 84 | You can also handle WebSocket connections in a bare TCP server, if you're not 85 | using an HTTP server and don't want to implement HTTP parsing yourself. 86 | 87 | The driver will emit a `connect` event when a request is received, and at this 88 | point you can detect whether it's a WebSocket and handle it as such. Here's an 89 | example using the Node `net` module: 90 | 91 | ```js 92 | var net = require('net'), 93 | websocket = require('websocket-driver'); 94 | 95 | var server = net.createServer(function(connection) { 96 | var driver = websocket.server(); 97 | 98 | driver.on('connect', function() { 99 | if (websocket.isWebSocket(driver)) { 100 | driver.start(); 101 | } else { 102 | // handle other HTTP requests 103 | } 104 | }); 105 | 106 | driver.on('close', function() { connection.end() }); 107 | connection.on('error', function() {}); 108 | 109 | connection.pipe(driver.io).pipe(connection); 110 | 111 | driver.messages.pipe(driver.messages); 112 | }); 113 | 114 | server.listen(4180); 115 | ``` 116 | 117 | In the `connect` event, the driver gains several properties to describe the 118 | request, similar to a Node request object, such as `method`, `url` and 119 | `headers`. However you should remember it's not a real request object; you 120 | cannot write data to it, it only tells you what request data we parsed from the 121 | input. 122 | 123 | If the request has a body, it will be in the `driver.body` buffer, but only as 124 | much of the body as has been piped into the driver when the `connect` event 125 | fires. 126 | 127 | 128 | ### Client-side 129 | 130 | Similarly, to implement a WebSocket client you just need to make a driver by 131 | passing in a URL. After this you use the driver API as described below to 132 | process incoming data and send outgoing data. 133 | 134 | 135 | ```js 136 | var net = require('net'), 137 | websocket = require('websocket-driver'); 138 | 139 | var driver = websocket.client('ws://www.example.com/socket'), 140 | tcp = net.connect(80, 'www.example.com'); 141 | 142 | tcp.pipe(driver.io).pipe(tcp); 143 | 144 | tcp.on('connect', function() { 145 | driver.start(); 146 | }); 147 | 148 | driver.messages.on('data', function(message) { 149 | console.log('Got a message', message); 150 | }); 151 | ``` 152 | 153 | Client drivers have two additional properties for reading the HTTP data that was 154 | sent back by the server: 155 | 156 | - `driver.statusCode` - the integer value of the HTTP status code 157 | - `driver.headers` - an object containing the response headers 158 | 159 | 160 | ### HTTP Proxies 161 | 162 | The client driver supports connections via HTTP proxies using the `CONNECT` 163 | method. Instead of sending the WebSocket handshake immediately, it will send a 164 | `CONNECT` request, wait for a `200` response, and then proceed as normal. 165 | 166 | To use this feature, call `driver.proxy(url)` where `url` is the origin of the 167 | proxy, including a username and password if required. This produces a duplex 168 | stream that you should pipe in and out of your TCP connection to the proxy 169 | server. When the proxy emits `connect`, you can then pipe `driver.io` to your 170 | TCP stream and call `driver.start()`. 171 | 172 | ```js 173 | var net = require('net'), 174 | websocket = require('websocket-driver'); 175 | 176 | var driver = websocket.client('ws://www.example.com/socket'), 177 | proxy = driver.proxy('http://username:password@proxy.example.com'), 178 | tcp = net.connect(80, 'proxy.example.com'); 179 | 180 | tcp.pipe(proxy).pipe(tcp, { end: false }); 181 | 182 | tcp.on('connect', function() { 183 | proxy.start(); 184 | }); 185 | 186 | proxy.on('connect', function() { 187 | driver.io.pipe(tcp).pipe(driver.io); 188 | driver.start(); 189 | }); 190 | 191 | driver.messages.on('data', function(message) { 192 | console.log('Got a message', message); 193 | }); 194 | ``` 195 | 196 | The proxy's `connect` event is also where you should perform a TLS handshake on 197 | your TCP stream, if you are connecting to a `wss:` endpoint. 198 | 199 | In the event that proxy connection fails, `proxy` will emit an `error`. You can 200 | inspect the proxy's response via `proxy.statusCode` and `proxy.headers`. 201 | 202 | ```js 203 | proxy.on('error', function(error) { 204 | console.error(error.message); 205 | console.log(proxy.statusCode); 206 | console.log(proxy.headers); 207 | }); 208 | ``` 209 | 210 | Before calling `proxy.start()` you can set custom headers using 211 | `proxy.setHeader()`: 212 | 213 | ```js 214 | proxy.setHeader('User-Agent', 'node'); 215 | proxy.start(); 216 | ``` 217 | 218 | 219 | ### Driver API 220 | 221 | Drivers are created using one of the following methods: 222 | 223 | ```js 224 | driver = websocket.http(request, options) 225 | driver = websocket.server(options) 226 | driver = websocket.client(url, options) 227 | ``` 228 | 229 | The `http` method returns a driver chosen using the headers from a Node HTTP 230 | request object. The `server` method returns a driver that will parse an HTTP 231 | request and then decide which driver to use for it using the `http` method. The 232 | `client` method always returns a driver for the RFC version of the protocol with 233 | masking enabled on outgoing frames. 234 | 235 | The `options` argument is optional, and is an object. It may contain the 236 | following fields: 237 | 238 | - `maxLength` - the maximum allowed size of incoming message frames, in bytes. 239 | The default value is `2^26 - 1`, or 1 byte short of 64 MiB. 240 | - `protocols` - an array of strings representing acceptable subprotocols for use 241 | over the socket. The driver will negotiate one of these to use via the 242 | `Sec-WebSocket-Protocol` header if supported by the other peer. 243 | 244 | A driver has two duplex streams attached to it: 245 | 246 | - **`driver.io`** - this stream should be attached to an I/O socket like a TCP 247 | stream. Pipe incoming TCP chunks to this stream for them to be parsed, and 248 | pipe this stream back into TCP to send outgoing frames. 249 | - **`driver.messages`** - this stream emits messages received over the 250 | WebSocket. Writing to it sends messages to the other peer by emitting frames 251 | via the `driver.io` stream. 252 | 253 | All drivers respond to the following API methods, but some of them are no-ops 254 | depending on whether the client supports the behaviour. 255 | 256 | Note that most of these methods are commands: if they produce data that should 257 | be sent over the socket, they will give this to you by emitting `data` events on 258 | the `driver.io` stream. 259 | 260 | #### `driver.on('open', function(event) {})` 261 | 262 | Adds a callback to execute when the socket becomes open. 263 | 264 | #### `driver.on('message', function(event) {})` 265 | 266 | Adds a callback to execute when a message is received. `event` will have a 267 | `data` attribute containing either a string in the case of a text message or a 268 | `Buffer` in the case of a binary message. 269 | 270 | You can also listen for messages using the `driver.messages.on('data')` event, 271 | which emits strings for text messages and buffers for binary messages. 272 | 273 | #### `driver.on('error', function(event) {})` 274 | 275 | Adds a callback to execute when a protocol error occurs due to the other peer 276 | sending an invalid byte sequence. `event` will have a `message` attribute 277 | describing the error. 278 | 279 | #### `driver.on('close', function(event) {})` 280 | 281 | Adds a callback to execute when the socket becomes closed. The `event` object 282 | has `code` and `reason` attributes. 283 | 284 | #### `driver.on('ping', function(event) {})` 285 | 286 | Adds a callback block to execute when a ping is received. You do not need to 287 | handle this by sending a pong frame yourself; the driver handles this for you. 288 | 289 | #### `driver.on('pong', function(event) {})` 290 | 291 | Adds a callback block to execute when a pong is received. If this was in 292 | response to a ping you sent, you can also handle this event via the 293 | `driver.ping(message, function() { ... })` callback. 294 | 295 | #### `driver.addExtension(extension)` 296 | 297 | Registers a protocol extension whose operation will be negotiated via the 298 | `Sec-WebSocket-Extensions` header. `extension` is any extension compatible with 299 | the [websocket-extensions](https://github.com/faye/websocket-extensions-node) 300 | framework. 301 | 302 | #### `driver.setHeader(name, value)` 303 | 304 | Sets a custom header to be sent as part of the handshake response, either from 305 | the server or from the client. Must be called before `start()`, since this is 306 | when the headers are serialized and sent. 307 | 308 | #### `driver.start()` 309 | 310 | Initiates the protocol by sending the handshake - either the response for a 311 | server-side driver or the request for a client-side one. This should be the 312 | first method you invoke. Returns `true` if and only if a handshake was sent. 313 | 314 | #### `driver.parse(string)` 315 | 316 | Takes a string and parses it, potentially resulting in message events being 317 | emitted (see `on('message')` above) or in data being sent to `driver.io`. You 318 | should send all data you receive via I/O to this method by piping a stream into 319 | `driver.io`. 320 | 321 | #### `driver.text(string)` 322 | 323 | Sends a text message over the socket. If the socket handshake is not yet 324 | complete, the message will be queued until it is. Returns `true` if the message 325 | was sent or queued, and `false` if the socket can no longer send messages. 326 | 327 | This method is equivalent to `driver.messages.write(string)`. 328 | 329 | #### `driver.binary(buffer)` 330 | 331 | Takes a `Buffer` and sends it as a binary message. Will queue and return `true` 332 | or `false` the same way as the `text` method. It will also return `false` if the 333 | driver does not support binary messages. 334 | 335 | This method is equivalent to `driver.messages.write(buffer)`. 336 | 337 | #### `driver.ping(string = '', function() {})` 338 | 339 | Sends a ping frame over the socket, queueing it if necessary. `string` and the 340 | callback are both optional. If a callback is given, it will be invoked when the 341 | socket receives a pong frame whose content matches `string`. Returns `false` if 342 | frames can no longer be sent, or if the driver does not support ping/pong. 343 | 344 | #### `driver.pong(string = '')` 345 | 346 | Sends a pong frame over the socket, queueing it if necessary. `string` is 347 | optional. Returns `false` if frames can no longer be sent, or if the driver does 348 | not support ping/pong. 349 | 350 | You don't need to call this when a ping frame is received; pings are replied to 351 | automatically by the driver. This method is for sending unsolicited pongs. 352 | 353 | #### `driver.close()` 354 | 355 | Initiates the closing handshake if the socket is still open. For drivers with no 356 | closing handshake, this will result in the immediate execution of the 357 | `on('close')` driver. For drivers with a closing handshake, this sends a closing 358 | frame and `emit('close')` will execute when a response is received or a protocol 359 | error occurs. 360 | 361 | #### `driver.version` 362 | 363 | Returns the WebSocket version in use as a string. Will either be `hixie-75`, 364 | `hixie-76` or `hybi-$version`. 365 | 366 | #### `driver.protocol` 367 | 368 | Returns a string containing the selected subprotocol, if any was agreed upon 369 | using the `Sec-WebSocket-Protocol` mechanism. This value becomes available after 370 | `emit('open')` has fired. 371 | -------------------------------------------------------------------------------- /lib/websocket/driver/hybi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Buffer = require('safe-buffer').Buffer, 4 | crypto = require('crypto'), 5 | util = require('util'), 6 | Extensions = require('websocket-extensions'), 7 | Base = require('./base'), 8 | Frame = require('./hybi/frame'), 9 | Message = require('./hybi/message'); 10 | 11 | var Hybi = function(request, url, options) { 12 | Base.apply(this, arguments); 13 | 14 | this._extensions = new Extensions(); 15 | this._stage = 0; 16 | this._masking = this._options.masking; 17 | this._protocols = this._options.protocols || []; 18 | this._requireMasking = this._options.requireMasking; 19 | this._pingCallbacks = {}; 20 | 21 | if (typeof this._protocols === 'string') 22 | this._protocols = this._protocols.split(/ *, */); 23 | 24 | if (!this._request) return; 25 | 26 | var protos = this._request.headers['sec-websocket-protocol'], 27 | supported = this._protocols; 28 | 29 | if (protos !== undefined) { 30 | if (typeof protos === 'string') protos = protos.split(/ *, */); 31 | this.protocol = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0]; 32 | } 33 | 34 | this.version = 'hybi-' + Hybi.VERSION; 35 | }; 36 | util.inherits(Hybi, Base); 37 | 38 | Hybi.VERSION = '13'; 39 | 40 | Hybi.mask = function(payload, mask, offset) { 41 | if (!mask || mask.length === 0) return payload; 42 | offset = offset || 0; 43 | 44 | for (var i = 0, n = payload.length - offset; i < n; i++) { 45 | payload[offset + i] = payload[offset + i] ^ mask[i % 4]; 46 | } 47 | return payload; 48 | }; 49 | 50 | Hybi.generateAccept = function(key) { 51 | var sha1 = crypto.createHash('sha1'); 52 | sha1.update(key + Hybi.GUID); 53 | return sha1.digest('base64'); 54 | }; 55 | 56 | Hybi.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 57 | 58 | var instance = { 59 | FIN: 0x80, 60 | MASK: 0x80, 61 | RSV1: 0x40, 62 | RSV2: 0x20, 63 | RSV3: 0x10, 64 | OPCODE: 0x0F, 65 | LENGTH: 0x7F, 66 | 67 | OPCODES: { 68 | continuation: 0, 69 | text: 1, 70 | binary: 2, 71 | close: 8, 72 | ping: 9, 73 | pong: 10 74 | }, 75 | 76 | OPCODE_CODES: [0, 1, 2, 8, 9, 10], 77 | MESSAGE_OPCODES: [0, 1, 2], 78 | OPENING_OPCODES: [1, 2], 79 | 80 | ERRORS: { 81 | normal_closure: 1000, 82 | going_away: 1001, 83 | protocol_error: 1002, 84 | unacceptable: 1003, 85 | encoding_error: 1007, 86 | policy_violation: 1008, 87 | too_large: 1009, 88 | extension_error: 1010, 89 | unexpected_condition: 1011 90 | }, 91 | 92 | ERROR_CODES: [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011], 93 | DEFAULT_ERROR_CODE: 1000, 94 | MIN_RESERVED_ERROR: 3000, 95 | MAX_RESERVED_ERROR: 4999, 96 | 97 | // http://www.w3.org/International/questions/qa-forms-utf-8.en.php 98 | 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})*$/, 99 | 100 | addExtension: function(extension) { 101 | this._extensions.add(extension); 102 | return true; 103 | }, 104 | 105 | parse: function(chunk) { 106 | this._reader.put(chunk); 107 | var buffer = true; 108 | while (buffer) { 109 | switch (this._stage) { 110 | case 0: 111 | buffer = this._reader.read(1); 112 | if (buffer) this._parseOpcode(buffer[0]); 113 | break; 114 | 115 | case 1: 116 | buffer = this._reader.read(1); 117 | if (buffer) this._parseLength(buffer[0]); 118 | break; 119 | 120 | case 2: 121 | buffer = this._reader.read(this._frame.lengthBytes); 122 | if (buffer) this._parseExtendedLength(buffer); 123 | break; 124 | 125 | case 3: 126 | buffer = this._reader.read(4); 127 | if (buffer) { 128 | this._stage = 4; 129 | this._frame.maskingKey = buffer; 130 | } 131 | break; 132 | 133 | case 4: 134 | buffer = this._reader.read(this._frame.length); 135 | if (buffer) { 136 | this._stage = 0; 137 | this._emitFrame(buffer); 138 | } 139 | break; 140 | 141 | default: 142 | buffer = null; 143 | } 144 | } 145 | }, 146 | 147 | text: function(message) { 148 | if (this.readyState > 1) return false; 149 | return this.frame(message, 'text'); 150 | }, 151 | 152 | binary: function(message) { 153 | if (this.readyState > 1) return false; 154 | return this.frame(message, 'binary'); 155 | }, 156 | 157 | ping: function(message, callback) { 158 | if (this.readyState > 1) return false; 159 | message = message || ''; 160 | if (callback) this._pingCallbacks[message] = callback; 161 | return this.frame(message, 'ping'); 162 | }, 163 | 164 | pong: function(message) { 165 | if (this.readyState > 1) return false; 166 | message = message ||''; 167 | return this.frame(message, 'pong'); 168 | }, 169 | 170 | close: function(reason, code) { 171 | reason = reason || ''; 172 | code = code || this.ERRORS.normal_closure; 173 | 174 | if (this.readyState <= 0) { 175 | this.readyState = 3; 176 | this.emit('close', new Base.CloseEvent(code, reason)); 177 | return true; 178 | } else if (this.readyState === 1) { 179 | this.readyState = 2; 180 | this._extensions.close(function() { this.frame(reason, 'close', code) }, this); 181 | return true; 182 | } else { 183 | return false; 184 | } 185 | }, 186 | 187 | frame: function(buffer, type, code) { 188 | if (this.readyState <= 0) return this._queue([buffer, type, code]); 189 | if (this.readyState > 2) return false; 190 | 191 | if (buffer instanceof Array) buffer = Buffer.from(buffer); 192 | if (typeof buffer === 'number') buffer = buffer.toString(); 193 | 194 | var message = new Message(), 195 | isText = (typeof buffer === 'string'), 196 | payload, copy; 197 | 198 | message.rsv1 = message.rsv2 = message.rsv3 = false; 199 | message.opcode = this.OPCODES[type || (isText ? 'text' : 'binary')]; 200 | 201 | payload = isText ? Buffer.from(buffer, 'utf8') : buffer; 202 | 203 | if (code) { 204 | copy = payload; 205 | payload = Buffer.allocUnsafe(2 + copy.length); 206 | payload.writeUInt16BE(code, 0); 207 | copy.copy(payload, 2); 208 | } 209 | message.data = payload; 210 | 211 | var onMessageReady = function(message) { 212 | var frame = new Frame(); 213 | 214 | frame.final = true; 215 | frame.rsv1 = message.rsv1; 216 | frame.rsv2 = message.rsv2; 217 | frame.rsv3 = message.rsv3; 218 | frame.opcode = message.opcode; 219 | frame.masked = !!this._masking; 220 | frame.length = message.data.length; 221 | frame.payload = message.data; 222 | 223 | if (frame.masked) frame.maskingKey = crypto.randomBytes(4); 224 | 225 | this._sendFrame(frame); 226 | }; 227 | 228 | if (this.MESSAGE_OPCODES.indexOf(message.opcode) >= 0) 229 | this._extensions.processOutgoingMessage(message, function(error, message) { 230 | if (error) return this._fail('extension_error', error.message); 231 | onMessageReady.call(this, message); 232 | }, this); 233 | else 234 | onMessageReady.call(this, message); 235 | 236 | return true; 237 | }, 238 | 239 | _sendFrame: function(frame) { 240 | var length = frame.length, 241 | header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10), 242 | offset = header + (frame.masked ? 4 : 0), 243 | buffer = Buffer.allocUnsafe(offset + length), 244 | masked = frame.masked ? this.MASK : 0; 245 | 246 | buffer[0] = (frame.final ? this.FIN : 0) | 247 | (frame.rsv1 ? this.RSV1 : 0) | 248 | (frame.rsv2 ? this.RSV2 : 0) | 249 | (frame.rsv3 ? this.RSV3 : 0) | 250 | frame.opcode; 251 | 252 | if (length <= 125) { 253 | buffer[1] = masked | length; 254 | } else if (length <= 65535) { 255 | buffer[1] = masked | 126; 256 | buffer.writeUInt16BE(length, 2); 257 | } else { 258 | buffer[1] = masked | 127; 259 | buffer.writeUInt32BE(Math.floor(length / 0x100000000), 2); 260 | buffer.writeUInt32BE(length % 0x100000000, 6); 261 | } 262 | 263 | frame.payload.copy(buffer, offset); 264 | 265 | if (frame.masked) { 266 | frame.maskingKey.copy(buffer, header); 267 | Hybi.mask(buffer, frame.maskingKey, offset); 268 | } 269 | 270 | this._write(buffer); 271 | }, 272 | 273 | _handshakeResponse: function() { 274 | var secKey = this._request.headers['sec-websocket-key'], 275 | version = this._request.headers['sec-websocket-version']; 276 | 277 | if (version !== Hybi.VERSION) 278 | throw new Error('Unsupported WebSocket version: ' + version); 279 | 280 | if (typeof secKey !== 'string') 281 | throw new Error('Missing handshake request header: Sec-WebSocket-Key'); 282 | 283 | this._headers.set('Upgrade', 'websocket'); 284 | this._headers.set('Connection', 'Upgrade'); 285 | this._headers.set('Sec-WebSocket-Accept', Hybi.generateAccept(secKey)); 286 | 287 | if (this.protocol) this._headers.set('Sec-WebSocket-Protocol', this.protocol); 288 | 289 | var extensions = this._extensions.generateResponse(this._request.headers['sec-websocket-extensions']); 290 | if (extensions) this._headers.set('Sec-WebSocket-Extensions', extensions); 291 | 292 | var start = 'HTTP/1.1 101 Switching Protocols', 293 | headers = [start, this._headers.toString(), '']; 294 | 295 | return Buffer.from(headers.join('\r\n'), 'utf8'); 296 | }, 297 | 298 | _shutdown: function(code, reason, error) { 299 | delete this._frame; 300 | delete this._message; 301 | this._stage = 5; 302 | 303 | var sendCloseFrame = (this.readyState === 1); 304 | this.readyState = 2; 305 | 306 | this._extensions.close(function() { 307 | if (sendCloseFrame) this.frame(reason, 'close', code); 308 | this.readyState = 3; 309 | if (error) this.emit('error', new Error(reason)); 310 | this.emit('close', new Base.CloseEvent(code, reason)); 311 | }, this); 312 | }, 313 | 314 | _fail: function(type, message) { 315 | if (this.readyState > 1) return; 316 | this._shutdown(this.ERRORS[type], message, true); 317 | }, 318 | 319 | _parseOpcode: function(octet) { 320 | var rsvs = [this.RSV1, this.RSV2, this.RSV3].map(function(rsv) { 321 | return (octet & rsv) === rsv; 322 | }); 323 | 324 | var frame = this._frame = new Frame(); 325 | 326 | frame.final = (octet & this.FIN) === this.FIN; 327 | frame.rsv1 = rsvs[0]; 328 | frame.rsv2 = rsvs[1]; 329 | frame.rsv3 = rsvs[2]; 330 | frame.opcode = (octet & this.OPCODE); 331 | 332 | this._stage = 1; 333 | 334 | if (!this._extensions.validFrameRsv(frame)) 335 | return this._fail('protocol_error', 336 | 'One or more reserved bits are on: reserved1 = ' + (frame.rsv1 ? 1 : 0) + 337 | ', reserved2 = ' + (frame.rsv2 ? 1 : 0) + 338 | ', reserved3 = ' + (frame.rsv3 ? 1 : 0)); 339 | 340 | if (this.OPCODE_CODES.indexOf(frame.opcode) < 0) 341 | return this._fail('protocol_error', 'Unrecognized frame opcode: ' + frame.opcode); 342 | 343 | if (this.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && !frame.final) 344 | return this._fail('protocol_error', 'Received fragmented control frame: opcode = ' + frame.opcode); 345 | 346 | if (this._message && this.OPENING_OPCODES.indexOf(frame.opcode) >= 0) 347 | return this._fail('protocol_error', 'Received new data frame but previous continuous frame is unfinished'); 348 | }, 349 | 350 | _parseLength: function(octet) { 351 | var frame = this._frame; 352 | frame.masked = (octet & this.MASK) === this.MASK; 353 | frame.length = (octet & this.LENGTH); 354 | 355 | if (frame.length >= 0 && frame.length <= 125) { 356 | this._stage = frame.masked ? 3 : 4; 357 | if (!this._checkFrameLength()) return; 358 | } else { 359 | this._stage = 2; 360 | frame.lengthBytes = (frame.length === 126 ? 2 : 8); 361 | } 362 | 363 | if (this._requireMasking && !frame.masked) 364 | return this._fail('unacceptable', 'Received unmasked frame but masking is required'); 365 | }, 366 | 367 | _parseExtendedLength: function(buffer) { 368 | var frame = this._frame; 369 | frame.length = this._readUInt(buffer); 370 | 371 | this._stage = frame.masked ? 3 : 4; 372 | 373 | if (this.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && frame.length > 125) 374 | return this._fail('protocol_error', 'Received control frame having too long payload: ' + frame.length); 375 | 376 | if (!this._checkFrameLength()) return; 377 | }, 378 | 379 | _checkFrameLength: function() { 380 | var length = this._message ? this._message.length : 0; 381 | 382 | if (length + this._frame.length > this._maxLength) { 383 | this._fail('too_large', 'WebSocket frame length too large'); 384 | return false; 385 | } else { 386 | return true; 387 | } 388 | }, 389 | 390 | _emitFrame: function(buffer) { 391 | var frame = this._frame, 392 | payload = frame.payload = Hybi.mask(buffer, frame.maskingKey), 393 | opcode = frame.opcode, 394 | message, 395 | code, reason, 396 | callbacks, callback; 397 | 398 | delete this._frame; 399 | 400 | if (opcode === this.OPCODES.continuation) { 401 | if (!this._message) return this._fail('protocol_error', 'Received unexpected continuation frame'); 402 | this._message.pushFrame(frame); 403 | } 404 | 405 | if (opcode === this.OPCODES.text || opcode === this.OPCODES.binary) { 406 | this._message = new Message(); 407 | this._message.pushFrame(frame); 408 | } 409 | 410 | if (frame.final && this.MESSAGE_OPCODES.indexOf(opcode) >= 0) 411 | return this._emitMessage(this._message); 412 | 413 | if (opcode === this.OPCODES.close) { 414 | code = (payload.length >= 2) ? payload.readUInt16BE(0) : null; 415 | reason = (payload.length > 2) ? this._encode(payload.slice(2)) : null; 416 | 417 | if (!(payload.length === 0) && 418 | !(code !== null && code >= this.MIN_RESERVED_ERROR && code <= this.MAX_RESERVED_ERROR) && 419 | this.ERROR_CODES.indexOf(code) < 0) 420 | code = this.ERRORS.protocol_error; 421 | 422 | if (payload.length > 125 || (payload.length > 2 && !reason)) 423 | code = this.ERRORS.protocol_error; 424 | 425 | this._shutdown(code || this.DEFAULT_ERROR_CODE, reason || ''); 426 | } 427 | 428 | if (opcode === this.OPCODES.ping) { 429 | this.frame(payload, 'pong'); 430 | this.emit('ping', new Base.PingEvent(payload.toString())) 431 | } 432 | 433 | if (opcode === this.OPCODES.pong) { 434 | callbacks = this._pingCallbacks; 435 | message = this._encode(payload); 436 | callback = callbacks[message]; 437 | 438 | delete callbacks[message]; 439 | if (callback) callback() 440 | 441 | this.emit('pong', new Base.PongEvent(payload.toString())) 442 | } 443 | }, 444 | 445 | _emitMessage: function(message) { 446 | var message = this._message; 447 | message.read(); 448 | 449 | delete this._message; 450 | 451 | this._extensions.processIncomingMessage(message, function(error, message) { 452 | if (error) return this._fail('extension_error', error.message); 453 | 454 | var payload = message.data; 455 | if (message.opcode === this.OPCODES.text) payload = this._encode(payload); 456 | 457 | if (payload === null) 458 | return this._fail('encoding_error', 'Could not decode a text frame as UTF-8'); 459 | else 460 | this.emit('message', new Base.MessageEvent(payload)); 461 | }, this); 462 | }, 463 | 464 | _encode: function(buffer) { 465 | try { 466 | var string = buffer.toString('binary', 0, buffer.length); 467 | if (!this.UTF8_MATCH.test(string)) return null; 468 | } catch (e) {} 469 | return buffer.toString('utf8', 0, buffer.length); 470 | }, 471 | 472 | _readUInt: function(buffer) { 473 | if (buffer.length === 2) return buffer.readUInt16BE(0); 474 | 475 | return buffer.readUInt32BE(0) * 0x100000000 + 476 | buffer.readUInt32BE(4); 477 | } 478 | }; 479 | 480 | for (var key in instance) 481 | Hybi.prototype[key] = instance[key]; 482 | 483 | module.exports = Hybi; 484 | -------------------------------------------------------------------------------- /spec/websocket/driver/hybi_spec.js: -------------------------------------------------------------------------------- 1 | var Hybi = require("../../../lib/websocket/driver/hybi"), 2 | test = require('jstest').Test 3 | 4 | test.describe("Hybi", function() { with(this) { 5 | define("request", function() { 6 | return this._request = this._request || { 7 | method: "GET", 8 | headers: { 9 | "connection": "Upgrade", 10 | "upgrade": "websocket", 11 | "origin": "http://www.example.com", 12 | // "sec-websocket-extensions": "x-webkit-deflate-frame", 13 | "sec-websocket-key": "JFBCWHksyIpXV+6Wlq/9pw==", 14 | "sec-websocket-version": "13" 15 | } 16 | } 17 | }) 18 | 19 | define("options", function() { 20 | return this._options = this._options || { masking: false } 21 | }) 22 | 23 | define("driver", function() { 24 | if (this._driver) return this._driver 25 | this._driver = new Hybi(this.request(), "ws://www.example.com/socket", this.options()) 26 | var self = this 27 | this._driver.on('open', function(e) { self.open = true }) 28 | this._driver.on('message', function(e) { self.message += e.data }) 29 | this._driver.on('error', function(e) { self.error = e }) 30 | this._driver.on('close', function(e) { self.close = [e.code, e.reason] }) 31 | this._driver.io.pipe(this.collector()) 32 | return this._driver 33 | }) 34 | 35 | before(function() { 36 | this.open = this.error = this.close = false 37 | this.message = "" 38 | }) 39 | 40 | describe("in the connecting state", function() { with(this) { 41 | it("starts in the connecting state", function() { with(this) { 42 | assertEqual( "connecting", driver().getState() ) 43 | }}) 44 | 45 | describe("start", function() { with(this) { 46 | it("writes the handshake response to the socket", function() { with(this) { 47 | expect(driver().io, "emit").given("data", buffer( 48 | "HTTP/1.1 101 Switching Protocols\r\n" + 49 | "Upgrade: websocket\r\n" + 50 | "Connection: Upgrade\r\n" + 51 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 52 | "\r\n")) 53 | driver().start() 54 | }}) 55 | 56 | it("returns true", function() { with(this) { 57 | assertEqual( true, driver().start() ) 58 | }}) 59 | 60 | describe("with subprotocols", function() { with(this) { 61 | before(function() { with(this) { 62 | request().headers["sec-websocket-protocol"] = "foo, bar, xmpp" 63 | options().protocols = ["xmpp"] 64 | }}) 65 | 66 | it("writes the handshake with Sec-WebSocket-Protocol", function() { with(this) { 67 | expect(driver().io, "emit").given("data", buffer( 68 | "HTTP/1.1 101 Switching Protocols\r\n" + 69 | "Upgrade: websocket\r\n" + 70 | "Connection: Upgrade\r\n" + 71 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 72 | "Sec-WebSocket-Protocol: xmpp\r\n" + 73 | "\r\n")) 74 | driver().start() 75 | }}) 76 | 77 | it("sets the subprotocol", function() { with(this) { 78 | driver().start() 79 | assertEqual( "xmpp", driver().protocol ) 80 | }}) 81 | }}) 82 | 83 | describe("with invalid extensions", function() { with(this) { 84 | before(function() { with(this) { 85 | request().headers["sec-websocket-extensions"] = "x-webkit- -frame" 86 | }}) 87 | 88 | it("writes a handshake error response", function() { with(this) { 89 | expect(driver().io, "emit").given("data", buffer( 90 | "HTTP/1.1 400 Bad Request\r\n" + 91 | "Content-Type: text/plain\r\n" + 92 | "Content-Length: 57\r\n" + 93 | "\r\n" + 94 | "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame")) 95 | driver().start() 96 | }}) 97 | 98 | it("does not trigger the onopen event", function() { with(this) { 99 | driver().start() 100 | assertEqual( false, open ) 101 | }}) 102 | 103 | it("triggers the onerror event", function() { with(this) { 104 | driver().start() 105 | assertEqual( "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame", error.message ) 106 | }}) 107 | 108 | it("triggers the onclose event", function() { with(this) { 109 | driver().start() 110 | assertEqual( [1002, "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame"], close ) 111 | }}) 112 | 113 | it("changes the state to closed", function() { with(this) { 114 | driver().start() 115 | assertEqual( "closed", driver().getState() ) 116 | }}) 117 | }}) 118 | 119 | describe("with custom headers", function() { with(this) { 120 | before(function() { with(this) { 121 | driver().setHeader("Authorization", "Bearer WAT") 122 | }}) 123 | 124 | it("writes the handshake with the custom headers", function() { with(this) { 125 | expect(driver().io, "emit").given("data", buffer( 126 | "HTTP/1.1 101 Switching Protocols\r\n" + 127 | "Authorization: Bearer WAT\r\n" + 128 | "Upgrade: websocket\r\n" + 129 | "Connection: Upgrade\r\n" + 130 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 131 | "\r\n")) 132 | driver().start() 133 | }}) 134 | }}) 135 | 136 | it("triggers the onopen event", function() { with(this) { 137 | driver().start() 138 | assertEqual( true, open ) 139 | }}) 140 | 141 | it("changes the state to open", function() { with(this) { 142 | driver().start() 143 | assertEqual( "open", driver().getState() ) 144 | }}) 145 | 146 | it("sets the protocol version", function() { with(this) { 147 | driver().start() 148 | assertEqual( "hybi-13", driver().version ) 149 | }}) 150 | }}) 151 | 152 | describe("frame", function() { with(this) { 153 | it("does not write to the socket", function() { with(this) { 154 | expect(driver().io, "emit").exactly(0) 155 | driver().frame("Hello, world") 156 | }}) 157 | 158 | it("returns true", function() { with(this) { 159 | assertEqual( true, driver().frame("whatever") ) 160 | }}) 161 | 162 | it("queues the frames until the handshake has been send", function() { with(this) { 163 | expect(driver().io, "emit").given("data", buffer( 164 | "HTTP/1.1 101 Switching Protocols\r\n" + 165 | "Upgrade: websocket\r\n" + 166 | "Connection: Upgrade\r\n" + 167 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 168 | "\r\n")) 169 | expect(driver().io, "emit").given("data", buffer([0x81, 0x02, 72, 105])) 170 | 171 | driver().frame("Hi") 172 | driver().start() 173 | }}) 174 | }}) 175 | 176 | describe("ping", function() { with(this) { 177 | it("does not write to the socket", function() { with(this) { 178 | expect(driver().io, "emit").exactly(0) 179 | driver().ping() 180 | }}) 181 | 182 | it("returns true", function() { with(this) { 183 | assertEqual( true, driver().ping() ) 184 | }}) 185 | 186 | it("queues the ping until the handshake has been send", function() { with(this) { 187 | expect(driver().io, "emit").given("data", buffer( 188 | "HTTP/1.1 101 Switching Protocols\r\n" + 189 | "Upgrade: websocket\r\n" + 190 | "Connection: Upgrade\r\n" + 191 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 192 | "\r\n")) 193 | expect(driver().io, "emit").given("data", buffer([0x89, 0])) 194 | 195 | driver().ping() 196 | driver().start() 197 | }}) 198 | }}) 199 | 200 | describe("pong", function() { with(this) { 201 | it("does not write to the socket", function() { with(this) { 202 | expect(driver().io, "emit").exactly(0) 203 | driver().pong() 204 | }}) 205 | 206 | it("returns true", function() { with(this) { 207 | assertEqual( true, driver().pong() ) 208 | }}) 209 | 210 | it("queues the pong until the handshake has been sent", function() { with(this) { 211 | expect(driver().io, "emit").given("data", buffer( 212 | "HTTP/1.1 101 Switching Protocols\r\n" + 213 | "Upgrade: websocket\r\n" + 214 | "Connection: Upgrade\r\n" + 215 | "Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" + 216 | "\r\n")) 217 | expect(driver().io, "emit").given("data", buffer([0x8a, 0])) 218 | 219 | driver().pong() 220 | driver().start() 221 | }}) 222 | }}) 223 | 224 | describe("close", function() { with(this) { 225 | it("does not write anything to the socket", function() { with(this) { 226 | expect(driver().io, "emit").exactly(0) 227 | driver().close() 228 | }}) 229 | 230 | it("returns true", function() { with(this) { 231 | assertEqual( true, driver().close() ) 232 | }}) 233 | 234 | it("triggers the onclose event", function() { with(this) { 235 | driver().close() 236 | assertEqual( [1000, ""], close ) 237 | }}) 238 | 239 | it("changes the state to closed", function() { with(this) { 240 | driver().close() 241 | assertEqual( "closed", driver().getState() ) 242 | }}) 243 | }}) 244 | }}) 245 | 246 | describe("in the open state", function() { with(this) { 247 | before(function() { this.driver().start() }) 248 | 249 | describe("parse", function() { with(this) { 250 | define("mask", function() { 251 | return this._mask = this._mask || 252 | [1,2,3,4].map(function() { return Math.floor(Math.random() * 256) }) 253 | }) 254 | 255 | define("maskMessage", function(bytes) { 256 | var output = [] 257 | for (var i = 0, n = bytes.length; i < n; i++) { 258 | output[i] = bytes[i] ^ this.mask()[i % 4] 259 | } 260 | return output 261 | }) 262 | 263 | it("parses unmasked text frames", function() { with(this) { 264 | driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 265 | assertEqual( "Hello", message ) 266 | }}) 267 | 268 | it("parses multiple frames from the same packet", function() { with(this) { 269 | driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 270 | assertEqual( "HelloHello", message ) 271 | }}) 272 | 273 | it("parses empty text frames", function() { with(this) { 274 | driver().parse([0x81, 0x00]) 275 | assertEqual( "", message ) 276 | }}) 277 | 278 | it("parses fragmented text frames", function() { with(this) { 279 | driver().parse([0x01, 0x03, 0x48, 0x65, 0x6c]) 280 | driver().parse([0x80, 0x02, 0x6c, 0x6f]) 281 | assertEqual( "Hello", message ) 282 | }}) 283 | 284 | it("parses masked text frames", function() { with(this) { 285 | driver().parse([0x81, 0x85]) 286 | driver().parse(mask()) 287 | driver().parse(maskMessage([0x48, 0x65, 0x6c, 0x6c, 0x6f])) 288 | assertEqual( "Hello", message ) 289 | }}) 290 | 291 | it("parses masked empty text frames", function() { with(this) { 292 | driver().parse([0x81, 0x80]) 293 | driver().parse(mask()) 294 | driver().parse(maskMessage([])) 295 | assertEqual( "", message ) 296 | }}) 297 | 298 | it("parses masked fragmented text frames", function() { with(this) { 299 | driver().parse([0x01, 0x81]) 300 | driver().parse(mask()) 301 | driver().parse(maskMessage([0x48])) 302 | 303 | driver().parse([0x80, 0x84]) 304 | driver().parse(mask()) 305 | driver().parse(maskMessage([0x65, 0x6c, 0x6c, 0x6f])) 306 | 307 | assertEqual( "Hello", message ) 308 | }}) 309 | 310 | it("closes the socket if the frame has an unrecognized opcode", function() { with(this) { 311 | driver().parse([0x83, 0x00]) 312 | assertEqual( [0x88, 0x1e, 0x03, 0xea], collector().bytes.slice(0,4) ) 313 | assertEqual( "Unrecognized frame opcode: 3", error.message ) 314 | assertEqual( [1002, "Unrecognized frame opcode: 3"], close ) 315 | assertEqual( "closed", driver().getState() ) 316 | }}) 317 | 318 | it("closes the socket if a close frame is received", function() { with(this) { 319 | driver().parse([0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 320 | assertEqual( [0x88, 0x07, 0x03, 0xe8, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes ) 321 | assertEqual( [1000, "Hello"], close ) 322 | assertEqual( "closed", driver().getState() ) 323 | }}) 324 | 325 | it("parses unmasked multibyte text frames", function() { with(this) { 326 | driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]) 327 | assertEqual( "Apple = ", message ) 328 | }}) 329 | 330 | it("parses frames received in several packets", function() { with(this) { 331 | driver().parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c]) 332 | driver().parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf]) 333 | assertEqual( "Apple = ", message ) 334 | }}) 335 | 336 | it("parses fragmented multibyte text frames", function() { with(this) { 337 | driver().parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3]) 338 | driver().parse([0x80, 0x01, 0xbf]) 339 | assertEqual( "Apple = ", message ) 340 | }}) 341 | 342 | it("parse masked multibyte text frames", function() { with(this) { 343 | driver().parse([0x81, 0x8b]) 344 | driver().parse(mask()) 345 | driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])) 346 | assertEqual( "Apple = ", message ) 347 | }}) 348 | 349 | it("parses masked fragmented multibyte text frames", function() { with(this) { 350 | driver().parse([0x01, 0x8a]) 351 | driver().parse(mask()) 352 | driver().parse(maskMessage([0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])) 353 | 354 | driver().parse([0x80, 0x81]) 355 | driver().parse(mask()) 356 | driver().parse(maskMessage([0xbf])) 357 | 358 | assertEqual( "Apple = ", message ) 359 | }}) 360 | 361 | it("parses unmasked medium-length text frames", function() { with(this) { 362 | driver().parse([0x81, 0x7e, 0x00, 0xc8]) 363 | var i = 40, result = "" 364 | while (i--) { 365 | driver().parse([0x48, 0x65, 0x6c, 0x6c, 0x6f]) 366 | result += "Hello" 367 | } 368 | assertEqual( result, message ) 369 | }}) 370 | 371 | it("returns an error for too-large frames", function() { with(this) { 372 | driver().parse([0x81, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00]) 373 | assertEqual( "WebSocket frame length too large", error.message ) 374 | assertEqual( [1009, "WebSocket frame length too large"], close ) 375 | assertEqual( "closed", driver().getState() ) 376 | }}) 377 | 378 | it("parses masked medium-length text frames", function() { with(this) { 379 | driver().parse([0x81, 0xfe, 0x00, 0xc8]) 380 | driver().parse(mask()) 381 | var i = 40, result = "", packet = [] 382 | while (i--) { 383 | packet = packet.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f]) 384 | result += "Hello" 385 | } 386 | driver().parse(maskMessage(packet)) 387 | assertEqual( result, message ) 388 | }}) 389 | 390 | it("replies to pings with a pong", function() { with(this) { 391 | driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49]) 392 | assertEqual( [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49], collector().bytes ) 393 | }}) 394 | 395 | it("triggers the onping event when a ping arrives", function() { with(this) { 396 | var ping, pong 397 | driver().on("ping", function(event) { ping = event }) 398 | driver().on("pong", function(event) { pong = event }) 399 | 400 | driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49]) 401 | 402 | assertEqual( "OHAI", ping.data ) 403 | assertEqual( undefined, pong ) 404 | }}) 405 | 406 | describe("when a message listener throws an error", function() { with(this) { 407 | before(function() { with(this) { 408 | this.messages = [] 409 | 410 | driver().on("message", function(msg) { 411 | messages.push(msg.data) 412 | throw new Error("an error") 413 | }) 414 | }}) 415 | 416 | it("is not trapped by the parser", function() { with(this) { 417 | var buffer = [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f] 418 | assertThrows(Error, function() { driver().parse(buffer) }) 419 | }}) 420 | 421 | it("parses unmasked text frames without dropping input", function() { with(this) { 422 | try { driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05]) } catch (e) {} 423 | try { driver().parse([0x57, 0x6f, 0x72, 0x6c, 0x64]) } catch (e) {} 424 | assertEqual( ["Hello", "World"], messages ) 425 | }}) 426 | }}) 427 | }}) 428 | 429 | describe("frame", function() { with(this) { 430 | it("formats the given string as a WebSocket frame", function() { with(this) { 431 | driver().frame("Hello") 432 | assertEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes ) 433 | }}) 434 | 435 | it("formats a byte array as a binary WebSocket frame", function() { with(this) { 436 | driver().frame([0x48, 0x65, 0x6c]) 437 | assertEqual( [0x82, 0x03, 0x48, 0x65, 0x6c], collector().bytes ) 438 | }}) 439 | 440 | it("converts numbers to strings", function() { with(this) { 441 | driver().frame(50) 442 | assertEqual( [0x81, 0x02, 0x35, 0x30], collector().bytes ) 443 | }}) 444 | 445 | it("encodes multibyte characters correctly", function() { with(this) { 446 | driver().frame("Apple = ") 447 | assertEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], collector().bytes ) 448 | }}) 449 | 450 | it("encodes medium-length strings using extra length bytes", function() { with(this) { 451 | var i = 40, frame = [0x81, 0x7e, 0x00, 0xc8], string = "" 452 | while (i--) { 453 | string += "Hello" 454 | frame = frame.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f]) 455 | } 456 | driver().frame(string) 457 | assertEqual( frame, collector(). bytes ) 458 | }}) 459 | 460 | it("encodes close frames with an error code", function() { with(this) { 461 | driver().frame("Hello", "close", 1002) 462 | assertEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], collector().bytes ) 463 | }}) 464 | 465 | it("encodes pong frames", function() { with(this) { 466 | driver().frame("", "pong") 467 | assertEqual( [0x8a, 0x00], collector().bytes ) 468 | }}) 469 | }}) 470 | 471 | describe("ping", function() { with(this) { 472 | it("writes a ping frame to the socket", function() { with(this) { 473 | driver().ping("mic check") 474 | assertEqual( [0x89, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b], collector().bytes ) 475 | }}) 476 | 477 | it("converts numbers to strings", function() { with(this) { 478 | driver().ping(50) 479 | assertEqual( [0x89, 0x02, 0x35, 0x30], collector().bytes ) 480 | }}) 481 | 482 | it("returns true", function() { with(this) { 483 | assertEqual( true, driver().ping() ) 484 | }}) 485 | 486 | it("runs the given callback on mathing pong", function() { with(this) { 487 | var reply = null 488 | driver().ping("Hi", function() { reply = true }) 489 | driver().parse([0x8a, 0x02, 72, 105]) 490 | assert( reply ) 491 | }}) 492 | 493 | it("triggers the onpong event when a pong arrives", function() { with(this) { 494 | var ping, pong 495 | driver().on("ping", function(event) { ping = event }) 496 | driver().on("pong", function(event) { pong = event }) 497 | 498 | driver().parse([0x8a, 0x02, 72, 105]) 499 | 500 | assertEqual( undefined, ping ) 501 | assertEqual( "Hi", pong.data ) 502 | }}) 503 | 504 | it("does not run the callback on non-matching pong", function() { with(this) { 505 | var reply = null 506 | driver().ping("Hi", function() { reply = true }) 507 | driver().parse([0x8a, 0x03, 119, 97, 116]) 508 | assert( !reply ) 509 | }}) 510 | }}) 511 | 512 | describe("pong", function() { with(this) { 513 | it("writes a pong frame to the socket", function() { with(this) { 514 | driver().pong("mic check") 515 | assertEqual([0x8a, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b], collector().bytes) 516 | }}) 517 | 518 | it("returns true", function() { with(this) { 519 | assertEqual(true, driver().pong()) 520 | }}) 521 | }}) 522 | 523 | describe("close", function() { with(this) { 524 | it("writes a close frame to the socket", function() { with(this) { 525 | driver().close("<%= reasons %>", 1003) 526 | assertEqual( [0x88, 0x10, 0x03, 0xeb, 0x3c, 0x25, 0x3d, 0x20, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x20, 0x25, 0x3e], collector().bytes ) 527 | }}) 528 | 529 | it("returns true", function() { with(this) { 530 | assertEqual( true, driver().close() ) 531 | }}) 532 | 533 | it("does not trigger the close event", function() { with(this) { 534 | driver().close() 535 | assertEqual( false, close ) 536 | }}) 537 | 538 | it("does not trigger the onerror event", function() { with(this) { 539 | driver().close() 540 | assertEqual( false, error ) 541 | }}) 542 | 543 | it("changes the state to closing", function() { with(this) { 544 | driver().close() 545 | assertEqual( "closing", driver().getState() ) 546 | }}) 547 | }}) 548 | }}) 549 | 550 | describe("when masking is required", function() { with(this) { 551 | before(function() { 552 | this.options().requireMasking = true 553 | this.driver().start() 554 | }) 555 | 556 | it("does not emit a message", function() { with(this) { 557 | driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 558 | assertEqual( "", message ) 559 | }}) 560 | 561 | it("returns an error", function() { with(this) { 562 | driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]) 563 | assertEqual( "Received unmasked frame but masking is required", error.message ) 564 | assertEqual( [1003, "Received unmasked frame but masking is required"], close ) 565 | }}) 566 | }}) 567 | 568 | describe("in the closing state", function() { with(this) { 569 | before(function() { 570 | this.driver().start() 571 | this.driver().close() 572 | }) 573 | 574 | describe("frame", function() { with(this) { 575 | it("does not write to the socket", function() { with(this) { 576 | expect(driver().io, "emit").exactly(0) 577 | driver().frame("dropped") 578 | }}) 579 | 580 | it("returns true", function() { with(this) { 581 | assertEqual( true, driver().frame("wut") ) 582 | }}) 583 | }}) 584 | 585 | describe("ping", function() { with(this) { 586 | it("does not write to the socket", function() { with(this) { 587 | expect(driver().io, "emit").exactly(0) 588 | driver().ping() 589 | }}) 590 | 591 | it("returns false", function() { with(this) { 592 | assertEqual( false, driver().ping() ) 593 | }}) 594 | }}) 595 | 596 | describe("pong", function() { with(this) { 597 | it("does not write to the socket", function() { with(this) { 598 | expect(driver().io, "emit").exactly(0) 599 | driver().pong() 600 | }}) 601 | 602 | it("returns false", function() { with(this) { 603 | assertEqual( false, driver().pong() ) 604 | }}) 605 | }}) 606 | 607 | describe("close", function() { with(this) { 608 | it("does not write to the socket", function() { with(this) { 609 | expect(driver().io, "emit").exactly(0) 610 | driver().close() 611 | }}) 612 | 613 | it("returns false", function() { with(this) { 614 | assertEqual( false, driver().close() ) 615 | }}) 616 | }}) 617 | 618 | describe("receiving a close frame", function() { with(this) { 619 | before(function() { 620 | this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b]) 621 | }) 622 | 623 | it("triggers the onclose event", function() { with(this) { 624 | assertEqual( [1001, "OK"], close ) 625 | }}) 626 | 627 | it("changes the state to closed", function() { with(this) { 628 | assertEqual( "closed", driver().getState() ) 629 | }}) 630 | 631 | it("does not write another close frame", function() { with(this) { 632 | expect(driver().io, "emit").exactly(0) 633 | this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b]) 634 | }}) 635 | }}) 636 | 637 | describe("receiving a close frame with a too-short payload", function() { with(this) { 638 | before(function() { 639 | this.driver().parse([0x88, 0x01, 0x03]) 640 | }) 641 | 642 | it("triggers the onclose event with a protocol error", function() { with(this) { 643 | assertEqual( [1002, ""], close ) 644 | }}) 645 | 646 | it("changes the state to closed", function() { with(this) { 647 | assertEqual( "closed", driver().getState() ) 648 | }}) 649 | }}) 650 | 651 | describe("receiving a close frame with no code", function() { with(this) { 652 | before(function() { with(this) { 653 | this.driver().parse([0x88, 0x00]) 654 | }}) 655 | 656 | it("triggers the onclose event with code 1000", function() { with(this) { 657 | assertEqual( [1000, ""], close ) 658 | }}) 659 | 660 | it("changes the state to closed", function() { with(this) { 661 | assertEqual( "closed", driver().getState() ) 662 | }}) 663 | }}) 664 | }}) 665 | 666 | describe("in the closed state", function() { with(this) { 667 | before(function() { 668 | this.driver().start() 669 | this.driver().close() 670 | this.driver().parse([0x88, 0x02, 0x03, 0xea]) 671 | }) 672 | 673 | describe("frame", function() { with(this) { 674 | it("does not write to the socket", function() { with(this) { 675 | expect(driver().io, "emit").exactly(0) 676 | driver().frame("dropped") 677 | }}) 678 | 679 | it("returns false", function() { with(this) { 680 | assertEqual( false, driver().frame("wut") ) 681 | }}) 682 | }}) 683 | 684 | describe("ping", function() { with(this) { 685 | it("does not write to the socket", function() { with(this) { 686 | expect(driver().io, "emit").exactly(0) 687 | driver().ping() 688 | }}) 689 | 690 | it("returns false", function() { with(this) { 691 | assertEqual( false, driver().ping() ) 692 | }}) 693 | }}) 694 | 695 | describe("pong", function() { with(this) { 696 | it("does not write to the socket", function() { with(this) { 697 | expect(driver().io, "emit").exactly(0) 698 | driver().pong() 699 | }}) 700 | 701 | it("returns false", function() { with(this) { 702 | assertEqual( false, driver().pong() ) 703 | }}) 704 | }}) 705 | 706 | describe("close", function() { with(this) { 707 | it("does not write to the socket", function() { with(this) { 708 | expect(driver().io, "emit").exactly(0) 709 | driver().close() 710 | }}) 711 | 712 | it("returns false", function() { with(this) { 713 | assertEqual( false, driver().close() ) 714 | }}) 715 | 716 | it("leaves the state as closed", function() { with(this) { 717 | driver().close() 718 | assertEqual( "closed", driver().getState() ) 719 | }}) 720 | }}) 721 | }}) 722 | }}) 723 | --------------------------------------------------------------------------------