├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── examples ├── autobahn_client.js ├── client.js ├── haproxy.conf ├── proxy_server.js ├── server.js ├── sse.html └── ws.html ├── lib └── faye │ ├── eventsource.js │ ├── websocket.js │ └── websocket │ ├── api.js │ ├── api │ ├── event.js │ └── event_target.js │ └── client.js ├── package.json └── spec ├── echo_server.js ├── faye └── websocket │ └── client_spec.js ├── proxy_server.js ├── runner.js ├── server.crt └── server.key /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.11.4 / 2021-05-24 2 | 3 | - Prevent the client hanging if `close()` is called when already closing 4 | 5 | ### 0.11.3 / 2019-06-10 6 | 7 | - Fix a race condition that caused a timeout not to be cancelled immediately 8 | when the WebSocket is closed 9 | - Change license from MIT to Apache 2.0 10 | 11 | ### 0.11.2 / 2019-06-10 12 | 13 | (This version was pulled due to an error when publishing) 14 | 15 | ### 0.11.1 / 2017-01-22 16 | 17 | - Forcibly close the I/O stream after a timeout if the peer does not respond 18 | after calling `close()` 19 | 20 | ### 0.11.0 / 2016-02-24 21 | 22 | - Introduce a `net` option to the `Client` class for setting things like, say, 23 | `servername` 24 | 25 | ### 0.10.0 / 2015-07-08 26 | 27 | - Add the standard `code` and `reason` parameters to the `close` method 28 | 29 | ### 0.9.4 / 2015-03-08 30 | 31 | - Don't send input to the driver before `start()` is called 32 | 33 | ### 0.9.3 / 2015-02-19 34 | 35 | - Make sure the TCP socket is not left open when closing the connection 36 | 37 | ### 0.9.2 / 2014-12-21 38 | 39 | - Only emit `error` once, and don't emit it after `close` 40 | 41 | ### 0.9.1 / 2014-12-18 42 | 43 | - Check that all options to the WebSocket constructor are recognized 44 | 45 | ### 0.9.0 / 2014-12-13 46 | 47 | - Allow protocol extensions to be passed into websocket-extensions 48 | 49 | ### 0.8.1 / 2014-11-12 50 | 51 | - Send the correct hostname when upgrading a connection to TLS 52 | 53 | ### 0.8.0 / 2014-11-08 54 | 55 | - Support connections via HTTP proxies 56 | - Close the connection cleanly if we're still waiting for a handshake response 57 | 58 | ### 0.7.3 / 2014-10-04 59 | 60 | - Allow sockets to be closed when they are in any state other than `CLOSED` 61 | 62 | ### 0.7.2 / 2013-12-29 63 | 64 | - Make sure the `close` event is emitted by clients on Node v0.10 65 | 66 | ### 0.7.1 / 2013-12-03 67 | 68 | - Support the `maxLength` websocket-driver option 69 | - Make the client emit `error` events on network errors 70 | 71 | ### 0.7.0 / 2013-09-09 72 | 73 | - Allow the server to send custom headers with EventSource responses 74 | 75 | ### 0.6.1 / 2013-07-05 76 | 77 | - Add `ca` option to the client for specifying certificate authorities 78 | - Start the server driver asynchronously so that `onopen` handlers can be added 79 | 80 | ### 0.6.0 / 2013-05-12 81 | 82 | - Add support for custom headers 83 | 84 | ### 0.5.0 / 2013-05-05 85 | 86 | - Extract the protocol handlers into the `websocket-driver` library 87 | - Support the Node streaming API 88 | 89 | ### 0.4.4 / 2013-02-14 90 | 91 | - Emit the `close` event if TCP is closed before CLOSE frame is acked 92 | 93 | ### 0.4.3 / 2012-07-09 94 | 95 | - Add `Connection: close` to EventSource response 96 | - Handle situations where `request.socket` is undefined 97 | 98 | ### 0.4.2 / 2012-04-06 99 | 100 | - Add WebSocket error code `1011`. 101 | - Handle URLs with no path correctly by sending `GET /` 102 | 103 | ### 0.4.1 / 2012-02-26 104 | 105 | - Treat anything other than a `Buffer` as a string when calling `send()` 106 | 107 | ### 0.4.0 / 2012-02-13 108 | 109 | - Add `ping()` method to server-side `WebSocket` and `EventSource` 110 | - Buffer `send()` calls until the draft-76 handshake is complete 111 | - Fix HTTPS problems on Node 0.7 112 | 113 | ### 0.3.1 / 2012-01-16 114 | 115 | - Call `setNoDelay(true)` on `net.Socket` objects to reduce latency 116 | 117 | ### 0.3.0 / 2012-01-13 118 | 119 | - Add support for `EventSource` connections 120 | 121 | ### 0.2.0 / 2011-12-21 122 | 123 | - Add support for `Sec-WebSocket-Protocol` negotiation 124 | - Support `hixie-76` close frames and 75/76 ignored segments 125 | - Improve performance of HyBi parsing/framing functions 126 | - Decouple parsers from TCP and reduce write volume 127 | 128 | ### 0.1.2 / 2011-12-05 129 | 130 | - Detect closed sockets on the server side when TCP connection breaks 131 | - Make `hixie-76` sockets work through HAProxy 132 | 133 | ### 0.1.1 / 2011-11-30 134 | 135 | - Fix `addEventListener()` interface methods 136 | 137 | ### 0.1.0 / 2011-11-27 138 | 139 | - Initial release, based on WebSocket components from Faye 140 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2010-2021 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faye-websocket 2 | 3 | This is a general-purpose WebSocket implementation extracted from the 4 | [Faye](http://faye.jcoglan.com) project. It provides classes for easily building 5 | WebSocket servers and clients in Node. It does not provide a server itself, but 6 | rather makes it easy to handle WebSocket connections within an existing 7 | [Node](https://nodejs.org/) application. It does not provide any abstraction 8 | other than the standard [WebSocket 9 | API](https://html.spec.whatwg.org/multipage/comms.html#network). 10 | 11 | It also provides an abstraction for handling 12 | [EventSource](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events) 13 | connections, which are one-way connections that allow the server to push data to 14 | the client. They are based on streaming HTTP responses and can be easier to access 15 | via proxies than WebSockets. 16 | 17 | 18 | ## Installation 19 | 20 | ``` 21 | $ npm install faye-websocket 22 | ``` 23 | 24 | 25 | ## Handling WebSocket connections in Node 26 | 27 | You can handle WebSockets on the server side by listening for HTTP Upgrade 28 | requests, and creating a new socket for the request. This socket object exposes 29 | the usual WebSocket methods for receiving and sending messages. For example this 30 | is how you'd implement an echo server: 31 | 32 | ```js 33 | var WebSocket = require('faye-websocket'), 34 | http = require('http'); 35 | 36 | var server = http.createServer(); 37 | 38 | server.on('upgrade', function(request, socket, body) { 39 | if (WebSocket.isWebSocket(request)) { 40 | var ws = new WebSocket(request, socket, body); 41 | 42 | ws.on('message', function(event) { 43 | ws.send(event.data); 44 | }); 45 | 46 | ws.on('close', function(event) { 47 | console.log('close', event.code, event.reason); 48 | ws = null; 49 | }); 50 | } 51 | }); 52 | 53 | server.listen(8000); 54 | ``` 55 | 56 | `WebSocket` objects are also duplex streams, so you could replace the 57 | `ws.on('message', ...)` line with: 58 | 59 | ```js 60 | ws.pipe(ws); 61 | ``` 62 | 63 | Note that under certain circumstances (notably a draft-76 client connecting 64 | through an HTTP proxy), the WebSocket handshake will not be complete after you 65 | call `new WebSocket()` because the server will not have received the entire 66 | handshake from the client yet. In this case, calls to `ws.send()` will buffer 67 | the message in memory until the handshake is complete, at which point any 68 | buffered messages will be sent to the client. 69 | 70 | If you need to detect when the WebSocket handshake is complete, you can use the 71 | `onopen` event. 72 | 73 | If the connection's protocol version supports it, you can call `ws.ping()` to 74 | send a ping message and wait for the client's response. This method takes a 75 | message string, and an optional callback that fires when a matching pong message 76 | is received. It returns `true` if and only if a ping message was sent. If the 77 | client does not support ping/pong, this method sends no data and returns 78 | `false`. 79 | 80 | ```js 81 | ws.ping('Mic check, one, two', function() { 82 | // fires when pong is received 83 | }); 84 | ``` 85 | 86 | 87 | ## Using the WebSocket client 88 | 89 | The client supports both the plain-text `ws` protocol and the encrypted `wss` 90 | protocol, and has exactly the same interface as a socket you would use in a web 91 | browser. On the wire it identifies itself as `hybi-13`. 92 | 93 | ```js 94 | var WebSocket = require('faye-websocket'), 95 | ws = new WebSocket.Client('ws://www.example.com/'); 96 | 97 | ws.on('open', function(event) { 98 | console.log('open'); 99 | ws.send('Hello, world!'); 100 | }); 101 | 102 | ws.on('message', function(event) { 103 | console.log('message', event.data); 104 | }); 105 | 106 | ws.on('close', function(event) { 107 | console.log('close', event.code, event.reason); 108 | ws = null; 109 | }); 110 | ``` 111 | 112 | The WebSocket client also lets you inspect the status and headers of the 113 | handshake response via its `statusCode` and `headers` properties. 114 | 115 | To connect via a proxy, set the `proxy` option to the HTTP origin of the proxy, 116 | including any authorization information, custom headers and TLS config you 117 | require. Only the `origin` setting is required. 118 | 119 | ```js 120 | var ws = new WebSocket.Client('ws://www.example.com/', [], { 121 | proxy: { 122 | origin: 'http://username:password@proxy.example.com', 123 | headers: { 'User-Agent': 'node' }, 124 | tls: { cert: fs.readFileSync('client.crt') } 125 | } 126 | }); 127 | ``` 128 | 129 | The `tls` value is an object that will be passed to 130 | [`tls.connect()`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback). 131 | 132 | 133 | ## Subprotocol negotiation 134 | 135 | The WebSocket protocol allows peers to select and identify the application 136 | protocol to use over the connection. On the client side, you can set which 137 | protocols the client accepts by passing a list of protocol names when you 138 | construct the socket: 139 | 140 | ```js 141 | var ws = new WebSocket.Client('ws://www.example.com/', ['irc', 'amqp']); 142 | ``` 143 | 144 | On the server side, you can likewise pass in the list of protocols the server 145 | supports after the other constructor arguments: 146 | 147 | ```js 148 | var ws = new WebSocket(request, socket, body, ['irc', 'amqp']); 149 | ``` 150 | 151 | If the client and server agree on a protocol, both the client- and server-side 152 | socket objects expose the selected protocol through the `ws.protocol` property. 153 | 154 | 155 | ## Protocol extensions 156 | 157 | faye-websocket is based on the 158 | [websocket-extensions](https://github.com/faye/websocket-extensions-node) 159 | framework that allows extensions to be negotiated via the 160 | `Sec-WebSocket-Extensions` header. To add extensions to a connection, pass an 161 | array of extensions to the `:extensions` option. For example, to add 162 | [permessage-deflate](https://github.com/faye/permessage-deflate-node): 163 | 164 | ```js 165 | var deflate = require('permessage-deflate'); 166 | 167 | var ws = new WebSocket(request, socket, body, [], { extensions: [deflate] }); 168 | ``` 169 | 170 | 171 | ## Initialization options 172 | 173 | Both the server- and client-side classes allow an options object to be passed in 174 | at initialization time, for example: 175 | 176 | ```js 177 | var ws = new WebSocket(request, socket, body, protocols, options); 178 | var ws = new WebSocket.Client(url, protocols, options); 179 | ``` 180 | 181 | `protocols` is an array of subprotocols as described above, or `null`. 182 | `options` is an optional object containing any of these fields: 183 | 184 | - `extensions` - an array of 185 | [websocket-extensions](https://github.com/faye/websocket-extensions-node) 186 | compatible extensions, as described above 187 | - `headers` - an object containing key-value pairs representing HTTP headers to 188 | be sent during the handshake process 189 | - `maxLength` - the maximum allowed size of incoming message frames, in bytes. 190 | The default value is `2^26 - 1`, or 1 byte short of 64 MiB. 191 | - `ping` - an integer that sets how often the WebSocket should send ping frames, 192 | measured in seconds 193 | 194 | The client accepts some additional options: 195 | 196 | - `proxy` - settings for a proxy as described above 197 | - `net` - an object containing settings for the origin server that will be 198 | passed to 199 | [`net.connect()`](https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener) 200 | - `tls` - an object containing TLS settings for the origin server, this will be 201 | passed to 202 | [`tls.connect()`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) 203 | - `ca` - (legacy) a shorthand for passing `{ tls: { ca: value } }` 204 | 205 | 206 | ## WebSocket API 207 | 208 | Both server- and client-side `WebSocket` objects support the following API. 209 | 210 | - **`on('open', function(event) {})`** fires when the socket connection is 211 | established. Event has no attributes. 212 | - **`on('message', function(event) {})`** fires when the socket receives a 213 | message. Event has one attribute, **`data`**, which is either a `String` (for 214 | text frames) or a `Buffer` (for binary frames). 215 | - **`on('error', function(event) {})`** fires when there is a protocol error due 216 | to bad data sent by the other peer. This event is purely informational, you do 217 | not need to implement error recover. 218 | - **`on('close', function(event) {})`** fires when either the client or the 219 | server closes the connection. Event has two optional attributes, **`code`** 220 | and **`reason`**, that expose the status code and message sent by the peer 221 | that closed the connection. 222 | - **`send(message)`** accepts either a `String` or a `Buffer` and sends a text 223 | or binary message over the connection to the other peer. 224 | - **`ping(message, function() {})`** sends a ping frame with an optional message 225 | and fires the callback when a matching pong is received. 226 | - **`close(code, reason)`** closes the connection, sending the given status code 227 | and reason text, both of which are optional. 228 | - **`version`** is a string containing the version of the `WebSocket` protocol 229 | the connection is using. 230 | - **`protocol`** is a string (which may be empty) identifying the subprotocol 231 | the socket is using. 232 | 233 | 234 | ## Handling EventSource connections in Node 235 | 236 | EventSource connections provide a very similar interface, although because they 237 | only allow the server to send data to the client, there is no `onmessage` API. 238 | EventSource allows the server to push text messages to the client, where each 239 | message has an optional event-type and ID. 240 | 241 | ```js 242 | var WebSocket = require('faye-websocket'), 243 | EventSource = WebSocket.EventSource, 244 | http = require('http'); 245 | 246 | var server = http.createServer(); 247 | 248 | server.on('request', function(request, response) { 249 | if (EventSource.isEventSource(request)) { 250 | var es = new EventSource(request, response); 251 | console.log('open', es.url, es.lastEventId); 252 | 253 | // Periodically send messages 254 | var loop = setInterval(function() { es.send('Hello') }, 1000); 255 | 256 | es.on('close', function() { 257 | clearInterval(loop); 258 | es = null; 259 | }); 260 | 261 | } else { 262 | // Normal HTTP request 263 | response.writeHead(200, { 'Content-Type': 'text/plain' }); 264 | response.end('Hello'); 265 | } 266 | }); 267 | 268 | server.listen(8000); 269 | ``` 270 | 271 | The `send` method takes two optional parameters, `event` and `id`. The default 272 | event-type is `'message'` with no ID. For example, to send a `notification` 273 | event with ID `99`: 274 | 275 | ```js 276 | es.send('Breaking News!', { event: 'notification', id: '99' }); 277 | ``` 278 | 279 | The `EventSource` object exposes the following properties: 280 | 281 | - **`url`** is a string containing the URL the client used to create the 282 | EventSource. 283 | - **`lastEventId`** is a string containing the last event ID received by the 284 | client. You can use this when the client reconnects after a dropped connection 285 | to determine which messages need resending. 286 | 287 | When you initialize an EventSource with ` new EventSource()`, you can pass 288 | configuration options after the `response` parameter. Available options are: 289 | 290 | - **`headers`** is an object containing custom headers to be set on the 291 | EventSource response. 292 | - **`retry`** is a number that tells the client how long (in seconds) it should 293 | wait after a dropped connection before attempting to reconnect. 294 | - **`ping`** is a number that tells the server how often (in seconds) to send 295 | 'ping' packets to the client to keep the connection open, to defeat timeouts 296 | set by proxies. The client will ignore these messages. 297 | 298 | For example, this creates a connection that allows access from any origin, pings 299 | every 15 seconds and is retryable every 10 seconds if the connection is broken: 300 | 301 | ```js 302 | var es = new EventSource(request, response, { 303 | headers: { 'Access-Control-Allow-Origin': '*' }, 304 | ping: 15, 305 | retry: 10 306 | }); 307 | ``` 308 | 309 | You can send a ping message at any time by calling `es.ping()`. Unlike 310 | WebSocket, the client does not send a response to this; it is merely to send 311 | some data over the wire to keep the connection alive. 312 | -------------------------------------------------------------------------------- /examples/autobahn_client.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('..').Client, 2 | deflate = require('permessage-deflate'), 3 | pace = require('pace'); 4 | 5 | var host = 'ws://0.0.0.0:9001', 6 | agent = encodeURIComponent('node-' + process.version), 7 | cases = 0, 8 | options = { extensions: [deflate] }; 9 | 10 | var socket = new WebSocket(host + '/getCaseCount'), 11 | url, progress; 12 | 13 | socket.onmessage = function(event) { 14 | console.log('Total cases to run: ' + event.data); 15 | cases = parseInt(event.data); 16 | progress = pace(cases); 17 | }; 18 | 19 | var runCase = function(n) { 20 | if (n > cases) { 21 | url = host + '/updateReports?agent=' + agent; 22 | socket = new WebSocket(url); 23 | socket.onclose = function() { process.exit() }; 24 | return; 25 | } 26 | 27 | url = host + '/runCase?case=' + n + '&agent=' + agent; 28 | socket = new WebSocket(url, [], options); 29 | socket.pipe(socket); 30 | 31 | socket.on('close', function() { 32 | progress.op(); 33 | runCase(n + 1); 34 | }); 35 | }; 36 | 37 | socket.onclose = function() { 38 | runCase(1); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('..').Client, 2 | deflate = require('permessage-deflate'), 3 | fs = require('fs'); 4 | 5 | var url = process.argv[2], 6 | proxy = process.argv[3], 7 | ca = fs.readFileSync(__dirname + '/../spec/server.crt'), 8 | tls = { ca: ca }; 9 | 10 | var ws = new WebSocket(url, [], { 11 | proxy: { origin: proxy, headers: { 'User-Agent': 'Echo' }, tls: tls }, 12 | tls: tls, 13 | headers: { Origin: 'http://faye.jcoglan.com' }, 14 | extensions: [deflate] 15 | }); 16 | 17 | ws.onopen = function() { 18 | console.log('[open]', ws.headers); 19 | ws.send('mic check'); 20 | }; 21 | 22 | ws.onclose = function(close) { 23 | console.log('[close]', close.code, close.reason); 24 | }; 25 | 26 | ws.onerror = function(error) { 27 | console.log('[error]', error.message); 28 | }; 29 | 30 | ws.onmessage = function(message) { 31 | console.log('[message]', message.data); 32 | }; 33 | -------------------------------------------------------------------------------- /examples/haproxy.conf: -------------------------------------------------------------------------------- 1 | defaults 2 | mode http 3 | timeout client 5s 4 | timeout connect 5s 5 | timeout server 5s 6 | 7 | frontend all 0.0.0.0:3000 8 | mode http 9 | timeout client 120s 10 | 11 | option forwardfor 12 | option http-server-close 13 | option http-pretend-keepalive 14 | 15 | default_backend sockets 16 | 17 | backend sockets 18 | balance uri depth 2 19 | timeout server 120s 20 | server socket1 127.0.0.1:7000 21 | -------------------------------------------------------------------------------- /examples/proxy_server.js: -------------------------------------------------------------------------------- 1 | var ProxyServer = require('../spec/proxy_server'); 2 | 3 | var port = process.argv[2], 4 | secure = process.argv[3] === 'tls', 5 | proxy = new ProxyServer({ debug: true, tls: secure }); 6 | 7 | proxy.listen(port); 8 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('..'), 2 | deflate = require('permessage-deflate'), 3 | fs = require('fs'), 4 | http = require('http'), 5 | https = require('https'); 6 | 7 | var port = process.argv[2] || 7000, 8 | secure = process.argv[3] === 'tls', 9 | options = { extensions: [deflate], ping: 5 }; 10 | 11 | var upgradeHandler = function(request, socket, head) { 12 | var ws = new WebSocket(request, socket, head, ['irc', 'xmpp'], options); 13 | console.log('[open]', ws.url, ws.version, ws.protocol, request.headers); 14 | 15 | ws.pipe(ws); 16 | 17 | ws.onclose = function(event) { 18 | console.log('[close]', event.code, event.reason); 19 | ws = null; 20 | }; 21 | }; 22 | 23 | var requestHandler = function(request, response) { 24 | if (!WebSocket.EventSource.isEventSource(request)) 25 | return staticHandler(request, response); 26 | 27 | var es = new WebSocket.EventSource(request, response), 28 | time = parseInt(es.lastEventId, 10) || 0; 29 | 30 | console.log('[open]', es.url, es.lastEventId); 31 | 32 | var loop = setInterval(function() { 33 | time += 1; 34 | es.send('Time: ' + time); 35 | setTimeout(function() { 36 | if (es) es.send('Update!!', { event: 'update', id: time }); 37 | }, 1000); 38 | }, 2000); 39 | 40 | fs.createReadStream(__dirname + '/haproxy.conf').pipe(es, { end: false }); 41 | 42 | es.onclose = function() { 43 | clearInterval(loop); 44 | console.log('[close]', es.url); 45 | es = null; 46 | }; 47 | }; 48 | 49 | var staticHandler = function(request, response) { 50 | var path = request.url; 51 | 52 | fs.readFile(__dirname + path, function(err, content) { 53 | var status = err ? 404 : 200; 54 | response.writeHead(status, { 'Content-Type': 'text/html' }); 55 | response.write(content || 'Not found'); 56 | response.end(); 57 | }); 58 | }; 59 | 60 | var server = secure 61 | ? https.createServer({ 62 | key: fs.readFileSync(__dirname + '/../spec/server.key'), 63 | cert: fs.readFileSync(__dirname + '/../spec/server.crt') 64 | }) 65 | : http.createServer(); 66 | 67 | server.on('request', requestHandler); 68 | server.on('upgrade', upgradeHandler); 69 | server.listen(port); 70 | -------------------------------------------------------------------------------- /examples/sse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EventSource test 6 | 7 | 8 | 9 |

