├── .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 |
--------------------------------------------------------------------------------