[Pkg-javascript-commits] [node-engine.io] 01/02: Imported Upstream version 1.5.1
Sebastiaan Couwenberg
sebastic at moszumanska.debian.org
Sun Mar 29 00:18:30 UTC 2015
This is an automated email from the git hooks/post-receive script.
sebastic pushed a commit to branch master
in repository node-engine.io.
commit 2af68e403ea979817451f6056ae54515e2d8f240
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date: Sun Mar 29 00:23:15 2015 +0100
Imported Upstream version 1.5.1
---
.gitignore | 4 +
.npmignore | 6 +
.travis.yml | 7 +
History.md | 408 ++++++++
Makefile | 23 +
README.md | 517 ++++++++++
examples/latency/README.md | 18 +
examples/latency/index.html | 14 +
examples/latency/index.js | 30 +
examples/latency/package.json | 11 +
examples/latency/public/index.js | 57 ++
examples/latency/public/style.css | 5 +
index.js | 4 +
lib/engine.io.js | 126 +++
lib/server.js | 391 ++++++++
lib/socket.js | 393 ++++++++
lib/transport.js | 114 +++
lib/transports/index.js | 36 +
lib/transports/polling-jsonp.js | 108 ++
lib/transports/polling-xhr.js | 101 ++
lib/transports/polling.js | 279 +++++
lib/transports/websocket.js | 110 ++
package.json | 32 +
test/common.js | 29 +
test/engine.io.js | 230 +++++
test/fixtures/ca.crt | 35 +
test/fixtures/ca.key | 51 +
test/fixtures/ca.key.org | 54 +
test/fixtures/client.crt | 23 +
test/fixtures/client.csr | 12 +
test/fixtures/client.key | 15 +
test/fixtures/client.key.orig | 18 +
test/fixtures/client.pfx | Bin 0 -> 1989 bytes
test/fixtures/server.crt | 23 +
test/fixtures/server.csr | 11 +
test/fixtures/server.key | 15 +
test/fixtures/server.key.orig | 18 +
test/jsonp.js | 232 +++++
test/server.js | 2010 +++++++++++++++++++++++++++++++++++++
39 files changed, 5570 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..0a5dfa3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+npm-debug.log
+coverage.html
+lib-cov/
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..f09f630
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,6 @@
+examples
+node_modules
+test
+npm-debug.log
+coverage.html
+.gitignore
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..04a6969
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+ - "0.10"
+ - "0.8"
+
+notifications:
+ irc: "irc.freenode.org#socket.io"
diff --git a/History.md b/History.md
new file mode 100644
index 0000000..2a37b0b
--- /dev/null
+++ b/History.md
@@ -0,0 +1,408 @@
+
+1.5.1 / 2015-01-19
+==================
+
+ * no change on this release
+ * package: bump `engine.io-client`
+
+1.5.0 / 2015-01-18
+==================
+
+ * package: bump `engine.io-parser`
+ * polling: correctly abort the ongoing data request when closing [lpinca]
+ * add cert-related client tests [rase-]
+
+1.4.3 / 2014-11-21
+==================
+
+ * package: bump `ws` to fix fd leaks
+ * socket: flush the write buffer before closing the socket [nkzawa]
+ * polling: close the pending poll request when closing transport [nkzawa]
+
+1.4.2 / 2014-10-08
+==================
+
+ * add iframe onload handling to jsonp tests [rase-]
+
+1.4.1 / 2014-10-03
+==================
+
+ * socket: allow upgrades if the socket is still in closing state
+ * README: fix typo
+
+1.4.0 / 2014-09-03
+==================
+
+ * readme: fix formatting for goals numbering
+ * server: ref fix by @nicokaiser
+ * server: fix ws memory leak (fixes #268)
+ * cache remote address in handshake since it might be lost later.
+ * correct git ref
+ * update client to commit with bumped parser
+ * package: bump parser
+ * npmignore: ignore `.gitignore`
+ * package: bump `debug`
+ * package: bump `engine.io-parser` for memleak fix
+
+1.3.1 / 2014-06-19
+==================
+
+ * package: bump `engine.io-client`
+
+1.3.0 / 2014-06-13
+==================
+
+ * update example to use v1.2.2
+ * fixed newline parsing in jsonp
+ * make require('engine.io')() return a new Server instance [defunctzombie]
+ * add Server.attach method [defunctzombie]
+ * fix GH-211, set CORS headers when sending error message [mokesmokes]
+
+1.2.2 / 2014-05-30
+==================
+
+ * package: bump `engine.io-parser` for binary utf8 fix
+
+1.2.1 / 2014-05-22
+==================
+
+ * package: bump engine.io-client
+
+1.2.0 / 2014-05-18
+==================
+
+ * removed flashsocket, moving to userland
+
+1.1.1 / 2014-05-14
+==================
+
+ * test: reduce packet size
+ * package: bump parser
+
+1.1.0 / 2014-04-27
+==================
+
+ * socket: removed unneeded `clearTimeout` (fixes #250)
+ * made the request verification process async
+ * package: bump `engine.io-parser`
+ * use _query instead of query, fixes compat with restify
+ * added a maximum buffer size to received data from polling
+ * fixing looping array via for in to normal loop
+
+1.0.5 / 2014-03-18
+==================
+
+ * package: bump `engine.io-parser` and `engine.io-client`
+
+1.0.4 / 2014-03-14
+==================
+
+ * package: bump `engine.io-client`
+
+1.0.3 / 2014-03-12
+==================
+
+ * package: bump `engine.io-client`
+
+1.0.2 / 2014-03-12
+==================
+
+ * bump engine.io-client
+
+1.0.1 / 2014-03-06
+==================
+
+ * package: bump `engine.io-parser`
+ * transports: fix jshint warnings and style
+
+1.0.0 / 2014-03-06
+==================
+
+ * polling-xhr: added `OPTIONS` support, fixes CORS
+ * close() properly when triggered in connection handler
+ * fix DDOS vector by setting up too many intervals
+ * binary support
+
+0.9.0 / 2014-02-09
+==================
+
+ * Prevent errors with connections behind proxies without WS support
+ like Squid [nicklagrow, samaanghani, davidhcummings]
+ * Socket#request a simple property [mokesmokes]
+ * Changed `Socket`'s `upgrade` event to happen after upgrade [mokesmokes]
+ * Document `Socket#id` [mokesmokes]
+
+0.8.2 / 2014-01-18
+==================
+
+ * package: bump `engine.io-client`
+
+0.8.1 / 2014-01-17
+==================
+
+ * package: bump `engine.io-client`
+ * package: pin dev deps
+ * examples: fix port output
+ * fix latency example
+
+0.8.0 / 2014-01-05
+==================
+
+ * package: bump `engine.io-client` to `0.8.0`
+ * test: fix syntax, remove globals
+
+0.7.14 / 2014-01-01
+===================
+
+ * package: bump `engine.io-client` to `0.7.14`
+
+0.7.13 / 2013-12-20
+===================
+
+ * package: bump `engine.io-client`
+ * transports: added support for XSS filters on IE [guille, 3rd-eden]
+
+0.7.12 / 2013-11-11
+===================
+
+ * package: bump `engine.io-client`
+
+0.7.11 / 2013-11-06
+===================
+
+ * package: bump engine.io-client
+ * fix GH-198
+
+0.7.10 / 2013-10-28
+===================
+
+ * package: bump `engine.io-client`
+ * package: update "ws" to v0.4.31
+
+0.7.9 / 2013-08-30
+==================
+
+ * package: bump `engine.io-client`
+
+0.7.8 / 2013-08-30
+==================
+
+ * package: bump `engine.io-client`
+ * package: bump ws
+
+0.7.7 / 2013-08-30
+==================
+
+ * package: bump `engine.io-client`
+
+0.7.6 / 2013-08-30
+==================
+
+ * package: bump engine.io-client
+
+0.7.5 / 2013-08-30
+==================
+
+ * package: bump engine.io-client
+
+0.7.4 / 2013-08-25
+==================
+
+ * package: bump `engine.io-client`
+
+0.7.3 / 2013-08-23
+==================
+
+ * package: bump engine.io-client (noop)
+ * package: fix regresison in upgrade cause by ws update
+
+0.7.2 / 2013-08-23
+==================
+
+ * package: bump `engine.io-client` for `WebSocket` browser fix
+
+0.7.1 / 2013-08-23
+==================
+
+ * package: bump engine.io-client for ws fix
+
+0.7.0 / 2013-08-23
+==================
+
+ * package: bump engine.io-client
+ * updated example
+ * inline merge
+ * added support node version 0.10 to .travis.yml
+ * fixed respond to flash policy request test. Closes #184
+ * fixed upgrade with timeout test. Closes #185
+ * engine.io: don't use __proto__, closes #170
+
+0.6.3 / 2013-06-21
+==================
+
+ * package: bumped `engine.io-client` to `0.6.3`
+
+0.6.2 / 2013-06-15
+==================
+
+ * fix upgrade stalling edge case introduced with #174 fix
+ * remove unneeded client code related to iOS
+ * added test for `engine.io-client` `0.6.1`
+
+0.6.1 / 2013-06-06
+==================
+
+ * package: bumped `engine.io-client` to `0.6.1`
+
+0.6.0 / 2013-05-31
+==================
+
+ * socket: clear timer after sending one noop packet (fixes #174)
+ * clear all timers on socket close
+ * sending error on transport creation upon a bad request
+ * added test for client-side buffer cleanup
+ * changed flushComplete to flush
+ * ended support for node 0.6
+
+0.5.0 / 2013-03-16
+==================
+
+ * polling: implemented new parser
+ * test writeBuffer isn't cleared onError, removed 'closing' check in .flush()
+ * fixed bug89 and added tests: writeBuffer not flushed until nextTick
+
+0.4.3 / 2013-02-08
+==================
+
+ * package: bumped `engine.io-client` to `0.4.3`
+
+0.4.2 / 2013-02-08
+==================
+
+ * Only end upgrade socket connections if unhandled
+ * Fix websocket dependency
+ * Close socket if upgrade is received and socket.readyState != open
+
+0.4.1 / 2013-01-18
+==================
+
+ * package: bumped versions
+ * Fixed bugs in previous send callback fix and updated test cases
+ * Added a test case which makes the code before the send callback fix fail
+ * socket: emit `data` event (synonym with `message`)
+ * socket: added `Socket#write`
+ * engine.io: cleanup
+ * engine.io: deprecated `resource`
+ * `npm docs engine.io` works now
+
+0.3.10 / 2012-12-03
+===================
+
+ * package: bumped `engine.io-client` with `close` fixes
+ * add packetCreate event [jxck]
+ * add packet event to socket [jxck]
+ * transport: remove `Connection` headers and let node handle it
+ * server: send validation failure reason to clients
+ * engine: invoking as a function causes attach
+ * socket: reset `writeBuffer` before send
+
+0.3.9 / 2012-10-23
+==================
+
+ * package: bumped `engine.io-client`
+
+0.3.8 / 2012-10-23
+==================
+
+ * package: bumped engine.io-client
+ * examples: added first example
+
+0.3.7 / 2012-10-21
+==================
+
+ * package: bumped `engine.io-client`
+
+0.3.6 / 2012-10-21
+==================
+
+ [skipped]
+
+0.3.5 / 2012-10-14
+==================
+
+ * package: reverted last commit - we use the parser from the client
+
+0.3.4 / 2012-10-14
+==================
+
+ * package: `engine.io-client` moved to `devDependencies`
+ * socket: added missing jsdoc
+
+0.3.3 / 2012-10-10
+==================
+
+ * socket: fixed check interval clearing [joewalnes]
+ * transports: improved instrumentation
+
+0.3.2 / 2012-10-08
+==================
+
+ * socket: improve check interval for upgrade
+
+0.3.1 / 2012-10-08
+==================
+
+ * socket: faster upgrades (we perform a check immediately)
+ * server: don't assume sid is numeric
+
+0.3.0 / 2012-10-04
+==================
+
+ * socket: `writeBuffer` now gets sliced, and is recoverable after `close` [afshinm]
+ * server: expect ping from client and send interval with handshake [cadorn]
+ * polling-jsonp: prevent client breakage with utf8 whitespace
+ * socket: fix `flush` and `drain` events
+ * socket: add `send` callback [afshinm]
+ * transport: avoid unhandled error events for stale transports
+ * README: documentation improvements [EugenDueck]
+
+0.2.2 / 2012-08-26
+==================
+
+ * server: remove buffering for flash policy requests
+ * transport: avoid unhandled error events for stale transports (fixes #69)
+ * readme: documented `toString` behavior on `send` [EugenDueck]
+
+0.2.1 / 2012-08-13
+==================
+
+ * polling-xhr: skip Keep-Alive when it's implied [EugenDueck]
+ * polling-jsonp: skip Keep-Alive when it's implied [EugenDueck]
+ * README: added plugins list with engine.io-conflation
+ * socket: added flush/drain events (fixes #56)
+ * server: avoid passing websocket to non-websocket transports (fixes #24)
+
+0.2.0 / 2012-08-06
+==================
+
+ * Bumped client
+ * test: added closing connection test
+ * server: implemented stronger id generator with collision detection
+
+0.1.2 / 2012-08-02
+==================
+
+ * Fixed a jsonp bug in Nokia mobile phones and potentially other UAs.
+
+0.1.1 / 2012-08-01
+==================
+
+ * Fixed errors when a socket is closed while upgrade probe is happening.
+ * Improved WS error handling
+ * Replaced websocket.io with ws, now that it supports older drafts
+ * README fixes
+
+0.1.0 / 2012-07-03
+==================
+
+ * Initial release.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5046ced
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+
+TESTS = test/*.js
+BENCHMARKS = $(shell find bench -type f ! -name 'runner.js')
+REPORTER = dot
+
+test:
+ @./node_modules/.bin/mocha \
+ --reporter $(REPORTER) \
+ --slow 500ms \
+ --bail \
+ --globals ___eio,document \
+ $(TESTS)
+
+test-cov: lib-cov
+ EIO_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html
+
+lib-cov:
+ jscoverage --no-highlight lib lib-cov
+
+bench:
+ @node $(PROFILEFLAGS) bench/runner.js $(BENCHMARKS)
+
+.PHONY: test test-cov bench
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ad198a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,517 @@
+
+# Engine.IO: the realtime engine
+
+[![Build Status](https://secure.travis-ci.org/Automattic/engine.io.png)](http://travis-ci.org/Automattic/engine.io)
+[![NPM version](https://badge.fury.io/js/engine.io.png)](http://badge.fury.io/js/engine.io)
+
+`Engine.IO` is the implementation of transport-based
+cross-browser/cross-device bi-directional communication layer for
+[Socket.IO](http://github.com/learnboost/socket.io).
+
+## How to use
+
+### Server
+
+#### (A) Listening on a port
+
+```js
+var engine = require('engine.io');
+var server = engine.listen(80);
+
+server.on('connection', function(socket){
+ socket.send('utf 8 string');
+ socket.send(new Buffer([0, 1, 2, 3, 4, 5])); // binary data
+});
+```
+
+#### (B) Intercepting requests for a http.Server
+
+```js
+var engine = require('engine.io');
+var http = require('http').createServer().listen(3000);
+var server = engine.attach(http);
+
+server.on('connection', function (socket) {
+ socket.on('message', function(data){ });
+ socket.on('close', function(){ });
+});
+```
+
+#### (C) Passing in requests
+
+```js
+var engine = require('engine.io');
+var server = new engine.Server();
+
+server.on('connection', function(socket){
+ socket.send('hi');
+});
+
+// …
+httpServer.on('upgrade', function(req, socket, head){
+ server.handleUpgrade(req, socket, head);
+});
+httpServer.on('request', function(req, res){
+ server.handleRequest(req, res);
+});
+```
+
+### Client
+
+```html
+<script src="/path/to/engine.io.js"></script>
+<script>
+ var socket = new eio.Socket('ws://localhost/');
+ socket.on('open', function(){
+ socket.on('message', function(data){});
+ socket.on('close', function(){});
+ });
+</script>
+```
+
+For more information on the client refer to the
+[engine-client](http://github.com/learnboost/engine.io-client) repository.
+
+## What features does it have?
+
+- **Maximum reliability**. Connections are established even in the presence of:
+ - proxies and load balancers.
+ - personal firewall and antivirus software.
+ - for more information refer to **Goals** and **Architecture** sections
+- **Minimal client size** aided by:
+ - lazy loading of flash transports.
+ - lack of redundant transports.
+- **Scalable**
+ - load balancer friendly
+- **Future proof**
+- **100% Node.JS core style**
+ - No API sugar (left for higher level projects)
+ - Written in readable vanilla JavaScript
+
+## API
+
+### Server
+
+<hr><br>
+
+#### Top-level
+
+These are exposed by `require('engine.io')`:
+
+##### Events
+
+- `flush`
+ - Called when a socket buffer is being flushed.
+ - **Arguments**
+ - `Socket`: socket being flushed
+ - `Array`: write buffer
+- `drain`
+ - Called when a socket buffer is drained
+ - **Arguments**
+ - `Socket`: socket being flushed
+
+##### Properties
+
+- `protocol` _(Number)_: protocol revision number
+- `Server`: Server class constructor
+- `Socket`: Socket class constructor
+- `Transport` _(Function)_: transport constructor
+- `transports` _(Object)_: map of available transports
+
+##### Methods
+
+- `()`
+ - Returns a new `Server` instance. If the first argument is an `http.Server` then the
+ new `Server` instance will be attached to it. Otherwise, the arguments are passed
+ directly to the `Server` constructor.
+ - **Parameters**
+ - `http.Server`: optional, server to attach to.
+ - `Object`: optional, options object (see `Server#constructor` api docs below)
+
+ The following are identical ways to instantiate a server and then attach it.
+ ```js
+ var httpServer; // previously created with `http.createServer();` from node.js api.
+
+ // create a server first, and then attach
+ var eioServer = require('engine.io').Server();
+ eioServer.attach(httpServer);
+
+ // or call the module as a function to get `Server`
+ var eioServer = require('engine.io')();
+ eioServer.attach(httpServer);
+
+ // immediately attach
+ var eioServer = require('engine.io')(httpServer);
+ ```
+
+- `listen`
+ - Creates an `http.Server` which listens on the given port and attaches WS
+ to it. It returns `501 Not Implemented` for regular http requests.
+ - **Parameters**
+ - `Number`: port to listen on.
+ - `Object`: optional, options object
+ - `Function`: callback for `listen`.
+ - **Options**
+ - All options from `Server.attach` method, documented below.
+ - **Additionally** See Server `constructor` below for options you can pass for creating the new Server
+ - **Returns** `Server`
+- `attach`
+ - Captures `upgrade` requests for a `http.Server`. In other words, makes
+ a regular http.Server WebSocket-compatible.
+ - **Parameters**
+ - `http.Server`: server to attach to.
+ - `Object`: optional, options object
+ - **Options**
+ - All options from `Server.attach` method, documented below.
+ - **Additionally** See Server `constructor` below for options you can pass for creating the new Server
+ - **Returns** `Server` a new Server instance.
+
+<hr><br>
+
+#### Server
+
+The main server/manager. _Inherits from EventEmitter_.
+
+##### Events
+
+- `connection`
+ - Fired when a new connection is established.
+ - **Arguments**
+ - `Socket`: a Socket object
+
+##### Properties
+
+**Important**: if you plan to use Engine.IO in a scalable way, please
+keep in mind the properties below will only reflect the clients connected
+to a single process.
+
+- `clients` _(Object)_: hash of connected clients by id.
+- `clientsCount` _(Number)_: number of connected clients.
+
+##### Methods
+
+- **constructor**
+ - Initializes the server
+ - **Parameters**
+ - `Object`: optional, options object
+ - **Options**
+ - `pingTimeout` (`Number`): how many ms without a pong packet to
+ consider the connection closed (`60000`)
+ - `pingInterval` (`Number`): how many ms before sending a new ping
+ packet (`25000`)
+ - `maxHttpBufferSize` (`Number`): how many bytes or characters a message
+ can be when polling, before closing the session (to avoid DoS). Default
+ value is `10E7`.
+ - `allowRequest` (`Function`): A function that receives a given handshake
+ or upgrade request as its first parameter, and can decide whether to
+ continue or not. The second argument is a function that needs to be
+ called with the decided information: `fn(err, success)`, where
+ `success` is a boolean value where false means that the request is
+ rejected, and err is an error code.
+ - `transports` (`<Array> String`): transports to allow connections
+ to (`['polling', 'websocket']`)
+ - `allowUpgrades` (`Boolean`): whether to allow transport upgrades
+ (`true`)
+ - `cookie` (`String|Boolean`): name of the HTTP cookie that
+ contains the client sid to send as part of handshake response
+ headers. Set to `false` to not send one. (`io`)
+- `close`
+ - Closes all clients
+ - **Returns** `Server` for chaining
+- `handleRequest`
+ - Called internally when a `Engine` request is intercepted.
+ - **Parameters**
+ - `http.ServerRequest`: a node request object
+ - `http.ServerResponse`: a node response object
+ - **Returns** `Server` for chaining
+- `handleUpgrade`
+ - Called internally when a `Engine` ws upgrade is intercepted.
+ - **Parameters** (same as `upgrade` event)
+ - `http.ServerRequest`: a node request object
+ - `net.Stream`: TCP socket for the request
+ - `Buffer`: legacy tail bytes
+ - **Returns** `Server` for chaining
+- `attach`
+ - Attach this Server instance to an `http.Server`
+ - Captures `upgrade` requests for a `http.Server`. In other words, makes
+ a regular http.Server WebSocket-compatible.
+ - **Parameters**
+ - `http.Server`: server to attach to.
+ - `Object`: optional, options object
+ - **Options**
+ - `path` (`String`): name of the path to capture (`/engine.io`).
+ - `destroyUpgrade` (`Boolean`): destroy unhandled upgrade requests (`true`)
+ - `destroyUpgradeTimeout` (`Number`): milliseconds after which unhandled requests are ended (`1000`)
+
+<hr><br>
+
+#### Socket
+
+A representation of a client. _Inherits from EventEmitter_.
+
+##### Events
+
+- `close`
+ - Fired when the client is disconnected.
+ - **Arguments**
+ - `String`: reason for closing
+ - `Object`: description object (optional)
+- `message`
+ - Fired when the client sends a message.
+ - **Arguments**
+ - `String` or `Buffer`: Unicode string or Buffer with binary contents
+- `error`
+ - Fired when an error occurs.
+ - **Arguments**
+ - `Error`: error object
+- `flush`
+ - Called when the write buffer is being flushed.
+ - **Arguments**
+ - `Array`: write buffer
+- `drain`
+ - Called when the write buffer is drained
+- `packet`
+ - Called when a socket received a packet (`message`, `ping`)
+ - **Arguments**
+ - `type`: packet type
+ - `data`: packet data (if type is message)
+- `packetCreate`
+ - Called before a socket sends a packet (`message`, `pong`)
+ - **Arguments**
+ - `type`: packet type
+ - `data`: packet data (if type is message)
+
+##### Properties
+
+- `id` _(String)_: unique identifier
+- `server` _(Server)_: engine parent reference
+- `request` _(http.ServerRequest)_: request that originated the Socket
+- `upgraded` _(Boolean)_: whether the transport has been upgraded
+- `readyState` _(String)_: opening|open|closing|closed
+- `transport` _(Transport)_: transport reference
+
+##### Methods
+
+- `send`:
+ - Sends a message, performing `message = toString(arguments[0])` unless
+ sending binary data, which is sent as is.
+ - **Parameters**
+ - `String` | `Buffer` | `ArrayBuffer` | `ArrayBufferView`: a string or any object implementing `toString()`, with outgoing data, or a Buffer or ArrayBuffer with binary data. Also any ArrayBufferView can be sent as is.
+ - `Function`: optional, a callback executed when the message gets flushed out by the transport
+ - **Returns** `Socket` for chaining
+- `close`
+ - Disconnects the client
+ - **Returns** `Socket` for chaining
+
+### Client
+
+<hr><br>
+
+Exposed in the `eio` global namespace (in the browser), or by
+`require('engine.io-client')` (in Node.JS).
+
+For the client API refer to the
+[engine-client](http://github.com/learnboost/engine.io-client) repository.
+
+## Debug / logging
+
+Engine.IO is powered by [debug](http://github.com/visionmedia/debug).
+In order to see all the debug output, run your app with the environment variable
+`DEBUG` including the desired scope.
+
+To see the output from all of Engine.IO's debugging scopes you can use:
+
+```
+DEBUG=engine* node myapp
+```
+
+## Transports
+
+- `polling`: XHR / JSONP polling transport.
+- `websocket`: WebSocket transport.
+
+## Plugins
+
+- [engine.io-conflation](https://github.com/EugenDueck/engine.io-conflation): Makes **conflation and aggregation** of messages straightforward.
+
+## Support
+
+The support channels for `engine.io` are the same as `socket.io`:
+ - irc.freenode.net **#socket.io**
+ - [Google Groups](http://groups.google.com/group/socket_io)
+ - [Website](http://socket.io)
+
+## Development
+
+To contribute patches, run tests or benchmarks, make sure to clone the
+repository:
+
+```
+git clone git://github.com/LearnBoost/engine.io.git
+```
+
+Then:
+
+```
+cd engine.io
+npm install
+```
+
+## Tests
+
+Tests run with `make test`. It runs the server tests that are aided by
+the usage of `engine.io-client`.
+
+Make sure `npm install` is run first.
+
+## Goals
+
+The main goal of `Engine` is ensuring the most reliable realtime communication.
+Unlike the previous Socket.IO core, it always establishes a long-polling
+connection first, then tries to upgrade to better transports that are "tested" on
+the side.
+
+During the lifetime of the Socket.IO projects, we've found countless drawbacks
+to relying on `HTML5 WebSocket` or `Flash Socket` as the first connection
+mechanisms.
+
+Both are clearly the _right way_ of establishing a bidirectional communication,
+with HTML5 WebSocket being the way of the future. However, to answer most business
+needs, alternative traditional HTTP 1.1 mechanisms are just as good as delivering
+the same solution.
+
+WebSocket based connections have two fundamental benefits:
+
+1. **Better server performance**
+ - _A: Load balancers_<br>
+ Load balancing a long polling connection poses a serious architectural nightmare
+ since requests can come from any number of open sockets by the user agent, but
+ they all need to be routed to the process and computer that owns the `Engine`
+ connection. This negatively impacts RAM and CPU usage.
+ - _B: Network traffic_<br>
+ WebSocket is designed around the premise that each message frame has to be
+ surrounded by the least amount of data. In HTTP 1.1 transports, each message
+ frame is surrounded by HTTP headers and chunked encoding frames. If you try to
+ send the message _"Hello world"_ with xhr-polling, the message ultimately
+ becomes larger than if you were to send it with WebSocket.
+ - _C: Lightweight parser_<br>
+ As an effect of **B**, the server has to do a lot more work to parse the network
+ data and figure out the message when traditional HTTP requests are used
+ (as in long polling). This means that another advantage of WebSocket is
+ less server CPU usage.
+
+2. **Better user experience**
+
+ Due to the reasons stated in point **1**, the most important effect of being able
+ to establish a WebSocket connection is raw data transfer speed, which translates
+ in _some_ cases in better user experience.
+
+ Applications with heavy realtime interaction (such as games) will benefit greatly,
+ whereas applications like realtime chat (Gmail/Facebook), newsfeeds (Facebook) or
+ timelines (Twitter) will have negligible user experience improvements.
+
+Having said this, attempting to establish a WebSocket connection directly so far has
+proven problematic:
+
+1. **Proxies**<br>
+ Many corporate proxies block WebSocket traffic.
+
+2. **Personal firewall and antivirus software**<br>
+ As a result of our research, we've found that at least 3 personal security
+ applications block WebSocket traffic.
+
+3. **Cloud application platforms**<br>
+ Platforms like Heroku or No.de have had trouble keeping up with the fast-paced
+ nature of the evolution of the WebSocket protocol. Applications therefore end up
+ inevitably using long polling, but the seamless installation experience of
+ Socket.IO we strive for (_"require() it and it just works"_) disappears.
+
+Some of these problems have solutions. In the case of proxies and personal programs,
+however, the solutions many times involve upgrading software. Experience has shown
+that relying on client software upgrades to deliver a business solution is
+fruitless: the very existence of this project has to do with a fragmented panorama
+of user agent distribution, with clients connecting with latest versions of the most
+modern user agents (Chrome, Firefox and Safari), but others with versions as low as
+IE 5.5.
+
+From the user perspective, an unsuccessful WebSocket connection can translate in
+up to at least 10 seconds of waiting for the realtime application to begin
+exchanging data. This **perceptively** hurts user experience.
+
+To summarize, **Engine** focuses on reliability and user experience first, marginal
+potential UX improvements and increased server performance second. `Engine` is the
+result of all the lessons learned with WebSocket in the wild.
+
+## Architecture
+
+The main premise of `Engine`, and the core of its existence, is the ability to
+swap transports on the fly. A connection starts as xhr-polling, but it can
+switch to WebSocket.
+
+The central problem this poses is: how do we switch transports without losing
+messages?
+
+`Engine` only switches from polling to another transport in between polling
+cycles. Since the server closes the connection after a certain timeout when
+there's no activity, and the polling transport implementation buffers messages
+in between connections, this ensures no message loss and optimal performance.
+
+Another benefit of this design is that we workaround almost all the limitations
+of **Flash Socket**, such as slow connection times, increased file size (we can
+safely lazy load it without hurting user experience), etc.
+
+## FAQ
+
+### Can I use engine without Socket.IO ?
+
+Absolutely. Although the recommended framework for building realtime applications
+is Socket.IO, since it provides fundamental features for real-world applications
+such as multiplexing, reconnection support, etc.
+
+`Engine` is to Socket.IO what Connect is to Express. An essential piece for building
+realtime frameworks, but something you _probably_ won't be using for building
+actual applications.
+
+### Does the server serve the client?
+
+No. The main reason is that `Engine` is meant to be bundled with frameworks.
+Socket.IO includes `Engine`, therefore serving two clients is not necessary. If
+you use Socket.IO, including
+
+```html
+<script src="/socket.io/socket.io.js">
+```
+
+has you covered.
+
+### Can I implement `Engine` in other languages?
+
+Absolutely. The [engine.io-protocol](https://github.com/LearnBoost/engine.io-protocol)
+repository contains the most up to date description of the specification
+at all times, and the parser implementation in JavaScript.
+
+## License
+
+(The MIT License)
+
+Copyright (c) 2014 Guillermo Rauch <guillermo at learnboost.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/examples/latency/README.md b/examples/latency/README.md
new file mode 100644
index 0000000..d59a71f
--- /dev/null
+++ b/examples/latency/README.md
@@ -0,0 +1,18 @@
+
+# eio-latency
+
+## Running
+
+First, execute:
+
+```
+$ npm install
+```
+
+Then execute the server:
+
+```
+$ node index
+```
+
+And point your browser to `localhost:3000` (if PORT is set in your environment, then it will use that instead).
diff --git a/examples/latency/index.html b/examples/latency/index.html
new file mode 100644
index 0000000..a89d95d
--- /dev/null
+++ b/examples/latency/index.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+ <head>
+ <title>EIO Latency</title>
+ <link rel="stylesheet" href="/style.css" />
+ </head>
+ <body>
+ <h1>EIO Latency <span id="latency"></span></h1>
+ <h2 id="transport">(connecting)</h2>
+ <canvas id="chart" height="200"></canvas>
+
+ <script src="/index.js"></script>
+ </body>
+</html>
diff --git a/examples/latency/index.js b/examples/latency/index.js
new file mode 100644
index 0000000..bcaae71
--- /dev/null
+++ b/examples/latency/index.js
@@ -0,0 +1,30 @@
+
+/**
+ * Module dependencies.
+ */
+
+var express = require('express')
+ , app = express()
+ , server = require('http').createServer(app)
+ , enchilada = require('enchilada')
+ , io = require('engine.io').attach(server);
+
+app.use(enchilada({
+ src: __dirname + '/public',
+ debug: true
+}));
+app.use(express.static(__dirname + '/public'));
+app.get('/', function(req, res, next){
+ res.sendfile('index.html');
+});
+
+io.on('connection', function(socket){
+ socket.on('message', function(v){
+ socket.send('pong');
+ });
+});
+
+var port = process.env.PORT || 3000;
+server.listen(port, function(){
+ console.log('\033[96mlistening on localhost:' + port + ' \033[39m');
+});
diff --git a/examples/latency/package.json b/examples/latency/package.json
new file mode 100644
index 0000000..948b7fc
--- /dev/null
+++ b/examples/latency/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "eio-latency",
+ "version": "0.1.0",
+ "dependencies": {
+ "express": "3.3.5",
+ "enchilada": "0.7.1",
+ "engine.io": "1.2.2",
+ "engine.io-client": "1.2.2",
+ "smoothie": "1.19.0"
+ }
+}
diff --git a/examples/latency/public/index.js b/examples/latency/public/index.js
new file mode 100644
index 0000000..5251218
--- /dev/null
+++ b/examples/latency/public/index.js
@@ -0,0 +1,57 @@
+
+/**
+ * Module dependencies.
+ */
+
+var SmoothieChart = require("smoothie").SmoothieChart
+ , TimeSeries = require("smoothie").TimeSeries
+ , eio = require("engine.io-client");
+
+
+// helper
+
+function $(id){ return document.getElementById(id); }
+
+// chart
+
+var smoothie;
+var time;
+
+function render(){
+ if (smoothie) smoothie.stop();
+ $('chart').width = document.body.clientWidth;
+ smoothie = new SmoothieChart();
+ smoothie.streamTo($('chart'), 1000);
+ time = new TimeSeries();
+ smoothie.addTimeSeries(time, {
+ strokeStyle: 'rgb(255, 0, 0)',
+ fillStyle: 'rgba(255, 0, 0, 0.4)',
+ lineWidth: 2
+ });
+}
+
+// socket
+var socket = new eio.Socket();
+var last;
+function send(){
+ last = new Date;
+ socket.send('ping');
+ $('transport').innerHTML = socket.transport.name;
+}
+socket.on('open', function(){
+ if ($('chart').getContext) {
+ render();
+ window.onresize = render;
+ }
+ send();
+});
+socket.on('close', function(){
+ if (smoothie) smoothie.stop();
+ $('transport').innerHTML = '(disconnected)';
+});
+socket.on('message', function(){
+ var latency = new Date - last;
+ $('latency').innerHTML = latency + 'ms';
+ if (time) time.append(+new Date, latency);
+ setTimeout(send, 100);
+});
diff --git a/examples/latency/public/style.css b/examples/latency/public/style.css
new file mode 100644
index 0000000..ab61214
--- /dev/null
+++ b/examples/latency/public/style.css
@@ -0,0 +1,5 @@
+
+body { margin: 0; padding: 0; font-family: Helvetica Neue; }
+h1 { margin: 100px 100px 10px; }
+h2 { color: #999; margin: 0 100px 30px; font-weight: normal; }
+#latency { color: red; }
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..ebb4e10
--- /dev/null
+++ b/index.js
@@ -0,0 +1,4 @@
+
+module.exports = process.env.EIO_COV
+ ? require('./lib-cov/engine.io')
+ : require('./lib/engine.io');
diff --git a/lib/engine.io.js b/lib/engine.io.js
new file mode 100644
index 0000000..2d1cf9e
--- /dev/null
+++ b/lib/engine.io.js
@@ -0,0 +1,126 @@
+/**
+ * Module dependencies.
+ */
+
+var http = require('http');
+
+/**
+ * Invoking the library as a function delegates to attach if the first argument
+ * is an `http.Server`.
+ *
+ * If there are no arguments or the first argument is an options object, then
+ * a new Server instance is returned.
+ *
+ * @param {http.Server} server (if specified, will be attached to by the new Server instance)
+ * @param {Object} options
+ * @return {Server} engine server
+ * @api public
+ */
+
+exports = module.exports = function() {
+ // backwards compatible use as `.attach`
+ // if first argument is an http server
+ if (arguments.length && arguments[0] instanceof http.Server) {
+ return attach.apply(this, arguments);
+ }
+
+ // if first argument is not an http server, then just make a regular eio server
+ return exports.Server.apply(null, arguments);
+};
+
+/**
+ * Protocol revision number.
+ *
+ * @api public
+ */
+
+exports.protocol = 1;
+
+/**
+ * Expose Server constructor.
+ *
+ * @api public
+ */
+
+exports.Server = require('./server');
+
+/**
+ * Expose Server constructor.
+ *
+ * @api public
+ */
+
+exports.Socket = require('./socket');
+
+/**
+ * Expose Transport constructor.
+ *
+ * @api public
+ */
+
+exports.Transport = require('./transport');
+
+/**
+ * Expose mutable list of available transports.
+ *
+ * @api public
+ */
+
+exports.transports = require('./transports');
+
+/**
+ * Exports parser.
+ *
+ * @api public
+ */
+
+exports.parser = require('engine.io-parser');
+
+/**
+ * Creates an http.Server exclusively used for WS upgrades.
+ *
+ * @param {Number} port
+ * @param {Function} callback
+ * @param {Object} options
+ * @return {Server} websocket.io server
+ * @api public
+ */
+
+exports.listen = listen;
+
+function listen(port, options, fn) {
+ if ('function' == typeof options) {
+ fn = options;
+ options = {};
+ }
+
+ var server = http.createServer(function (req, res) {
+ res.writeHead(501);
+ res.end('Not Implemented');
+ });
+
+ server.listen(port, fn);
+
+ // create engine server
+ var engine = exports.attach(server, options);
+ engine.httpServer = server;
+
+ return engine;
+};
+
+/**
+ * Captures upgrade requests for a http.Server.
+ *
+ * @param {http.Server} server
+ * @param {Object} options
+ * @return {Server} engine server
+ * @api public
+ */
+
+exports.attach = attach;
+
+function attach(server, options) {
+ var engine = new exports.Server(options);
+ engine.attach(server, options);
+ return engine;
+};
diff --git a/lib/server.js b/lib/server.js
new file mode 100644
index 0000000..b2a0b7f
--- /dev/null
+++ b/lib/server.js
@@ -0,0 +1,391 @@
+
+/**
+ * Module dependencies.
+ */
+
+var qs = require('querystring')
+ , parse = require('url').parse
+ , readFileSync = require('fs').readFileSync
+ , crypto = require('crypto')
+ , base64id = require('base64id')
+ , transports = require('./transports')
+ , EventEmitter = require('events').EventEmitter
+ , Socket = require('./socket')
+ , WebSocketServer = require('ws').Server
+ , debug = require('debug')('engine');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Server;
+
+/**
+ * Server constructor.
+ *
+ * @param {Object} options
+ * @api public
+ */
+
+function Server(opts){
+ if (!(this instanceof Server)) {
+ return new Server(opts);
+ }
+
+ this.clients = {};
+ this.clientsCount = 0;
+
+ opts = opts || {};
+ this.pingTimeout = opts.pingTimeout || 60000;
+ this.pingInterval = opts.pingInterval || 25000;
+ this.upgradeTimeout = opts.upgradeTimeout || 10000;
+ this.maxHttpBufferSize = opts.maxHttpBufferSize || 10E7;
+ this.transports = opts.transports || Object.keys(transports);
+ this.allowUpgrades = false !== opts.allowUpgrades;
+ this.allowRequest = opts.allowRequest;
+ this.cookie = false !== opts.cookie ? (opts.cookie || 'io') : false;
+
+ // initialize websocket server
+ if (~this.transports.indexOf('websocket')) {
+ this.ws = new WebSocketServer({ noServer: true, clientTracking: false });
+ }
+}
+
+/**
+ * Protocol errors mappings.
+ */
+
+Server.errors = {
+ UNKNOWN_TRANSPORT: 0,
+ UNKNOWN_SID: 1,
+ BAD_HANDSHAKE_METHOD: 2,
+ BAD_REQUEST: 3
+};
+
+Server.errorMessages = {
+ 0: 'Transport unknown',
+ 1: 'Session ID unknown',
+ 2: 'Bad handshake method',
+ 3: 'Bad request'
+};
+
+/**
+ * Inherits from EventEmitter.
+ */
+
+Server.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Hash of open clients.
+ *
+ * @api public
+ */
+
+Server.prototype.clients;
+
+/**
+ * Returns a list of available transports for upgrade given a certain transport.
+ *
+ * @return {Array}
+ * @api public
+ */
+
+Server.prototype.upgrades = function(transport){
+ if (!this.allowUpgrades) return [];
+ return transports[transport].upgradesTo || [];
+};
+
+/**
+ * Verifies a request.
+ *
+ * @param {http.ServerRequest}
+ * @return {Boolean} whether the request is valid
+ * @api private
+ */
+
+Server.prototype.verify = function(req, upgrade, fn){
+ // transport check
+ var transport = req._query.transport;
+ if (!~this.transports.indexOf(transport)) {
+ debug('unknown transport "%s"', transport);
+ return fn(Server.errors.UNKNOWN_TRANSPORT, false);
+ }
+
+ // sid check
+ var sid = req._query.sid;
+ if (sid) {
+ if (!this.clients.hasOwnProperty(sid))
+ return fn(Server.errors.UNKNOWN_SID, false);
+ if (!upgrade && this.clients[sid].transport.name !== transport) {
+ debug('bad request: unexpected transport without upgrade');
+ return fn(Server.errors.BAD_REQUEST, false);
+ }
+ } else {
+ // handshake is GET only
+ if ('GET' != req.method) return fn(Server.errors.BAD_HANDSHAKE_METHOD, false);
+ if (!this.allowRequest) return fn(null, true);
+ return this.allowRequest(req, fn);
+ }
+
+ fn(null, true);
+};
+
+/**
+ * Prepares a request by processing the query string.
+ *
+ * @api private
+ */
+
+Server.prototype.prepare = function(req){
+ // try to leverage pre-existing `req._query` (e.g: from connect)
+ if (!req._query) {
+ req._query = ~req.url.indexOf('?') ? qs.parse(parse(req.url).query) : {};
+ }
+};
+
+/**
+ * Closes all clients.
+ *
+ * @api public
+ */
+
+Server.prototype.close = function(){
+ debug('closing all open clients');
+ for (var i in this.clients) {
+ this.clients[i].close();
+ }
+ return this;
+};
+
+/**
+ * Handles an Engine.IO HTTP request.
+ *
+ * @param {http.ServerRequest} request
+ * @param {http.ServerResponse|http.OutgoingMessage} response
+ * @api public
+ */
+
+Server.prototype.handleRequest = function(req, res){
+ debug('handling "%s" http request "%s"', req.method, req.url);
+ this.prepare(req);
+ req.res = res;
+
+ var self = this;
+ this.verify(req, false, function(err, success) {
+ if (!success) {
+ sendErrorMessage(req, res, err);
+ return;
+ }
+
+ if (req._query.sid) {
+ debug('setting new request for existing client');
+ self.clients[req._query.sid].transport.onRequest(req);
+ } else {
+ self.handshake(req._query.transport, req);
+ }
+ });
+};
+
+/**
+ * Sends an Engine.IO Error Message
+ *
+ * @param {http.ServerResponse} response
+ * @param {code} error code
+ * @api private
+ */
+
+ function sendErrorMessage(req, res, code) {
+ var headers = { 'Content-Type': 'application/json' };
+
+ if (req.headers.origin) {
+ headers['Access-Control-Allow-Credentials'] = 'true';
+ headers['Access-Control-Allow-Origin'] = req.headers.origin;
+ } else {
+ headers['Access-Control-Allow-Origin'] = '*';
+ }
+ res.writeHead(400, headers);
+ res.end(JSON.stringify({
+ code: code,
+ message: Server.errorMessages[code]
+ }));
+ }
+
+/**
+ * Handshakes a new client.
+ *
+ * @param {String} transport name
+ * @param {Object} request object
+ * @api private
+ */
+
+Server.prototype.handshake = function(transport, req){
+ var id = base64id.generateId();
+
+ debug('handshaking client "%s"', id);
+
+ var transportName = transport;
+ try {
+ var transport = new transports[transport](req);
+ if ('polling' == transportName) {
+ transport.maxHttpBufferSize = this.maxHttpBufferSize;
+ }
+
+ if (req._query && req._query.b64) {
+ transport.supportsBinary = false;
+ } else {
+ transport.supportsBinary = true;
+ }
+ }
+ catch (e) {
+ sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST);
+ return;
+ }
+ var socket = new Socket(id, this, transport, req);
+ var self = this;
+
+ if (false !== this.cookie) {
+ transport.on('headers', function(headers){
+ headers['Set-Cookie'] = self.cookie + '=' + id;
+ });
+ }
+
+ transport.onRequest(req);
+
+ this.clients[id] = socket;
+ this.clientsCount++;
+
+ socket.once('close', function(){
+ delete self.clients[id];
+ self.clientsCount--;
+ });
+
+ this.emit('connection', socket);
+};
+
+/**
+ * Handles an Engine.IO HTTP Upgrade.
+ *
+ * @api public
+ */
+
+Server.prototype.handleUpgrade = function(req, socket, upgradeHead){
+ this.prepare(req);
+
+ var self = this;
+ this.verify(req, true, function(err, success) {
+ if (!success) {
+ socket.end();
+ return;
+ }
+
+ var head = new Buffer(upgradeHead.length);
+ upgradeHead.copy(head);
+ upgradeHead = null;
+
+ // delegate to ws
+ self.ws.handleUpgrade(req, socket, head, function(conn){
+ self.onWebSocket(req, conn);
+ });
+ });
+};
+
+/**
+ * Called upon a ws.io connection.
+ *
+ * @param {ws.Socket} websocket
+ * @api private
+ */
+
+Server.prototype.onWebSocket = function(req, socket){
+ if (!transports[req._query.transport].prototype.handlesUpgrades) {
+ debug('transport doesnt handle upgraded requests');
+ socket.close();
+ return;
+ }
+
+ // get client id
+ var id = req._query.sid;
+
+ // keep a reference to the ws.Socket
+ req.websocket = socket;
+
+ if (id) {
+ if (!this.clients[id]) {
+ debug('upgrade attempt for closed client');
+ socket.close();
+ } else if (this.clients[id].upgraded) {
+ debug('transport had already been upgraded');
+ socket.close();
+ } else {
+ debug('upgrading existing transport');
+ var transport = new transports[req._query.transport](req);
+ if (req._query && req._query.b64) {
+ transport.supportsBinary = false;
+ } else {
+ transport.supportsBinary = true;
+ }
+ this.clients[id].maybeUpgrade(transport);
+ }
+ } else {
+ this.handshake(req._query.transport, req);
+ }
+};
+
+/**
+ * Captures upgrade requests for a http.Server.
+ *
+ * @param {http.Server} server
+ * @param {Object} options
+ * @api public
+ */
+
+Server.prototype.attach = function(server, options){
+ var self = this;
+ var options = options || {};
+ var path = (options.path || '/engine.io').replace(/\/$/, '');
+
+ var destroyUpgrade = (options.destroyUpgrade !== undefined) ? options.destroyUpgrade : true;
+ var destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000;
+
+ // normalize path
+ path += '/';
+
+ function check (req) {
+ return path == req.url.substr(0, path.length);
+ }
+
+ // cache and clean up listeners
+ var listeners = server.listeners('request').slice(0);
+ server.removeAllListeners('request');
+ server.on('close', self.close.bind(self));
+
+ // add request handler
+ server.on('request', function(req, res){
+ if (check(req)) {
+ debug('intercepting request for path "%s"', path);
+ self.handleRequest(req, res);
+ } else {
+ for (var i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].call(server, req, res);
+ }
+ }
+ });
+
+ if(~self.transports.indexOf('websocket')) {
+ server.on('upgrade', function (req, socket, head) {
+ if (check(req)) {
+ self.handleUpgrade(req, socket, head);
+ } else if (false !== options.destroyUpgrade) {
+ // default node behavior is to disconnect when no handlers
+ // but by adding a handler, we prevent that
+ // and if no eio thing handles the upgrade
+ // then the socket needs to die!
+ setTimeout(function() {
+ if (socket.writable && socket.bytesWritten <= 0) {
+ return socket.end();
+ }
+ }, options.destroyUpgradeTimeout);
+ }
+ });
+ }
+};
diff --git a/lib/socket.js b/lib/socket.js
new file mode 100644
index 0000000..08b2b8b
--- /dev/null
+++ b/lib/socket.js
@@ -0,0 +1,393 @@
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('events').EventEmitter;
+var debug = require('debug')('engine:socket');
+
+/**
+ * Module exports.
+ */
+
+module.exports = Socket;
+
+/**
+ * Client class (abstract).
+ *
+ * @api private
+ */
+
+function Socket (id, server, transport, req) {
+ this.id = id;
+ this.server = server;
+ this.upgraded = false;
+ this.readyState = 'opening';
+ this.writeBuffer = [];
+ this.packetsFn = [];
+ this.sentCallbackFn = [];
+ this.request = req;
+
+ // Cache IP since it might not be in the req later
+ this.remoteAddress = req.connection.remoteAddress;
+
+ this.checkIntervalTimer = null;
+ this.upgradeTimeoutTimer = null;
+ this.pingTimeoutTimer = null;
+
+ this.setTransport(transport);
+ this.onOpen();
+}
+
+/**
+ * Inherits from EventEmitter.
+ */
+
+Socket.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Called upon transport considered open.
+ *
+ * @api private
+ */
+
+Socket.prototype.onOpen = function () {
+ this.readyState = 'open';
+
+ // sends an `open` packet
+ this.transport.sid = this.id;
+ this.sendPacket('open', JSON.stringify({
+ sid: this.id
+ , upgrades: this.getAvailableUpgrades()
+ , pingInterval: this.server.pingInterval
+ , pingTimeout: this.server.pingTimeout
+ }));
+
+ this.emit('open');
+ this.setPingTimeout();
+};
+
+/**
+ * Called upon transport packet.
+ *
+ * @param {Object} packet
+ * @api private
+ */
+
+Socket.prototype.onPacket = function (packet) {
+ if ('open' == this.readyState) {
+ // export packet event
+ debug('packet');
+ this.emit('packet', packet);
+
+ // Reset ping timeout on any packet, incoming data is a good sign of
+ // other side's liveness
+ this.setPingTimeout();
+
+ switch (packet.type) {
+
+ case 'ping':
+ debug('got ping');
+ this.sendPacket('pong');
+ this.emit('heartbeat');
+ break;
+
+ case 'error':
+ this.onClose('parse error');
+ break;
+
+ case 'message':
+ this.emit('data', packet.data);
+ this.emit('message', packet.data);
+ break;
+ }
+ } else {
+ debug('packet received with closed socket');
+ }
+};
+
+/**
+ * Called upon transport error.
+ *
+ * @param {Error} error object
+ * @api private
+ */
+
+Socket.prototype.onError = function (err) {
+ debug('transport error');
+ this.onClose('transport error', err);
+};
+
+/**
+ * Sets and resets ping timeout timer based on client pings.
+ *
+ * @api private
+ */
+
+Socket.prototype.setPingTimeout = function () {
+ var self = this;
+ clearTimeout(self.pingTimeoutTimer);
+ self.pingTimeoutTimer = setTimeout(function () {
+ self.onClose('ping timeout');
+ }, self.server.pingInterval + self.server.pingTimeout);
+};
+
+/**
+ * Attaches handlers for the given transport.
+ *
+ * @param {Transport} transport
+ * @api private
+ */
+
+Socket.prototype.setTransport = function (transport) {
+ this.transport = transport;
+ this.transport.once('error', this.onError.bind(this));
+ this.transport.on('packet', this.onPacket.bind(this));
+ this.transport.on('drain', this.flush.bind(this));
+ this.transport.once('close', this.onClose.bind(this, 'transport close'));
+ //this function will manage packet events (also message callbacks)
+ this.setupSendCallback();
+};
+
+/**
+ * Upgrades socket to the given transport
+ *
+ * @param {Transport} transport
+ * @api private
+ */
+
+Socket.prototype.maybeUpgrade = function (transport) {
+ debug('might upgrade socket transport from "%s" to "%s"'
+ , this.transport.name, transport.name);
+
+ var self = this;
+
+ // set transport upgrade timer
+ self.upgradeTimeoutTimer = setTimeout(function () {
+ debug('client did not complete upgrade - closing transport');
+ clearInterval(self.checkIntervalTimer);
+ self.checkIntervalTimer = null;
+ if ('open' == transport.readyState) {
+ transport.close();
+ }
+ }, this.server.upgradeTimeout);
+
+ function onPacket(packet){
+ if ('ping' == packet.type && 'probe' == packet.data) {
+ transport.send([{ type: 'pong', data: 'probe' }]);
+ clearInterval(self.checkIntervalTimer);
+ self.checkIntervalTimer = setInterval(check, 100);
+ } else if ('upgrade' == packet.type && self.readyState != 'closed') {
+ debug('got upgrade packet - upgrading');
+ self.upgraded = true;
+ self.clearTransport();
+ self.setTransport(transport);
+ self.emit('upgrade', transport);
+ self.setPingTimeout();
+ self.flush();
+ clearInterval(self.checkIntervalTimer);
+ self.checkIntervalTimer = null;
+ clearTimeout(self.upgradeTimeoutTimer);
+ transport.removeListener('packet', onPacket);
+ if (self.readyState == 'closing') {
+ transport.close(function () {
+ self.onClose('forced close');
+ });
+ }
+ } else {
+ transport.close();
+ }
+ }
+
+ // we force a polling cycle to ensure a fast upgrade
+ function check(){
+ if ('polling' == self.transport.name && self.transport.writable) {
+ debug('writing a noop packet to polling for fast upgrade');
+ self.transport.send([{ type: 'noop' }]);
+ }
+ }
+
+ transport.on('packet', onPacket);
+};
+
+/**
+ * Clears listeners and timers associated with current transport.
+ *
+ * @api private
+ */
+
+Socket.prototype.clearTransport = function () {
+ // silence further transport errors and prevent uncaught exceptions
+ this.transport.on('error', function(){
+ debug('error triggered by discarded transport');
+ });
+ clearTimeout(this.pingTimeoutTimer);
+};
+
+/**
+ * Called upon transport considered closed.
+ * Possible reasons: `ping timeout`, `client error`, `parse error`,
+ * `transport error`, `server close`, `transport close`
+ */
+
+Socket.prototype.onClose = function (reason, description) {
+ if ('closed' != this.readyState) {
+ clearTimeout(this.pingTimeoutTimer);
+ clearInterval(this.checkIntervalTimer);
+ this.checkIntervalTimer = null;
+ clearTimeout(this.upgradeTimeoutTimer);
+ var self = this;
+ // clean writeBuffer in next tick, so developers can still
+ // grab the writeBuffer on 'close' event
+ process.nextTick(function() {
+ self.writeBuffer = [];
+ });
+ this.packetsFn = [];
+ this.sentCallbackFn = [];
+ this.clearTransport();
+ this.readyState = 'closed';
+ this.emit('close', reason, description);
+ }
+};
+
+/**
+ * Setup and manage send callback
+ *
+ * @api private
+ */
+
+Socket.prototype.setupSendCallback = function () {
+ var self = this;
+ //the message was sent successfully, execute the callback
+ this.transport.on('drain', function() {
+ if (self.sentCallbackFn.length > 0) {
+ var seqFn = self.sentCallbackFn.splice(0,1)[0];
+ if ('function' == typeof seqFn) {
+ debug('executing send callback');
+ seqFn(self.transport);
+ } else if (Array.isArray(seqFn)) {
+ debug('executing batch send callback');
+ for (var l = seqFn.length, i = 0; i < l; i++) {
+ if ('function' == typeof seqFn[i]) {
+ seqFn[i](self.transport);
+ }
+ }
+ }
+ }
+ });
+};
+
+/**
+ * Sends a message packet.
+ *
+ * @param {String} message
+ * @param {Function} callback
+ * @return {Socket} for chaining
+ * @api public
+ */
+
+Socket.prototype.send =
+Socket.prototype.write = function(data, callback){
+ this.sendPacket('message', data, callback);
+ return this;
+};
+
+/**
+ * Sends a packet.
+ *
+ * @param {String} packet type
+ * @param {String} optional, data
+ * @api private
+ */
+
+Socket.prototype.sendPacket = function (type, data, callback) {
+ if ('closing' != this.readyState) {
+ debug('sending packet "%s" (%s)', type, data);
+
+ var packet = { type: type };
+ if (data) packet.data = data;
+
+ // exports packetCreate event
+ this.emit('packetCreate', packet);
+
+ this.writeBuffer.push(packet);
+
+ //add send callback to object
+ this.packetsFn.push(callback);
+
+ this.flush();
+ }
+};
+
+/**
+ * Attempts to flush the packets buffer.
+ *
+ * @api private
+ */
+
+Socket.prototype.flush = function () {
+ if ('closed' != this.readyState && this.transport.writable
+ && this.writeBuffer.length) {
+ debug('flushing buffer to transport');
+ this.emit('flush', this.writeBuffer);
+ this.server.emit('flush', this, this.writeBuffer);
+ var wbuf = this.writeBuffer;
+ this.writeBuffer = [];
+ if (!this.transport.supportsFraming) {
+ this.sentCallbackFn.push(this.packetsFn);
+ } else {
+ this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn);
+ }
+ this.packetsFn = [];
+ this.transport.send(wbuf);
+ this.emit('drain');
+ this.server.emit('drain', this);
+ }
+};
+
+/**
+ * Get available upgrades for this socket.
+ *
+ * @api private
+ */
+
+Socket.prototype.getAvailableUpgrades = function () {
+ var availableUpgrades = [];
+ var allUpgrades = this.server.upgrades(this.transport.name);
+ for (var i = 0, l = allUpgrades.length; i < l; ++i) {
+ var upg = allUpgrades[i];
+ if (this.server.transports.indexOf(upg) != -1) {
+ availableUpgrades.push(upg);
+ }
+ }
+ return availableUpgrades;
+};
+
+/**
+ * Closes the socket and underlying transport.
+ *
+ * @return {Socket} for chaining
+ * @api public
+ */
+
+Socket.prototype.close = function () {
+ if ('open' != this.readyState) return;
+
+ this.readyState = 'closing';
+
+ if (this.writeBuffer.length) {
+ this.once('drain', this.closeTransport.bind(this));
+ return;
+ }
+
+ this.closeTransport();
+};
+
+/**
+ * Closes the underlying transport.
+ *
+ * @api private
+ */
+
+Socket.prototype.closeTransport = function () {
+ this.transport.close(this.onClose.bind(this, 'forced close'));
+};
diff --git a/lib/transport.js b/lib/transport.js
new file mode 100644
index 0000000..993896b
--- /dev/null
+++ b/lib/transport.js
@@ -0,0 +1,114 @@
+
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('events').EventEmitter
+ , parser = require('engine.io-parser')
+ , debug = require('debug')('engine:transport');
+
+/**
+ * Expose the constructor.
+ */
+
+module.exports = Transport;
+
+/**
+ * Noop function.
+ *
+ * @api private
+ */
+
+function noop () {};
+
+/**
+ * Transport constructor.
+ *
+ * @param {http.ServerRequest} request
+ * @api public
+ */
+
+function Transport (req) {
+ this.readyState = 'opening';
+};
+
+/**
+ * Inherits from EventEmitter.
+ */
+
+Transport.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Called with an incoming HTTP request.
+ *
+ * @param {http.ServerRequest} request
+ * @api private
+ */
+
+Transport.prototype.onRequest = function (req) {
+ debug('setting request');
+ this.req = req;
+};
+
+/**
+ * Closes the transport.
+ *
+ * @api private
+ */
+
+Transport.prototype.close = function (fn) {
+ this.readyState = 'closing';
+ this.doClose(fn || noop);
+};
+
+/**
+ * Called with a transport error.
+ *
+ * @param {String} message error
+ * @param {Object} error description
+ * @api private
+ */
+
+Transport.prototype.onError = function (msg, desc) {
+ if (this.listeners('error').length) {
+ var err = new Error(msg);
+ err.type = 'TransportError';
+ err.description = desc;
+ this.emit('error', err);
+ } else {
+ debug('ignored transport error %s (%s)', msg, desc);
+ }
+};
+
+/**
+ * Called with parsed out a packets from the data stream.
+ *
+ * @param {Object} packet
+ * @api private
+ */
+
+Transport.prototype.onPacket = function (packet) {
+ this.emit('packet', packet);
+};
+
+/**
+ * Called with the encoded packet data.
+ *
+ * @param {String} data
+ * @api private
+ */
+
+Transport.prototype.onData = function (data) {
+ this.onPacket(parser.decodePacket(data));
+};
+
+/**
+ * Called upon transport close.
+ *
+ * @api private
+ */
+
+Transport.prototype.onClose = function () {
+ this.readyState = 'closed';
+ this.emit('close');
+};
diff --git a/lib/transports/index.js b/lib/transports/index.js
new file mode 100644
index 0000000..ecedb58
--- /dev/null
+++ b/lib/transports/index.js
@@ -0,0 +1,36 @@
+
+/**
+ * Module dependencies.
+ */
+
+var XHR = require('./polling-xhr');
+var JSONP = require('./polling-jsonp');
+
+/**
+ * Export transports.
+ */
+
+module.exports = exports = {
+ polling: polling,
+ websocket: require('./websocket')
+};
+
+/**
+ * Export upgrades map.
+ */
+
+exports.polling.upgradesTo = ['websocket'];
+
+/**
+ * Polling polimorphic constructor.
+ *
+ * @api private
+ */
+
+function polling (req) {
+ if ('string' == typeof req._query.j) {
+ return new JSONP(req);
+ } else {
+ return new XHR(req);
+ }
+}
diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js
new file mode 100644
index 0000000..e9e416b
--- /dev/null
+++ b/lib/transports/polling-jsonp.js
@@ -0,0 +1,108 @@
+
+/**
+ * Module dependencies.
+ */
+
+var Polling = require('./polling');
+var qs = require('querystring');
+var rDoubleSlashes = /\\\\n/g;
+var rSlashes = /(\\)?\\n/g;
+
+/**
+ * Module exports.
+ */
+
+module.exports = JSONP;
+
+/**
+ * JSON-P polling transport.
+ *
+ * @api public
+ */
+
+function JSONP (req) {
+ Polling.call(this, req);
+
+ this.head = '___eio[' + (req._query.j || '').replace(/[^0-9]/g, '') + '](';
+ this.foot = ');';
+};
+
+/**
+ * Inherits from Polling.
+ */
+
+JSONP.prototype.__proto__ = Polling.prototype;
+
+/**
+ * Handles incoming data.
+ * Due to a bug in \n handling by browsers, we expect a escaped string.
+ *
+ * @api private
+ */
+
+JSONP.prototype.onData = function (data) {
+ // we leverage the qs module so that we get built-in DoS protection
+ // and the fast alternative to decodeURIComponent
+ data = qs.parse(data).d;
+ if ('string' == typeof data) {
+ //client will send already escaped newlines as \\\\n and newlines as \\n
+ // \\n must be replaced with \n and \\\\n with \\n
+ data = data.replace(rSlashes, function(match, slashes) {
+ return slashes ? match : '\n';
+ });
+ Polling.prototype.onData.call(this, data.replace(rDoubleSlashes, '\\n'));
+ }
+};
+
+/**
+ * Performs the write.
+ *
+ * @api private
+ */
+
+JSONP.prototype.doWrite = function (data) {
+ // we must output valid javascript, not valid json
+ // see: http://timelessrepo.com/json-isnt-a-javascript-subset
+ var js = JSON.stringify(data)
+ .replace(/\u2028/g, '\\u2028')
+ .replace(/\u2029/g, '\\u2029');
+
+ // prepare response
+ data = this.head + js + this.foot;
+
+ // explicit UTF-8 is required for pages not served under utf
+ var headers = {
+ 'Content-Type': 'text/javascript; charset=UTF-8',
+ 'Content-Length': Buffer.byteLength(data)
+ };
+
+ // prevent XSS warnings on IE
+ // https://github.com/LearnBoost/socket.io/pull/1333
+ var ua = this.req.headers['user-agent'];
+ if (ua && (~ua.indexOf(';MSIE') || ~ua.indexOf('Trident/'))) {
+ headers['X-XSS-Protection'] = '0';
+ }
+
+ this.res.writeHead(200, this.headers(this.req, headers));
+ this.res.end(data);
+};
+
+/**
+ * Returns headers for a response.
+ *
+ * @param {http.ServerRequest} request
+ * @param {Object} extra headers
+ * @api private
+ */
+
+JSONP.prototype.headers = function (req, headers) {
+ headers = headers || {};
+
+ // disable XSS protection for IE
+ if (/MSIE 8\.0/.test(req.headers['user-agent'])) {
+ headers['X-XSS-Protection'] = '0';
+ }
+
+ this.emit('headers', headers);
+ return headers;
+};
diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js
new file mode 100644
index 0000000..2a8293c
--- /dev/null
+++ b/lib/transports/polling-xhr.js
@@ -0,0 +1,101 @@
+
+/**
+ * Module dependencies.
+ */
+
+var Polling = require('./polling');
+var Transport = require('../transport');
+var debug = require('debug')('engine:polling-xhr');
+
+/**
+ * Module exports.
+ */
+
+module.exports = XHR;
+
+/**
+ * Ajax polling transport.
+ *
+ * @api public
+ */
+
+function XHR(req){
+ Polling.call(this, req);
+}
+
+/**
+ * Inherits from Polling.
+ */
+
+XHR.prototype.__proto__ = Polling.prototype;
+
+/**
+ * Overrides `onRequest` to handle `OPTIONS`..
+ *
+ * @param {http.ServerRequest}
+ * @api private
+ */
+
+XHR.prototype.onRequest = function (req) {
+ if ('OPTIONS' == req.method) {
+ var res = req.res;
+ var headers = this.headers(req);
+ headers['Access-Control-Allow-Headers'] = 'Content-Type';
+ res.writeHead(200, headers);
+ res.end();
+ } else {
+ Polling.prototype.onRequest.call(this, req);
+ }
+};
+
+/**
+ * Frames data prior to write.
+ *
+ * @api private
+ */
+
+XHR.prototype.doWrite = function(data){
+ // explicit UTF-8 is required for pages not served under utf
+ var isString = typeof data == 'string';
+ var contentType = isString
+ ? 'text/plain; charset=UTF-8'
+ : 'application/octet-stream';
+ var contentLength = '' + (isString ? Buffer.byteLength(data) : data.length);
+
+ var headers = {
+ 'Content-Type': contentType,
+ 'Content-Length': contentLength
+ };
+
+ // prevent XSS warnings on IE
+ // https://github.com/LearnBoost/socket.io/pull/1333
+ var ua = this.req.headers['user-agent'];
+ if (ua && (~ua.indexOf(';MSIE') || ~ua.indexOf('Trident/'))) {
+ headers['X-XSS-Protection'] = '0';
+ }
+
+ this.res.writeHead(200, this.headers(this.req, headers));
+ this.res.end(data);
+};
+
+/**
+ * Returns headers for a response.
+ *
+ * @param {http.ServerRequest} request
+ * @param {Object} extra headers
+ * @api private
+ */
+
+XHR.prototype.headers = function(req, headers){
+ headers = headers || {};
+
+ if (req.headers.origin) {
+ headers['Access-Control-Allow-Credentials'] = 'true';
+ headers['Access-Control-Allow-Origin'] = req.headers.origin;
+ } else {
+ headers['Access-Control-Allow-Origin'] = '*';
+ }
+
+ this.emit('headers', headers);
+ return headers;
+};
diff --git a/lib/transports/polling.js b/lib/transports/polling.js
new file mode 100644
index 0000000..4a3a8aa
--- /dev/null
+++ b/lib/transports/polling.js
@@ -0,0 +1,279 @@
+
+/**
+ * Module requirements.
+ */
+
+var Transport = require('../transport')
+ , parser = require('engine.io-parser')
+ , debug = require('debug')('engine:polling');
+
+/**
+ * Exports the constructor.
+ */
+
+module.exports = Polling;
+
+/**
+ * HTTP polling constructor.
+ *
+ * @api public.
+ */
+
+function Polling (req) {
+ Transport.call(this, req);
+}
+
+/**
+ * Inherits from Transport.
+ *
+ * @api public.
+ */
+
+Polling.prototype.__proto__ = Transport.prototype;
+
+/**
+ * Transport name
+ *
+ * @api public
+ */
+
+Polling.prototype.name = 'polling';
+
+/**
+ * Overrides onRequest.
+ *
+ * @param {http.ServerRequest}
+ * @api private
+ */
+
+Polling.prototype.onRequest = function (req) {
+ var res = req.res;
+
+ if ('GET' == req.method) {
+ this.onPollRequest(req, res);
+ } else if ('POST' == req.method) {
+ this.onDataRequest(req, res);
+ } else {
+ res.writeHead(500);
+ res.end();
+ }
+};
+
+/**
+ * The client sends a request awaiting for us to send data.
+ *
+ * @api private
+ */
+
+Polling.prototype.onPollRequest = function (req, res) {
+ if (this.req) {
+ debug('request overlap');
+ // assert: this.res, '.req and .res should be (un)set together'
+ this.onError('overlap from client');
+ res.writeHead(500);
+ return;
+ }
+
+ debug('setting request');
+
+ this.req = req;
+ this.res = res;
+
+ var self = this;
+
+ function onClose () {
+ self.onError('poll connection closed prematurely');
+ }
+
+ function cleanup () {
+ req.removeListener('close', onClose);
+ self.req = self.res = null;
+ }
+
+ req.cleanup = cleanup;
+ req.on('close', onClose);
+
+ this.writable = true;
+ this.emit('drain');
+
+ // if we're still writable but had a pending close, trigger an empty send
+ if (this.writable && this.shouldClose) {
+ debug('triggering empty send to append close packet');
+ this.send([{ type: 'noop' }]);
+ }
+};
+
+/**
+ * The client sends a request with data.
+ *
+ * @api private
+ */
+
+Polling.prototype.onDataRequest = function (req, res) {
+ if (this.dataReq) {
+ // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together'
+ this.onError('data request overlap from client');
+ res.writeHead(500);
+ return;
+ }
+
+ var isBinary = 'application/octet-stream' == req.headers['content-type'];
+
+ this.dataReq = req;
+ this.dataRes = res;
+
+ var chunks = isBinary ? new Buffer(0) : '';
+ var self = this;
+
+ function cleanup () {
+ chunks = isBinary ? new Buffer(0) : '';
+ req.removeListener('data', onData);
+ req.removeListener('end', onEnd);
+ req.removeListener('close', onClose);
+ self.dataReq = self.dataRes = null;
+ }
+
+ function onClose () {
+ cleanup();
+ self.onError('data request connection closed prematurely');
+ }
+
+ function onData (data) {
+ var contentLength;
+ if (typeof data == 'string') {
+ chunks += data;
+ contentLength = Buffer.byteLength(chunks);
+ } else {
+ chunks = Buffer.concat([chunks, data]);
+ contentLength = chunks.length;
+ }
+
+ if (contentLength > self.maxHttpBufferSize) {
+ chunks = '';
+ req.connection.destroy();
+ }
+ }
+
+ function onEnd () {
+ self.onData(chunks);
+
+ var headers = {
+ // text/html is required instead of text/plain to avoid an
+ // unwanted download dialog on certain user-agents (GH-43)
+ 'Content-Type': 'text/html',
+ 'Content-Length': 2
+ };
+
+ // prevent XSS warnings on IE
+ // https://github.com/LearnBoost/socket.io/pull/1333
+ var ua = req.headers['user-agent'];
+ if (ua && (~ua.indexOf(';MSIE') || ~ua.indexOf('Trident/'))) {
+ headers['X-XSS-Protection'] = '0';
+ }
+
+ res.writeHead(200, self.headers(req, headers));
+ res.end('ok');
+ cleanup();
+ }
+
+ req.on('close', onClose);
+ req.on('data', onData);
+ req.on('end', onEnd);
+ if (!isBinary) req.setEncoding('utf8');
+};
+
+/**
+ * Processes the incoming data payload.
+ *
+ * @param {String} encoded payload
+ * @api private
+ */
+
+Polling.prototype.onData = function (data) {
+ debug('received "%s"', data);
+ var self = this;
+ var callback = function(packet) {
+ if ('close' == packet.type) {
+ debug('got xhr close packet');
+ self.onClose();
+ return false;
+ }
+
+ self.onPacket(packet);
+ };
+
+ parser.decodePayload(data, callback);
+};
+
+/**
+ * Overrides onClose.
+ *
+ * @api private
+ */
+
+Polling.prototype.onClose = function () {
+ if (this.writable) {
+ // close pending poll request
+ this.send([{ type: 'noop' }]);
+ }
+ Transport.prototype.onClose.call(this);
+};
+
+/**
+ * Writes a packet payload.
+ *
+ * @param {Object} packet
+ * @api private
+ */
+
+Polling.prototype.send = function (packets) {
+ if (this.shouldClose) {
+ debug('appending close packet to payload');
+ packets.push({ type: 'close' });
+ this.shouldClose();
+ this.shouldClose = null;
+ }
+
+ var self = this;
+ parser.encodePayload(packets, this.supportsBinary, function(data) {
+ self.write(data);
+ });
+};
+
+/**
+ * Writes data as response to poll request.
+ *
+ * @param {String} data
+ * @api private
+ */
+
+Polling.prototype.write = function (data) {
+ debug('writing "%s"', data);
+ this.doWrite(data);
+ this.req.cleanup();
+ this.writable = false;
+};
+
+/**
+ * Closes the transport.
+ *
+ * @api private
+ */
+
+Polling.prototype.doClose = function (fn) {
+ debug('closing');
+
+ if (this.dataReq) {
+ debug('aborting ongoing data request');
+ this.dataReq.destroy();
+ }
+
+ if (this.writable) {
+ debug('transport writable - closing right away');
+ this.send([{ type: 'close' }]);
+ fn();
+ } else {
+ debug('transport not writable - buffering orderly close');
+ this.shouldClose = fn;
+ }
+};
diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js
new file mode 100644
index 0000000..59b8eb7
--- /dev/null
+++ b/lib/transports/websocket.js
@@ -0,0 +1,110 @@
+
+/**
+ * Module dependencies.
+ */
+
+var Transport = require('../transport')
+ , parser = require('engine.io-parser')
+ , debug = require('debug')('engine:ws')
+
+/**
+ * Export the constructor.
+ */
+
+module.exports = WebSocket;
+
+/**
+ * WebSocket transport
+ *
+ * @param {http.ServerRequest}
+ * @api public
+ */
+
+function WebSocket (req) {
+ Transport.call(this, req);
+ var self = this;
+ this.socket = req.websocket;
+ this.socket.on('message', this.onData.bind(this));
+ this.socket.once('close', this.onClose.bind(this));
+ this.socket.on('error', this.onError.bind(this));
+ this.socket.on('headers', function (headers) {
+ self.emit('headers', headers);
+ });
+ this.writable = true;
+};
+
+/**
+ * Inherits from Transport.
+ */
+
+WebSocket.prototype.__proto__ = Transport.prototype;
+
+/**
+ * Transport name
+ *
+ * @api public
+ */
+
+WebSocket.prototype.name = 'websocket';
+
+/**
+ * Advertise upgrade support.
+ *
+ * @api public
+ */
+
+WebSocket.prototype.handlesUpgrades = true;
+
+/**
+ * Advertise framing support.
+ *
+ * @api public
+ */
+
+WebSocket.prototype.supportsFraming = true;
+
+/**
+ * Processes the incoming data.
+ *
+ * @param {String} encoded packet
+ * @api private
+ */
+
+WebSocket.prototype.onData = function (data) {
+ debug('received "%s"', data);
+ Transport.prototype.onData.call(this, data);
+};
+
+/**
+ * Writes a packet payload.
+ *
+ * @param {Array} packets
+ * @api private
+ */
+
+WebSocket.prototype.send = function (packets) {
+ var self = this;
+ for (var i = 0, l = packets.length; i < l; i++) {
+ parser.encodePacket(packets[i], this.supportsBinary, function(data) {
+ debug('writing "%s"', data);
+ self.writable = false;
+ self.socket.send(data, function (err){
+ if (err) return self.onError('write error', err.stack);
+ self.writable = true;
+ self.emit('drain');
+ });
+ });
+ }
+};
+
+/**
+ * Closes the transport.
+ *
+ * @api private
+ */
+
+WebSocket.prototype.doClose = function (fn) {
+ debug('closing');
+ this.socket.close();
+ fn && fn();
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4920772
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "engine.io",
+ "version": "1.5.1",
+ "description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server",
+ "main": "./lib/engine.io",
+ "author": "Guillermo Rauch <guillermo at learnboost.com>",
+ "homepage": "https://github.com/LearnBoost/engine.io",
+ "contributors": [
+ { "name": "Eugen Dueck", "web": "https://github.com/EugenDueck" },
+ { "name": "Afshin Mehrabani", "web": "https://github.com/afshinm" },
+ { "name": "Christoph Dorn", "web": "https://github.com/cadorn" },
+ { "name": "Mark Mokryn", "email": "mokesmokes at gmail.com"}
+ ],
+ "dependencies": {
+ "debug": "1.0.3",
+ "ws": "0.5.0",
+ "engine.io-parser": "1.2.1",
+ "base64id": "0.1.0"
+ },
+ "devDependencies": {
+ "mocha": "1.12.0",
+ "expect.js": "0.2.0",
+ "superagent": "0.15.4",
+ "engine.io-client": "1.5.1",
+ "s": "0.1.1"
+ },
+ "scripts": { "test" : "make test" },
+ "repository": {
+ "type": "git",
+ "url": "git at github.com:Automattic/engine.io.git"
+ }
+}
diff --git a/test/common.js b/test/common.js
new file mode 100644
index 0000000..a754306
--- /dev/null
+++ b/test/common.js
@@ -0,0 +1,29 @@
+
+/**
+ * Module dependencies.
+ */
+
+var eio = require('..');
+
+/**
+ * Listen shortcut that fires a callback on an epheemal port.
+ */
+
+exports.listen = function (opts, fn) {
+ if ('function' == typeof opts) {
+ fn = opts;
+ opts = {};
+ }
+
+ var e = eio.listen(null, opts, function () {
+ fn(e.httpServer.address().port);
+ });
+
+ return e;
+};
+
+/**
+ * Sprintf util.
+ */
+
+require('s').extend();
diff --git a/test/engine.io.js b/test/engine.io.js
new file mode 100644
index 0000000..01e2eb8
--- /dev/null
+++ b/test/engine.io.js
@@ -0,0 +1,230 @@
+
+/**
+ * Test dependencies.
+ */
+
+var net = require('net');
+var eio = require('..');
+var listen = require('./common').listen;
+var expect = require('expect.js');
+var request = require('superagent');
+var http = require('http');
+
+/**
+ * Tests.
+ */
+
+describe('engine', function () {
+
+ it('should expose protocol number', function () {
+ expect(eio.protocol).to.be.a('number');
+ });
+
+ it('should be the same version as client', function(){
+ expect(eio.protocol).to.be.a('number');
+ var version = require('../package').version;
+ expect(version).to.be(require('engine.io-client/package').version);
+ });
+
+ describe('engine()', function () {
+ it('should create a Server when require called with no arguments', function () {
+ var engine = eio();
+ expect(engine).to.be.an(eio.Server);
+ });
+ });
+
+ describe('listen', function () {
+ it('should open a http server that returns 501', function (done) {
+ var server = listen(function (port) {
+ request.get('http://localhost:%d/'.s(port), function (res) {
+ expect(res.status).to.be(501);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('attach()', function () {
+ it('should work from require()', function () {
+ var server = http.createServer();
+ var engine = eio(server);
+
+ expect(engine).to.be.an(eio.Server);
+ });
+
+ it('should return an engine.Server', function () {
+ var server = http.createServer()
+ , engine = eio.attach(server);
+
+ expect(engine).to.be.an(eio.Server);
+ });
+
+ it('should attach engine to an http server', function (done) {
+ var server = http.createServer()
+ , engine = eio.attach(server);
+
+ server.listen(function () {
+ var uri = 'http://localhost:%d/engine.io/default/'.s(server.address().port);
+ request.get(uri, function (res) {
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(0);
+ expect(res.body.message).to.be('Transport unknown');
+ server.once('close', done);
+ server.close();
+ });
+ });
+ });
+
+ it('should destroy upgrades not handled by engine', function (done) {
+ var server = http.createServer()
+ , engine = eio.attach(server);
+
+ server.listen(function () {
+ var client = net.createConnection(server.address().port);
+ client.setEncoding('ascii');
+ client.write([
+ 'GET / HTTP/1.1'
+ , 'Upgrade: IRC/6.9'
+ , '', ''
+ ].join('\r\n'));
+
+ var check = setTimeout(function () {
+ done(new Error('Client should have ended'));
+ }, 20);
+
+ client.on('end', function () {
+ clearTimeout(check);
+ done();
+ });
+ });
+ });
+
+ it('should not destroy unhandled upgrades with destroyUpgrade:false', function (done) {
+ var server = http.createServer()
+ , engine = eio.attach(server, { destroyUpgrade: false, destroyUpgradeTimeout: 50 });
+
+ server.listen(function () {
+ var client = net.createConnection(server.address().port);
+ client.on('connect', function () {
+ client.setEncoding('ascii');
+ client.write([
+ 'GET / HTTP/1.1'
+ , 'Upgrade: IRC/6.9'
+ , '', ''
+ ].join('\r\n'));
+
+ var check = setTimeout(function () {
+ client.removeListener('end', onEnd);
+ done();
+ }, 100);
+
+ function onEnd () {
+ done(new Error('Client should not end'));
+ }
+
+ client.on('end', onEnd);
+ });
+ });
+ });
+
+ it('should destroy unhandled upgrades with after a timeout', function (done) {
+ var server = http.createServer()
+ , engine = eio.attach(server, { destroyUpgradeTimeout: 200 });
+
+ server.listen(function () {
+ var client = net.createConnection(server.address().port);
+ client.on('connect', function () {
+ client.setEncoding('ascii');
+ client.write([
+ 'GET / HTTP/1.1'
+ , 'Upgrade: IRC/6.9'
+ , '', ''
+ ].join('\r\n'));
+
+ // send from client to server
+ // tests that socket is still alive
+ // this will not keep the socket open as the server does not handle it
+ setTimeout(function() {
+ client.write('foo');
+ }, 100);
+
+ function onEnd () {
+ done();
+ }
+
+ client.on('end', onEnd);
+ });
+ });
+ });
+
+ it('should not destroy handled upgrades with after a timeout', function (done) {
+ var server = http.createServer()
+ , engine = eio.attach(server, { destroyUpgradeTimeout: 100 });
+
+ // write to the socket to keep engine.io from closing it by writing before the timeout
+ server.on('upgrade', function(req, socket) {
+ socket.write('foo');
+ socket.on('data', function(chunk) {
+ expect(chunk.toString()).to.be('foo');
+ socket.end();
+ });
+ });
+
+ server.listen(function () {
+ var client = net.createConnection(server.address().port);
+
+ client.on('connect', function () {
+ client.setEncoding('ascii');
+ client.write([
+ 'GET / HTTP/1.1'
+ , 'Upgrade: IRC/6.9'
+ , '', ''
+ ].join('\r\n'));
+
+ // test that socket is still open by writing after the timeout period
+ setTimeout(function() {
+ client.write('foo');
+ }, 200);
+
+ client.on('data', function (data) {
+ });
+
+ client.on('end', done);
+ });
+ });
+ });
+
+ it('should preserve original request listeners', function (done) {
+ var listeners = 0
+ , server = http.createServer(function (req, res) {
+ expect(req && res).to.be.ok();
+ listeners++;
+ });
+
+ server.on('request', function (req, res) {
+ expect(req && res).to.be.ok();
+ res.writeHead(200);
+ res.end('');
+ listeners++;
+ });
+
+ eio.attach(server);
+
+ server.listen(function () {
+ var port = server.address().port;
+ request.get('http://localhost:%d/engine.io/default/'.s(port), function (res) {
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(0);
+ expect(res.body.message).to.be('Transport unknown');
+ request.get('http://localhost:%d/test'.s(port), function (res) {
+ expect(res.status).to.be(200);
+ expect(listeners).to.eql(2);
+ server.once('close', done);
+ server.close();
+ });
+ });
+ });
+ });
+ });
+
+});
diff --git a/test/fixtures/ca.crt b/test/fixtures/ca.crt
new file mode 100644
index 0000000..974a2a8
--- /dev/null
+++ b/test/fixtures/ca.crt
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/test/fixtures/ca.key b/test/fixtures/ca.key
new file mode 100644
index 0000000..4568779
--- /dev/null
+++ b/test/fixtures/ca.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/ca.key.org b/test/fixtures/ca.key.org
new file mode 100644
index 0000000..185be8d
--- /dev/null
+++ b/test/fixtures/ca.key.org
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,47A0291BAB679B36
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/client.crt b/test/fixtures/client.crt
new file mode 100644
index 0000000..4c652a2
--- /dev/null
+++ b/test/fixtures/client.crt
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxjCCAa4CAQEwDQYJKoZIhvcNAQEFBQAwaTELMAkGA1UEBhMCRkkxEzARBgNV
+BAgTClNvbWUtU3RhdGUxETAPBgNVBAcTCEhlbHNpbmtpMSEwHwYDVQQKExhJbnRl
+cm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBkZvbyBDQTAeFw0xNDEwMTEy
+MjQ3NDBaFw0xNTEwMTEyMjQ3NDBaMG0xCzAJBgNVBAYTAkZJMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMREwDwYDVQQHEwhIZWxzaW5raTEhMB8GA1UEChMYSW50ZXJuZXQg
+V2lkZ2l0cyBQdHkgTHRkMRMwEQYDVQQDEwpGb28gQ2xpZW50MIGfMA0GCSqGSIb3
+DQEBAQUAA4GNADCBiQKBgQD66Rn8P8O+MK13sPxIIEMHXDRZheRLqGNlNsXzaBLW
+nKSlV+Wxi1OdCimtAh4ZAVRt1JkK9mQEAGdxC8TRwDMS02+EUK1H1zvh77Ek4ZcH
+W8p5CVEm53FTmO+jhL+7BQYXW1yi/XURBv2xm3Q95I7895agprMFI8HiOu/Hdi/i
+DQIDAQABMA0GCSqGSIb3DQEBBQUAA4ICAQCQJeIJFmt0fV+eeNGWcgeNJ8EqahDU
+5qn37Jew9W+t8qSsBkizPvnc/SEQDeFNnm8QAPMA1ffpYcdMllFs3eUIZ0bRZPxw
+SacGUmGRA2VAM8tcH3sXiqpwMyYt2r2Ccg52lsnPvnkytJZaMUi64sYj57ECbIj4
+lPgCNLbIgaR9EP4Pep8QZxdjrtBhG2XwKZjAMn4taJQdz797sjSW+Zo243BvKoqn
+ZaBNnvcN/TL5kyZd9WK02BeO+ChadKeIQbnBIYLNJ40EHb5YCKk/hU1x1uVyyXcd
+mtONDXjwJn4jBMo8fdHtq3iashhiGJGLJT5qQ3iM90fg6JJDthjMMFL/YKIzt1+q
+ORyU2jMcYH83sZ3aEHxiaKhXb/Sr55iMUfIEK+Xn16WXX0YsLoq2UJ0BDRrWqiyO
+QfQt6k8V4KDtoTDEmPw0/8oGLJv2/nzQyL5e+MAiORobuzFP+xIynWclNtRw2zCM
+DT5cqkwzBqCwVBNghehH0im1dBnQiQz56+TSx2X01aY2In29ZR887858EmtQP9gK
+y3lAboC2/HO7eQtTUlos9b4m/trst+eC6iewz45VbIOrB31PAN5DBiSdrr/ZMxom
+hyBdkMUD2a/BG+zctDRqpN/d6udiT9E74lGmYIfVNa5Yu5hb8zs5mnbZ05Xi4kBm
+EGC15ScHiTib5w==
+-----END CERTIFICATE-----
diff --git a/test/fixtures/client.csr b/test/fixtures/client.csr
new file mode 100644
index 0000000..ca1ae28
--- /dev/null
+++ b/test/fixtures/client.csr
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBrTCCARYCAQAwbTELMAkGA1UEBhMCRkkxEzARBgNVBAgTClNvbWUtU3RhdGUx
+ETAPBgNVBAcTCEhlbHNpbmtpMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0
+eSBMdGQxEzARBgNVBAMTCkZvbyBDbGllbnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A
+MIGJAoGBAPrpGfw/w74wrXew/EggQwdcNFmF5EuoY2U2xfNoEtacpKVX5bGLU50K
+Ka0CHhkBVG3UmQr2ZAQAZ3ELxNHAMxLTb4RQrUfXO+HvsSThlwdbynkJUSbncVOY
+76OEv7sFBhdbXKL9dREG/bGbdD3kjvz3lqCmswUjweI678d2L+INAgMBAAGgADAN
+BgkqhkiG9w0BAQUFAAOBgQDKGUqjkUxGOisFN70X7ZOW7H99veR9QlixKl5e0W+7
+UtJ+GUtH2WQEb4F72+ruHrdDWQI1VaH9hPOvTRCjlgXiT0RHXpGPbJK/Nc+Eq5dm
+kuk/tQeXv6+S1fgYOm0w09rE7pBjQtuAybB55lGZ7k84UE2xTc97Ru14nYFCsZ4z
+RA==
+-----END CERTIFICATE REQUEST-----
diff --git a/test/fixtures/client.key b/test/fixtures/client.key
new file mode 100644
index 0000000..e52bafb
--- /dev/null
+++ b/test/fixtures/client.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQD66Rn8P8O+MK13sPxIIEMHXDRZheRLqGNlNsXzaBLWnKSlV+Wx
+i1OdCimtAh4ZAVRt1JkK9mQEAGdxC8TRwDMS02+EUK1H1zvh77Ek4ZcHW8p5CVEm
+53FTmO+jhL+7BQYXW1yi/XURBv2xm3Q95I7895agprMFI8HiOu/Hdi/iDQIDAQAB
+AoGBAOFHTXd4YO1wky82Dy1LGiOPm8kNOC7d33BOv2iN9uwN9J4nzymbqNUE/OpD
+TnaxBPcfvNFk6+PT4QxUvsB8ytzDMZ3YC4xyJf5GPP/hfzyWCRjB557WZl1cx7nC
+2gA93PBZE7WT1SySXmjsiC7o/2T/0cUaawXOBczHP8oXoEkBAkEA/c1MHs13ojxh
+oOj/ibCpYpd2Zv5Hrc5tsh+otDdIrb79IAHnNw7WhMkLs6cLk1MY6jLeCvQtjlUY
+H5C/6Ez84QJBAP0VZMgWPw3FVNXPrj833OA6XjyWO+TADpnlrahuDQqWnR3C29Uc
+Iq/ApVX2pt2cNIZpiuJ4BYNc44cHjvu6vq0CQQChan1cJc9NhluNLELBfnLsOmpa
+bKSH3P8VR19TZsm5fvub7Lnx4WT7xKXFl5scEsCIyts/WjbTDDmwca4r/zLhAkB4
+wkeHbY4CnSDgsKr9AUPEPjWPBURo3vdYmY4mKvTQE5O+iqboZfdrEyoQ/ZMbdRhe
+9mdNrmU7DAyI9qNUHAQ1AkAlq/vdkrcq5SRR9uti/1M0/Jaw7l3JutBaW93kdvXx
+BX568ezO1PQtXwVSv+uJEkDoST1bkvhqt7hlMu/RkmfG
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/client.key.orig b/test/fixtures/client.key.orig
new file mode 100644
index 0000000..d9c39ba
--- /dev/null
+++ b/test/fixtures/client.key.orig
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,081B123E7DBC9687
+
+c8gE0LQMMtV7GhUcP7GHBotlNNAsbyG/gERB//BFlvRPjs43ALvm5xMGUkcsZkw9
+H6ljvvgyRkud06x1mGxqlZ4fLlty0/mdXf6791R9gA/9bKLakHGlwnoVUrEPIeh3
+Iu/3IF0Uz8o3uljSp9eehR8sW5V4o+Z1MszQ+GkcpCyYnDPykyN52QLTM3RcUJ0v
+2iJrgxYEAyMA14iIpYM8IsFDYIBvwVfQxyBZnQ+8dDQgVOokz56g+/9emgOsVjWj
+bdUKZ7+95RLxCq0hjJd8GnoDn7qtxIPOGVqYcdEwwNyzO43FnpjOFhNDWwsrNrgf
+vUHKhZRHB99CkuCvGg4fjVbFd/1/n3eE5VmiQuqoHzPnXH3RDIx2rZSjFo/NkzYx
+4XzZ4YOe6vOP7kR18CL+bl32WneMELoh+TPLmsSC3rIP48M8+oQINSZaBrl59Ocw
+NW36xgDNSQoia2splVNo51vtZomq1Hb3co6hD43D4xnrc6Aqucm3BsW6UCc/PVKv
+HAXLCb+3awIy5NJSYb0qRETE2rKB6LjmKfILtOrto6QYSlkJmQUxqoPXgRAWWNlw
+1Ngws84+6UjmGWDBlpZHn0hcO/B7KJAAS/xNSFYDoSu6dAbabxTI/dZCWhw1aN5c
+QeYPihCi66F6Vuq/QT89dHtZE4IMPH7R95Yp18tCcyVGxaEiphw3HJYepMxoJesQ
+YH8tWQwvD5LaADzNJIKBxMjCOK23GuXWQLJJRf4QWiXaQar69qXULxBT3iqBp/rQ
+VKsQByBwJlEo/YSEFjhhMgo0zSbqIRAY1XCDo+dgB2IB1KPAobhSCQ==
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/client.pfx b/test/fixtures/client.pfx
new file mode 100644
index 0000000..98ecf33
Binary files /dev/null and b/test/fixtures/client.pfx differ
diff --git a/test/fixtures/server.crt b/test/fixtures/server.crt
new file mode 100644
index 0000000..abef474
--- /dev/null
+++ b/test/fixtures/server.crt
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAa0CAQEwDQYJKoZIhvcNAQEFBQAwaTELMAkGA1UEBhMCRkkxEzARBgNV
+BAgTClNvbWUtU3RhdGUxETAPBgNVBAcTCEhlbHNpbmtpMSEwHwYDVQQKExhJbnRl
+cm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBkZvbyBDQTAeFw0xNDEwMTEy
+MjQ2MzRaFw0xNTEwMTEyMjQ2MzRaMGwxCzAJBgNVBAYTAkZJMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMREwDwYDVQQHEwhIZWxzaW5raTEhMB8GA1UEChMYSW50ZXJuZXQg
+V2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcN
+AQEBBQADgY0AMIGJAoGBAMBAs08Mguowi6W8+BrqiByLr3FhpKnnRTtO/wfDDebg
+P3Da5vIapHt0NbbSIYkju1M1y/9S0uqCScSIIFeSr8wOvmB1c0jRX+P6oA6p8lJX
+G/HEfEuu3uK+Olkds7sHcrtuRaYLEAXNYCUTlT0VdfVvxV960m/5wMRpexjC5h+j
+AgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAElgxIBym6RrXNc0WALCmvZLzWXPBOUc
+zCbN318AuRqbTJP56XHGU3IKUI5GDfAIIlH+tXjxsVDLCzx/1LQv77ko3OgcCTvL
+oxyOQe7k+L8LWmU5/F4V/ndP4VgqF34APfQrdHzsveD5p/93SoVYQQMUoEjDKIqM
+RJMxUbcTTLGvCb9LrAhQuBT/9s+OlmtS84JkX7HHp7I2SlqL44gzpGBIZZE8Pj/D
+h6q1t2WR5YM8ybScmuNb62ws9mZiej51e+QYnGCpRq7DSv6gSSo8y7L24vGxMFrS
+2X/ISgYfy3UE1ziddGYpEpEpRcKzYSPTSQc5Juzn/iKhlnIBf8ALzCC/s1GteM+p
+wRAGGsTueiiksuDmohI9qjJL9+kLVgL41alfXGpYkNStlWEXTryMv0VT/Exr/prK
+vadVfwa9/T+ywzmwDNPrZFN/LEtPSaMclMzmclLjebqoqUAF05UUdnTQabh5gMni
+VcHaxwZsBDeDhXqowhlHKfdXNZzi2skHA04g4FPxys7llwW0J/gj7hUYpqCxxsLp
+4O1tb30dxBSYKLXJDaEXl07kQ1RTi7vkzUPh3P4totQYq4/Tc1O03YXkOm+wxpC3
+ByxFHMt9glEySrpvZmRvmsmoMuBoFklqBd4gLeUerOVg521lpCBoAWE/tFvWkOmd
+MsONbxQDiiVP
+-----END CERTIFICATE-----
diff --git a/test/fixtures/server.csr b/test/fixtures/server.csr
new file mode 100644
index 0000000..9906b70
--- /dev/null
+++ b/test/fixtures/server.csr
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBrDCCARUCAQAwbDELMAkGA1UEBhMCRkkxEzARBgNVBAgTClNvbWUtU3RhdGUx
+ETAPBgNVBAcTCEhlbHNpbmtpMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0
+eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEAwECzTwyC6jCLpbz4GuqIHIuvcWGkqedFO07/B8MN5uA/cNrm8hqke3Q1
+ttIhiSO7UzXL/1LS6oJJxIggV5KvzA6+YHVzSNFf4/qgDqnyUlcb8cR8S67e4r46
+WR2zuwdyu25FpgsQBc1gJROVPRV19W/FX3rSb/nAxGl7GMLmH6MCAwEAAaAAMA0G
+CSqGSIb3DQEBBQUAA4GBAClj/K2DAH5S64T6s7jervmk4N956Ho3aTLBgE+ReXLj
+btcTdk3vFbQApAlG6MrSKys4HjpKpP/RENx3Js0HHeb8ELmWtIQNxRhwIpl0K5AD
+xorKj+mwngLtVyARb/M7O3E8jYHzBPzpsolKWIY4AavYdmHu+Zhgm4hPKUcW+bAv
+-----END CERTIFICATE REQUEST-----
diff --git a/test/fixtures/server.key b/test/fixtures/server.key
new file mode 100644
index 0000000..3465786
--- /dev/null
+++ b/test/fixtures/server.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDAQLNPDILqMIulvPga6ogci69xYaSp50U7Tv8Hww3m4D9w2uby
+GqR7dDW20iGJI7tTNcv/UtLqgknEiCBXkq/MDr5gdXNI0V/j+qAOqfJSVxvxxHxL
+rt7ivjpZHbO7B3K7bkWmCxAFzWAlE5U9FXX1b8VfetJv+cDEaXsYwuYfowIDAQAB
+AoGBAL7tQmXl2fmz/mu5kHhCpKwcuT6TpxEo4aN132aY+qxn1flBHAwiE2mbTmDi
+rHViq/2GNrK5UUed3p60RdJSlgwIkyqtcGxWhUJGYCR/hU60qeeLp3MhhOoOFbiV
+YTDsoC7V/SuWbX+1qG5FxnHSnTZhAIRkZXS4uTZ5WDcQm/7BAkEA+TlZ1IT9CeU/
+FpHpqc8RgR8377Ehjy8o4Z4EGFnxQlAUWASnhs6dw4isr3+c7hA1OEmqmcRClPVZ
+t1JbHAPC4QJBAMV60WSJzPUccCF47T9Ao2CeWFl/9dmgQQe9idpTNuKMXNtPJN44
+0MQvnb+xS828woJOoRI+/UTVLLBc4xwMtwMCQQDZTadExTw4v5l1nX5GoJUbp9PG
+/ARN64nSx0u8y9evwVErucs0oL0we+BOGZAEhz9QN/M3pceESDWUwYtNbv4hAkBB
+Ku2MqvjK7k6GjTxlgjQn/zkSl+qOnZa4MjEarhlPm5hM+wokl0U1aK07BAwK4b6i
+d8YpmkXEAEEWFiEQMZX3AkEA1SkdiFj1u7HnzO7onLJsnFzowX3pm1UFl0azOMlM
+2GkjYxWeJ/4VL7Y6QkhHE0Nj3my2+MJQI9xpYgMbw/l11w==
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/server.key.orig b/test/fixtures/server.key.orig
new file mode 100644
index 0000000..eca29e6
--- /dev/null
+++ b/test/fixtures/server.key.orig
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,8A14F3F31BF3CDF7
+
+84UpLjX5GbfCoP/+ZRLwHZHqmgKyc2cjQOQKhp2YTk+4E0JSO7KsKwJax55r5VqE
+ECGvbd8yvVZ3GRpU6bDCLelDN+Ob5ZSGhynI0f9is6Qle1/SbvaK+qL6MLf4H38l
+Y91eZxjE8hLvOMHBlbobLmy9UUzl2osBDAM1nm5d1X7pF7mN4xgY8s2G7UPq3ZZS
+NWwMsnwtb/9Ahm7WXfsJbebyKspkTSHYp/ozaOGW58fuKxkwRFd1UlyblZWU3ezP
+JCpvJQ7HS6Dfy6+GUaxcC6pyxqnoJHYccB5usJ2h4QD4Es3sT7Vw7M80JKw3vAWm
+TH7VkFX3yGfJ1p1jNzifN5687QqrjeI3/ecTs1rFhIC4TUPN9EDvw86Y6l6Mvo04
+Hl2cVzCnCrZYq0ICD0op3+7f9kuKl7bz54S/iRG2qQdICohPs7ra2yaUy+NFVDs1
+XdXyF5/xew+Rr2z7ygEd+OrvXxPV0zTFbicg9GXGeB/pIYAclmoSNXD5T3voN7y9
+5MjSGL375N3z+kqPzMNawYCnZLwQ5jYTDUkDTATYpjcIDVDQkzbl8mFp4i+Z9GhL
+H8FRAgmBbkgy3dxhFjxr/WzuaTkUCAbGhrtPd8WCPhBXJBzWdrBVR9SE8zT9n8lq
+ZwaPwkPLBrLe/XZFEQJ5cdvMVNy4QQwsyxHnCgO3VUc68UYXb39MvyM6t8+S1rSm
+SyvVAT+jB8T4VlE7tedQGuvyKMmeNIJe9znd3u4S+Aq2+vw6bOKeNYBMaupR5Gyl
+bJrscuTG1aLUe+XH9BuFBUoIwdXuBv4Ko/pDL0MYPghDAEGkp4Acmg==
+-----END RSA PRIVATE KEY-----
diff --git a/test/jsonp.js b/test/jsonp.js
new file mode 100644
index 0000000..934d1ff
--- /dev/null
+++ b/test/jsonp.js
@@ -0,0 +1,232 @@
+
+/**
+ * Module dependencies.
+ */
+
+var http = require('http');
+var eioc = require('engine.io-client');
+var listen = require('./common').listen;
+var expect = require('expect.js');
+var request = require('superagent');
+var WebSocket = require('ws');
+
+describe('JSONP', function () {
+ before(function () {
+ // we have to override the browser's functionality for JSONP
+ document = {
+ body: {
+ appendChild: function(){},
+ removeChild: function(){}
+ }
+ };
+
+ document.createElement = function (name) {
+ var self = this;
+
+ if('script' == name) {
+ var script = {};
+
+ script.__defineGetter__('parentNode', function () {
+ return document.body;
+ });
+
+ script.__defineSetter__('src', function (uri) {
+ request.get(uri).end(function(res) {
+ eval(res.text);
+ });
+ });
+ return script;
+ } else if ('form' == name) {
+ var form = {
+ style: {},
+ action: '',
+ parentNode: { removeChild: function(){} },
+ removeChild: function(){},
+ setAttribute: function(){},
+ appendChild: function(){},
+ submit: function(){
+ request
+ .post(this.action)
+ .type('form')
+ .send({ d: self.areaValue })
+ .end(function(){});
+ }
+ };
+ return form;
+ } else if ('textarea' == name) {
+ var textarea = {};
+
+ //a hack to be able to access the area data when form is sent
+ textarea.__defineSetter__('value', function (data) {
+ self.areaValue = data;
+ });
+ return textarea;
+ } else if (~name.indexOf('iframe')) {
+ var iframe = {};
+ setTimeout(function() {
+ if (iframe.onload) iframe.onload();
+ }, 0);
+
+ return iframe;
+ } else {
+ return {};
+ }
+ }
+
+ document.getElementsByTagName = function (name) {
+ return [{
+ parentNode: {
+ insertBefore: function () {}
+ }
+ }]
+ }
+ });
+
+ after(function () {
+ delete document.getElementsByTagName
+ , document.createElement;
+ delete global.document;
+ });
+
+ describe('handshake', function () {
+ it('should open with polling JSONP when requested', function (done) {
+ var engine = listen( { allowUpgrades: false, transports: ['polling'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:' + port
+ , { transports: ['polling'], forceJSONP: true, upgrade: false });
+ engine.on('connection', function (socket) {
+ expect(socket.transport.name).to.be('polling');
+ expect(socket.transport.head).to.be('___eio[0](');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('messages', function () {
+ var engine, port, socket;
+
+ beforeEach(function(done) {
+ engine = listen( { allowUpgrades: false, transports: ['polling'] }, function (p) {
+ port = p;
+
+ socket = new eioc.Socket('ws://localhost:' + port
+ , { transports: ['polling'], forceJSONP: true, upgrade: false });
+
+ done();
+ });
+ });
+
+ it('should arrive from client to server and back (pollingJSONP)', function (done) {
+ engine.on('connection', function (conn) {
+ conn.on('message', function (msg) {
+ conn.send('a');
+ });
+ });
+
+ socket.on('open', function () {
+ socket.send('a');
+ socket.on('message', function (msg) {
+ expect(socket.transport.query.j).to.not.be(undefined);
+ expect(msg).to.be('a');
+ done();
+ });
+ });
+ });
+
+ it('should not fail JSON.parse for stringified messages', function (done) {
+ engine.on('connection', function(conn) {
+ conn.on('message', function(message) {
+ expect(JSON.parse(message)).to.be.eql({test : 'a\r\nb\n\n\n\nc'});
+ done();
+ });
+ });
+ socket.on('open', function () {
+ socket.send(JSON.stringify({test : 'a\r\nb\n\n\n\nc'}));
+ });
+ });
+
+ it('should parse newlines in message correctly', function (done) {
+ engine.on('connection', function(conn) {
+ conn.on('message', function(message) {
+ expect(message).to.be.equal('a\r\nb\n\n\n\nc');
+ done();
+ });
+ });
+ socket.on('open', function () {
+ socket.send('a\r\nb\n\n\n\nc');
+ });
+ });
+
+ it('should arrive from server to client and back with binary data (pollingJSONP)', function(done) {
+ var binaryData = new Buffer(5);
+ for (var i = 0; i < 5; i++) binaryData[i] = i;
+ engine.on('connection', function (conn) {
+ conn.on('message', function (msg) {
+ conn.send(msg);
+ });
+ });
+
+ socket.on('open', function() {
+ socket.send(binaryData);
+ socket.on('message', function (msg) {
+ for (var i = 0; i < msg.length; i++) expect(msg[i]).to.be(i);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('close', function () {
+ it('should trigger when server closes a client', function (done) {
+ var engine = listen( { allowUpgrades: false, transports: ['polling'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:' + port
+ , { transports: ['polling'], forceJSONP: true, upgrade: false })
+ , total = 2;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ --total || done();
+ });
+ setTimeout(function () {
+ conn.close();
+ }, 10);
+ });
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ --total || done();
+ });
+ });
+ });
+ });
+
+ it('should trigger when client closes', function (done) {
+ var engine = listen( { allowUpgrades: false, transports: ['polling'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:' + port
+ , { transports: ['polling'], forceJSONP: true, upgrade: false })
+ , total = 2;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ --total || done();
+ });
+ });
+
+ socket.on('open', function () {
+ socket.send('a');
+ socket.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ --total || done();
+ });
+
+ setTimeout(function () {
+ socket.close();
+ }, 10);
+ });
+ });
+ });
+ });
+});
diff --git a/test/server.js b/test/server.js
new file mode 100644
index 0000000..fa07772
--- /dev/null
+++ b/test/server.js
@@ -0,0 +1,2010 @@
+
+/**
+ * Tests dependencies.
+ */
+
+var http = require('http');
+var https = require('https');
+var fs = require('fs');
+var eio = require('..');
+var eioc = require('engine.io-client');
+var listen = require('./common').listen;
+var expect = require('expect.js');
+var request = require('superagent');
+var WebSocket = require('ws');
+
+/**
+ * Tests.
+ */
+
+describe('server', function () {
+
+ describe('verification', function () {
+ it('should disallow non-existent transports', function (done) {
+ var engine = listen(function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .query({ transport: 'tobi' }) // no tobi transport - outrageous
+ .end(function (res) {
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(0);
+ expect(res.body.message).to.be('Transport unknown');
+ expect(res.header['access-control-allow-origin']).to.be('*');
+ done();
+ });
+ });
+ });
+
+ it('should disallow `constructor` as transports', function (done) {
+ // make sure we check for actual properties - not those present on every {}
+ var engine = listen(function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .set('Origin', 'http://engine.io')
+ .query({ transport: 'constructor' })
+ .end(function (res) {
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(0);
+ expect(res.body.message).to.be('Transport unknown');
+ expect(res.header['access-control-allow-credentials']).to.be('true');
+ expect(res.header['access-control-allow-origin']).to.be('http://engine.io');
+ done();
+ });
+ });
+ });
+
+ it('should disallow non-existent sids', function (done) {
+ var engine = listen(function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .set('Origin', 'http://engine.io')
+ .query({ transport: 'polling', sid: 'test' })
+ .end(function (res) {
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(1);
+ expect(res.body.message).to.be('Session ID unknown');
+ expect(res.header['access-control-allow-credentials']).to.be('true');
+ expect(res.header['access-control-allow-origin']).to.be('http://engine.io');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('handshake', function () {
+ it('should send the io cookie', function (done) {
+ var engine = listen(function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .query({ transport: 'polling', b64: 1 })
+ .end(function (res) {
+ // hack-obtain sid
+ var sid = res.text.match(/"sid":"([^"]+)"/)[1];
+ expect(res.headers['set-cookie'][0]).to.be('io=' + sid);
+ done();
+ });
+ });
+ });
+
+ it('should send the io cookie custom name', function (done) {
+ var engine = listen({ cookie: 'woot' }, function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .query({ transport: 'polling', b64: 1 })
+ .end(function (res) {
+ var sid = res.text.match(/"sid":"([^"]+)"/)[1];
+ expect(res.headers['set-cookie'][0]).to.be('woot=' + sid);
+ done();
+ });
+ });
+ });
+
+ it('should not send the io cookie', function (done) {
+ var engine = listen({ cookie: false }, function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .query({ transport: 'polling' })
+ .end(function (res) {
+ expect(res.headers['set-cookie']).to.be(undefined);
+ done();
+ });
+ });
+ });
+
+ it('should register a new client', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ expect(Object.keys(engine.clients)).to.have.length(0);
+ expect(engine.clientsCount).to.be(0);
+
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function () {
+ expect(Object.keys(engine.clients)).to.have.length(1);
+ expect(engine.clientsCount).to.be(1);
+ done();
+ });
+ });
+ });
+
+ it('should exchange handshake data', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('handshake', function (obj) {
+ expect(obj.sid).to.be.a('string');
+ expect(obj.pingTimeout).to.be.a('number');
+ expect(obj.upgrades).to.be.an('array');
+ done();
+ });
+ });
+ });
+
+ it('should allow custom ping timeouts', function (done) {
+ var engine = listen({ allowUpgrades: false, pingTimeout: 123 }, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+ socket.on('handshake', function (obj) {
+ expect(obj.pingTimeout).to.be(123);
+ done();
+ });
+ });
+ });
+
+ it('should trigger a connection event with a Socket', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (socket) {
+ expect(socket).to.be.an(eio.Socket);
+ done();
+ });
+ });
+ });
+
+ it('should open with polling by default', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (socket) {
+ expect(socket.transport.name).to.be('polling');
+ done();
+ });
+ });
+ });
+
+ it('should be able to open with ws directly', function (done) {
+ var engine = listen({ transports: ['websocket'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ engine.on('connection', function (socket) {
+ expect(socket.transport.name).to.be('websocket');
+ done();
+ });
+ });
+ });
+
+ it('should not suggest any upgrades for websocket', function (done) {
+ var engine = listen({ transports: ['websocket'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ socket.on('handshake', function (obj) {
+ expect(obj.upgrades).to.have.length(0);
+ done();
+ });
+ });
+ });
+
+ it('should not suggest upgrades when none are availble', function (done) {
+ var engine = listen({ transports: ['polling'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { });
+ socket.on('handshake', function (obj) {
+ expect(obj.upgrades).to.have.length(0);
+ done();
+ });
+ });
+ });
+
+ it('should only suggest available upgrades', function (done) {
+ var engine = listen({ transports: ['polling'] }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { });
+ socket.on('handshake', function (obj) {
+ expect(obj.upgrades).to.have.length(0);
+ done();
+ });
+ });
+ });
+
+ it('should suggest all upgrades when no transports are disabled', function (done) {
+ var engine = listen({}, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { });
+ socket.on('handshake', function (obj) {
+ expect(obj.upgrades).to.have.length(1);
+ expect(obj.upgrades).to.have.contain('websocket');
+ done();
+ });
+ });
+ });
+
+ it('default to polling when proxy doesn\'t support websocket', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+
+ engine.on('connection', function (socket) {
+ socket.on('message', function (msg) {
+ if ('echo' == msg) socket.send(msg);
+ });
+ });
+
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function () {
+ request.get('http://localhost:%d/engine.io/'.s(port))
+ .set({ connection: 'close' })
+ .query({ transport: 'websocket', sid: socket.id })
+ .end(function (err, res) {
+ expect(err).to.be(null);
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(3);
+ socket.send('echo');
+ socket.on('message', function (msg) {
+ expect(msg).to.be('echo');
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('should allow arbitrary data through query string', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { query: { a: 'b' } });
+ engine.on('connection', function (conn) {
+ expect(conn.request._query).to.have.keys('transport', 'a');
+ expect(conn.request._query.a).to.be('b');
+ done();
+ });
+ });
+ });
+
+ it('should allow data through query string in uri', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d?a=b&c=d'.s(port));
+ engine.on('connection', function (conn) {
+ expect(conn.request._query.EIO).to.be.a('string');
+ expect(conn.request._query.a).to.be('b');
+ expect(conn.request._query.c).to.be('d');
+ done();
+ });
+ });
+ });
+
+
+
+ it('should disallow bad requests', function (done) {
+ var engine = listen(function (port) {
+ request.get('http://localhost:%d/engine.io/default/'.s(port))
+ .set('Origin', 'http://engine.io')
+ .query({ transport: 'websocket' })
+ .end(function (res) {
+ expect(res.status).to.be(400);
+ expect(res.body.code).to.be(3);
+ expect(res.body.message).to.be('Bad request');
+ expect(res.header['access-control-allow-credentials']).to.be('true');
+ expect(res.header['access-control-allow-origin']).to.be('http://engine.io');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('close', function () {
+ it('should be able to access non-empty writeBuffer at closing (server)', function(done) {
+ var opts = {allowUpgrades: false};
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(conn.writeBuffer.length).to.be(1);
+ setTimeout(function () {
+ expect(conn.writeBuffer.length).to.be(0); // writeBuffer has been cleared
+ }, 10);
+ done();
+ });
+ conn.writeBuffer.push({ type: 'message', data: 'foo'});
+ conn.onError('');
+ });
+ });
+ });
+
+ it('should be able to access non-empty writeBuffer at closing (client)', function(done) {
+ var opts = {allowUpgrades: false};
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+ socket.on('open', function() {
+ socket.on('close', function (reason) {
+ expect(socket.writeBuffer.length).to.be(1);
+ expect(socket.callbackBuffer.length).to.be(1);
+ setTimeout(function() {
+ expect(socket.writeBuffer.length).to.be(0);
+ expect(socket.callbackBuffer.length).to.be(0);
+ }, 10);
+ done();
+ });
+ socket.writeBuffer.push({ type: 'message', data: 'foo'});
+ socket.callbackBuffer.push(function() {});
+ socket.onError('');
+ });
+ });
+ });
+
+ it('should trigger on server if the client does not pong', function (done) {
+ var opts = { allowUpgrades: false, pingInterval: 5, pingTimeout: 5 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+ socket.sendPacket = function (){};
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('ping timeout');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should trigger on server even when there is no outstanding polling request (GH-198)', function (done) {
+ var opts = { allowUpgrades: false, pingInterval: 500, pingTimeout: 500 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('ping timeout');
+ done();
+ });
+ // client abruptly disconnects, no polling request on this tick since we've just connected
+ socket.sendPacket = socket.onPacket = function (){};
+ socket.close();
+ // then server app tries to close the socket, since client disappeared
+ conn.close();
+ });
+ });
+ });
+
+ it('should trigger on client if server does not meet ping timeout', function (done) {
+ var opts = { allowUpgrades: false, pingInterval: 50, pingTimeout: 30 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function () {
+ // override onPacket to simulate an inactive server after handshake
+ socket.onPacket = function(){};
+ socket.on('close', function (reason, err) {
+ expect(reason).to.be('ping timeout');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should trigger on both ends upon ping timeout', function (done) {
+ var opts = { allowUpgrades: false, pingTimeout: 10, pingInterval: 10 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port))
+ , total = 2;
+
+ function onClose (reason, err) {
+ expect(reason).to.be('ping timeout');
+ --total || done();
+ }
+
+ engine.on('connection', function (conn) {
+ conn.on('close', onClose);
+ });
+
+ socket.on('open', function () {
+ // override onPacket to simulate an inactive server after handshake
+ socket.onPacket = socket.sendPacket = function(){};
+ socket.on('close', onClose);
+ });
+ });
+ });
+
+ it('should trigger when server closes a client', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port))
+ , total = 2;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ --total || done();
+ });
+ setTimeout(function () {
+ conn.close();
+ }, 10);
+ });
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ --total || done();
+ });
+ });
+ });
+ });
+
+ it('should trigger when server closes a client (ws)', function (done) {
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] })
+ , total = 2;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ --total || done();
+ });
+ setTimeout(function () {
+ conn.close();
+ }, 10);
+ });
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ --total || done();
+ });
+ });
+ });
+ });
+
+ it('should trigger when client closes', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port))
+ , total = 2;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ --total || done();
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ --total || done();
+ });
+
+ setTimeout(function () {
+ socket.close();
+ }, 10);
+ });
+ });
+ });
+
+ it('should trigger when client closes (ws)', function (done) {
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] })
+ , total = 2;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ --total || done();
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ --total || done();
+ });
+
+ setTimeout(function () {
+ socket.close();
+ }, 10);
+ });
+ });
+ });
+
+ it('should trigger when calling socket.close() in payload', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+
+ engine.on('connection', function (conn) {
+ conn.send(null, function () {socket.close();});
+ conn.send('this should not be handled');
+
+ conn.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ done();
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ expect(msg).to.not.be('this should not be handled');
+ });
+
+ socket.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ });
+ });
+ });
+ });
+
+ it('should abort upgrade if socket is closed (GH-35)', function (done) {
+ var engine = listen({ allowUpgrades: true }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function () {
+ socket.close();
+ // we wait until complete to see if we get an uncaught EPIPE
+ setTimeout(function(){
+ done();
+ }, 100);
+ });
+ });
+ });
+
+ it('should trigger if a poll request is ongoing and the underlying ' +
+ 'socket closes, as in a browser tab close', function ($done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ // hack to access the sockets created by node-xmlhttprequest
+ // see: https://github.com/driverdan/node-XMLHttpRequest/issues/44
+ var request = require('http').request;
+ var sockets = [];
+ http.request = function(opts){
+ var req = request.apply(null, arguments);
+ req.on('socket', function(socket){
+ sockets.push(socket);
+ });
+ return req;
+ };
+
+ function done(){
+ http.request = request;
+ $done();
+ }
+
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port))
+ , serverSocket;
+
+ engine.on('connection', function(s){
+ serverSocket = s;
+ });
+
+ socket.transport.on('poll', function(){
+ // we set a timer to wait for the request to actually reach
+ setTimeout(function(){
+ // at this time server's `connection` should have been fired
+ expect(serverSocket).to.be.an('object');
+
+ // OPENED readyState is expected - we qre actually polling
+ expect(socket.transport.pollXhr.xhr.readyState).to.be(1);
+
+ // 2 requests sent to the server over an unique port means
+ // we should have been assigned 2 sockets
+ expect(sockets.length).to.be(2);
+
+ // expect the socket to be open at this point
+ expect(serverSocket.readyState).to.be('open');
+
+ // kill the underlying connection
+ sockets[1].end();
+ serverSocket.on('close', function(reason, err){
+ expect(reason).to.be('transport error');
+ expect(err.message).to.be('poll connection closed prematurely');
+ done();
+ });
+ }, 50);
+ });
+ });
+ });
+
+ it('should not trigger with connection: close header', function($done){
+ var engine = listen({ allowUpgrades: false }, function(port){
+ // intercept requests to add connection: close
+ var request = http.request;
+ http.request = function(){
+ var opts = arguments[0];
+ opts.headers = opts.headers || {};
+ opts.headers.Connection = 'close';
+ return request.apply(this, arguments);
+ };
+
+ function done(){
+ http.request = request;
+ $done();
+ }
+
+ engine.on('connection', function(socket){
+ socket.on('message', function(msg){
+ expect(msg).to.equal('test');
+ socket.send('woot');
+ });
+ });
+
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function(){
+ socket.send('test');
+ });
+ socket.on('message', function(msg){
+ expect(msg).to.be('woot');
+ done();
+ });
+ });
+ });
+
+ it('should not trigger early with connection `ping timeout`' +
+ 'after post handshake timeout', function (done) {
+ // first timeout should trigger after `pingInterval + pingTimeout`,
+ // not just `pingTimeout`.
+ var opts = { allowUpgrades: false, pingInterval: 300, pingTimeout: 100 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var clientCloseReason = null;
+
+ socket.on('handshake', function() {
+ socket.onPacket = function(){};
+ });
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ clientCloseReason = reason;
+ });
+ });
+
+ setTimeout(function() {
+ expect(clientCloseReason).to.be(null);
+ done();
+ }, 200);
+ });
+ });
+
+ it('should not trigger early with connection `ping timeout` ' +
+ 'after post ping timeout', function (done) {
+ // ping timeout should trigger after `pingInterval + pingTimeout`,
+ // not just `pingTimeout`.
+ var opts = { allowUpgrades: false, pingInterval: 80, pingTimeout: 50 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var clientCloseReason = null;
+
+ engine.on('connection', function(conn){
+ conn.on('heartbeat', function() {
+ conn.onPacket = function(){};
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ clientCloseReason = reason;
+ });
+ });
+
+ setTimeout(function() {
+ expect(clientCloseReason).to.be(null);
+ done();
+ }, 100);
+ });
+ });
+
+ it('should trigger early with connection `transport close` ' +
+ 'after missing pong', function (done) {
+ // ping timeout should trigger after `pingInterval + pingTimeout`,
+ // not just `pingTimeout`.
+ var opts = { allowUpgrades: false, pingInterval: 80, pingTimeout: 50 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var clientCloseReason = null;
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ clientCloseReason = reason;
+ });
+ });
+
+ engine.on('connection', function(conn){
+ conn.on('heartbeat', function() {
+ setTimeout(function() {
+ conn.close();
+ }, 20);
+ setTimeout(function() {
+ expect(clientCloseReason).to.be('transport close');
+ done();
+ }, 100);
+ });
+ });
+ });
+ });
+
+ it('should trigger with connection `ping timeout` ' +
+ 'after `pingInterval + pingTimeout`', function (done) {
+ var opts = { allowUpgrades: false, pingInterval: 300, pingTimeout: 100 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var clientCloseReason = null;
+
+ socket.on('open', function () {
+ socket.on('close', function (reason) {
+ clientCloseReason = reason;
+ });
+ });
+
+ engine.on('connection', function(conn){
+ conn.once('heartbeat', function() {
+ setTimeout(function() {
+ socket.onPacket = function(){};
+ expect(clientCloseReason).to.be(null);
+ }, 150);
+ setTimeout(function() {
+ expect(clientCloseReason).to.be(null);
+ }, 350);
+ setTimeout(function() {
+ expect(clientCloseReason).to.be('ping timeout');
+ done();
+ }, 500);
+ });
+ });
+ });
+ });
+
+ it('should abort the polling data request if it is ' +
+ 'in progress', function (done) {
+ var engine = listen({ transports: [ 'polling' ] }, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+
+ engine.on('connection', function (conn) {
+ var onDataRequest = conn.transport.onDataRequest;
+ conn.transport.onDataRequest = function (req, res) {
+ engine.httpServer.close(done);
+ onDataRequest.call(conn.transport, req, res);
+ req.removeAllListeners();
+ conn.close();
+ };
+ });
+
+ socket.on('open', function () {
+ socket.send('test');
+ });
+ });
+ });
+
+ // tests https://github.com/LearnBoost/engine.io-client/issues/207
+ // websocket test, transport error
+ it('should trigger transport close before open for ws', function(done){
+ var opts = { transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://invalidserver:%d'.s(port));
+ socket.on('open', function(){
+ done(new Error('Test invalidation'));
+ });
+ socket.on('close', function(reason){
+ expect(reason).to.be('transport error');
+ done();
+ });
+ });
+ });
+
+ // tests https://github.com/LearnBoost/engine.io-client/issues/207
+ // polling test, transport error
+ it('should trigger transport close before open for xhr', function(done){
+ var opts = { transports: ['polling'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('http://invalidserver:%d'.s(port));
+ socket.on('open', function(){
+ done(new Error('Test invalidation'));
+ });
+ socket.on('close', function(reason){
+ expect(reason).to.be('transport error');
+ done();
+ });
+ });
+ });
+
+ // tests https://github.com/LearnBoost/engine.io-client/issues/207
+ // websocket test, force close
+ it('should trigger force close before open for ws', function(done){
+ var opts = { transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function(){
+ done(new Error('Test invalidation'));
+ });
+ socket.on('close', function(reason){
+ expect(reason).to.be('forced close');
+ done();
+ });
+ socket.close();
+ });
+ });
+
+ // tests https://github.com/LearnBoost/engine.io-client/issues/207
+ // polling test, force close
+ it('should trigger force close before open for xhr', function(done){
+ var opts = { transports: ['polling'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('http://localhost:%d'.s(port));
+ socket.on('open', function(){
+ done(new Error('Test invalidation'));
+ });
+ socket.on('close', function(reason){
+ expect(reason).to.be('forced close');
+ done();
+ });
+ socket.close();
+ });
+ });
+
+ });
+
+ describe('messages', function () {
+ this.timeout(5000);
+
+ it('should arrive from server to client', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.send('a');
+ });
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ expect(msg).to.be('a');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive from server to client (multiple)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port))
+ , expected = ['a', 'b', 'c']
+ , i = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('a');
+ // we use set timeouts to ensure the messages are delivered as part
+ // of different.
+ setTimeout(function () {
+ conn.send('b');
+
+ setTimeout(function () {
+ // here we make sure we buffer both the close packet and
+ // a regular packet
+ conn.send('c');
+ conn.close();
+ }, 50);
+ }, 50);
+
+ conn.on('close', function () {
+ // since close fires right after the buffer is drained
+ setTimeout(function () {
+ expect(i).to.be(3);
+ done();
+ }, 50);
+ });
+ });
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ expect(msg).to.be(expected[i++]);
+ });
+ });
+ });
+ });
+
+ it('should not be receiving data when getting a message longer than maxHttpBufferSize when polling', function(done) {
+ var opts = { allowUpgrades: false, transports: ['polling'], maxHttpBufferSize: 5 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ console.log(msg);
+ });
+ });
+ socket.on('open', function () {
+ socket.send('aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha');
+ });
+ });
+ setTimeout(done, 1000);
+ });
+
+ it('should receive data when getting a message shorter than maxHttpBufferSize when polling', function(done) {
+ var opts = { allowUpgrades: false, transports: ['polling'], maxHttpBufferSize: 5 };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ expect(msg).to.be('a');
+ done();
+ });
+ });
+ socket.on('open', function () {
+ socket.send('a');
+ });
+ });
+ });
+
+
+ it('should arrive from server to client (ws)', function (done) {
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ engine.on('connection', function (conn) {
+ conn.send('a');
+ });
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ expect(msg).to.be('a');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive from server to client (multiple, ws)', function (done) {
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] })
+ , expected = ['a', 'b', 'c']
+ , i = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('a');
+ setTimeout(function () {
+ conn.send('b');
+ setTimeout(function () {
+ conn.send('c');
+ conn.close();
+ }, 50);
+ }, 50);
+ conn.on('close', function () {
+ setTimeout(function () {
+ expect(i).to.be(3);
+ done();
+ }, 50);
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ expect(msg).to.be(expected[i++]);
+ });
+ });
+ });
+ });
+
+ it('should arrive from server to client (multiple, no delay, ws)', function (done) {
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] })
+ , expected = ['a', 'b', 'c']
+ , i = 0;
+
+ engine.on('connection', function (conn) {
+ conn.on('close', function () {
+ setTimeout(function () {
+ expect(i).to.be(3);
+ done();
+ }, 50);
+ });
+ conn.send('a');
+ conn.send('b');
+ conn.send('c');
+ conn.close();
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ expect(msg).to.be(expected[i++]);
+ });
+ });
+ });
+ });
+
+ it('should arrive when binary data is sent as Int8Array (ws)', function (done) {
+ var binaryData = new Int8Array(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData[i] = i;
+ }
+
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData);
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ for (var i = 0; i < binaryData.length; i++) {
+ var num = msg.readInt8(i);
+ expect(num).to.be(i);
+ }
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive when binary data is sent as Int32Array (ws)', function (done) {
+ var binaryData = new Int32Array(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData[i] = (i + 100) * 9823;
+ }
+
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData);
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ for (var i = 0, ii = 0; i < binaryData.length; i += 4, ii++) {
+ var num = msg.readInt32LE(i);
+ expect(num).to.be((ii + 100) * 9823);
+ }
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive when binary data is sent as Int32Array, given as ArrayBuffer(ws)', function (done) {
+ var binaryData = new Int32Array(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData[i] = (i + 100) * 9823;
+ }
+
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData.buffer);
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ for (var i = 0, ii = 0; i < binaryData.length; i += 4, ii++) {
+ var num = msg.readInt32LE(i);
+ expect(num).to.be((ii + 100) * 9823);
+ }
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive when binary data is sent as Buffer (ws)', function (done) {
+ var binaryData = Buffer(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData.writeInt8(i, i);
+ }
+
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData);
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ for (var i = 0; i < binaryData.length; i++) {
+ var num = msg.readInt8(i);
+ expect(num).to.be(i);
+ }
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive when binary data sent as Buffer (polling)', function (done) {
+ var binaryData = Buffer(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData.writeInt8(i, i);
+ }
+
+ var opts = { allowUpgrades: false, transports: ['polling'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData);
+ });
+
+ socket.on('open', function() {
+ socket.on('message', function(msg) {
+ for (var i = 0; i < binaryData.length; i++) {
+ var num = msg.readInt8(i);
+ expect(num).to.be(i);
+ }
+
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive as ArrayBuffer if requested when binary data sent as Buffer (ws)', function (done) {
+ var binaryData = Buffer(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData.writeInt8(i, i);
+ }
+
+ var opts = { allowUpgrades: false, transports: ['websocket'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ socket.binaryType = 'arraybuffer';
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData);
+ });
+
+ socket.on('open', function() {
+ socket.on('message', function(msg) {
+ expect(msg instanceof ArrayBuffer).to.be(true);
+ var intArray = new Int8Array(msg);
+ for (var i = 0; i < binaryData.length; i++) {
+ expect(intArray[i]).to.be(i);
+ }
+
+ done();
+ });
+ });
+ });
+ });
+
+ it('should arrive as ArrayBuffer if requested when binary data sent as Buffer (polling)', function (done) {
+ var binaryData = Buffer(5);
+ for (var i = 0; i < binaryData.length; i++) {
+ binaryData.writeInt8(i, i);
+ }
+
+ var opts = { allowUpgrades: false, transports: ['polling'] };
+ var engine = listen(opts, function(port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+ socket.binaryType = 'arraybuffer';
+
+ engine.on('connection', function (conn) {
+ conn.send(binaryData);
+ });
+
+ socket.on('open', function() {
+ socket.on('message', function(msg) {
+ expect(msg instanceof ArrayBuffer).to.be(true);
+ var intArray = new Int8Array(msg);
+ for (var i = 0; i < binaryData.length; i++) {
+ expect(intArray[i]).to.be(i);
+ }
+
+ done();
+ });
+ });
+ });
+ });
+
+
+ it('should trigger a flush/drain event', function(done){
+ var engine = listen({ allowUpgrades: false }, function(port){
+ engine.on('connection', function(socket){
+ var totalEvents = 4;
+
+ engine.on('flush', function(sock, buf){
+ expect(sock).to.be(socket);
+ expect(buf).to.be.an('array');
+ --totalEvents || done();
+ });
+ socket.on('flush', function(buf){
+ expect(buf).to.be.an('array');
+ --totalEvents || done();
+ });
+
+ engine.on('drain', function(sock){
+ expect(sock).to.be(socket);
+ expect(socket.writeBuffer.length).to.be(0);
+ --totalEvents || done();
+ });
+ socket.on('drain', function(){
+ expect(socket.writeBuffer.length).to.be(0);
+ --totalEvents || done();
+ });
+
+ socket.send('aaaa');
+ });
+
+ new eioc.Socket('ws://localhost:%d'.s(port));
+ });
+ });
+
+ it('should interleave with pongs if many messages buffered ' +
+ 'after connection open', function (done) {
+ this.slow(4000);
+ this.timeout(8000);
+
+ var opts = {
+ transports: ['websocket'],
+ pingInterval: 200,
+ pingTimeout: 100
+ };
+
+ var engine = listen(opts, function (port) {
+ var messageCount = 100;
+ var messagePayload = new Array(256 * 256).join('a');
+ var connection = null;
+ engine.on('connection', function (conn) {
+ connection = conn;
+ });
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ socket.on('open', function () {
+ for (var i=0;i<messageCount;i++) {
+// connection.send('message: ' + i); // works
+ connection.send(messagePayload + '|message: ' + i); // does not work
+ }
+ var receivedCount = 0;
+ socket.on('message', function (msg) {
+ receivedCount += 1;
+ if (receivedCount === messageCount) {
+ done();
+ }
+ });
+ });
+ });
+ });
+
+ it('should support chinese', function(done){
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var shi = '石室詩士施氏,嗜獅,誓食十獅。';
+ var shi2 = '氏時時適市視獅。';
+ engine.on('connection', function (conn) {
+ conn.send('.');
+ conn.send(shi);
+ conn.send(shi2);
+ conn.once('message', function(msg0){
+ expect(msg0).to.be('.');
+ conn.once('message', function(msg){
+ expect(msg).to.be(shi);
+ conn.once('message', function(msg2){
+ expect(msg2).to.be(shi2);
+ done();
+ });
+ });
+ });
+ });
+ socket.on('open', function(){
+ socket.once('message', function(msg0){
+ expect(msg0).to.be('.');
+ socket.once('message', function(msg){
+ expect(msg).to.be(shi);
+ socket.once('message', function(msg2){
+ expect(msg2).to.be(shi2);
+ socket.send('.');
+ socket.send(shi);
+ socket.send(shi2);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ it('should send and receive data with key and cert (polling)', function(done){
+ var srvOpts = {
+ key: fs.readFileSync('test/fixtures/server.key'),
+ cert: fs.readFileSync('test/fixtures/server.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ requestCert: true,
+ rejectUnauthorized: true
+ };
+
+ var opts = {
+ key: fs.readFileSync('test/fixtures/client.key'),
+ cert: fs.readFileSync('test/fixtures/client.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ transports: ['polling'],
+ };
+
+ var srv = https.createServer(srvOpts, function(req, res){
+ res.writeHead(200);
+ res.end('hello world\n');
+ });
+
+ var engine = eio({ transports: ['polling'], allowUpgrades: false });
+ engine.attach(srv);
+ srv.listen(null, function() {
+ var port = srv.address().port;
+ var socket = new eioc.Socket('https://localhost:%d'.s(port), opts);
+
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ expect(msg).to.be('hello');
+ done();
+ });
+ });
+
+ socket.on('open', function() {
+ socket.send('hello');
+ });
+ });
+ });
+
+ it('should send and receive data with ca when not requiring auth (polling)', function(done){
+ var srvOpts = {
+ key: fs.readFileSync('test/fixtures/server.key'),
+ cert: fs.readFileSync('test/fixtures/server.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ requestCert: true
+ };
+
+ var opts = {
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ transports: ['polling']
+ };
+
+ var srv = https.createServer(srvOpts, function(req, res){
+ res.writeHead(200);
+ res.end('hello world\n');
+ });
+
+ var engine = eio({ transports: ['polling'], allowUpgrades: false });
+ engine.attach(srv);
+ srv.listen(null, function() {
+ var port = srv.address().port;
+ var socket = new eioc.Socket('https://localhost:%d'.s(port), opts);
+
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ expect(msg).to.be('hello');
+ done();
+ });
+ });
+
+ socket.on('open', function() {
+ socket.send('hello');
+ });
+ });
+ });
+
+ it('should send and receive data with key and cert (ws)', function(done){
+ var srvOpts = {
+ key: fs.readFileSync('test/fixtures/server.key'),
+ cert: fs.readFileSync('test/fixtures/server.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ requestCert: true,
+ rejectUnauthorized: true
+ };
+
+ var opts = {
+ key: fs.readFileSync('test/fixtures/client.key'),
+ cert: fs.readFileSync('test/fixtures/client.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ transports: ['websocket']
+ };
+
+ var srv = https.createServer(srvOpts, function(req, res){
+ res.writeHead(200);
+ res.end('hello world\n');
+ });
+
+ var engine = eio({ transports: ['websocket'], allowUpgrades: false });
+ engine.attach(srv);
+ srv.listen(null, function() {
+ var port = srv.address().port;
+ var socket = new eioc.Socket('https://localhost:%d'.s(port), opts);
+
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ expect(msg).to.be('hello');
+ done();
+ });
+ });
+
+ socket.on('open', function() {
+ socket.send('hello');
+ });
+ });
+ });
+
+ it('should send and receive data with pfx (polling)', function(done){
+ var srvOpts = {
+ key: fs.readFileSync('test/fixtures/server.key'),
+ cert: fs.readFileSync('test/fixtures/server.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ requestCert: true,
+ rejectUnauthorized: true
+ };
+
+ var opts = {
+ pfx: fs.readFileSync('test/fixtures/client.pfx'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ transports: ['polling']
+ };
+
+ var srv = https.createServer(srvOpts, function(req, res){
+ res.writeHead(200);
+ res.end('hello world\n');
+ });
+
+ var engine = eio({ transports: ['polling'], allowUpgrades: false });
+ engine.attach(srv);
+ srv.listen(null, function() {
+ var port = srv.address().port;
+ var socket = new eioc.Socket('https://localhost:%d'.s(port), opts);
+
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ expect(msg).to.be('hello');
+ done();
+ });
+ });
+
+ socket.on('open', function() {
+ socket.send('hello');
+ });
+ });
+ });
+
+ it('should send and receive data with pfx (ws)', function(done){
+ var srvOpts = {
+ key: fs.readFileSync('test/fixtures/server.key'),
+ cert: fs.readFileSync('test/fixtures/server.crt'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ requestCert: true,
+ rejectUnauthorized: true
+ };
+
+ var opts = {
+ pfx: fs.readFileSync('test/fixtures/client.pfx'),
+ ca: fs.readFileSync('test/fixtures/ca.crt'),
+ transports: ['websocket']
+ };
+
+ var srv = https.createServer(srvOpts, function(req, res){
+ res.writeHead(200);
+ res.end('hello world\n');
+ });
+
+ var engine = eio({ transports: ['websocket'], allowUpgrades: false });
+ engine.attach(srv);
+ srv.listen(null, function() {
+ var port = srv.address().port;
+ var socket = new eioc.Socket('https://localhost:%d'.s(port), opts);
+
+ engine.on('connection', function (conn) {
+ conn.on('message', function(msg) {
+ expect(msg).to.be('hello');
+ done();
+ });
+ });
+
+ socket.on('open', function() {
+ socket.send('hello');
+ });
+ });
+ });
+ });
+
+ describe('send', function() {
+ describe('writeBuffer', function() {
+ it('should not empty until `drain` event (polling)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+ var totalEvents = 2;
+ socket.on('open', function() {
+ socket.send('a');
+ socket.send('b');
+ // writeBuffer should be nonempty, with 'a' still in it
+ expect(socket.writeBuffer.length).to.eql(2);
+ });
+ socket.transport.on('drain', function() {
+ expect(socket.writeBuffer.length).to.eql(--totalEvents);
+ totalEvents || done();
+ });
+ });
+ });
+
+ it('should not empty until `drain` event (websocket)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ var totalEvents = 2;
+ socket.on('open', function() {
+ socket.send('a');
+ socket.send('b');
+ // writeBuffer should be nonempty, with 'a' still in it
+ expect(socket.writeBuffer.length).to.eql(2);
+ });
+ socket.transport.on('drain', function() {
+ expect(socket.writeBuffer.length).to.eql(--totalEvents);
+ totalEvents || done();
+ });
+ });
+ });
+ });
+
+ describe('callback', function() {
+ it('should execute in order when message sent (client) (polling)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+ var i = 0;
+ var j = 0;
+
+ engine.on('connection', function(conn) {
+ conn.on('message', function(msg) {
+ conn.send(msg);
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ // send another packet until we've sent 3 total
+ if (++i < 3) {
+ expect(i).to.eql(j);
+ sendFn();
+ } else {
+ done();
+ }
+ });
+
+ function sendFn() {
+ socket.send(j, (function(value) {
+ j++;
+ })(j));
+ }
+
+ sendFn();
+ });
+ });
+ });
+
+ it('should execute in order when message sent (client) (websocket)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ var i = 0;
+ var j = 0;
+
+ engine.on('connection', function(conn) {
+ conn.on('message', function(msg) {
+ conn.send(msg);
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ // send another packet until we've sent 3 total
+ if (++i < 3) {
+ expect(i).to.eql(j);
+ sendFn();
+ } else {
+ done();
+ }
+ });
+
+ function sendFn() {
+ socket.send(j, (function(value) {
+ j++;
+ })(j));
+ }
+
+ sendFn();
+ });
+ });
+ });
+
+ it('should execute in order with payloads (client) (polling)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+ var i = 0;
+ var lastCbFired = 0;
+
+ engine.on('connection', function(conn) {
+ conn.on('message', function(msg) {
+ conn.send(msg);
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ expect(msg).to.eql(i + 1);
+ i++;
+ });
+
+ function cb(value) {
+ expect(value).to.eql(lastCbFired + 1);
+ lastCbFired = value;
+ if (value == 3) {
+ done();
+ }
+ }
+
+ // 2 and 3 will be in the same payload
+ socket.once('flush', function() {
+ socket.send(2, function() { cb(2); });
+ socket.send(3, function() { cb(3); });
+ });
+
+ socket.send(1, function() { cb(1); });
+ });
+ });
+ });
+
+ it('should execute in order with payloads (client) (websocket)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ var i = 0;
+ var lastCbFired = 0;
+
+ engine.on('connection', function(conn) {
+ conn.on('message', function(msg) {
+ conn.send(msg);
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function(msg) {
+ expect(msg).to.eql(i + 1);
+ i++;
+ });
+
+ function cb(value) {
+ expect(value).to.eql(lastCbFired + 1);
+ lastCbFired = value;
+ if (value == 3) {
+ done();
+ }
+ }
+
+ // 2 and 3 will be in the same payload
+ socket.once('flush', function() {
+ socket.send(2, function() { cb(2); });
+ socket.send(3, function() { cb(3); });
+ });
+
+ socket.send(1, function() { cb(1); });
+ });
+ });
+ });
+
+ it('should execute when message sent (polling)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+ var i = 0;
+ var j = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('a', function (transport) {
+ i++;
+ });
+ });
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ j++;
+ });
+ });
+
+ setTimeout(function() {
+ expect(i).to.be(j);
+ done();
+ }, 10);
+ });
+ });
+
+ it('should execute when message sent (websocket)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] });
+ var i = 0;
+ var j = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('a', function (transport) {
+ i++;
+ });
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ j++;
+ });
+ });
+
+ setTimeout(function () {
+ expect(i).to.be(j);
+ done();
+ }, 10);
+ });
+ });
+
+ it('should execute once for each send', function (done) {
+ var engine = listen(function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var a = 0;
+ var b = 0;
+ var c = 0;
+ var all = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('a');
+ conn.send('b');
+ conn.send('c');
+ });
+
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ if (msg === 'a') a ++;
+ if (msg === 'b') b ++;
+ if (msg === 'c') c ++;
+
+ if(++all === 3) {
+ expect(a).to.be(1);
+ expect(b).to.be(1);
+ expect(c).to.be(1);
+ done();
+ }
+ });
+ });
+ });
+ });
+
+ it('should execute in multipart packet', function (done) {
+ var engine = listen(function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ var i = 0;
+ var j = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('b', function (transport) {
+ i++;
+ });
+
+ conn.send('a', function (transport) {
+ i++;
+ });
+
+ });
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ j++;
+ });
+ });
+
+ setTimeout(function () {
+ expect(i).to.be(j);
+ done();
+ }, 200);
+ });
+ });
+
+ it('should execute in multipart packet (polling)', function (done) {
+ var engine = listen(function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+ var i = 0;
+ var j = 0;
+
+ engine.on('connection', function (conn) {
+ conn.send('d', function (transport) {
+ i++;
+ });
+
+ conn.send('c', function (transport) {
+ i++;
+ });
+
+ conn.send('b', function (transport) {
+ i++;
+ });
+
+ conn.send('a', function (transport) {
+ i++;
+ });
+
+ });
+ socket.on('open', function () {
+ socket.on('message', function (msg) {
+ j++;
+ });
+ });
+
+ setTimeout(function () {
+ expect(i).to.be(j);
+ done();
+ }, 200);
+ });
+ });
+
+ it('should clean callback references when socket gets closed with pending callbacks', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+
+ engine.on('connection', function (conn) {
+ socket.transport.on('pollComplete', function () {
+ conn.send('a', function (transport) {
+ done(new Error('Test invalidation'));
+ });
+
+ if (!conn.writeBuffer.length) {
+ done(new Error('Test invalidation'));
+ }
+
+ // force to close the socket when we have one or more packet(s) in buffer
+ socket.close();
+ });
+
+ conn.on('close', function (reason) {
+ expect(conn.packetsFn).to.be.empty();
+ expect(conn.sentCallbackFn).to.be.empty();
+ done();
+ });
+ });
+ });
+ });
+
+ it('should not execute when it is not actually sent (polling)', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] });
+
+ socket.transport.on('pollComplete', function(msg) {
+ socket.close();
+ });
+
+ engine.on('connection', function (conn) {
+ var err;
+ conn.send('a');
+ conn.send('b', function (transport) {
+ err = new Error('Test invalidation');
+ });
+ conn.on('close', function (reason) {
+ done(err);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('packet', function() {
+ it('should emit when socket receives packet', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('packet', function (packet) {
+ expect(packet.type).to.be('message');
+ expect(packet.data).to.be('a');
+ done();
+ });
+ });
+ socket.on('open', function () {
+ socket.send('a');
+ });
+ });
+ });
+
+ it('should emit when receives ping', function (done) {
+ var engine = listen({ allowUpgrades: false, pingInterval: 4 }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('packet', function (packet) {
+ conn.close();
+ expect(packet.type).to.be('ping');
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('packetCreate', function() {
+ it('should emit before socket send message', function (done) {
+ var engine = listen({ allowUpgrades: false }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('packetCreate', function(packet) {
+ expect(packet.type).to.be('message');
+ expect(packet.data).to.be('a');
+ done();
+ });
+ conn.send('a');
+ });
+ });
+ });
+
+ it('should emit before send pong', function (done) {
+ var engine = listen({ allowUpgrades: false, pingInterval: 4 }, function (port) {
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ engine.on('connection', function (conn) {
+ conn.on('packetCreate', function (packet) {
+ conn.close();
+ expect(packet.type).to.be('pong');
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('upgrade', function () {
+ it('should upgrade', function (done) {
+ var engine = listen(function (port) {
+ // it takes both to send 50 to verify
+ var ready = 2, closed = 2;
+ function finish () {
+ setTimeout(function () {
+ socket.close();
+ }, 10);
+ }
+
+ // server
+ engine.on('connection', function (conn) {
+ var lastSent = 0, lastReceived = 0, upgraded = false;
+ var interval = setInterval(function () {
+ lastSent++;
+ conn.send(lastSent);
+ if (50 == lastSent) {
+ clearInterval(interval);
+ --ready || finish();
+ }
+ }, 2);
+
+ expect(conn.request._query.transport).to.be('polling');
+
+ conn.on('message', function (msg) {
+ expect(conn.request._query).to.be.an('object');
+ lastReceived++;
+ expect(msg).to.eql(lastReceived);
+ });
+
+ conn.on('upgrade', function (to) {
+ expect(conn.request._query.transport).to.be('polling');
+ upgraded = true;
+ expect(to.name).to.be('websocket');
+ expect(conn.transport.name).to.be('websocket');
+ });
+
+ conn.on('close', function (reason) {
+ expect(reason).to.be('transport close');
+ expect(lastSent).to.be(50);
+ expect(lastReceived).to.be(50);
+ expect(upgraded).to.be(true);
+ --closed || done();
+ });
+ });
+
+ // client
+ var socket = new eioc.Socket('ws://localhost:%d'.s(port));
+ socket.on('open', function () {
+ var lastSent = 0, lastReceived = 0, upgrades = 0;
+ var interval = setInterval(function () {
+ lastSent++;
+ socket.send(lastSent);
+ if (50 == lastSent) {
+ clearInterval(interval);
+ --ready || finish();
+ }
+ }, 2);
+ socket.on('upgrading', function (to) {
+ // we want to make sure for the sake of this test that we have a buffer
+ expect(to.name).to.equal('websocket');
+ upgrades++;
+
+ // force send a few packets to ensure we test buffer transfer
+ lastSent++;
+ socket.send(lastSent);
+ lastSent++;
+ socket.send(lastSent);
+
+ expect(socket.writeBuffer).to.not.be.empty();
+ });
+ socket.on('upgrade', function (to) {
+ expect(to.name).to.equal('websocket');
+ upgrades++;
+ });
+ socket.on('message', function (msg) {
+ lastReceived++;
+ expect(lastReceived).to.eql(msg);
+ });
+ socket.on('close', function (reason) {
+ expect(reason).to.be('forced close');
+ expect(lastSent).to.be(50);
+ expect(lastReceived).to.be(50);
+ expect(upgrades).to.be(2);
+ --closed || done();
+ });
+ });
+ });
+
+ // attach another engine to make sure it doesn't break upgrades
+ var e2 = eio.attach(engine.httpServer, { path: '/foo' });
+ });
+ });
+
+});
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-engine.io.git
More information about the Pkg-javascript-commits
mailing list