EventSource test

10 | 11 | 12 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/ws.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket test 6 | 7 | 8 | 9 |

WebSocket test

10 | 11 | 12 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/faye/eventsource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = require('stream').Stream, 4 | util = require('util'), 5 | driver = require('websocket-driver'), 6 | Headers = require('websocket-driver/lib/websocket/driver/headers'), 7 | API = require('./websocket/api'), 8 | EventTarget = require('./websocket/api/event_target'), 9 | Event = require('./websocket/api/event'); 10 | 11 | var EventSource = function(request, response, options) { 12 | this.writable = true; 13 | options = options || {}; 14 | 15 | this._stream = response.socket; 16 | this._ping = options.ping || this.DEFAULT_PING; 17 | this._retry = options.retry || this.DEFAULT_RETRY; 18 | 19 | var scheme = driver.isSecureRequest(request) ? 'https:' : 'http:'; 20 | this.url = scheme + '//' + request.headers.host + request.url; 21 | this.lastEventId = request.headers['last-event-id'] || ''; 22 | this.readyState = API.CONNECTING; 23 | 24 | var headers = new Headers(), 25 | self = this; 26 | 27 | if (options.headers) { 28 | for (var key in options.headers) headers.set(key, options.headers[key]); 29 | } 30 | 31 | if (!this._stream || !this._stream.writable) return; 32 | process.nextTick(function() { self._open() }); 33 | 34 | this._stream.setTimeout(0); 35 | this._stream.setNoDelay(true); 36 | 37 | var handshake = 'HTTP/1.1 200 OK\r\n' + 38 | 'Content-Type: text/event-stream\r\n' + 39 | 'Cache-Control: no-cache, no-store\r\n' + 40 | 'Connection: close\r\n' + 41 | headers.toString() + 42 | '\r\n' + 43 | 'retry: ' + Math.floor(this._retry * 1000) + '\r\n\r\n'; 44 | 45 | this._write(handshake); 46 | 47 | this._stream.on('drain', function() { self.emit('drain') }); 48 | 49 | if (this._ping) 50 | this._pingTimer = setInterval(function() { self.ping() }, this._ping * 1000); 51 | 52 | ['error', 'end'].forEach(function(event) { 53 | self._stream.on(event, function() { self.close() }); 54 | }); 55 | }; 56 | util.inherits(EventSource, Stream); 57 | 58 | EventSource.isEventSource = function(request) { 59 | if (request.method !== 'GET') return false; 60 | var accept = (request.headers.accept || '').split(/\s*,\s*/); 61 | return accept.indexOf('text/event-stream') >= 0; 62 | }; 63 | 64 | var instance = { 65 | DEFAULT_PING: 10, 66 | DEFAULT_RETRY: 5, 67 | 68 | _write: function(chunk) { 69 | if (!this.writable) return false; 70 | try { 71 | return this._stream.write(chunk, 'utf8'); 72 | } catch (e) { 73 | return false; 74 | } 75 | }, 76 | 77 | _open: function() { 78 | if (this.readyState !== API.CONNECTING) return; 79 | 80 | this.readyState = API.OPEN; 81 | 82 | var event = new Event('open'); 83 | event.initEvent('open', false, false); 84 | this.dispatchEvent(event); 85 | }, 86 | 87 | write: function(message) { 88 | return this.send(message); 89 | }, 90 | 91 | end: function(message) { 92 | if (message !== undefined) this.write(message); 93 | this.close(); 94 | }, 95 | 96 | send: function(message, options) { 97 | if (this.readyState > API.OPEN) return false; 98 | 99 | message = String(message).replace(/(\r\n|\r|\n)/g, '$1data: '); 100 | options = options || {}; 101 | 102 | var frame = ''; 103 | if (options.event) frame += 'event: ' + options.event + '\r\n'; 104 | if (options.id) frame += 'id: ' + options.id + '\r\n'; 105 | frame += 'data: ' + message + '\r\n\r\n'; 106 | 107 | return this._write(frame); 108 | }, 109 | 110 | ping: function() { 111 | return this._write(':\r\n\r\n'); 112 | }, 113 | 114 | close: function() { 115 | if (this.readyState > API.OPEN) return false; 116 | 117 | this.readyState = API.CLOSED; 118 | this.writable = false; 119 | if (this._pingTimer) clearInterval(this._pingTimer); 120 | if (this._stream) this._stream.end(); 121 | 122 | var event = new Event('close'); 123 | event.initEvent('close', false, false); 124 | this.dispatchEvent(event); 125 | 126 | return true; 127 | } 128 | }; 129 | 130 | for (var method in instance) EventSource.prototype[method] = instance[method]; 131 | for (var key in EventTarget) EventSource.prototype[key] = EventTarget[key]; 132 | 133 | module.exports = EventSource; 134 | -------------------------------------------------------------------------------- /lib/faye/websocket.js: -------------------------------------------------------------------------------- 1 | // API references: 2 | // 3 | // * https://html.spec.whatwg.org/multipage/comms.html#network 4 | // * https://dom.spec.whatwg.org/#interface-eventtarget 5 | // * https://dom.spec.whatwg.org/#interface-event 6 | 7 | 'use strict'; 8 | 9 | var util = require('util'), 10 | driver = require('websocket-driver'), 11 | API = require('./websocket/api'); 12 | 13 | var WebSocket = function(request, socket, body, protocols, options) { 14 | options = options || {}; 15 | 16 | this._stream = socket; 17 | this._driver = driver.http(request, { maxLength: options.maxLength, protocols: protocols }); 18 | 19 | var self = this; 20 | if (!this._stream || !this._stream.writable) return; 21 | if (!this._stream.readable) return this._stream.end(); 22 | 23 | var catchup = function() { self._stream.removeListener('data', catchup) }; 24 | this._stream.on('data', catchup); 25 | 26 | API.call(this, options); 27 | 28 | process.nextTick(function() { 29 | self._driver.start(); 30 | self._driver.io.write(body); 31 | }); 32 | }; 33 | util.inherits(WebSocket, API); 34 | 35 | WebSocket.isWebSocket = function(request) { 36 | return driver.isWebSocket(request); 37 | }; 38 | 39 | WebSocket.validateOptions = function(options, validKeys) { 40 | driver.validateOptions(options, validKeys); 41 | }; 42 | 43 | WebSocket.WebSocket = WebSocket; 44 | WebSocket.Client = require('./websocket/client'); 45 | WebSocket.EventSource = require('./eventsource'); 46 | 47 | module.exports = WebSocket; 48 | -------------------------------------------------------------------------------- /lib/faye/websocket/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = require('stream').Stream, 4 | util = require('util'), 5 | driver = require('websocket-driver'), 6 | EventTarget = require('./api/event_target'), 7 | Event = require('./api/event'); 8 | 9 | var API = function(options) { 10 | options = options || {}; 11 | driver.validateOptions(options, ['headers', 'extensions', 'maxLength', 'ping', 'proxy', 'tls', 'ca']); 12 | 13 | this.readable = this.writable = true; 14 | 15 | var headers = options.headers; 16 | if (headers) { 17 | for (var name in headers) this._driver.setHeader(name, headers[name]); 18 | } 19 | 20 | var extensions = options.extensions; 21 | if (extensions) { 22 | [].concat(extensions).forEach(this._driver.addExtension, this._driver); 23 | } 24 | 25 | this._ping = options.ping; 26 | this._pingId = 0; 27 | this.readyState = API.CONNECTING; 28 | this.bufferedAmount = 0; 29 | this.protocol = ''; 30 | this.url = this._driver.url; 31 | this.version = this._driver.version; 32 | 33 | var self = this; 34 | 35 | this._driver.on('open', function(e) { self._open() }); 36 | this._driver.on('message', function(e) { self._receiveMessage(e.data) }); 37 | this._driver.on('close', function(e) { self._beginClose(e.reason, e.code) }); 38 | 39 | this._driver.on('error', function(error) { 40 | self._emitError(error.message); 41 | }); 42 | this.on('error', function() {}); 43 | 44 | this._driver.messages.on('drain', function() { 45 | self.emit('drain'); 46 | }); 47 | 48 | if (this._ping) 49 | this._pingTimer = setInterval(function() { 50 | self._pingId += 1; 51 | self.ping(self._pingId.toString()); 52 | }, this._ping * 1000); 53 | 54 | this._configureStream(); 55 | 56 | if (!this._proxy) { 57 | this._stream.pipe(this._driver.io); 58 | this._driver.io.pipe(this._stream); 59 | } 60 | }; 61 | util.inherits(API, Stream); 62 | 63 | API.CONNECTING = 0; 64 | API.OPEN = 1; 65 | API.CLOSING = 2; 66 | API.CLOSED = 3; 67 | 68 | API.CLOSE_TIMEOUT = 30000; 69 | 70 | var instance = { 71 | write: function(data) { 72 | return this.send(data); 73 | }, 74 | 75 | end: function(data) { 76 | if (data !== undefined) this.send(data); 77 | this.close(); 78 | }, 79 | 80 | pause: function() { 81 | return this._driver.messages.pause(); 82 | }, 83 | 84 | resume: function() { 85 | return this._driver.messages.resume(); 86 | }, 87 | 88 | send: function(data) { 89 | if (this.readyState > API.OPEN) return false; 90 | if (!(data instanceof Buffer)) data = String(data); 91 | return this._driver.messages.write(data); 92 | }, 93 | 94 | ping: function(message, callback) { 95 | if (this.readyState > API.OPEN) return false; 96 | return this._driver.ping(message, callback); 97 | }, 98 | 99 | close: function(code, reason) { 100 | if (code === undefined) code = 1000; 101 | if (reason === undefined) reason = ''; 102 | 103 | if (code !== 1000 && (code < 3000 || code > 4999)) 104 | throw new Error("Failed to execute 'close' on WebSocket: " + 105 | "The code must be either 1000, or between 3000 and 4999. " + 106 | code + " is neither."); 107 | 108 | if (this.readyState < API.CLOSING) { 109 | var self = this; 110 | this._closeTimer = setTimeout(function() { 111 | self._beginClose('', 1006); 112 | }, API.CLOSE_TIMEOUT); 113 | } 114 | 115 | if (this.readyState !== API.CLOSED) this.readyState = API.CLOSING; 116 | 117 | this._driver.close(reason, code); 118 | }, 119 | 120 | _configureStream: function() { 121 | var self = this; 122 | 123 | this._stream.setTimeout(0); 124 | this._stream.setNoDelay(true); 125 | 126 | ['close', 'end'].forEach(function(event) { 127 | this._stream.on(event, function() { self._finalizeClose() }); 128 | }, this); 129 | 130 | this._stream.on('error', function(error) { 131 | self._emitError('Network error: ' + self.url + ': ' + error.message); 132 | self._finalizeClose(); 133 | }); 134 | }, 135 | 136 | _open: function() { 137 | if (this.readyState !== API.CONNECTING) return; 138 | 139 | this.readyState = API.OPEN; 140 | this.protocol = this._driver.protocol || ''; 141 | 142 | var event = new Event('open'); 143 | event.initEvent('open', false, false); 144 | this.dispatchEvent(event); 145 | }, 146 | 147 | _receiveMessage: function(data) { 148 | if (this.readyState > API.OPEN) return false; 149 | 150 | if (this.readable) this.emit('data', data); 151 | 152 | var event = new Event('message', { data: data }); 153 | event.initEvent('message', false, false); 154 | this.dispatchEvent(event); 155 | }, 156 | 157 | _emitError: function(message) { 158 | if (this.readyState >= API.CLOSING) return; 159 | 160 | var event = new Event('error', { message: message }); 161 | event.initEvent('error', false, false); 162 | this.dispatchEvent(event); 163 | }, 164 | 165 | _beginClose: function(reason, code) { 166 | if (this.readyState === API.CLOSED) return; 167 | this.readyState = API.CLOSING; 168 | this._closeParams = [reason, code]; 169 | 170 | if (this._stream) { 171 | this._stream.destroy(); 172 | if (!this._stream.readable) this._finalizeClose(); 173 | } 174 | }, 175 | 176 | _finalizeClose: function() { 177 | if (this.readyState === API.CLOSED) return; 178 | this.readyState = API.CLOSED; 179 | 180 | if (this._closeTimer) clearTimeout(this._closeTimer); 181 | if (this._pingTimer) clearInterval(this._pingTimer); 182 | if (this._stream) this._stream.end(); 183 | 184 | if (this.readable) this.emit('end'); 185 | this.readable = this.writable = false; 186 | 187 | var reason = this._closeParams ? this._closeParams[0] : '', 188 | code = this._closeParams ? this._closeParams[1] : 1006; 189 | 190 | var event = new Event('close', { code: code, reason: reason }); 191 | event.initEvent('close', false, false); 192 | this.dispatchEvent(event); 193 | } 194 | }; 195 | 196 | for (var method in instance) API.prototype[method] = instance[method]; 197 | for (var key in EventTarget) API.prototype[key] = EventTarget[key]; 198 | 199 | module.exports = API; 200 | -------------------------------------------------------------------------------- /lib/faye/websocket/api/event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Event = function(eventType, options) { 4 | this.type = eventType; 5 | for (var key in options) 6 | this[key] = options[key]; 7 | }; 8 | 9 | Event.prototype.initEvent = function(eventType, canBubble, cancelable) { 10 | this.type = eventType; 11 | this.bubbles = canBubble; 12 | this.cancelable = cancelable; 13 | }; 14 | 15 | Event.prototype.stopPropagation = function() {}; 16 | Event.prototype.preventDefault = function() {}; 17 | 18 | Event.CAPTURING_PHASE = 1; 19 | Event.AT_TARGET = 2; 20 | Event.BUBBLING_PHASE = 3; 21 | 22 | module.exports = Event; 23 | -------------------------------------------------------------------------------- /lib/faye/websocket/api/event_target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Event = require('./event'); 4 | 5 | var EventTarget = { 6 | onopen: null, 7 | onmessage: null, 8 | onerror: null, 9 | onclose: null, 10 | 11 | addEventListener: function(eventType, listener, useCapture) { 12 | this.on(eventType, listener); 13 | }, 14 | 15 | removeEventListener: function(eventType, listener, useCapture) { 16 | this.removeListener(eventType, listener); 17 | }, 18 | 19 | dispatchEvent: function(event) { 20 | event.target = event.currentTarget = this; 21 | event.eventPhase = Event.AT_TARGET; 22 | 23 | if (this['on' + event.type]) 24 | this['on' + event.type](event); 25 | 26 | this.emit(event.type, event); 27 | } 28 | }; 29 | 30 | module.exports = EventTarget; 31 | -------------------------------------------------------------------------------- /lib/faye/websocket/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'), 4 | net = require('net'), 5 | tls = require('tls'), 6 | url = require('url'), 7 | driver = require('websocket-driver'), 8 | API = require('./api'), 9 | Event = require('./api/event'); 10 | 11 | var DEFAULT_PORTS = { 'http:': 80, 'https:': 443, 'ws:':80, 'wss:': 443 }, 12 | SECURE_PROTOCOLS = ['https:', 'wss:']; 13 | 14 | var Client = function(_url, protocols, options) { 15 | options = options || {}; 16 | 17 | this.url = _url; 18 | this._driver = driver.client(this.url, { maxLength: options.maxLength, protocols: protocols }); 19 | 20 | ['open', 'error'].forEach(function(event) { 21 | this._driver.on(event, function() { 22 | self.headers = self._driver.headers; 23 | self.statusCode = self._driver.statusCode; 24 | }); 25 | }, this); 26 | 27 | var proxy = options.proxy || {}, 28 | endpoint = url.parse(proxy.origin || this.url), 29 | port = endpoint.port || DEFAULT_PORTS[endpoint.protocol], 30 | secure = SECURE_PROTOCOLS.indexOf(endpoint.protocol) >= 0, 31 | onConnect = function() { self._onConnect() }, 32 | netOptions = options.net || {}, 33 | originTLS = options.tls || {}, 34 | socketTLS = proxy.origin ? (proxy.tls || {}) : originTLS, 35 | self = this; 36 | 37 | netOptions.host = socketTLS.host = endpoint.hostname; 38 | netOptions.port = socketTLS.port = port; 39 | 40 | originTLS.ca = originTLS.ca || options.ca; 41 | socketTLS.servername = socketTLS.servername || endpoint.hostname; 42 | 43 | this._stream = secure 44 | ? tls.connect(socketTLS, onConnect) 45 | : net.connect(netOptions, onConnect); 46 | 47 | if (proxy.origin) this._configureProxy(proxy, originTLS); 48 | 49 | API.call(this, options); 50 | }; 51 | util.inherits(Client, API); 52 | 53 | Client.prototype._onConnect = function() { 54 | var worker = this._proxy || this._driver; 55 | worker.start(); 56 | }; 57 | 58 | Client.prototype._configureProxy = function(proxy, originTLS) { 59 | var uri = url.parse(this.url), 60 | secure = SECURE_PROTOCOLS.indexOf(uri.protocol) >= 0, 61 | self = this, 62 | name; 63 | 64 | this._proxy = this._driver.proxy(proxy.origin); 65 | 66 | if (proxy.headers) { 67 | for (name in proxy.headers) this._proxy.setHeader(name, proxy.headers[name]); 68 | } 69 | 70 | this._proxy.pipe(this._stream, { end: false }); 71 | this._stream.pipe(this._proxy); 72 | 73 | this._proxy.on('connect', function() { 74 | if (secure) { 75 | var options = { socket: self._stream, servername: uri.hostname }; 76 | for (name in originTLS) options[name] = originTLS[name]; 77 | self._stream = tls.connect(options); 78 | self._configureStream(); 79 | } 80 | self._driver.io.pipe(self._stream); 81 | self._stream.pipe(self._driver.io); 82 | self._driver.start(); 83 | }); 84 | 85 | this._proxy.on('error', function(error) { 86 | self._driver.emit('error', error); 87 | }); 88 | }; 89 | 90 | module.exports = Client; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faye-websocket", 3 | "description": "Standards-compliant WebSocket server and client", 4 | "homepage": "https://github.com/faye/faye-websocket-node", 5 | "author": "James Coglan (http://jcoglan.com/)", 6 | "keywords": [ 7 | "websocket", 8 | "eventsource" 9 | ], 10 | "license": "Apache-2.0", 11 | "version": "0.11.4", 12 | "engines": { 13 | "node": ">=0.8.0" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "main": "./lib/faye/websocket", 19 | "dependencies": { 20 | "websocket-driver": ">=0.5.1" 21 | }, 22 | "devDependencies": { 23 | "jstest": "*", 24 | "pace": "*", 25 | "permessage-deflate": "*" 26 | }, 27 | "scripts": { 28 | "test": "jstest spec/runner.js" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/faye/faye-websocket-node.git" 33 | }, 34 | "bugs": "https://github.com/faye/faye-websocket-node/issues" 35 | } 36 | -------------------------------------------------------------------------------- /spec/echo_server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | http = require('http'), 3 | https = require('https'); 4 | 5 | var WebSocket = require('../lib/faye/websocket'); 6 | 7 | var EchoServer = function(secure) { 8 | var server = secure 9 | ? https.createServer({ 10 | key: fs.readFileSync(__dirname + '/server.key'), 11 | cert: fs.readFileSync(__dirname + '/server.crt') 12 | }) 13 | : http.createServer(); 14 | 15 | server.on('upgrade', function(request, socket, head) { 16 | var ws = new WebSocket(request, socket, head, ["echo"]); 17 | ws.pipe(ws); 18 | }); 19 | 20 | this._httpServer = server; 21 | }; 22 | 23 | EchoServer.prototype.listen = function(port) { 24 | this._httpServer.listen(port); 25 | }; 26 | 27 | EchoServer.prototype.stop = function() { 28 | this._httpServer.close(); 29 | }; 30 | 31 | module.exports = EchoServer; 32 | -------------------------------------------------------------------------------- /spec/faye/websocket/client_spec.js: -------------------------------------------------------------------------------- 1 | var Client = require('../../../lib/faye/websocket/client'), 2 | EchoServer = require('../../echo_server'), 3 | ProxyServer = require('../../proxy_server'), 4 | test = require('jstest').Test, 5 | fs = require('fs') 6 | 7 | var WebSocketSteps = test.asyncSteps({ 8 | server: function(port, secure, callback) { 9 | this._echoServer = new EchoServer(secure) 10 | this._echoServer.listen(port) 11 | process.nextTick(callback) 12 | }, 13 | 14 | stop: function(callback) { 15 | this._echoServer.stop() 16 | process.nextTick(callback) 17 | }, 18 | 19 | proxy: function(port, secure, callback) { 20 | this._proxyServer = new ProxyServer({ tls: secure }) 21 | this._proxyServer.listen(port) 22 | process.nextTick(callback) 23 | }, 24 | 25 | stop_proxy: function(callback) { 26 | this._proxyServer.stop() 27 | process.nextTick(callback) 28 | }, 29 | 30 | open_socket: function(url, protocols, callback) { 31 | var done = false, 32 | self = this, 33 | 34 | tlsOptions = { ca: fs.readFileSync(__dirname + '/../../server.crt') }, 35 | 36 | resume = function(open) { 37 | if (done) return 38 | done = true 39 | self._open = open 40 | callback() 41 | } 42 | 43 | this._ws = new Client(url, protocols, { 44 | proxy: { origin: this.proxy_url, tls: tlsOptions }, 45 | tls: tlsOptions 46 | }) 47 | 48 | this._ws.onopen = function() { resume(true) } 49 | this._ws.onclose = function() { resume(false) } 50 | }, 51 | 52 | open_socket_and_close_it_fast: function(url, protocols, callback) { 53 | var self = this 54 | 55 | this._ws = new Client(url, protocols, { 56 | ca: fs.readFileSync(__dirname + '/../../server.crt') 57 | }) 58 | 59 | this._ws.onopen = function() { self._open = self._ever_opened = true } 60 | this._ws.onclose = function() { self._open = false } 61 | 62 | this._ws.close() 63 | 64 | callback() 65 | }, 66 | 67 | close_socket: function(callback) { 68 | var self = this 69 | this._ws.onclose = function() { 70 | self._open = false 71 | callback() 72 | } 73 | this._ws.close() 74 | }, 75 | 76 | check_open: function(status, headers, callback) { 77 | this.assert( this._open ) 78 | this.assertEqual( status, this._ws.statusCode ) 79 | for (var name in headers) 80 | this.assertEqual( headers[name], this._ws.headers[name.toLowerCase()] ) 81 | callback() 82 | }, 83 | 84 | check_closed: function(callback) { 85 | this.assert( !this._open ) 86 | callback() 87 | }, 88 | 89 | check_never_opened: function(callback) { 90 | this.assert( !this._ever_opened ) 91 | callback() 92 | }, 93 | 94 | check_readable: function(callback) { 95 | this.assert( this._ws.readable ) 96 | callback() 97 | }, 98 | 99 | check_not_readable: function(callback) { 100 | this.assert( ! this._ws.readable ) 101 | callback() 102 | }, 103 | 104 | check_protocol: function(protocol, callback) { 105 | this.assertEqual( protocol, this._ws.protocol ) 106 | callback() 107 | }, 108 | 109 | listen_for_message: function(callback) { 110 | var time = new Date().getTime(), self = this 111 | this._ws.addEventListener('message', function(message) { self._message = message.data }) 112 | var timer = setInterval(function() { 113 | if (self._message || new Date().getTime() - time > 3000) { 114 | clearInterval(timer) 115 | callback() 116 | } 117 | }, 100) 118 | }, 119 | 120 | send_message: function(message, callback) { 121 | var ws = this._ws 122 | setTimeout(function() { ws.send(message) }, 500) 123 | process.nextTick(callback) 124 | }, 125 | 126 | check_response: function(message, callback) { 127 | this.assertEqual( message, this._message ) 128 | callback() 129 | }, 130 | 131 | check_no_response: function(callback) { 132 | this.assert( !this._message ) 133 | callback() 134 | }, 135 | 136 | wait: function (ms, callback) { 137 | setTimeout(callback, ms) 138 | } 139 | }) 140 | 141 | 142 | test.describe("Client", function() { with(this) { 143 | include(WebSocketSteps) 144 | 145 | before(function() { 146 | this.protocols = ["foo", "echo"] 147 | 148 | this.plain_text_url = "ws://localhost:4180/bayeux" 149 | this.secure_url = "wss://localhost:4180/bayeux" 150 | this.port = 4180 151 | 152 | this.plain_text_proxy_url = "http://localhost:4181" 153 | this.secure_text_proxy_url = "https://localhost:4181" 154 | this.proxy_port = 4181 155 | }) 156 | 157 | sharedBehavior("socket client", function() { with(this) { 158 | it("can open a connection", function() { with(this) { 159 | open_socket(socket_url, protocols) 160 | check_open(101, { "Upgrade": "websocket" }) 161 | check_protocol("echo") 162 | }}) 163 | 164 | it("can close the connection", function() { with(this) { 165 | open_socket(socket_url, protocols) 166 | check_readable() 167 | close_socket() 168 | check_closed() 169 | check_not_readable() 170 | }}) 171 | 172 | describe("in the OPEN state", function() { with(this) { 173 | before(function() { with(this) { 174 | open_socket(socket_url, protocols) 175 | }}) 176 | 177 | it("can send and receive messages", function() { with(this) { 178 | send_message("I expect this to be echoed") 179 | listen_for_message() 180 | check_response("I expect this to be echoed") 181 | }}) 182 | 183 | it("sends numbers as strings", function() { with(this) { 184 | send_message(13) 185 | listen_for_message() 186 | check_response("13") 187 | }}) 188 | 189 | it("sends booleans as strings", function() { with(this) { 190 | send_message(false) 191 | listen_for_message() 192 | check_response("false") 193 | }}) 194 | 195 | it("sends arrays as strings", function() { with(this) { 196 | send_message([13,14,15]) 197 | listen_for_message() 198 | check_response("13,14,15") 199 | }}) 200 | }}) 201 | 202 | describe("in the CLOSED state", function() { with(this) { 203 | before(function() { with(this) { 204 | open_socket(socket_url, protocols) 205 | close_socket() 206 | }}) 207 | 208 | it("cannot send and receive messages", function() { with(this) { 209 | send_message("I expect this to be echoed") 210 | listen_for_message() 211 | check_no_response() 212 | }}) 213 | }}) 214 | 215 | it("can be closed before connecting", function() { with(this) { 216 | open_socket_and_close_it_fast(socket_url, protocols) 217 | wait(100) 218 | check_closed() 219 | check_never_opened() 220 | check_not_readable() 221 | }}) 222 | }}) 223 | 224 | sharedBehavior("socket server", function() { with(this) { 225 | describe("with a plain-text server", function() { with(this) { 226 | before(function() { 227 | this.socket_url = this.plain_text_url 228 | this.blocked_url = this.secure_url 229 | }) 230 | 231 | before(function() { this.server(this.port, false) }) 232 | after (function() { this.stop() }) 233 | 234 | behavesLike("socket client") 235 | }}) 236 | 237 | describe("with a secure server", function() { with(this) { 238 | before(function() { 239 | this.socket_url = this.secure_url 240 | this.blocked_url = this.plain_text_url 241 | }) 242 | 243 | before(function() { this.server(this.port, true) }) 244 | after (function() { this.stop() }) 245 | 246 | behavesLike("socket client") 247 | }}) 248 | }}) 249 | 250 | describe("with no proxy", function() { with(this) { 251 | behavesLike("socket server") 252 | }}) 253 | 254 | describe("with a proxy", function() { with(this) { 255 | before(function() { 256 | this.proxy_url = this.plain_text_proxy_url 257 | }) 258 | 259 | before(function() { this.proxy(this.proxy_port, false) }) 260 | after (function() { this.stop_proxy() }) 261 | 262 | behavesLike("socket server") 263 | }}) 264 | 265 | describe("with a secure proxy", function() { with(this) { 266 | before(function() { 267 | this.proxy_url = this.secure_text_proxy_url 268 | }) 269 | 270 | before(function() { this.proxy(this.proxy_port, true) }) 271 | after (function() { this.stop_proxy() }) 272 | 273 | behavesLike("socket server") 274 | }}) 275 | }}) 276 | -------------------------------------------------------------------------------- /spec/proxy_server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | http = require('http'), 3 | https = require('https'), 4 | net = require('net'), 5 | url = require('url'); 6 | 7 | var AGENTS = { 'http:': http, 'https:': https }, 8 | PORTS = { 'http:': 80, 'https:': 443 }; 9 | 10 | var ProxyServer = function(options) { 11 | var proxy = options.tls 12 | ? https.createServer({ 13 | key: fs.readFileSync(__dirname + '/server.key'), 14 | cert: fs.readFileSync(__dirname + '/server.crt') 15 | }) 16 | : http.createServer(); 17 | 18 | options = options || {}; 19 | 20 | var onRequest = function(request, response) { 21 | if (options.debug) console.log(request.method, request.url, request.headers); 22 | 23 | var uri = url.parse(request.url), 24 | agent = AGENTS[uri.protocol], 25 | headers = {}; 26 | 27 | for (var key in request.headers) { 28 | if (key.split('-')[0] !== 'proxy') headers[key] = request.headers[key]; 29 | } 30 | 31 | var backend = agent.request({ 32 | method: request.method, 33 | host: uri.hostname, 34 | port: uri.port || PORTS[uri.protocol], 35 | path: uri.path, 36 | headers: headers, 37 | rejectUnauthorized: false 38 | }); 39 | 40 | request.pipe(backend); 41 | 42 | backend.on('response', function(resp) { 43 | if (options.debug) console.log(resp.statusCode, resp.headers); 44 | response.writeHead(resp.statusCode, resp.headers); 45 | resp.pipe(response); 46 | }); 47 | }; 48 | 49 | var onConnect = function(request, frontend, body) { 50 | var parts = request.url.split(':'), 51 | backend = net.connect(parts[1], parts[0]); 52 | 53 | frontend.pipe(backend); 54 | backend.pipe(frontend); 55 | 56 | backend.on('connect', function() { 57 | frontend.write('HTTP/1.1 200 OK\r\n\r\n'); 58 | }); 59 | 60 | if (!options.debug) return; 61 | console.log(request.method, request.url, request.headers); 62 | 63 | frontend.on('data', function(data) { console.log('I', data) }); 64 | backend.on( 'data', function(data) { console.log('O', data) }); 65 | }; 66 | 67 | proxy.on('request', onRequest); 68 | proxy.on('connect', onConnect); 69 | proxy.on('upgrade', onConnect); 70 | 71 | this._proxy = proxy; 72 | }; 73 | 74 | ProxyServer.prototype.listen = function(port) { 75 | this._proxy.listen(port); 76 | }; 77 | 78 | ProxyServer.prototype.stop = function() { 79 | this._proxy.close(); 80 | }; 81 | 82 | module.exports = ProxyServer; 83 | -------------------------------------------------------------------------------- /spec/runner.js: -------------------------------------------------------------------------------- 1 | require('./faye/websocket/client_spec') 2 | -------------------------------------------------------------------------------- /spec/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfECFGK6xC8kSXk3aNAjEuRTAeHoO1BFMA0GCSqGSIb3DQEBCwUAMEEx 3 | CzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDTALBgNVBAoMBEZheWUxEjAQ 4 | BgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA5MDcxNzU4NTVaFw0yNDA5MDYxNzU4NTVa 5 | MEExCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDTALBgNVBAoMBEZheWUx 6 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 7 | ggEBAMGREBi/onEuDpfFOi+6ugGeYTEV+n/KrYDWdT5FCFrL3Ab/kCpWCpwzv6A5 8 | lEPoqyf1Y0+cpcxwZm24fZV8uaaQ53xamzp7/6K+6IeNdI3Lq1J7gJx5wU5tdm2U 9 | jbWv7U0R+XX6MzWYqRFWuCNu2n9uvTl0AKDnyAWmRbhMKXriD5IWbPvMqgI/FnXh 10 | VnK7z+UHzLb214Q0VlfUs4U8oJjA5K7IEiMF2FkA3HPnxdvtjDNDCYyahumWLozP 11 | lbSo8Zlyr4xpp7yUv9TAhJozs+Y320cBpLlEeQoFIel+nOc7xgD2Wmu11ds0p/4j 12 | 8qdFOv1h9d2DsLQ5ZFJxbXVNu0UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAaMQl 13 | AnLCKsdodSl6KV3joaGSSkY8WuD1Y76/bieArwRRqdSA4eGYcAHnsyEUqP2otgCj 14 | +vS9h9jGMbLLjPindE+0jij9TpA9oNk8exUQSN5CNgBbrxWakDR6GmK7nWYD4/uh 15 | wcj7QuysExGqpvY1eXh8ki81a5g3if2wjSkprF0VEtulv/MB7eBqvyEZ2d6IuNUc 16 | IG3FmHcCRiGW0S+fPshmSN+OkUtMBJxHXRbr7TFy3Iz4vSDcq0XznVksNTavz5ve 17 | r4qWhPn0J3lfo9y5GBPCv7RWnxqnQWNM8dXPmS8Wh8hteSde8rtHkPuRDF/1VdER 18 | myxYuyLx/dhkxp57kw== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /spec/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBkRAYv6JxLg6X 3 | xTovuroBnmExFfp/yq2A1nU+RQhay9wG/5AqVgqcM7+gOZRD6Ksn9WNPnKXMcGZt 4 | uH2VfLmmkOd8Wps6e/+ivuiHjXSNy6tSe4CcecFObXZtlI21r+1NEfl1+jM1mKkR 5 | Vrgjbtp/br05dACg58gFpkW4TCl64g+SFmz7zKoCPxZ14VZyu8/lB8y29teENFZX 6 | 1LOFPKCYwOSuyBIjBdhZANxz58Xb7YwzQwmMmobpli6Mz5W0qPGZcq+Maae8lL/U 7 | wISaM7PmN9tHAaS5RHkKBSHpfpznO8YA9lprtdXbNKf+I/KnRTr9YfXdg7C0OWRS 8 | cW11TbtFAgMBAAECggEAB+Pxa4jYRtPRTXeBNTPf5DQAEz+pm+73nqJfWh/3RLg9 9 | ql1dk5Q5T3++hnoCZLhFzdWvbC3fBlPooP1dxSu156fNf+CzpjEqzQgKM4hdIXCV 10 | dcHKDtbZvegxZSsika7vte4PJLODxeIItke6LtuUdZBej0p+whBgs3ZBLk+Qe01M 11 | la73SCNi0Pjg8auyMRvncLqFvKE/KplemeCS30+AMoyhtLKQmrOo+Ub94N+tp8fR 12 | COtsmIJQX6uj1cg3Ikn23G5Kll66gt3kzNRGgNfkcygWmfPFE4WHBZXvOiLHhs7j 13 | FsAtA+vVODDyofmgT686LsEg70aM58SCXhX5DtsxYQKBgQDsejwS1Po4rrAqZj7j 14 | ny9lnZuCGo/P9CnDzcNnsJAxVBh5knKkVNC8r6ubMBwrf3lq+I7RbI3AAS/vxcON 15 | Rhj39cvUMSv5/fkMAGDIHL2yW8LBufYbyyQm2ChFuas51560aAsCrFN1gB0I8A1M 16 | eRkNFcoPzeS8GHkjhE8P4EyRkQKBgQDRi/EQ+nA5nfLggl+G67ACiMe2i3FZs1mP 17 | 4WWA7fvfty/L6WBGuxCU1v9hMBj63LhHrlKF2MOHbKNuBwGYjeNgqaBWw+ahwdPX 18 | N06x/FhR1q8AiVH26S5nRAaoh5ode5H2dx17e0QteEFe29nFoSkT47sAOxRfkWjx 19 | QqBdBgH0dQKBgG54zAegJyTDttiX21lKzEGUV0l4Tya+0aP/RAH0oefpeWWR3KyY 20 | UstS5cAhwYcwjfBDHbUIGVBRPautn6Un0hJEaWw/bGPlGatZodzaUGQ6KcmGrkpd 21 | pA3hfS7VhgAHksSEtmARUQvbRbUfL5dCG0nZnAO2E90rMaw96xFnn12BAoGAeX9J 22 | jA2Zal7hhzkwiDs5t451NauOUnNCF8GZp/LU2rcNWI79SqWGDLbIJiLMKRA3LSCv 23 | KnovjOL5s38OdtS2JMLVe9lkbR/EY4Hm+B4XW4Q9vfLg+mfjhu6Tab4OJtASJrST 24 | /JfRRQf35zdUAlnaRnUBZTXcLzlRfqmh763fDk0CgYEAuBYr1xVFrdwn/YSHn5m5 25 | amexAv821RCWvuZMUPfnSHGoxOGGI0DEOocznIuExNoRVrsKMeuskbpoVuo/b4S5 26 | BVjzP+lNlkDEJVcYLnlX3t4elA9IYj7RqwZTmT8BrVEoH3AFns3O2jDzZw47I7dK 27 | bCewTY0p5+4zEWlgsxX/Mtk= 28 | -----END PRIVATE KEY----- 29 | --------------------------------------------------------------------------------