mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfebed38ab | ||
|
|
51782fc5d7 | ||
|
|
11f1a7c491 | ||
|
|
1d743cfc84 | ||
|
|
21c01558fd | ||
|
|
6d57445167 | ||
|
|
52f6a5b124 | ||
|
|
2c3c73f045 | ||
|
|
54fc513fc9 | ||
|
|
0b1e43cb87 | ||
|
|
c8dabb225c | ||
|
|
245dc12ade | ||
|
|
7a405232a5 | ||
|
|
00f7ca1d02 | ||
|
|
f1cea7e788 | ||
|
|
050fcf7a83 | ||
|
|
dfa350bea7 | ||
|
|
e5d5b99f0e | ||
|
|
ed7cedd78f | ||
|
|
a22eb70cfb | ||
|
|
2a81b25a5b | ||
|
|
7db146df47 | ||
|
|
6182dfff39 | ||
|
|
1ccd8cea6b | ||
|
|
f9ea04eb6b | ||
|
|
ab9a5a1578 | ||
|
|
3364a73a97 | ||
|
|
d02a7f415b | ||
|
|
1874fd7c30 | ||
|
|
3c4a04ea02 | ||
|
|
1468917743 | ||
|
|
6df152cc5d | ||
|
|
c6b3549b61 | ||
|
|
f80ab2aae8 | ||
|
|
b2f9f19d99 | ||
|
|
cb7304837c | ||
|
|
9e6f58fe27 | ||
|
|
e3fb39da3d | ||
|
|
cc275813b5 | ||
|
|
9d57245d65 | ||
|
|
9a05b3597e | ||
|
|
41f38b60e8 | ||
|
|
a9bbc38919 | ||
|
|
e282ab0e63 | ||
|
|
546d5203d4 | ||
|
|
69941e602b | ||
|
|
7ac9c2e888 | ||
|
|
fa1f50d173 | ||
|
|
20ddd5f11a | ||
|
|
e1891fd615 | ||
|
|
cc0a96a8d9 | ||
|
|
7b2b302022 | ||
|
|
4d66f78ca2 | ||
|
|
6db6db41a2 | ||
|
|
713baa40e1 | ||
|
|
7c196f5b32 | ||
|
|
bd360a15ef | ||
|
|
ae7f25332a | ||
|
|
63fc15d276 | ||
|
|
d88575dadf | ||
|
|
93c963e30f | ||
|
|
72a79e5cec | ||
|
|
140ed41907 | ||
|
|
12fc168516 | ||
|
|
2c3dc42ae8 | ||
|
|
48dadd8e10 | ||
|
|
ce4c46b37d | ||
|
|
c8938a99b2 | ||
|
|
b7998e815a | ||
|
|
444229a9dc | ||
|
|
b69dac6f4d | ||
|
|
94fdbadaec | ||
|
|
c4b23246b4 | ||
|
|
5186788969 | ||
|
|
169ad8245f | ||
|
|
71e013a197 | ||
|
|
7e60d37171 | ||
|
|
a9e9e64eab | ||
|
|
39ae8d4629 | ||
|
|
a075870308 | ||
|
|
186649102d | ||
|
|
bb2e100e7f | ||
|
|
2c5fa40c0d | ||
|
|
0b61eda84c | ||
|
|
23ba929f3f | ||
|
|
13647075f2 | ||
|
|
4c9414c4c1 | ||
|
|
ec88f95722 | ||
|
|
f377cd631e | ||
|
|
e41aab84f8 | ||
|
|
0a6c78cbb8 | ||
|
|
004130cb11 | ||
|
|
a300223122 | ||
|
|
0769c40368 | ||
|
|
355203afdb | ||
|
|
46bfcd0d83 | ||
|
|
46b2f86372 | ||
|
|
e5c86178f5 | ||
|
|
0c31d6eabf | ||
|
|
0e08d67e48 | ||
|
|
42904cb3d7 | ||
|
|
8efb1bc6e2 | ||
|
|
8bdf221935 | ||
|
|
899fb7faa1 | ||
|
|
34622b74ef | ||
|
|
d327976064 | ||
|
|
07b9c4696d | ||
|
|
dd30de3c5a | ||
|
|
7a5913b8a6 | ||
|
|
fbb268fbce | ||
|
|
a31c267e83 | ||
|
|
b82fd79f57 | ||
|
|
e269fcaf0d | ||
|
|
a8c61b0001 | ||
|
|
4708480e7d | ||
|
|
00b75759f1 | ||
|
|
f85ce74a1f | ||
|
|
30284944b1 | ||
|
|
65f1399a44 | ||
|
|
559d36601d | ||
|
|
894ec9f84e | ||
|
|
d86ffcf06d | ||
|
|
ab5beaff63 | ||
|
|
f0ef33b45f | ||
|
|
1fa158c663 | ||
|
|
a631f86b3b | ||
|
|
a4ec5aafa6 | ||
|
|
984639ba67 | ||
|
|
9923c1dee9 | ||
|
|
1b0a4849df | ||
|
|
4fc43f322f | ||
|
|
5c0f78ab02 | ||
|
|
d8c7060cc8 | ||
|
|
b0335b0a61 | ||
|
|
a1797ccd4b | ||
|
|
a1c997bc58 | ||
|
|
0b9b28d251 | ||
|
|
a79b2fa761 | ||
|
|
195eba74de | ||
|
|
3edebe5d61 | ||
|
|
b56389fbc8 | ||
|
|
203293db0b | ||
|
|
2b7ea448c4 | ||
|
|
c627f1b7d0 | ||
|
|
831f1baa4a | ||
|
|
4c20afd4b7 | ||
|
|
f689434f61 | ||
|
|
b694ee68c9 | ||
|
|
8b22ca2ffd | ||
|
|
5c50c4844f | ||
|
|
4fcad6e4bc | ||
|
|
3b2316e0d8 | ||
|
|
a821cce390 | ||
|
|
abe142ac66 | ||
|
|
5eff0e5ca7 | ||
|
|
c06242efd3 | ||
|
|
f69f387e1d | ||
|
|
f5c10aec7f | ||
|
|
34bd9d9092 | ||
|
|
c30151d03a | ||
|
|
f784c477f0 | ||
|
|
0d3441d8b3 | ||
|
|
23e14223bd | ||
|
|
4b94f2b8bf | ||
|
|
f88eedc3c8 | ||
|
|
5c24bf8c1d | ||
|
|
9cd51b1f6b | ||
|
|
2f8eb63557 | ||
|
|
4bdc30734c | ||
|
|
4743744efc | ||
|
|
c2d98bde72 | ||
|
|
c899c98f31 | ||
|
|
9e97e6c691 | ||
|
|
afdfdb815e | ||
|
|
d043c33351 | ||
|
|
53f0f4d66d | ||
|
|
168b207c6d | ||
|
|
f6ebb7b8d6 | ||
|
|
c826fadb9f | ||
|
|
dfebed6c2f | ||
|
|
3a2545b497 | ||
|
|
097094cd7a | ||
|
|
c3fa1bf5af | ||
|
|
8798cfbced | ||
|
|
81d71ebb08 | ||
|
|
074e74c6c5 | ||
|
|
65b8272724 | ||
|
|
ca3f3379cb | ||
|
|
faad10baee | ||
|
|
d3eac92eaa | ||
|
|
fb5b9bc0b1 | ||
|
|
34505071f4 | ||
|
|
0a2d0b9d0b | ||
|
|
4495f5987a | ||
|
|
aad29d5d92 | ||
|
|
ad8452035d | ||
|
|
ffa17e1205 | ||
|
|
59e250b186 | ||
|
|
b2ffed891b | ||
|
|
9bf10ed051 | ||
|
|
9cfdb8ed97 | ||
|
|
27ab98dca4 | ||
|
|
a8ca11cb47 | ||
|
|
d39d1401c4 | ||
|
|
159c75096d | ||
|
|
a70347b15f | ||
|
|
1a2c8aa31f | ||
|
|
15e1e68cfd | ||
|
|
167da44211 | ||
|
|
1372838092 | ||
|
|
7257e1ec36 | ||
|
|
af5960bc28 | ||
|
|
206cfdf9c6 | ||
|
|
65d7229079 | ||
|
|
c18aa40ba6 | ||
|
|
249f33da16 | ||
|
|
b9a7c8be90 | ||
|
|
e4ac72a316 | ||
|
|
dff9cbfe1b | ||
|
|
489bc860d2 | ||
|
|
29a8fff576 | ||
|
|
e66a68f0fa | ||
|
|
4058eacbd4 | ||
|
|
8cbd1544b9 | ||
|
|
f302744fec | ||
|
|
a3ba4e2c10 | ||
|
|
a483f9cafd | ||
|
|
b43c82f3db | ||
|
|
84f0099c1d | ||
|
|
c6a883c8a9 | ||
|
|
af2d9f285b | ||
|
|
40c1f8da2b | ||
|
|
9b40977616 | ||
|
|
964f040b58 | ||
|
|
44919189ec | ||
|
|
6c4cc5113f | ||
|
|
06e29fef67 | ||
|
|
16518c0715 | ||
|
|
09a713da18 | ||
|
|
3e0239ed07 | ||
|
|
d3d2a70944 | ||
|
|
1bcf87eb14 | ||
|
|
f9c210c11c | ||
|
|
06421dd008 | ||
|
|
af7c7141f0 | ||
|
|
def5498f61 | ||
|
|
494b2d4487 | ||
|
|
ba3b854190 | ||
|
|
abc3723b54 | ||
|
|
ba10280071 | ||
|
|
fc95d340ef | ||
|
|
d47bcd51ba | ||
|
|
be7f56b819 | ||
|
|
6a83d6879c | ||
|
|
56f5911b60 | ||
|
|
2fd38866d2 | ||
|
|
4398271ff5 | ||
|
|
6ce89b80ef | ||
|
|
1449b18681 | ||
|
|
f921164bf8 | ||
|
|
ea1f5822ae | ||
|
|
2fd406f7b2 | ||
|
|
34d0cbdbf0 | ||
|
|
2110bac72d | ||
|
|
299e097fa7 | ||
|
|
a1963fcf7d | ||
|
|
8abe712294 | ||
|
|
df21bea4df | ||
|
|
45f2718954 | ||
|
|
24aef9e40b | ||
|
|
a00ce7eee6 | ||
|
|
17dd94e3c9 | ||
|
|
81fe9e0117 | ||
|
|
c9bf814580 | ||
|
|
78b546e185 | ||
|
|
e7deb32a6f | ||
|
|
4e4199c15e | ||
|
|
fb79657476 | ||
|
|
f2212f6962 | ||
|
|
c9d21c5ff5 | ||
|
|
6b6f17b098 | ||
|
|
9740b42866 | ||
|
|
2a1b2bd37e | ||
|
|
b58c2e4afe | ||
|
|
a2f8fb4970 | ||
|
|
a4ef10d6a2 | ||
|
|
91e43064f3 | ||
|
|
76e6b9ea6f | ||
|
|
a517efcaca | ||
|
|
8f6860b155 | ||
|
|
72283a9078 | ||
|
|
c9d87ebb98 | ||
|
|
4a591f191f | ||
|
|
779816ded7 | ||
|
|
2b90edb3a2 | ||
|
|
a99016df79 | ||
|
|
78c7a55ce4 | ||
|
|
3fbce85315 | ||
|
|
9aa5320730 | ||
|
|
2fb28c8843 | ||
|
|
767ca4f100 | ||
|
|
733569ae68 | ||
|
|
f736302b97 | ||
|
|
7bfcf54f90 | ||
|
|
fa5ef9ff2d | ||
|
|
df457440fd | ||
|
|
375cbf49de | ||
|
|
2c66814b82 | ||
|
|
2a4e4e1300 | ||
|
|
6f12de98fc | ||
|
|
73ea4dd13f | ||
|
|
772afe897d | ||
|
|
0b75d09090 | ||
|
|
95e787dcc5 | ||
|
|
5342dd6d76 | ||
|
|
195e227393 | ||
|
|
c28a85e520 | ||
|
|
bf8b1e6879 | ||
|
|
240cf3fde6 | ||
|
|
d40cde8503 | ||
|
|
db9d81971d | ||
|
|
54d2c0111d | ||
|
|
cfd5315b1e | ||
|
|
cbc12ecdcf | ||
|
|
9aa650e430 | ||
|
|
f06fefab14 | ||
|
|
5b6efb784f | ||
|
|
c210241379 | ||
|
|
8594d684ef | ||
|
|
970621d5ee | ||
|
|
c9d9c2e8b3 | ||
|
|
8f44f026ee | ||
|
|
c369073a72 | ||
|
|
b700f0546d | ||
|
|
0701408061 | ||
|
|
bafb347e4a | ||
|
|
802da70bf7 | ||
|
|
f2711daa37 | ||
|
|
7200dfed84 | ||
|
|
b348f4de99 | ||
|
|
6452602603 | ||
|
|
6f93bb2ec7 | ||
|
|
ffd3e8bc96 | ||
|
|
bdea4b11a7 | ||
|
|
041b5655f9 | ||
|
|
080676bf6e | ||
|
|
2e7076abcc | ||
|
|
b87e51bae3 | ||
|
|
cc5bc44ef0 | ||
|
|
4672ab65d7 | ||
|
|
28b396c3fc | ||
|
|
808e794ec5 | ||
|
|
cb7aa0a79c | ||
|
|
1bddfc45dd | ||
|
|
836eb1d1c2 | ||
|
|
4c5dfd53f0 | ||
|
|
a62bced081 | ||
|
|
397dfbc51d |
142
History.md
142
History.md
@@ -1,4 +1,146 @@
|
||||
|
||||
0.8.5 / 2011-10-07
|
||||
==================
|
||||
|
||||
* Added websocket draft HyBi-16 support. [einaros]
|
||||
* Fixed websocket continuation bugs. [einaros]
|
||||
* Fixed flashsocket transport name.
|
||||
* Fixed websocket tests.
|
||||
* Ensured `parser#decodePayload` doesn't choke.
|
||||
* Added http referrer verification to manager verifyOrigin.
|
||||
* Added access control for cross domain xhr handshakes [3rd-Eden]
|
||||
* Added support for automatic generation of socket.io files [3rd-Eden]
|
||||
* Added websocket binary support [einaros]
|
||||
* Added gzip support for socket.io.js [3rd-Eden]
|
||||
* Expose socket.transport [3rd-Eden]
|
||||
* Updated client.
|
||||
|
||||
0.8.4 / 2011-09-06
|
||||
==================
|
||||
|
||||
* Client build
|
||||
|
||||
0.8.3 / 2011-09-03
|
||||
==================
|
||||
|
||||
* Fixed `\n` parsing for non-JSON packets (fixes #479).
|
||||
* Fixed parsing of certain unicode characters (fixes #451).
|
||||
* Fixed transport message packet logging.
|
||||
* Fixed emission of `error` event resulting in an uncaught exception if unhandled (fixes #476).
|
||||
* Fixed; allow for falsy values as the configuration value of `log level` (fixes #491).
|
||||
* Fixed repository URI in `package.json`. Fixes #504.
|
||||
* Added text/plain content-type to handshake responses [einaros]
|
||||
* Improved single byte writes [einaros]
|
||||
* Updated socket.io-flashsocket default port from 843 to 10843 [3rd-Eden]
|
||||
* Updated client.
|
||||
|
||||
0.8.2 / 2011-08-29
|
||||
==================
|
||||
|
||||
* Updated client.
|
||||
|
||||
0.8.1 / 2011-08-29
|
||||
==================
|
||||
|
||||
* Fixed utf8 bug in send framing in websocket [einaros]
|
||||
* Fixed typo in docs [Znarkus]
|
||||
* Fixed bug in send framing for over 64kB of data in websocket [einaros]
|
||||
* Corrected ping handling in websocket transport [einaros]
|
||||
|
||||
0.8.0 / 2011-08-28
|
||||
==================
|
||||
|
||||
* Updated to work with two-level websocket versioning. [einaros]
|
||||
* Added hybi07 support. [einaros]
|
||||
* Added hybi10 support. [einaros]
|
||||
* Added http referrer verification to manager.js verifyOrigin. [einaors]
|
||||
|
||||
0.7.11 / 2011-08-27
|
||||
===================
|
||||
|
||||
* Updated socket.io-client.
|
||||
|
||||
0.7.10 / 2011-08-27
|
||||
===================
|
||||
|
||||
* Updated socket.io-client.
|
||||
|
||||
0.7.9 / 2011-08-12
|
||||
==================
|
||||
|
||||
* Updated socket.io-client.
|
||||
* Make sure we only do garbage collection when the server we receive is actually run.
|
||||
|
||||
0.7.8 / 2011-08-08
|
||||
==================
|
||||
|
||||
* Changed; make sure sio#listen passes options to both HTTP server and socket.io manager.
|
||||
* Added docs for sio#listen.
|
||||
* Added options parameter support for Manager constructor.
|
||||
* Added memory leaks tests and test-leaks Makefile task.
|
||||
* Removed auto npm-linking from make test.
|
||||
* Make sure that you can disable heartbeats. [3rd-Eden]
|
||||
* Fixed rooms memory leak [3rd-Eden]
|
||||
* Send response once we got all POST data, not immediately [Pita]
|
||||
* Fixed onLeave behavior with missing clientsk [3rd-Eden]
|
||||
* Prevent duplicate references in rooms.
|
||||
* Added alias for `to` to `in` and `in` to `to`.
|
||||
* Fixed roomClients definition.
|
||||
* Removed dependency on redis for installation without npm [3rd-Eden]
|
||||
* Expose path and querystring in handshakeData [3rd-Eden]
|
||||
|
||||
0.7.7 / 2011-07-12
|
||||
==================
|
||||
|
||||
* Fixed double dispatch handling with emit to closed clients.
|
||||
* Added test for emitting to closed clients to prevent regression.
|
||||
* Fixed race condition in redis test.
|
||||
* Changed Transport#end instrumentation.
|
||||
* Leveraged $emit instead of emit internally.
|
||||
* Made tests faster.
|
||||
* Fixed double disconnect events.
|
||||
* Fixed disconnect logic
|
||||
* Simplified remote events handling in Socket.
|
||||
* Increased testcase timeout.
|
||||
* Fixed unknown room emitting (GH-291). [3rd-Eden]
|
||||
* Fixed `address` in handshakeData. [3rd-Eden]
|
||||
* Removed transports definition in chat example.
|
||||
* Fixed room cleanup
|
||||
* Fixed; make sure the client is cleaned up after booting.
|
||||
* Make sure to mark the client as non-open if the connection is closed.
|
||||
* Removed unneeded `buffer` declarations.
|
||||
* Fixed; make sure to clear socket handlers and subscriptions upon transport close.
|
||||
|
||||
0.7.6 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Fixed general dispatching when a client has closed.
|
||||
|
||||
0.7.5 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Fixed dispatching to clients that are disconnected.
|
||||
|
||||
0.7.4 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Fixed; only clear handlers if they were set. [level09]
|
||||
|
||||
0.7.3 / 2011-06-30
|
||||
==================
|
||||
|
||||
* Exposed handshake data to clients.
|
||||
* Refactored dispatcher interface.
|
||||
* Changed; Moved id generation method into the manager.
|
||||
* Added sub-namespace authorization. [3rd-Eden]
|
||||
* Changed; normalized SocketNamespace local eventing [dvv]
|
||||
* Changed; Use packet.reason or default to 'packet' [3rd-Eden]
|
||||
* Changed console.error to console.log.
|
||||
* Fixed; bind both servers at the same time do that the test never times out.
|
||||
* Added 304 support.
|
||||
* Removed `Transport#name` for abstract interface.
|
||||
* Changed; lazily require http and https module only when needed. [3rd-Eden]
|
||||
|
||||
0.7.2 / 2011-06-22
|
||||
==================
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -2,8 +2,8 @@
|
||||
ALL_TESTS = $(shell find test/ -name '*.test.js')
|
||||
|
||||
run-tests:
|
||||
@npm link > /dev/null --local
|
||||
@./node_modules/.bin/expresso \
|
||||
-t 3000 \
|
||||
-I support \
|
||||
-I lib \
|
||||
--serial \
|
||||
@@ -16,4 +16,7 @@ test:
|
||||
test-cov:
|
||||
@TESTFLAGS=--cov $(MAKE) test
|
||||
|
||||
test-leaks:
|
||||
@ls test/leaks/* | xargs node --expose_debug_as=debug --expose_gc
|
||||
|
||||
.PHONY: test
|
||||
|
||||
18
Readme.md
18
Readme.md
@@ -1,4 +1,3 @@
|
||||
|
||||
# Socket.IO
|
||||
|
||||
Socket.IO is a Node.JS project that makes WebSockets and realtime possible in
|
||||
@@ -21,7 +20,7 @@ Next, attach it to a HTTP/HTTPS server. If you're using the fantastic `express`
|
||||
web framework:
|
||||
|
||||
```js
|
||||
var app = express.createServer();
|
||||
var app = express.createServer()
|
||||
, io = io.listen(app);
|
||||
|
||||
app.listen(80);
|
||||
@@ -61,7 +60,7 @@ Besides `connect`, `message` and `disconnect`, you can emit custom events:
|
||||
var io = require('socket.io').listen(80);
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
io.sockets.emit('this', { will: 'be received by everyone');
|
||||
io.sockets.emit('this', { will: 'be received by everyone' });
|
||||
|
||||
socket.on('private message', function (from, msg) {
|
||||
console.log('I received a private message by ', from, ' saying ', msg);
|
||||
@@ -89,7 +88,7 @@ io.sockets.on('connection', function (socket) {
|
||||
});
|
||||
|
||||
socket.on('msg', function () {
|
||||
socket.get('nickname', function (name) {
|
||||
socket.get('nickname', function (err, name) {
|
||||
console.log('Chat message by ', name);
|
||||
});
|
||||
});
|
||||
@@ -149,9 +148,8 @@ var news = io
|
||||
|
||||
```html
|
||||
<script>
|
||||
var socket = io.connect('http://localhost/')
|
||||
, chat = socket.of('/chat')
|
||||
, news = socket.of('/news');
|
||||
var chat = io.connect('http://localhost/chat')
|
||||
, news = io.connect('http://localhost/news');
|
||||
|
||||
chat.on('connect', function () {
|
||||
chat.emit('hi!');
|
||||
@@ -224,7 +222,7 @@ io.sockets.on('connection', function (socket) {
|
||||
```html
|
||||
<script>
|
||||
var socket = io.connect(); // TIP: .connect with no args does auto-discovery
|
||||
socket.on('connection', function () {
|
||||
socket.on('connect', function () { // TIP: you can avoid listening on `connect` and listen on events directly too!
|
||||
socket.emit('ferret', 'tobi', function (data) {
|
||||
console.log(data); // data will be 'woot'
|
||||
});
|
||||
@@ -277,7 +275,7 @@ Simply leverage `send` and listen on the `message` event:
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
var io = require('socket.io-node').listen(80);
|
||||
var io = require('socket.io').listen(80);
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
socket.on('message', function () { });
|
||||
@@ -307,7 +305,7 @@ Configuration in socket.io is TJ-style:
|
||||
#### Server side
|
||||
|
||||
```js
|
||||
var io = require('socket.io-node').listen(80);
|
||||
var io = require('socket.io').listen(80);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('transports', ['websocket', 'flashsocket', 'xhr-polling']);
|
||||
|
||||
@@ -61,14 +61,6 @@ app.listen(3000, function () {
|
||||
var io = sio.listen(app)
|
||||
, nicknames = {};
|
||||
|
||||
io.set('transports', [
|
||||
'websocket'
|
||||
, 'flashsocket'
|
||||
, 'htmlfile'
|
||||
, 'xhr-polling'
|
||||
, 'jsonp-polling'
|
||||
]);
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
socket.on('user message', function (msg) {
|
||||
socket.broadcast.emit('user message', socket.nickname, msg);
|
||||
|
||||
@@ -74,7 +74,7 @@ Logger.prototype.log = function (type) {
|
||||
if (index > this.level)
|
||||
return this;
|
||||
|
||||
console.error.apply(
|
||||
console.log.apply(
|
||||
console
|
||||
, [this.colors
|
||||
? ' \033[' + colors[index] + 'm' + pad(type) + ' -\033[39m'
|
||||
|
||||
610
lib/manager.js
610
lib/manager.js
@@ -1,4 +1,3 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
@@ -9,9 +8,7 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var http = require('http')
|
||||
, https = require('https')
|
||||
, fs = require('fs')
|
||||
var fs = require('fs')
|
||||
, url = require('url')
|
||||
, util = require('./util')
|
||||
, store = require('./store')
|
||||
@@ -21,6 +18,7 @@ var http = require('http')
|
||||
, Socket = require('./socket')
|
||||
, MemoryStore = require('./stores/memory')
|
||||
, SocketNamespace = require('./namespace')
|
||||
, Static = require('./static')
|
||||
, EventEmitter = process.EventEmitter;
|
||||
|
||||
/**
|
||||
@@ -55,7 +53,7 @@ var parent = module.parent.exports
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Manager (server) {
|
||||
function Manager (server, options) {
|
||||
this.server = server;
|
||||
this.namespaces = {};
|
||||
this.sockets = this.of('');
|
||||
@@ -64,6 +62,7 @@ function Manager (server) {
|
||||
, log: true
|
||||
, store: new MemoryStore
|
||||
, logger: new Logger
|
||||
, static: new Static(this)
|
||||
, heartbeats: true
|
||||
, resource: '/socket.io'
|
||||
, transports: defaultTransports
|
||||
@@ -74,19 +73,33 @@ function Manager (server) {
|
||||
, 'heartbeat interval': 20
|
||||
, 'polling duration': 20
|
||||
, 'flash policy server': true
|
||||
, 'flash policy port': 843
|
||||
, 'flash policy port': 10843
|
||||
, 'destroy upgrade': true
|
||||
, 'browser client': true
|
||||
, 'browser client cache': true
|
||||
, 'browser client minification': false
|
||||
, 'browser client etag': false
|
||||
, 'browser client gzip': false
|
||||
, 'browser client handler': false
|
||||
, 'client store expiration': 15
|
||||
};
|
||||
|
||||
for (var i in options) {
|
||||
this.settings[i] = options[i];
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.initStore();
|
||||
|
||||
this.on('set:store', function() {
|
||||
self.initStore();
|
||||
});
|
||||
|
||||
// reset listeners
|
||||
this.oldListeners = server.listeners('request');
|
||||
server.removeAllListeners('request');
|
||||
|
||||
var self = this;
|
||||
|
||||
server.on('request', function (req, res) {
|
||||
self.handleRequest(req, res);
|
||||
});
|
||||
@@ -95,6 +108,14 @@ function Manager (server) {
|
||||
self.handleUpgrade(req, socket, head);
|
||||
});
|
||||
|
||||
server.on('close', function () {
|
||||
clearInterval(self.gc);
|
||||
});
|
||||
|
||||
server.once('listening', function () {
|
||||
self.gc = setInterval(self.garbageCollection.bind(self), 10000);
|
||||
});
|
||||
|
||||
for (var i in transports) {
|
||||
if (transports[i].init) {
|
||||
transports[i].init(this);
|
||||
@@ -128,11 +149,21 @@ Manager.prototype.__defineGetter__('log', function () {
|
||||
if (this.disabled('log')) return;
|
||||
|
||||
var logger = this.get('logger');
|
||||
logger.level = this.get('log level');
|
||||
logger.level = this.get('log level') || -1;
|
||||
|
||||
return logger;
|
||||
});
|
||||
|
||||
/**
|
||||
* Static accessor.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Manager.prototype.__defineGetter__('static', function () {
|
||||
return this.get('static');
|
||||
});
|
||||
|
||||
/**
|
||||
* Get settings.
|
||||
*
|
||||
@@ -216,6 +247,274 @@ Manager.prototype.configure = function (env, fn) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes everything related to the message dispatcher.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.initStore = function () {
|
||||
this.handshaken = {};
|
||||
this.connected = {};
|
||||
this.open = {};
|
||||
this.closed = {};
|
||||
this.closedA = [];
|
||||
this.rooms = {};
|
||||
this.roomClients = {};
|
||||
|
||||
var self = this;
|
||||
|
||||
this.store.subscribe('handshake', function (id, data) {
|
||||
self.onHandshake(id, data);
|
||||
});
|
||||
|
||||
this.store.subscribe('connect', function (id) {
|
||||
self.onConnect(id);
|
||||
});
|
||||
|
||||
this.store.subscribe('open', function (id) {
|
||||
self.onOpen(id);
|
||||
});
|
||||
|
||||
this.store.subscribe('join', function (id, room) {
|
||||
self.onJoin(id, room);
|
||||
});
|
||||
|
||||
this.store.subscribe('leave', function (id, room) {
|
||||
self.onLeave(id, room);
|
||||
});
|
||||
|
||||
this.store.subscribe('close', function (id) {
|
||||
self.onClose(id);
|
||||
});
|
||||
|
||||
this.store.subscribe('dispatch', function (room, packet, volatile, exceptions) {
|
||||
self.onDispatch(room, packet, volatile, exceptions);
|
||||
});
|
||||
|
||||
this.store.subscribe('disconnect', function (id) {
|
||||
self.onDisconnect(id);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client handshakes.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
|
||||
Manager.prototype.onHandshake = function (id, data) {
|
||||
this.handshaken[id] = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client connects (ie: transport first opens)
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onConnect = function (id) {
|
||||
this.connected[id] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client opens a request in a different node.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onOpen = function (id) {
|
||||
this.open[id] = true;
|
||||
|
||||
// if we were buffering messages for the client, clear them
|
||||
if (this.closed[id]) {
|
||||
var self = this;
|
||||
|
||||
this.closedA.splice(this.closedA.indexOf(id), 1);
|
||||
|
||||
this.store.unsubscribe('dispatch:' + id, function () {
|
||||
delete self.closed[id];
|
||||
});
|
||||
}
|
||||
|
||||
// clear the current transport
|
||||
if (this.transports[id]) {
|
||||
this.transports[id].discard();
|
||||
this.transports[id] = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a message is sent to a namespace and/or room.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onDispatch = function (room, packet, volatile, exceptions) {
|
||||
if (this.rooms[room]) {
|
||||
for (var i = 0, l = this.rooms[room].length; i < l; i++) {
|
||||
var id = this.rooms[room][i];
|
||||
|
||||
if (!~exceptions.indexOf(id)) {
|
||||
if (this.transports[id] && this.transports[id].open) {
|
||||
this.transports[id].onDispatch(packet, volatile);
|
||||
} else if (!volatile) {
|
||||
this.onClientDispatch(id, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client joins a nsp / room.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onJoin = function (id, name) {
|
||||
if (!this.roomClients[id]) {
|
||||
this.roomClients[id] = {};
|
||||
}
|
||||
|
||||
if (!this.rooms[name]) {
|
||||
this.rooms[name] = [];
|
||||
}
|
||||
|
||||
if (!~this.rooms[name].indexOf(id)) {
|
||||
this.rooms[name].push(id);
|
||||
this.roomClients[id][name] = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client leaves a nsp / room.
|
||||
*
|
||||
* @param private
|
||||
*/
|
||||
|
||||
Manager.prototype.onLeave = function (id, room) {
|
||||
if (this.rooms[room]) {
|
||||
var index = this.rooms[room].indexOf(id);
|
||||
|
||||
if (index >= 0) {
|
||||
this.rooms[room].splice(index, 1);
|
||||
}
|
||||
|
||||
if (!this.rooms[room].length) {
|
||||
delete this.rooms[room];
|
||||
}
|
||||
delete this.roomClients[id][room];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client closes a request in different node.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onClose = function (id) {
|
||||
if (this.open[id]) {
|
||||
delete this.open[id];
|
||||
}
|
||||
|
||||
this.closed[id] = [];
|
||||
this.closedA.push(id);
|
||||
|
||||
var self = this;
|
||||
|
||||
this.store.subscribe('dispatch:' + id, function (packet, volatile) {
|
||||
if (!volatile) {
|
||||
self.onClientDispatch(id, packet);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches a message for a closed client.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onClientDispatch = function (id, packet) {
|
||||
if (this.closed[id]) {
|
||||
this.closed[id].push(packet);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Receives a message for a client.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onClientMessage = function (id, packet) {
|
||||
if (this.namespaces[packet.endpoint]) {
|
||||
this.namespaces[packet.endpoint].handlePacket(id, packet);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fired when a client disconnects (not triggered).
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.onClientDisconnect = function (id, reason) {
|
||||
for (var name in this.namespaces) {
|
||||
if (this.roomClients[id][name]) {
|
||||
this.namespaces[name].handleDisconnect(id, reason);
|
||||
}
|
||||
}
|
||||
|
||||
this.onDisconnect(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client disconnects.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
|
||||
Manager.prototype.onDisconnect = function (id, local) {
|
||||
delete this.handshaken[id];
|
||||
|
||||
if (this.open[id]) {
|
||||
delete this.open[id];
|
||||
}
|
||||
|
||||
if (this.connected[id]) {
|
||||
delete this.connected[id];
|
||||
}
|
||||
|
||||
if (this.transports[id]) {
|
||||
this.transports[id].discard();
|
||||
delete this.transports[id];
|
||||
}
|
||||
|
||||
if (this.closed[id]) {
|
||||
delete this.closed[id];
|
||||
this.closedA.splice(this.closedA.indexOf(id), 1);
|
||||
}
|
||||
|
||||
if (this.roomClients[id]) {
|
||||
for (var room in this.roomClients[id]) {
|
||||
this.onLeave(id, room);
|
||||
}
|
||||
delete this.roomClients[id]
|
||||
}
|
||||
|
||||
this.store.destroyClient(id, this.get('client store expiration'));
|
||||
|
||||
this.store.unsubscribe('dispatch:' + id);
|
||||
|
||||
if (local) {
|
||||
this.store.unsubscribe('message:' + id);
|
||||
this.store.unsubscribe('disconnect:' + id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles an HTTP request.
|
||||
*
|
||||
@@ -235,7 +534,7 @@ Manager.prototype.handleRequest = function (req, res) {
|
||||
|
||||
if (data.static || !data.transport && !data.protocol) {
|
||||
if (data.static && this.enabled('browser client')) {
|
||||
this.handleClientRequest(req, res, data);
|
||||
this.static.write(data.path, req, res);
|
||||
} else {
|
||||
res.writeHead(200);
|
||||
res.end('Welcome to socket.io.');
|
||||
@@ -302,12 +601,15 @@ Manager.prototype.handleHTTPRequest = function (data, req, res) {
|
||||
|
||||
Manager.prototype.handleClient = function (data, req) {
|
||||
var socket = req.socket
|
||||
, newTransport = false
|
||||
, store = this.store
|
||||
, self = this;
|
||||
|
||||
if (undefined != data.query.disconnect) {
|
||||
self.log.debug('handling disconnection url');
|
||||
self.store.disconnect(data.id, true);
|
||||
if (this.transports[data.id] && this.transports[data.id].open) {
|
||||
this.transports[data.id].onForcedDisconnect();
|
||||
} else {
|
||||
this.store.publish('disconnect-force:' + data.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -317,124 +619,66 @@ Manager.prototype.handleClient = function (data, req) {
|
||||
return;
|
||||
}
|
||||
|
||||
var transport = new transports[data.transport](this, data);
|
||||
transport.pause();
|
||||
transport.request = req;
|
||||
var transport = new transports[data.transport](this, data, req)
|
||||
, handshaken = this.handshaken[data.id];
|
||||
|
||||
if (!transport.open) {
|
||||
this.log.debug('transport not writeable, not subscribing');
|
||||
return;
|
||||
}
|
||||
if (handshaken) {
|
||||
if (transport.open) {
|
||||
if (this.closed[data.id] && this.closed[data.id].length) {
|
||||
transport.payload(this.closed[data.id]);
|
||||
this.closed[data.id] = [];
|
||||
}
|
||||
|
||||
this.store.isHandshaken(data.id, function (err, handshaken) {
|
||||
if (err || !handshaken) {
|
||||
if (err) console.error(err);
|
||||
transport.error('client not handshaken', 'reconnect');
|
||||
return;
|
||||
this.onOpen(data.id);
|
||||
this.store.publish('open', data.id);
|
||||
this.transports[data.id] = transport;
|
||||
}
|
||||
|
||||
self.store.client(data.id).count(function (err, count) {
|
||||
transport.resume();
|
||||
if (!this.connected[data.id]) {
|
||||
this.onConnect(data.id);
|
||||
this.store.publish('connect', data.id);
|
||||
|
||||
if (count == 1) {
|
||||
// initialize the socket for all namespaces
|
||||
for (var i in self.namespaces) {
|
||||
var socket = self.namespaces[i].socket(data.id, true);
|
||||
// flag as used
|
||||
delete handshaken.issued;
|
||||
this.onHandshake(data.id, handshaken);
|
||||
this.store.publish('handshake', data.id, handshaken);
|
||||
|
||||
// echo back connect packet and fire connection event
|
||||
if (i === '') {
|
||||
self.namespaces[i].handlePacket(data.id, { type: 'connect' });
|
||||
}
|
||||
// initialize the socket for all namespaces
|
||||
for (var i in this.namespaces) {
|
||||
var socket = this.namespaces[i].socket(data.id, true);
|
||||
|
||||
// echo back connect packet and fire connection event
|
||||
if (i === '') {
|
||||
this.namespaces[i].handlePacket(data.id, { type: 'connect' });
|
||||
}
|
||||
|
||||
// handle packets for the client (all namespaces)
|
||||
self.store.on('message:' + data.id, function (packet) {
|
||||
self.handlePacket(data.id, packet);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dictionary for static file serving
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
this.store.subscribe('message:' + data.id, function (packet) {
|
||||
self.onClientMessage(data.id, packet);
|
||||
});
|
||||
|
||||
Manager.static = {
|
||||
cache:{}
|
||||
, paths: {
|
||||
'/static/flashsocket/WebSocketMain.swf': client.dist + '/WebSocketMain.swf'
|
||||
, '/static/flashsocket/WebSocketMainInsecure.swf': client.dist + '/WebSocketMainInsecure.swf'
|
||||
, '/socket.io.js': client.dist + '/socket.io.js'
|
||||
, '/socket.io.js.min': client.dist + '/socket.io.min.js'
|
||||
}
|
||||
, contentType: {
|
||||
'js': 'application/javascript'
|
||||
, 'swf': 'application/x-shockwave-flash'
|
||||
}
|
||||
, encoding:{
|
||||
'js': 'utf8'
|
||||
, 'swf': 'binary'
|
||||
this.store.subscribe('disconnect:' + data.id, function (reason) {
|
||||
self.onClientDisconnect(data.id, reason);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (transport.open) {
|
||||
transport.error('client not handshaken', 'reconnect');
|
||||
}
|
||||
|
||||
transport.discard();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Serves the client.
|
||||
* Generates a session id.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.handleClientRequest = function (req, res, data) {
|
||||
var static = Manager.static
|
||||
, extension = data.path.split('.').pop()
|
||||
, file = data.path + (this.enabled('browser client minification') && extension == 'js' ? '.min' : '')
|
||||
, location = static.paths[file]
|
||||
, cache = static.cache[file];
|
||||
|
||||
var self = this;
|
||||
|
||||
function serve () {
|
||||
var headers = {
|
||||
'Content-Type': static.contentType[extension]
|
||||
, 'Content-Length': cache.length
|
||||
};
|
||||
|
||||
if (self.enabled('browser client etag') && cache.Etag) {
|
||||
headers.Etag = cache.Etag;
|
||||
}
|
||||
|
||||
res.writeHead(200, headers);
|
||||
res.end(cache.content, cache.encoding);
|
||||
|
||||
self.log.debug('served static ' + data.path);
|
||||
}
|
||||
|
||||
if (this.get('browser client handler')) {
|
||||
this.get('browser client handler').call(this, req, res);
|
||||
} else if (!cache) {
|
||||
fs.readFile(location, function (err, data) {
|
||||
if (err) {
|
||||
res.writeHead(500);
|
||||
res.end('Error serving socket.io client.');
|
||||
|
||||
self.log.warn('Can\'t cache socket.io client, ' + err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
cache = Manager.static.cache[file] = {
|
||||
content: data
|
||||
, length: data.length
|
||||
, Etag: client.version
|
||||
, encoding: static.encoding[extension]
|
||||
};
|
||||
|
||||
serve();
|
||||
});
|
||||
} else {
|
||||
serve();
|
||||
}
|
||||
Manager.prototype.generateId = function () {
|
||||
return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString()
|
||||
+ Math.abs(Math.random() * Math.random() * Date.now() | 0).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -444,14 +688,18 @@ Manager.prototype.handleClientRequest = function (req, res, data) {
|
||||
*/
|
||||
|
||||
Manager.prototype.handleHandshake = function (data, req, res) {
|
||||
var self = this;
|
||||
var self = this
|
||||
, origin = req.headers.origin
|
||||
, headers = {
|
||||
'Content-Type': 'text/plain'
|
||||
};
|
||||
|
||||
function writeErr (status, message) {
|
||||
if (data.query.jsonp) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end('io.j[' + data.query.jsonp + '](new Error("' + message + '"));');
|
||||
} else {
|
||||
res.writeHead(status);
|
||||
res.writeHead(status, headers);
|
||||
res.end(message);
|
||||
}
|
||||
};
|
||||
@@ -466,38 +714,84 @@ Manager.prototype.handleHandshake = function (data, req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.authorize(data, function (err, authorized) {
|
||||
var handshakeData = this.handshakeData(data);
|
||||
|
||||
if (origin) {
|
||||
// https://developer.mozilla.org/En/HTTP_Access_Control
|
||||
headers['Access-Control-Allow-Origin'] = '*';
|
||||
|
||||
if (req.headers.cookie) {
|
||||
headers['Access-Control-Allow-Credentials'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
this.authorize(handshakeData, function (err, authorized, newData) {
|
||||
if (err) return error(err);
|
||||
|
||||
self.log.info('handshake ' + (authorized ? 'authorized' : 'unauthorized'));
|
||||
|
||||
if (authorized) {
|
||||
self.store.handshake(data, function (err, id) {
|
||||
if (err) return error(err);
|
||||
var id = self.generateId()
|
||||
, hs = [
|
||||
id
|
||||
, self.enabled('heartbeats') ? self.get('heartbeat timeout') || '' : ''
|
||||
, self.get('close timeout') || ''
|
||||
, self.transports(data).join(',')
|
||||
].join(':');
|
||||
|
||||
var hs = [
|
||||
id
|
||||
, self.get('heartbeat timeout') || ''
|
||||
, self.get('close timeout') || ''
|
||||
, self.transports(data).join(',')
|
||||
].join(':');
|
||||
if (data.query.jsonp) {
|
||||
hs = 'io.j[' + data.query.jsonp + '](' + JSON.stringify(hs) + ');';
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
} else {
|
||||
res.writeHead(200, headers);
|
||||
}
|
||||
|
||||
if (data.query.jsonp) {
|
||||
hs = 'io.j[' + data.query.jsonp + '](' + JSON.stringify(hs) + ');';
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
} else {
|
||||
res.writeHead(200);
|
||||
}
|
||||
res.end(hs);
|
||||
|
||||
res.end(hs);
|
||||
self.log.info('handshaken', id);
|
||||
});
|
||||
self.onHandshake(id, newData || handshakeData);
|
||||
self.store.publish('handshake', id, newData || handshakeData);
|
||||
|
||||
self.log.info('handshake authorized', id);
|
||||
} else {
|
||||
writeErr(403, 'handshake unauthorized');
|
||||
self.log.info('handshake unauthorized');
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets normalized handshake data
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.handshakeData = function (data) {
|
||||
var connection = data.request.connection
|
||||
, connectionAddress
|
||||
, date = new Date;
|
||||
|
||||
if (connection.remoteAddress) {
|
||||
connectionAddress = {
|
||||
address: connection.remoteAddress
|
||||
, port: connection.remotePort
|
||||
};
|
||||
} else if (connection.socket && connection.socket.remoteAddress) {
|
||||
connectionAddress = {
|
||||
address: connection.socket.remoteAddress
|
||||
, port: connection.socket.remotePort
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
headers: data.headers
|
||||
, address: connectionAddress
|
||||
, time: date.toString()
|
||||
, query: data.query
|
||||
, url: data.request.url
|
||||
, xdomain: !!data.request.headers.origin
|
||||
, secure: data.request.connection.secure
|
||||
, issued: +date
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies the origin of a request.
|
||||
*
|
||||
@@ -505,7 +799,7 @@ Manager.prototype.handleHandshake = function (data, req, res) {
|
||||
*/
|
||||
|
||||
Manager.prototype.verifyOrigin = function (request) {
|
||||
var origin = request.headers.origin
|
||||
var origin = request.headers.origin || request.headers.referer
|
||||
, origins = this.get('origins');
|
||||
|
||||
if (origin === 'null') origin = '*';
|
||||
@@ -517,14 +811,19 @@ Manager.prototype.verifyOrigin = function (request) {
|
||||
if (origin) {
|
||||
try {
|
||||
var parts = url.parse(origin);
|
||||
|
||||
return
|
||||
~origins.indexOf(parts.host + ':' + parts.port) ||
|
||||
~origins.indexOf(parts.host + ':*') ||
|
||||
var ok =
|
||||
~origins.indexOf(parts.hostname + ':' + parts.port) ||
|
||||
~origins.indexOf(parts.hostname + ':*') ||
|
||||
~origins.indexOf('*:' + parts.port);
|
||||
} catch (ex) {}
|
||||
if (!ok) this.log.warn('illegal origin: ' + origin);
|
||||
return ok;
|
||||
} catch (ex) {
|
||||
this.log.warn('error parsing origin');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.log.warn('origin missing from handshake, yet required by config');
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -613,7 +912,7 @@ Manager.prototype.checkRequest = function (req) {
|
||||
data.protocol = Number(pieces[1]);
|
||||
data.transport = pieces[2];
|
||||
data.id = pieces[3];
|
||||
data.static = !!Manager.static.paths[path];
|
||||
data.static = !!this.static.has(path);
|
||||
};
|
||||
|
||||
return data;
|
||||
@@ -624,6 +923,8 @@ Manager.prototype.checkRequest = function (req) {
|
||||
|
||||
/**
|
||||
* Declares a socket namespace
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Manager.prototype.of = function (nsp) {
|
||||
@@ -633,3 +934,26 @@ Manager.prototype.of = function (nsp) {
|
||||
|
||||
return this.namespaces[nsp] = new SocketNamespace(this, nsp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform garbage collection on long living objects and properties that cannot
|
||||
* be removed automatically.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Manager.prototype.garbageCollection = function () {
|
||||
// clean up unused handshakes
|
||||
var ids = Object.keys(this.handshaken)
|
||||
, i = ids.length
|
||||
, now = Date.now()
|
||||
, handshake;
|
||||
|
||||
while (i--) {
|
||||
handshake = this.handshaken[ids[i]];
|
||||
|
||||
if ('issued' in handshake && (now - handshake.issued) >= 3E4) {
|
||||
this.onDisconnect(ids[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
150
lib/namespace.js
150
lib/namespace.js
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
@@ -24,6 +23,7 @@ function SocketNamespace (mgr, name) {
|
||||
this.manager = mgr;
|
||||
this.name = name || '';
|
||||
this.sockets = {};
|
||||
this.auth = false;
|
||||
this.setFlags();
|
||||
};
|
||||
|
||||
@@ -33,6 +33,33 @@ function SocketNamespace (mgr, name) {
|
||||
|
||||
SocketNamespace.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Copies emit since we override it
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.$emit = EventEmitter.prototype.emit;
|
||||
|
||||
/**
|
||||
* Retrieves all clients as Socket instances as an array.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.clients = function (room) {
|
||||
var room = this.name + (room !== undefined ?
|
||||
'/' + room : '');
|
||||
|
||||
if (!this.manager.rooms[room]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.manager.rooms[room].map(function (id) {
|
||||
return this.socket(id);
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Access logger interface.
|
||||
*
|
||||
@@ -81,8 +108,8 @@ SocketNamespace.prototype.__defineGetter__('volatile', function () {
|
||||
* @api public
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.in = function (room) {
|
||||
this.flags.endpoint = (this.name === '' ? '' : (this.name + '/')) + room;
|
||||
SocketNamespace.prototype.in = SocketNamespace.prototype.to = function (room) {
|
||||
this.flags.endpoint = this.name + (room ? '/' + room : '');
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -126,20 +153,8 @@ SocketNamespace.prototype.packet = function (packet) {
|
||||
, exceptions = this.flags.exceptions
|
||||
, packet = parser.encodePacket(packet);
|
||||
|
||||
store.clients(this.flags.endpoint, function (clients) {
|
||||
clients.forEach(function (id) {
|
||||
if (~exceptions.indexOf(id)) {
|
||||
log.debug('ignoring packet to ', id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (volatile) {
|
||||
store.publish('volatile:' + id, packet);
|
||||
} else {
|
||||
store.client(id).publish(packet);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.manager.onDispatch(this.flags.endpoint, packet, volatile, exceptions);
|
||||
this.store.publish('dispatch', this.flags.endpoint, packet, volatile, exceptions);
|
||||
|
||||
this.setFlags();
|
||||
|
||||
@@ -166,8 +181,8 @@ SocketNamespace.prototype.send = function (data) {
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.emit = function (name) {
|
||||
if (name == 'connection' || name == 'newListener') {
|
||||
return EventEmitter.prototype.emit.apply(this, arguments);
|
||||
if (name == 'newListener') {
|
||||
return this.$emit.apply(this, arguments);
|
||||
}
|
||||
|
||||
return this.packet({
|
||||
@@ -192,6 +207,54 @@ SocketNamespace.prototype.socket = function (sid, readable) {
|
||||
return this.sockets[sid];
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets authorization for this namespace
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.authorization = function (fn) {
|
||||
this.auth = fn;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a socket disconnects entirely.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.handleDisconnect = function (sid, reason) {
|
||||
if (this.sockets[sid] && this.sockets[sid].readable) {
|
||||
this.sockets[sid].onDisconnect(reason);
|
||||
delete this.sockets[sid];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs authentication.
|
||||
*
|
||||
* @param Object client request data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
SocketNamespace.prototype.authorize = function (data, fn) {
|
||||
if (this.auth) {
|
||||
var self = this;
|
||||
|
||||
this.auth.call(this, data, function (err, authorized) {
|
||||
self.log.debug('client ' +
|
||||
(authorized ? '' : 'un') + 'authorized for ' + self.name);
|
||||
fn(err, authorized);
|
||||
});
|
||||
} else {
|
||||
this.log.debug('client authorized for ' + this.name);
|
||||
fn(null, true);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a packet.
|
||||
*
|
||||
@@ -212,13 +275,42 @@ SocketNamespace.prototype.handlePacket = function (sessid, packet) {
|
||||
});
|
||||
};
|
||||
|
||||
function error (err) {
|
||||
self.log.warn('handshake error ' + err + ' for ' + self.name);
|
||||
socket.packet({ type: 'error', reason: err });
|
||||
};
|
||||
|
||||
function connect () {
|
||||
self.manager.onJoin(sessid, self.name);
|
||||
self.store.publish('join', sessid, self.name);
|
||||
|
||||
// packet echo
|
||||
socket.packet({ type: 'connect' });
|
||||
|
||||
// emit connection event
|
||||
self.$emit('connection', socket);
|
||||
};
|
||||
|
||||
switch (packet.type) {
|
||||
case 'connect':
|
||||
this.store.join(sessid, this.name, function () {
|
||||
// packet echo
|
||||
socket.packet({ type: 'connect' });
|
||||
self.emit('connection', socket);
|
||||
});
|
||||
if (packet.endpoint == '') {
|
||||
connect();
|
||||
} else {
|
||||
var manager = this.manager
|
||||
, handshakeData = manager.handshaken[sessid];
|
||||
|
||||
this.authorize(handshakeData, function (err, authorized, newData) {
|
||||
if (err) return error(err);
|
||||
|
||||
if (authorized) {
|
||||
manager.onHandshake(sessid, newData || handshakeData);
|
||||
self.store.publish('handshake', sessid, newData || handshakeData);
|
||||
connect();
|
||||
} else {
|
||||
error('unauthorized');
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ack':
|
||||
@@ -232,14 +324,18 @@ SocketNamespace.prototype.handlePacket = function (sessid, packet) {
|
||||
case 'event':
|
||||
var params = [packet.name].concat(packet.args);
|
||||
|
||||
if (dataAck)
|
||||
if (dataAck) {
|
||||
params.push(ack);
|
||||
}
|
||||
|
||||
socket.$emit.apply(socket, params);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
socket.emit('disconnect');
|
||||
this.manager.onLeave(sessid, this.name);
|
||||
this.store.publish('leave', sessid, this.name);
|
||||
|
||||
socket.$emit('disconnect', packet.reason || 'packet');
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
@@ -249,6 +345,6 @@ SocketNamespace.prototype.handlePacket = function (sessid, packet) {
|
||||
if (dataAck)
|
||||
params.push(ack);
|
||||
|
||||
socket.emit.apply(socket, params);
|
||||
socket.$emit.apply(socket, params);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -138,7 +138,7 @@ exports.encodePayload = function (packets) {
|
||||
* @api private
|
||||
*/
|
||||
|
||||
var regexp = /^([^:]+):([0-9]+)?(\+)?:([^:]+)?:?(.*)?$/;
|
||||
var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/;
|
||||
|
||||
exports.decodePacket = function (data) {
|
||||
var pieces = data.match(regexp);
|
||||
@@ -223,6 +223,10 @@ exports.decodePacket = function (data) {
|
||||
*/
|
||||
|
||||
exports.decodePayload = function (data) {
|
||||
if (undefined == data || null == data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (data[0] == '\ufffd') {
|
||||
var ret = [];
|
||||
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var http = require('http')
|
||||
, https = require('https')
|
||||
, client = require('socket.io-client');
|
||||
var client = require('socket.io-client');
|
||||
|
||||
/**
|
||||
* Version.
|
||||
*/
|
||||
|
||||
exports.version = '0.7.2';
|
||||
exports.version = '0.8.5';
|
||||
|
||||
/**
|
||||
* Supported protocol version.
|
||||
@@ -34,6 +32,9 @@ exports.clientVersion = client.version;
|
||||
/**
|
||||
* Attaches a manager
|
||||
*
|
||||
* @param {HTTPServer/Number} a HTTP/S server or a port number to listen on.
|
||||
* @param {Object} opts to be passed to Manager and/or http server
|
||||
* @param {Function} callback if a port is supplied
|
||||
* @api public
|
||||
*/
|
||||
|
||||
@@ -53,9 +54,9 @@ exports.listen = function (server, options, fn) {
|
||||
var port = server;
|
||||
|
||||
if (options && options.key)
|
||||
server = https.createServer(options, server);
|
||||
server = require('https').createServer(options);
|
||||
else
|
||||
server = http.createServer();
|
||||
server = require('http').createServer();
|
||||
|
||||
// default response
|
||||
server.on('request', function (req, res) {
|
||||
@@ -67,7 +68,7 @@ exports.listen = function (server, options, fn) {
|
||||
}
|
||||
|
||||
// otherwise assume a http/s server
|
||||
return new exports.Manager(server);
|
||||
return new exports.Manager(server, options);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,6 +95,14 @@ exports.Transport = require('./transport');
|
||||
|
||||
exports.Socket = require('./socket');
|
||||
|
||||
/**
|
||||
* Static constructor.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.Static = require('./static');
|
||||
|
||||
/**
|
||||
* Store constructor.
|
||||
*
|
||||
@@ -102,6 +111,22 @@ exports.Socket = require('./socket');
|
||||
|
||||
exports.Store = require('./store');
|
||||
|
||||
/**
|
||||
* Memory Store constructor.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.MemoryStore = require('./stores/memory');
|
||||
|
||||
/**
|
||||
* Redis Store constructor.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.RedisStore = require('./stores/redis');
|
||||
|
||||
/**
|
||||
* Parser.
|
||||
*
|
||||
|
||||
135
lib/socket.js
135
lib/socket.js
@@ -11,7 +11,7 @@
|
||||
|
||||
var parser = require('./parser')
|
||||
, util = require('./util')
|
||||
, EventEmitter = process.EventEmitter;
|
||||
, EventEmitter = process.EventEmitter
|
||||
|
||||
/**
|
||||
* Export the constructor.
|
||||
@@ -20,20 +20,10 @@ var parser = require('./parser')
|
||||
exports = module.exports = Socket;
|
||||
|
||||
/**
|
||||
* Reserved event names.
|
||||
* Default error event listener to prevent uncaught exceptions.
|
||||
*/
|
||||
|
||||
var events = {
|
||||
message: 1
|
||||
, connect: 1
|
||||
, disconnect: 1
|
||||
, open: 1
|
||||
, close: 1
|
||||
, error: 1
|
||||
, retry: 1
|
||||
, reconnect: 1
|
||||
, newListener: 1
|
||||
};
|
||||
var defaultError = function () {};
|
||||
|
||||
/**
|
||||
* Socket constructor.
|
||||
@@ -53,14 +43,9 @@ function Socket (manager, id, nsp, readable) {
|
||||
this.ackPackets = 0;
|
||||
this.acks = {};
|
||||
this.setFlags();
|
||||
|
||||
if (readable) {
|
||||
var self = this;
|
||||
|
||||
this.store.once('disconnect:' + id, function (reason) {
|
||||
self.onDisconnect(reason);
|
||||
});
|
||||
}
|
||||
this.readable = readable;
|
||||
this.store = this.manager.store.client(this.id);
|
||||
this.on('error', defaultError);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -70,13 +55,23 @@ function Socket (manager, id, nsp, readable) {
|
||||
Socket.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Accessor shortcut for the store.
|
||||
* Accessor shortcut for the handshake data
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Socket.prototype.__defineGetter__('store', function () {
|
||||
return this.manager.store;
|
||||
Socket.prototype.__defineGetter__('handshake', function () {
|
||||
return this.manager.handshaken[this.id];
|
||||
});
|
||||
|
||||
/**
|
||||
* Accessor shortcut for the transport type
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Socket.prototype.__defineGetter__('transport', function () {
|
||||
return this.manager.transports[this.id].name;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -128,7 +123,7 @@ Socket.prototype.__defineGetter__('broadcast', function () {
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Socket.prototype.to = function (room) {
|
||||
Socket.prototype.to = Socket.prototype.in = function (room) {
|
||||
this.flags.room = room;
|
||||
return this;
|
||||
};
|
||||
@@ -155,7 +150,7 @@ Socket.prototype.setFlags = function () {
|
||||
|
||||
Socket.prototype.onDisconnect = function (reason) {
|
||||
if (!this.disconnected) {
|
||||
this.emit('disconnect', reason);
|
||||
this.$emit('disconnect', reason);
|
||||
this.disconnected = true;
|
||||
}
|
||||
};
|
||||
@@ -167,20 +162,38 @@ Socket.prototype.onDisconnect = function (reason) {
|
||||
*/
|
||||
|
||||
Socket.prototype.join = function (name, fn) {
|
||||
var nsp = this.namespace.name;
|
||||
this.store.join(this.id, (nsp === '' ? '' : (nsp + '/')) + name, fn);
|
||||
var nsp = this.namespace.name
|
||||
, name = (nsp + '/') + name;
|
||||
|
||||
this.manager.onJoin(this.id, name);
|
||||
this.manager.store.publish('join', this.id, name);
|
||||
|
||||
if (fn) {
|
||||
this.log.warn('Client#join callback is deprecated');
|
||||
fn();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Joins a user to a room.
|
||||
* Un-joins a user from a room.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Socket.prototype.leave = function (name, fn) {
|
||||
var nsp = this.namespace.name;
|
||||
this.store.leave(this.id, (nsp === '' ? '' : (nsp + '/')) + name, fn);
|
||||
var nsp = this.namespace.name
|
||||
, name = (nsp + '/') + name;
|
||||
|
||||
this.manager.onLeave(this.id, name);
|
||||
this.manager.store.publish('leave', this.id, name);
|
||||
|
||||
if (fn) {
|
||||
this.log.warn('Client#leave callback is deprecated');
|
||||
fn();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -198,11 +211,7 @@ Socket.prototype.packet = function (packet) {
|
||||
packet.endpoint = this.flags.endpoint;
|
||||
packet = parser.encodePacket(packet);
|
||||
|
||||
if (this.flags.volatile) {
|
||||
this.store.publish('volatile:' + this.id, packet);
|
||||
} else {
|
||||
this.store.client(this.id).publish(packet);
|
||||
}
|
||||
this.dispatch(packet, this.flags.volatile);
|
||||
}
|
||||
|
||||
this.setFlags();
|
||||
@@ -210,6 +219,24 @@ Socket.prototype.packet = function (packet) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches a packet
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Socket.prototype.dispatch = function (packet, volatile) {
|
||||
if (this.manager.transports[this.id] && this.manager.transports[this.id].open) {
|
||||
this.manager.transports[this.id].onDispatch(packet, volatile);
|
||||
} else {
|
||||
if (!volatile) {
|
||||
this.manager.onClientDispatch(this.id, packet, volatile);
|
||||
}
|
||||
|
||||
this.manager.store.publish('dispatch:' + this.id, packet, volatile);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores data for the client.
|
||||
*
|
||||
@@ -217,7 +244,7 @@ Socket.prototype.packet = function (packet) {
|
||||
*/
|
||||
|
||||
Socket.prototype.set = function (key, value, fn) {
|
||||
this.store.client(this.id).set(key, value, fn);
|
||||
this.store.set(key, value, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -228,7 +255,29 @@ Socket.prototype.set = function (key, value, fn) {
|
||||
*/
|
||||
|
||||
Socket.prototype.get = function (key, fn) {
|
||||
this.store.client(this.id).get(key, fn);
|
||||
this.store.get(key, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks data for the client
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Socket.prototype.has = function (key, fn) {
|
||||
this.store.has(key, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes data for the client
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Socket.prototype.del = function (key, fn) {
|
||||
this.store.del(key, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -241,7 +290,13 @@ Socket.prototype.get = function (key, fn) {
|
||||
Socket.prototype.disconnect = function () {
|
||||
if (!this.disconnected) {
|
||||
this.log.info('booting client');
|
||||
this.store.disconnect(this.id, true);
|
||||
|
||||
if (this.manager.transports[this.id] && this.manager.transports[this.id].open) {
|
||||
this.manager.transports[this.id].onForcedDisconnect();
|
||||
} else {
|
||||
this.manager.onClientDisconnect(this.id);
|
||||
this.manager.store.publish('disconnect:' + this.id);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -283,7 +338,7 @@ Socket.prototype.$emit = EventEmitter.prototype.emit;
|
||||
*/
|
||||
|
||||
Socket.prototype.emit = function (ev) {
|
||||
if (events[ev]) {
|
||||
if (ev == 'newListener') {
|
||||
return this.$emit.apply(this, arguments);
|
||||
}
|
||||
|
||||
|
||||
374
lib/static.js
Normal file
374
lib/static.js
Normal file
@@ -0,0 +1,374 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var client = require('socket.io-client')
|
||||
, cp = require('child_process')
|
||||
, fs = require('fs')
|
||||
, util = require('./util');
|
||||
|
||||
/**
|
||||
* File type details.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
var mime = {
|
||||
js: {
|
||||
type: 'application/javascript'
|
||||
, encoding: 'utf8'
|
||||
, gzip: true
|
||||
}
|
||||
, swf: {
|
||||
type: 'application/x-shockwave-flash'
|
||||
, encoding: 'binary'
|
||||
, gzip: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Regexp for matching custom transport patterns. Users can configure their own
|
||||
* socket.io bundle based on the url structure. Different transport names are
|
||||
* concatinated using the `+` char. /socket.io/socket.io+websocket.js should
|
||||
* create a bundle that only contains support for the websocket.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
var bundle = /\+((?:\+)?[\w\-]+)*(?:\.js)$/g;
|
||||
|
||||
/**
|
||||
* Export the constructor
|
||||
*/
|
||||
|
||||
exports = module.exports = Static;
|
||||
|
||||
/**
|
||||
* Static constructor
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Static (manager) {
|
||||
this.manager = manager;
|
||||
this.cache = {};
|
||||
this.paths = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Static by adding default file paths.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Static.prototype.init = function () {
|
||||
/**
|
||||
* Generates a unique id based the supplied transports array
|
||||
*
|
||||
* @param {Array} transports The array with transport types
|
||||
* @api private
|
||||
*/
|
||||
function id (transports) {
|
||||
var id = transports.join('').split('').map(function (char) {
|
||||
return ('' + char.charCodeAt(0)).split('').pop();
|
||||
}).reduce(function (char, id) {
|
||||
return char +id;
|
||||
});
|
||||
|
||||
return client.version + ':' + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a socket.io-client file based on the supplied transports.
|
||||
*
|
||||
* @param {Array} transports The array with transport types
|
||||
* @param {Function} callback Callback for the static.write
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function build (transports, callback) {
|
||||
client.builder(transports, {
|
||||
minify: self.manager.enabled('browser client minification')
|
||||
}, function (err, content) {
|
||||
callback(err, content ? new Buffer(content) : null, id(transports));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// add our default static files
|
||||
this.add('/static/flashsocket/WebSocketMain.swf', {
|
||||
file: client.dist + '/WebSocketMain.swf'
|
||||
});
|
||||
|
||||
this.add('/static/flashsocket/WebSocketMainInsecure.swf', {
|
||||
file: client.dist + '/WebSocketMainInsecure.swf'
|
||||
});
|
||||
|
||||
// generates dedicated build based on the available transports
|
||||
this.add('/socket.io.js', function (path, callback) {
|
||||
build(self.manager.get('transports'), callback);
|
||||
});
|
||||
|
||||
// allow custom builds based on url paths
|
||||
this.add('/socket.io+', { mime: mime.js }, function (path, callback) {
|
||||
var available = self.manager.get('transports')
|
||||
, matches = bundle.exec(path)
|
||||
, transports = [];
|
||||
|
||||
if (!matches) return callback('No valid transports');
|
||||
|
||||
// make sure they valid transports
|
||||
matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) {
|
||||
if (!!~available.indexOf(transport)) {
|
||||
transports.push(transport);
|
||||
}
|
||||
});
|
||||
|
||||
if (!transports.length) return callback('No valid transports');
|
||||
build(transports, callback);
|
||||
});
|
||||
|
||||
// clear cache when transports change
|
||||
this.manager.on('set:transports', function (key, value) {
|
||||
delete self.cache['/socket.io.js'];
|
||||
Object.keys(self.cache).forEach(function (key) {
|
||||
if (bundle.test(key)) {
|
||||
delete self.cache[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gzip compress buffers.
|
||||
*
|
||||
* @param {Buffer} data The buffer that needs gzip compression
|
||||
* @param {Function} callback
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Static.prototype.gzip = function (data, callback) {
|
||||
var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n'])
|
||||
, encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8'
|
||||
, buffer = []
|
||||
, err;
|
||||
|
||||
gzip.stdout.on('data', function (data) {
|
||||
buffer.push(data);
|
||||
});
|
||||
|
||||
gzip.stderr.on('data', function (data) {
|
||||
err = data +'';
|
||||
buffer.length = 0;
|
||||
});
|
||||
|
||||
gzip.on('exit', function () {
|
||||
if (err) return callback(err);
|
||||
|
||||
var size = 0
|
||||
, index = 0
|
||||
, i = buffer.length
|
||||
, content;
|
||||
|
||||
while (i--) {
|
||||
size += buffer[i].length;
|
||||
}
|
||||
|
||||
content = new Buffer(size);
|
||||
i = buffer.length;
|
||||
|
||||
buffer.forEach(function (buffer) {
|
||||
var length = buffer.length;
|
||||
|
||||
buffer.copy(content, index, 0, length);
|
||||
index += length;
|
||||
});
|
||||
|
||||
buffer.length = 0;
|
||||
callback(null, content);
|
||||
});
|
||||
|
||||
gzip.stdin.end(data, encoding);
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the path a static file?
|
||||
*
|
||||
* @param {String} path The path that needs to be checked
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Static.prototype.has = function (path) {
|
||||
// fast case
|
||||
if (this.paths[path]) return this.paths[path];
|
||||
|
||||
var keys = Object.keys(this.paths)
|
||||
, i = keys.length;
|
||||
|
||||
while (i--) {
|
||||
if (!!~path.indexOf(keys[i])) return this.paths[keys[i]];
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add new paths new paths that can be served using the static provider.
|
||||
*
|
||||
* @param {String} path The path to respond to
|
||||
* @param {Options} options Options for writing out the response
|
||||
* @param {Function} [callback] Optional callback if no options.file is
|
||||
* supplied this would be called instead.
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Static.prototype.add = function (path, options, callback) {
|
||||
var extension = /(?:\.(\w{1,4}))$/.exec(path);
|
||||
|
||||
if (!callback && typeof options == 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
options.mime = options.mime || (extension ? mime[extension[1]] : false);
|
||||
|
||||
if (callback) options.callback = callback;
|
||||
if (!(options.file || options.callback) || !options.mime) return false;
|
||||
|
||||
this.paths[path] = options;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a static response.
|
||||
*
|
||||
* @param {String} path The path for the static content
|
||||
* @param {HTTPRequest} req The request object
|
||||
* @param {HTTPResponse} res The response object
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Static.prototype.write = function (path, req, res) {
|
||||
/**
|
||||
* Write a response without throwing errors because can throw error if the
|
||||
* response is no longer writable etc.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function write (status, headers, content, encoding) {
|
||||
try {
|
||||
res.writeHead(status, headers || undefined);
|
||||
res.end(content || '', encoding || undefined);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers requests depending on the request properties and the reply object.
|
||||
*
|
||||
* @param {Object} reply The details and content to reply the response with
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function answer (reply) {
|
||||
var cached = req.headers['if-none-match'] === reply.etag;
|
||||
if (cached && self.manager.enabled('browser client etag')) {
|
||||
return write(304);
|
||||
}
|
||||
|
||||
var accept = req.headers['accept-encoding'] || ''
|
||||
, gzip = !!~accept.toLowerCase().indexOf('gzip')
|
||||
, mime = reply.mime
|
||||
, headers = {
|
||||
'Content-Type': mime.type
|
||||
};
|
||||
|
||||
// check if we can add a etag
|
||||
if (self.manager.enabled('browser client etag') && reply.etag) {
|
||||
headers['Etag'] = reply.etag;
|
||||
}
|
||||
|
||||
// check if we can send gzip data
|
||||
if (gzip && reply.gzip) {
|
||||
headers['Content-Length'] = reply.gzip.length;
|
||||
headers['Content-Encoding'] = 'gzip';
|
||||
write(200, headers, reply.gzip.content, mime.encoding);
|
||||
} else {
|
||||
headers['Content-Length'] = reply.length;
|
||||
write(200, headers, reply.content, mime.encoding);
|
||||
}
|
||||
|
||||
self.manager.log.debug('served static content ' + path);
|
||||
}
|
||||
|
||||
var self = this
|
||||
, details;
|
||||
|
||||
// most common case first
|
||||
if (this.manager.enabled('browser client cache') && this.cache[path]) {
|
||||
return answer(this.cache[path]);
|
||||
} else if (this.manager.get('browser client handler')) {
|
||||
return this.manager.get('browser client handler').call(this, req, res);
|
||||
} else if ((details = this.has(path))) {
|
||||
/**
|
||||
* A small helper function that will let us deal with fs and dynamic files
|
||||
*
|
||||
* @param {Object} err Optional error
|
||||
* @param {Buffer} content The data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function ready (err, content, etag) {
|
||||
if (err) {
|
||||
self.manager.log.warn('Unable to serve file. ' + (err.message || err));
|
||||
return write(500, null, 'Error serving static ' + path);
|
||||
}
|
||||
|
||||
// store the result in the cache
|
||||
var reply = self.cache[path] = {
|
||||
content: content
|
||||
, length: content.length
|
||||
, mime: details.mime
|
||||
, etag: etag || client.version
|
||||
};
|
||||
|
||||
// check if gzip is enabled
|
||||
if (details.mime.gzip && self.manager.enabled('browser client gzip')) {
|
||||
self.gzip(content, function (err, content) {
|
||||
if (!err) {
|
||||
reply.gzip = {
|
||||
content: content
|
||||
, length: content.length
|
||||
}
|
||||
}
|
||||
|
||||
answer(reply);
|
||||
});
|
||||
} else {
|
||||
answer(reply);
|
||||
}
|
||||
}
|
||||
|
||||
if (details.file) {
|
||||
fs.readFile(details.file, ready);
|
||||
} else if(details.callback) {
|
||||
details.callback.call(this, path, ready);
|
||||
} else {
|
||||
write(404, null, 'File handle not found');
|
||||
}
|
||||
} else {
|
||||
write(404, null, 'File not found');
|
||||
}
|
||||
};
|
||||
71
lib/store.js
71
lib/store.js
@@ -5,41 +5,86 @@
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var EventEmitter = process.EventEmitter;
|
||||
|
||||
/**
|
||||
* Expose the constructor.
|
||||
*/
|
||||
|
||||
exports = module.exports = Store;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var EventEmitter = process.EventEmitter;
|
||||
|
||||
/**
|
||||
* Store interface
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Store () {};
|
||||
function Store (options) {
|
||||
this.options = options;
|
||||
this.clients = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter
|
||||
* Inherit from EventEmitter.
|
||||
*/
|
||||
|
||||
Store.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Log accessor.
|
||||
* Initializes a client store
|
||||
*
|
||||
* @param {String} id
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Store.prototype.__defineGetter__('log', function () {
|
||||
return this.manager.log;
|
||||
});
|
||||
Store.prototype.client = function (id) {
|
||||
if (!this.clients[id]) {
|
||||
this.clients[id] = new (this.constructor.Client)(this, id);
|
||||
}
|
||||
|
||||
return this.clients[id];
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys a client
|
||||
*
|
||||
* @api {String} sid
|
||||
* @param {Number} number of seconds to expire client data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Store.prototype.destroyClient = function (id, expiration) {
|
||||
if (this.clients[id]) {
|
||||
this.clients[id].destroy(expiration);
|
||||
delete this.clients[id];
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the store
|
||||
*
|
||||
* @param {Number} number of seconds to expire client data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Store.prototype.destroy = function (clientExpiration) {
|
||||
var keys = Object.keys(this.clients)
|
||||
, count = keys.length;
|
||||
|
||||
for (var i = 0, l = count; i < l; i++) {
|
||||
this.destroyClient(keys[i], clientExpiration);
|
||||
}
|
||||
|
||||
this.clients = {};
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Client.
|
||||
@@ -50,6 +95,4 @@ Store.prototype.__defineGetter__('log', function () {
|
||||
Store.Client = function (store, id) {
|
||||
this.store = store;
|
||||
this.id = id;
|
||||
this.buffer = [];
|
||||
this.dict = {};
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
var crypto = require('crypto')
|
||||
, Store = require('../store')
|
||||
, Store = require('../store');
|
||||
|
||||
/**
|
||||
* Exports the constructor.
|
||||
@@ -26,9 +26,7 @@ Memory.Client = Client;
|
||||
*/
|
||||
|
||||
function Memory (opts) {
|
||||
this.handshaken = [];
|
||||
this.clientsMap = {};
|
||||
this.rooms = {};
|
||||
Store.call(this, opts);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,192 +36,28 @@ function Memory (opts) {
|
||||
Memory.prototype.__proto__ = Store.prototype;
|
||||
|
||||
/**
|
||||
* Handshake a client.
|
||||
*
|
||||
* @param {Object} client request object
|
||||
* @param {Function} callback
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.handshake = function (data, fn) {
|
||||
var id = this.generateId();
|
||||
this.handshaken.push(id);
|
||||
fn(null, id);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a client is handshaken.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.isHandshaken = function (id, fn) {
|
||||
fn(null, ~this.handshaken.indexOf(id));
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random id.
|
||||
* Publishes a message.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Memory.prototype.generateId = function () {
|
||||
var rand = String(Math.random() * Math.random() * Date.now());
|
||||
return crypto.createHash('md5').update(rand).digest('hex');
|
||||
};
|
||||
Memory.prototype.publish = function () { };
|
||||
|
||||
/**
|
||||
* Retrieves a client store instance.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.client = function (id) {
|
||||
if (!this.clientsMap[id]) {
|
||||
this.clientsMap[id] = new Memory.Client(this, id);
|
||||
this.log.debug('initializing client store for', id);
|
||||
}
|
||||
|
||||
return this.clientsMap[id];
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a client disconnects.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.disconnect = function (id, force, reason) {
|
||||
if (~this.handshaken.indexOf(id)) {
|
||||
this.log.debug('destroying dispatcher for', id);
|
||||
|
||||
this.handshaken.splice(this.handshaken.indexOf(id), 1);
|
||||
this.clientsMap[id].destroy();
|
||||
this.clientsMap[id] = null;
|
||||
|
||||
if (force)
|
||||
this.publish('disconnect-force:' + id, reason);
|
||||
|
||||
this.publish('disconnect:' + id, reason);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Relays a heartbeat message.
|
||||
* Subscribes to a channel
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Memory.prototype.heartbeat = function (id) {
|
||||
return this.publish('heartbeat-clear:' + id);
|
||||
};
|
||||
Memory.prototype.subscribe = function () { };
|
||||
|
||||
/**
|
||||
* Relays a packet
|
||||
* Unsubscribes
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Memory.prototype.message = function (id, packet) {
|
||||
return this.publish('message:' + id, packet);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns client ids in a particular room
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.clients = function (room, fn) {
|
||||
if ('function' == typeof room) {
|
||||
fn = room;
|
||||
room = '';
|
||||
}
|
||||
|
||||
fn && fn(this.rooms[room] || []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Joins a user to a room
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Memory.prototype.join = function (sid, room, fn) {
|
||||
if (!this.rooms[room]) {
|
||||
this.rooms[room] = [];
|
||||
}
|
||||
|
||||
this.client(sid).rooms[room] = this.rooms[room].length;
|
||||
this.rooms[room].push(sid);
|
||||
|
||||
fn && fn();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a user from a room
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Memory.prototype.leave = function (sid, room, fn) {
|
||||
if (!this.rooms[room] || this.client(sid).rooms[room] === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var i = this.client(sid).rooms[room];
|
||||
this.rooms[room][i] = null;
|
||||
delete this.client(sid).rooms[room];
|
||||
|
||||
fn && fn();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple publish
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.publish = function (ev, data, fn) {
|
||||
if ('function' == typeof data) {
|
||||
fn = data;
|
||||
data = undefined;
|
||||
}
|
||||
|
||||
this.emit(ev, data);
|
||||
if (fn) fn();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple subscribe
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.subscribe = function (chn, fn) {
|
||||
this.on(chn, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple unsubscribe
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Memory.prototype.unsubscribe = function (chn) {
|
||||
this.removeAllListeners(chn);
|
||||
};
|
||||
Memory.prototype.unsubscribe = function () { };
|
||||
|
||||
/**
|
||||
* Client constructor
|
||||
@@ -233,9 +67,7 @@ Memory.prototype.unsubscribe = function (chn) {
|
||||
|
||||
function Client () {
|
||||
Store.Client.apply(this, arguments);
|
||||
this.reqs = 0;
|
||||
this.paused = true;
|
||||
this.rooms = {};
|
||||
this.data = {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -244,73 +76,6 @@ function Client () {
|
||||
|
||||
Client.prototype.__proto__ = Store.Client;
|
||||
|
||||
/**
|
||||
* Counts transport requests.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.count = function (fn) {
|
||||
fn(null, ++this.reqs);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up queue consumption
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.consume = function (fn) {
|
||||
this.consumer = fn;
|
||||
this.paused = false;
|
||||
|
||||
if (this.buffer.length) {
|
||||
fn(this.buffer, null);
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Publishes a message to be sent to the client.
|
||||
*
|
||||
* @String encoded message
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.publish = function (msg) {
|
||||
if (this.paused) {
|
||||
this.buffer.push(msg);
|
||||
} else {
|
||||
this.consumer(null, msg);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pauses the stream.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.pause = function () {
|
||||
this.paused = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the client.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.destroy = function () {
|
||||
this.buffer = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a key
|
||||
*
|
||||
@@ -318,7 +83,7 @@ Client.prototype.destroy = function () {
|
||||
*/
|
||||
|
||||
Client.prototype.get = function (key, fn) {
|
||||
fn(null, this.dict[key]);
|
||||
fn(null, this.data[key] === undefined ? null : this.data[key]);
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -329,18 +94,50 @@ Client.prototype.get = function (key, fn) {
|
||||
*/
|
||||
|
||||
Client.prototype.set = function (key, value, fn) {
|
||||
this.dict[key] = value;
|
||||
this.data[key] = value;
|
||||
fn && fn(null);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits a message incoming from client.
|
||||
* Has a key
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.has = function (key, fn) {
|
||||
fn(null, key in this.data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a key
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Client.prototype.del = function (key, fn) {
|
||||
delete this.data[key];
|
||||
fn && fn(null);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the client.
|
||||
*
|
||||
* @param {Number} number of seconds to expire data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Client.prototype.onMessage = function (msg) {
|
||||
this.store.emit('message:' + id, msg);
|
||||
};
|
||||
Client.prototype.destroy = function (expiration) {
|
||||
if ('number' != typeof expiration) {
|
||||
this.data = {};
|
||||
} else {
|
||||
var self = this;
|
||||
|
||||
setTimeout(function () {
|
||||
self.data = {};
|
||||
}, expiration * 1000);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
266
lib/stores/redis.js
Normal file
266
lib/stores/redis.js
Normal file
@@ -0,0 +1,266 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var crypto = require('crypto')
|
||||
, Store = require('../store')
|
||||
, assert = require('assert');
|
||||
|
||||
/**
|
||||
* Exports the constructor.
|
||||
*/
|
||||
|
||||
exports = module.exports = Redis;
|
||||
Redis.Client = Client;
|
||||
|
||||
/**
|
||||
* Redis store.
|
||||
* Options:
|
||||
* - nodeId (fn) gets an id that uniquely identifies this node
|
||||
* - redis (fn) redis constructor, defaults to redis
|
||||
* - redisPub (object) options to pass to the pub redis client
|
||||
* - redisSub (object) options to pass to the sub redis client
|
||||
* - redisClient (object) options to pass to the general redis client
|
||||
* - pack (fn) custom packing, defaults to JSON or msgpack if installed
|
||||
* - unpack (fn) custom packing, defaults to JSON or msgpack if installed
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Redis (opts) {
|
||||
opts = opts || {};
|
||||
|
||||
// node id to uniquely identify this node
|
||||
var nodeId = opts.nodeId || function () {
|
||||
// by default, we generate a random id
|
||||
return Math.abs(Math.random() * Math.random() * Date.now() | 0);
|
||||
};
|
||||
|
||||
this.nodeId = nodeId();
|
||||
|
||||
// packing / unpacking mechanism
|
||||
if (opts.pack) {
|
||||
this.pack = opts.pack;
|
||||
this.unpack = opts.unpack;
|
||||
} else {
|
||||
try {
|
||||
var msgpack = require('msgpack');
|
||||
this.pack = msgpack.pack;
|
||||
this.unpack = msgpack.unpack;
|
||||
} catch (e) {
|
||||
this.pack = JSON.stringify;
|
||||
this.unpack = JSON.parse;
|
||||
}
|
||||
}
|
||||
|
||||
var redis = opts.redis || require('redis')
|
||||
, RedisClient = redis.RedisClient;
|
||||
|
||||
// initialize a pubsub client and a regular client
|
||||
if (opts.redisPub instanceof RedisClient) {
|
||||
this.pub = opts.redisPub;
|
||||
} else {
|
||||
opts.redisPub || (opts.redisPub = {});
|
||||
this.pub = redis.createClient(opts.redisPub.port, opts.redisPub.host, opts.redisPub);
|
||||
}
|
||||
if (opts.redisSub instanceof RedisClient) {
|
||||
this.sub = opts.redisSub;
|
||||
} else {
|
||||
opts.redisSub || (opts.redisSub = {});
|
||||
this.sub = redis.createClient(opts.redisSub.port, opts.redisSub.host, opts.redisSub);
|
||||
}
|
||||
if (opts.redisClient instanceof RedisClient) {
|
||||
this.cmd = opts.redisClient;
|
||||
} else {
|
||||
opts.redisClient || (opts.redisClient = {});
|
||||
this.cmd = redis.createClient(opts.redisClient.port, opts.redisClient.host, opts.redisClient);
|
||||
}
|
||||
|
||||
Store.call(this, opts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from Store.
|
||||
*/
|
||||
|
||||
Redis.prototype.__proto__ = Store.prototype;
|
||||
|
||||
/**
|
||||
* Publishes a message.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Redis.prototype.publish = function (name) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
this.pub.publish(name, this.pack({ nodeId: this.nodeId, args: args }));
|
||||
this.emit.apply(this, ['publish', name].concat(args));
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to a channel
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Redis.prototype.subscribe = function (name, consumer, fn) {
|
||||
this.sub.subscribe(name);
|
||||
|
||||
if (consumer || fn) {
|
||||
var self = this;
|
||||
|
||||
self.sub.on('subscribe', function subscribe (ch) {
|
||||
if (name == ch) {
|
||||
function message (ch, msg) {
|
||||
if (name == ch) {
|
||||
msg = self.unpack(msg);
|
||||
|
||||
// we check that the message consumed wasnt emitted by this node
|
||||
if (self.nodeId != msg.nodeId) {
|
||||
consumer.apply(null, msg.args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.sub.on('message', message);
|
||||
|
||||
self.on('unsubscribe', function unsubscribe (ch) {
|
||||
if (name == ch) {
|
||||
self.sub.removeListener('message', message);
|
||||
self.removeListener('unsubscribe', unsubscribe);
|
||||
}
|
||||
});
|
||||
|
||||
self.sub.removeListener('subscribe', subscribe);
|
||||
|
||||
fn && fn();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('subscribe', name, consumer, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribes
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Redis.prototype.unsubscribe = function (name, fn) {
|
||||
this.sub.unsubscribe(name);
|
||||
|
||||
if (fn) {
|
||||
var client = this.sub;
|
||||
|
||||
client.on('unsubscribe', function unsubscribe (ch) {
|
||||
if (name == ch) {
|
||||
fn();
|
||||
client.removeListener('unsubscribe', unsubscribe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('unsubscribe', name, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the store
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Redis.prototype.destroy = function () {
|
||||
Store.prototype.destroy.call(this);
|
||||
|
||||
this.pub.end();
|
||||
this.sub.end();
|
||||
this.cmd.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* Client constructor
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function Client (store, id) {
|
||||
Store.Client.call(this, store, id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from Store.Client
|
||||
*/
|
||||
|
||||
Client.prototype.__proto__ = Store.Client;
|
||||
|
||||
/**
|
||||
* Redis hash get
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Client.prototype.get = function (key, fn) {
|
||||
this.store.cmd.hget(this.id, key, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Redis hash set
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Client.prototype.set = function (key, value, fn) {
|
||||
this.store.cmd.hset(this.id, key, value, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Redis hash del
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Client.prototype.del = function (key, fn) {
|
||||
this.store.cmd.hdel(this.id, key, fn);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Redis hash has
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Client.prototype.has = function (key, fn) {
|
||||
this.store.cmd.hexists(this.id, key, function (err, has) {
|
||||
if (err) return fn(err);
|
||||
fn(null, !!has);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys client
|
||||
*
|
||||
* @param {Number} number of seconds to expire data
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Client.prototype.destroy = function (expiration) {
|
||||
if ('number' != typeof expiration) {
|
||||
this.store.cmd.del(this.id);
|
||||
} else {
|
||||
this.store.cmd.expire(this.id, expiration);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
244
lib/transport.js
244
lib/transport.js
@@ -23,22 +23,13 @@ exports = module.exports = Transport;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Transport (mng, data) {
|
||||
function Transport (mng, data, req) {
|
||||
this.manager = mng;
|
||||
this.id = data.id;
|
||||
this.paused = true;
|
||||
this.disconnected = false;
|
||||
this.drained = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the corresponding request object.
|
||||
*/
|
||||
|
||||
Transport.prototype.__defineSetter__('request', function (req) {
|
||||
this.log.debug('setting request', req.method, req.url);
|
||||
this.handleRequest(req);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Access the logger.
|
||||
@@ -67,29 +58,14 @@ Transport.prototype.__defineGetter__('store', function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.handleRequest = function (req) {
|
||||
this.log.debug('setting request', req.method, req.url);
|
||||
this.req = req;
|
||||
|
||||
if (req.method == 'GET') {
|
||||
this.socket = req.socket;
|
||||
this.open = true;
|
||||
this.drained = true;
|
||||
|
||||
this.log.debug('publishing that', this.id, 'connected');
|
||||
|
||||
var self = this;
|
||||
|
||||
this.store.publish('open:' + this.id, function () {
|
||||
self.store.once('open:' + self.id, function () {
|
||||
self.log.info('request for existing session connection change');
|
||||
self.close();
|
||||
self.clearTimeouts();
|
||||
self.clearHandlers();
|
||||
});
|
||||
|
||||
if (!self.paused) {
|
||||
self.subscribe();
|
||||
}
|
||||
});
|
||||
this.setHeartbeatInterval();
|
||||
|
||||
this.setHandlers();
|
||||
this.onSocketConnect();
|
||||
@@ -113,17 +89,18 @@ Transport.prototype.onSocketConnect = function () { };
|
||||
Transport.prototype.setHandlers = function () {
|
||||
var self = this;
|
||||
|
||||
this.store.once('disconnect-force:' + this.id, function () {
|
||||
// we need to do this in a pub/sub way since the client can POST the message
|
||||
// over a different socket (ie: different Transport instance)
|
||||
this.store.subscribe('heartbeat-clear:' + this.id, function () {
|
||||
self.onHeartbeatClear();
|
||||
});
|
||||
|
||||
this.store.subscribe('disconnect-force:' + this.id, function () {
|
||||
self.onForcedDisconnect();
|
||||
});
|
||||
|
||||
this.store.on('heartbeat-clear:' + this.id, function () {
|
||||
self.clearHeartbeatTimeout();
|
||||
self.setHeartbeatInterval();
|
||||
});
|
||||
|
||||
this.store.on('volatile:' + this.id, function (packet) {
|
||||
self.writeVolatile(packet);
|
||||
this.store.subscribe('dispatch:' + this.id, function (packet, volatile) {
|
||||
self.onDispatch(packet, volatile);
|
||||
});
|
||||
|
||||
this.bound = {
|
||||
@@ -137,6 +114,8 @@ Transport.prototype.setHandlers = function () {
|
||||
this.socket.on('close', this.bound.close);
|
||||
this.socket.on('error', this.bound.error);
|
||||
this.socket.on('drain', this.bound.drain);
|
||||
|
||||
this.handlersSet = true;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -146,14 +125,16 @@ Transport.prototype.setHandlers = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.clearHandlers = function () {
|
||||
this.store.unsubscribe('disconnect-force:' + this.id);
|
||||
this.store.unsubscribe('heartbeat-clear:' + this.id);
|
||||
this.store.unsubscribe('volatile:' + this.id);
|
||||
if (this.handlersSet) {
|
||||
this.store.unsubscribe('disconnect-force:' + this.id);
|
||||
this.store.unsubscribe('heartbeat-clear:' + this.id);
|
||||
this.store.unsubscribe('dispatch:' + this.id);
|
||||
|
||||
this.socket.removeListener('end', this.bound.end);
|
||||
this.socket.removeListener('close', this.bound.close);
|
||||
this.socket.removeListener('error', this.bound.error);
|
||||
this.socket.removeListener('drain', this.bound.drain);
|
||||
this.socket.removeListener('end', this.bound.end);
|
||||
this.socket.removeListener('close', this.bound.close);
|
||||
this.socket.removeListener('error', this.bound.error);
|
||||
this.socket.removeListener('drain', this.bound.drain);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -163,10 +144,7 @@ Transport.prototype.clearHandlers = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.onSocketEnd = function () {
|
||||
// we check that the socket wasn't swapped
|
||||
// we don't want to sever a connection that's not active, since we don't kill
|
||||
// inactive sockets that the browser might reuse for other purposes
|
||||
this.end(false, 'socket end');
|
||||
this.end('socket end');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -176,7 +154,7 @@ Transport.prototype.onSocketEnd = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.onSocketClose = function (error) {
|
||||
this.end(false, error ? 'socket error' : 'socket close');
|
||||
this.end(error ? 'socket error' : 'socket close');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -204,6 +182,17 @@ Transport.prototype.onSocketDrain = function () {
|
||||
this.drained = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called upon receiving a heartbeat packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Transport.prototype.onHeartbeatClear = function () {
|
||||
this.clearHeartbeatTimeout();
|
||||
this.setHeartbeatInterval();
|
||||
};
|
||||
|
||||
/**
|
||||
* Called upon a forced disconnection.
|
||||
*
|
||||
@@ -216,7 +205,21 @@ Transport.prototype.onForcedDisconnect = function () {
|
||||
if (this.open) {
|
||||
this.packet({ type: 'disconnect' });
|
||||
}
|
||||
this.end(true);
|
||||
this.end('booted');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches a packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Transport.prototype.onDispatch = function (packet, volatile) {
|
||||
if (volatile) {
|
||||
this.writeVolatile(packet);
|
||||
} else {
|
||||
this.write(packet);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,7 +234,7 @@ Transport.prototype.setCloseTimeout = function () {
|
||||
this.closeTimeout = setTimeout(function () {
|
||||
self.log.debug('fired close timeout for client', self.id);
|
||||
self.closeTimeout = null;
|
||||
self.end(false, 'close timeout');
|
||||
self.end('close timeout');
|
||||
}, this.manager.get('close timeout') * 1000);
|
||||
|
||||
this.log.debug('set close timeout for client', this.id);
|
||||
@@ -256,13 +259,13 @@ Transport.prototype.clearCloseTimeout = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.setHeartbeatTimeout = function () {
|
||||
if (!this.heartbeatTimeout) {
|
||||
if (!this.heartbeatTimeout && this.manager.enabled('heartbeats')) {
|
||||
var self = this;
|
||||
|
||||
this.heartbeatTimeout = setTimeout(function () {
|
||||
self.log.debug('fired heartbeat timeout for client', self.id);
|
||||
self.heartbeatTimeout = null;
|
||||
self.end(false, 'heartbeat timeout');
|
||||
self.end('heartbeat timeout');
|
||||
}, this.manager.get('heartbeat timeout') * 1000);
|
||||
|
||||
this.log.debug('set heartbeat timeout for client', this.id);
|
||||
@@ -276,7 +279,7 @@ Transport.prototype.setHeartbeatTimeout = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.clearHeartbeatTimeout = function () {
|
||||
if (this.heartbeatTimeout) {
|
||||
if (this.heartbeatTimeout && this.manager.enabled('heartbeats')) {
|
||||
clearTimeout(this.heartbeatTimeout);
|
||||
this.heartbeatTimeout = null;
|
||||
this.log.debug('cleared heartbeat timeout for client', this.id);
|
||||
@@ -291,11 +294,12 @@ Transport.prototype.clearHeartbeatTimeout = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.setHeartbeatInterval = function () {
|
||||
if (!this.heartbeatTimeout) {
|
||||
if (!this.heartbeatInterval && this.manager.enabled('heartbeats')) {
|
||||
var self = this;
|
||||
|
||||
this.heartbeatInterval = setTimeout(function () {
|
||||
self.heartbeat();
|
||||
self.heartbeatInterval = null;
|
||||
}, this.manager.get('heartbeat interval') * 1000);
|
||||
|
||||
this.log.debug('set heartbeat interval for client', this.id);
|
||||
@@ -338,25 +342,52 @@ Transport.prototype.heartbeat = function () {
|
||||
*/
|
||||
|
||||
Transport.prototype.onMessage = function (packet) {
|
||||
var current = this.manager.transports[this.id];
|
||||
|
||||
if ('heartbeat' == packet.type) {
|
||||
this.log.debug('got heartbeat packet');
|
||||
this.store.heartbeat(this.id);
|
||||
} else if ('disconnect' == packet.type && packet.endpoint == '') {
|
||||
this.log.debug('got disconnection packet');
|
||||
this.store.disconnect(this.id, true);
|
||||
|
||||
if (current && current.open) {
|
||||
current.onHeartbeatClear();
|
||||
} else {
|
||||
this.store.publish('heartbeat-clear:' + this.id);
|
||||
}
|
||||
} else {
|
||||
this.log.debug('got packet');
|
||||
if ('disconnect' == packet.type && packet.endpoint == '') {
|
||||
this.log.debug('got disconnection packet');
|
||||
|
||||
if (current) {
|
||||
current.onForcedDisconnect();
|
||||
} else {
|
||||
this.store.publish('disconnect-force:' + this.id);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.id && packet.ack != 'data') {
|
||||
this.log.debug('acknowledging packet automatically');
|
||||
this.store.client(this.id).publish(parser.encodePacket({
|
||||
|
||||
var ack = parser.encodePacket({
|
||||
type: 'ack'
|
||||
, ackId: packet.id
|
||||
, endpoint: packet.endpoint || ''
|
||||
}));
|
||||
});
|
||||
|
||||
if (current && current.open) {
|
||||
current.onDispatch(ack);
|
||||
} else {
|
||||
this.manager.onClientDispatch(this.id, ack);
|
||||
this.store.publish('dispatch:' + this.id, ack);
|
||||
}
|
||||
}
|
||||
|
||||
this.store.message(this.id, packet);
|
||||
// handle packet locally or publish it
|
||||
if (current) {
|
||||
this.manager.onClientMessage(this.id, packet);
|
||||
} else {
|
||||
this.store.publish('message:' + this.id, packet);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -367,7 +398,7 @@ Transport.prototype.onMessage = function (packet) {
|
||||
*/
|
||||
|
||||
Transport.prototype.clearHeartbeatInterval = function () {
|
||||
if (this.heartbeatInterval) {
|
||||
if (this.heartbeatInterval && this.manager.enabled('heartbeats')) {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
this.log.debug('cleared heartbeat interval for client', this.id);
|
||||
@@ -382,7 +413,7 @@ Transport.prototype.clearHeartbeatInterval = function () {
|
||||
|
||||
Transport.prototype.disconnect = function (reason) {
|
||||
this.packet({ type: 'disconnect' });
|
||||
this.end(false, reason);
|
||||
this.end(reason);
|
||||
|
||||
return this;
|
||||
};
|
||||
@@ -409,8 +440,10 @@ Transport.prototype.close = function () {
|
||||
Transport.prototype.onClose = function () {
|
||||
if (this.open) {
|
||||
this.setCloseTimeout();
|
||||
this.unsubscribe();
|
||||
this.clearHandlers();
|
||||
this.open = false;
|
||||
this.manager.onClose(this.id);
|
||||
this.store.publish('close', this.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -420,43 +453,36 @@ Transport.prototype.onClose = function () {
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Transport.prototype.end = function (forced, reason) {
|
||||
Transport.prototype.end = function (reason) {
|
||||
if (!this.disconnected) {
|
||||
this.log.info('ending socket');
|
||||
this.log.info('transport end');
|
||||
|
||||
var local = this.manager.transports[this.id];
|
||||
|
||||
this.close();
|
||||
this.clearTimeouts();
|
||||
|
||||
if (!forced)
|
||||
this.store.disconnect(this.id, false, reason);
|
||||
|
||||
this.disconnected = true;
|
||||
|
||||
if (local) {
|
||||
this.manager.onClientDisconnect(this.id, reason, true);
|
||||
} else {
|
||||
this.store.publish('disconnect:' + this.id, reason);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Signals that the transport can start flushing buffers.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Transport.prototype.resume = function () {
|
||||
if (!this.disconnected) {
|
||||
this.paused = false;
|
||||
this.setHeartbeatInterval();
|
||||
this.subscribe();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Signals that the transport should pause and buffer data.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Transport.prototype.pause = function () {
|
||||
this.paused = true;
|
||||
Transport.prototype.discard = function () {
|
||||
this.log.debug('discarding transport');
|
||||
this.discarded = true;
|
||||
this.clearTimeouts();
|
||||
this.clearHandlers();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -476,7 +502,7 @@ Transport.prototype.error = function (reason, advice) {
|
||||
});
|
||||
|
||||
this.log.warn(reason, advice ? ('client should ' + advice) : '');
|
||||
this.end(false, 'error');
|
||||
this.end('error');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -489,44 +515,6 @@ Transport.prototype.packet = function (obj) {
|
||||
return this.write(parser.encodePacket(obj));
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe client.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Transport.prototype.subscribe = function () {
|
||||
if (!this.subscribed) {
|
||||
this.log.debug('subscribing', this.id);
|
||||
|
||||
var self = this;
|
||||
|
||||
// subscribe to buffered + normal messages
|
||||
this.store.client(this.id).consume(function (payload, packet) {
|
||||
if (payload) {
|
||||
self.payload(payload);
|
||||
} else {
|
||||
self.write(packet);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribed = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribe client.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Transport.prototype.unsubscribe = function () {
|
||||
this.log.info('unsubscribing', this.id);
|
||||
|
||||
this.store.client(this.id).pause();
|
||||
this.subscribed = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a volatile message.
|
||||
*
|
||||
|
||||
@@ -23,8 +23,8 @@ exports = module.exports = FlashSocket;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function FlashSocket () {
|
||||
WebSocket.apply(this, arguments);
|
||||
function FlashSocket (mng, data, req) {
|
||||
return WebSocket.call(this, mng, data, req);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,6 +33,14 @@ function FlashSocket () {
|
||||
|
||||
FlashSocket.prototype.__proto__ = WebSocket.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
FlashSocket.prototype.name = 'flashsocket';
|
||||
|
||||
/**
|
||||
* Listens for new configuration changes of the Manager
|
||||
* this way we can enable and disable the flash server.
|
||||
|
||||
@@ -23,8 +23,8 @@ exports = module.exports = HTMLFile;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function HTMLFile (mng, data) {
|
||||
HTTPTransport.call(this, mng, data);
|
||||
function HTMLFile (mng, data, req) {
|
||||
HTTPTransport.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +33,14 @@ function HTMLFile (mng, data) {
|
||||
|
||||
HTMLFile.prototype.__proto__ = HTTPTransport.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
HTMLFile.prototype.name = 'htmlfile';
|
||||
|
||||
/**
|
||||
* Handles the request.
|
||||
*
|
||||
@@ -70,5 +78,5 @@ HTMLFile.prototype.write = function (data) {
|
||||
this.drained = true;
|
||||
}
|
||||
|
||||
this.log.debug('htmlfile writing', data);
|
||||
this.log.debug(this.name + ' writing', data);
|
||||
};
|
||||
|
||||
@@ -23,8 +23,8 @@ exports = module.exports = HTTPPolling;
|
||||
* @api public.
|
||||
*/
|
||||
|
||||
function HTTPPolling (mng, data) {
|
||||
HTTPTransport.call(this, mng, data);
|
||||
function HTTPPolling (mng, data, req) {
|
||||
HTTPTransport.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,6 +35,14 @@ function HTTPPolling (mng, data) {
|
||||
|
||||
HTTPPolling.prototype.__proto__ = HTTPTransport.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
HTTPPolling.prototype.name = 'httppolling';
|
||||
|
||||
/**
|
||||
* Removes heartbeat timeouts for polling.
|
||||
*/
|
||||
@@ -57,7 +65,7 @@ HTTPPolling.prototype.handleRequest = function (req) {
|
||||
|
||||
this.pollTimeout = setTimeout(function () {
|
||||
self.packet({ type: 'noop' });
|
||||
self.log.debug('polling closed due to exceeded duration');
|
||||
self.log.debug(self.name + ' closed due to exceeded duration');
|
||||
}, this.manager.get('polling duration') * 1000);
|
||||
|
||||
this.log.debug('setting poll timeout');
|
||||
|
||||
@@ -25,8 +25,8 @@ exports = module.exports = HTTPTransport;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function HTTPTransport (mng, data) {
|
||||
Transport.call(this, mng, data);
|
||||
function HTTPTransport (mng, data, req) {
|
||||
Transport.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -54,6 +54,9 @@ HTTPTransport.prototype.handleRequest = function (req) {
|
||||
});
|
||||
|
||||
req.on('end', function () {
|
||||
res.writeHead(200, headers);
|
||||
res.end('1');
|
||||
|
||||
self.onData(self.postEncoded ? qs.parse(buffer).d : buffer);
|
||||
});
|
||||
|
||||
@@ -65,9 +68,6 @@ HTTPTransport.prototype.handleRequest = function (req) {
|
||||
headers['Access-Control-Allow-Credentials'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, headers);
|
||||
res.end('1');
|
||||
} else {
|
||||
this.response = req.res;
|
||||
|
||||
@@ -83,9 +83,9 @@ HTTPTransport.prototype.handleRequest = function (req) {
|
||||
|
||||
HTTPTransport.prototype.onData = function (data) {
|
||||
var messages = parser.decodePayload(data);
|
||||
this.log.debug(this.name + ' received data packet', data);
|
||||
|
||||
for (var i = 0, l = messages.length; i < l; i++) {
|
||||
this.log.debug('xhr received data packet', data);
|
||||
this.onMessage(messages[i]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,8 +23,8 @@ exports = module.exports = JSONPPolling;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function JSONPPolling (mng, data) {
|
||||
HTTPPolling.call(this, mng, data);
|
||||
function JSONPPolling (mng, data, req) {
|
||||
HTTPPolling.call(this, mng, data, req);
|
||||
|
||||
this.head = 'io.j[0](';
|
||||
this.foot = ');';
|
||||
@@ -40,6 +40,14 @@ function JSONPPolling (mng, data) {
|
||||
|
||||
JSONPPolling.prototype.__proto__ = HTTPPolling.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
JSONPPolling.prototype.name = 'jsonppolling';
|
||||
|
||||
/**
|
||||
* Make sure POST are decoded.
|
||||
*/
|
||||
@@ -62,8 +70,9 @@ JSONPPolling.prototype.doWrite = function (data) {
|
||||
'Content-Type': 'text/javascript; charset=UTF-8'
|
||||
, 'Content-Length': Buffer.byteLength(data)
|
||||
, 'Connection': 'Keep-Alive'
|
||||
, 'X-XSS-Protection': '0'
|
||||
});
|
||||
|
||||
this.response.write(data);
|
||||
this.log.debug('json-p writing', data);
|
||||
this.log.debug(this.name + ' writing', data);
|
||||
};
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
* Module requirements.
|
||||
*/
|
||||
|
||||
var Transport = require('../transport')
|
||||
, EventEmitter = process.EventEmitter
|
||||
, crypto = require('crypto')
|
||||
, parser = require('../parser');
|
||||
var protocolVersions = require('./websocket/');
|
||||
|
||||
/**
|
||||
* Export the constructor.
|
||||
@@ -27,316 +24,13 @@ exports = module.exports = WebSocket;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function WebSocket (mng, data) {
|
||||
// parser
|
||||
var self = this;
|
||||
|
||||
this.parser = new Parser();
|
||||
this.parser.on('data', function (packet) {
|
||||
self.log.debug('websocket received data packet', packet);
|
||||
self.onMessage(parser.decodePacket(packet));
|
||||
});
|
||||
this.parser.on('close', function () {
|
||||
self.end();
|
||||
});
|
||||
this.parser.on('error', function () {
|
||||
self.end();
|
||||
});
|
||||
|
||||
Transport.call(this, mng, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from Transport.
|
||||
*/
|
||||
|
||||
WebSocket.prototype.__proto__ = Transport.prototype;
|
||||
|
||||
/**
|
||||
* Called when the socket connects.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.onSocketConnect = function () {
|
||||
var self = this;
|
||||
|
||||
this.socket.setNoDelay(true);
|
||||
|
||||
this.buffer = true;
|
||||
this.buffered = [];
|
||||
|
||||
if (this.req.headers.upgrade !== 'WebSocket') {
|
||||
this.log.warn('WebSocket connection invalid');
|
||||
this.end();
|
||||
return;
|
||||
function WebSocket (mng, data, req) {
|
||||
var transport
|
||||
, version = req.headers['sec-websocket-version'];
|
||||
if (typeof version !== 'undefined' && typeof protocolVersions[version] !== 'undefined') {
|
||||
transport = new protocolVersions[version](mng, data, req);
|
||||
}
|
||||
|
||||
var origin = this.req.headers.origin
|
||||
, location = (this.socket.encrypted ? 'wss' : 'ws')
|
||||
+ '://' + this.req.headers.host + this.req.url
|
||||
, waitingForNonce = false;
|
||||
|
||||
if (this.req.headers['sec-websocket-key1']) {
|
||||
// If we don't have the nonce yet, wait for it (HAProxy compatibility).
|
||||
if (! (this.req.head && this.req.head.length >= 8)) {
|
||||
waitingForNonce = true;
|
||||
}
|
||||
|
||||
var headers = [
|
||||
'HTTP/1.1 101 WebSocket Protocol Handshake'
|
||||
, 'Upgrade: WebSocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'Sec-WebSocket-Origin: ' + origin
|
||||
, 'Sec-WebSocket-Location: ' + location
|
||||
];
|
||||
|
||||
if (this.req.headers['sec-websocket-protocol']){
|
||||
headers.push('Sec-WebSocket-Protocol: '
|
||||
+ this.req.headers['sec-websocket-protocol']);
|
||||
}
|
||||
} else {
|
||||
var headers = [
|
||||
'HTTP/1.1 101 Web Socket Protocol Handshake'
|
||||
, 'Upgrade: WebSocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'WebSocket-Origin: ' + origin
|
||||
, 'WebSocket-Location: ' + location
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
this.socket.write(headers.concat('', '').join('\r\n'));
|
||||
this.socket.setTimeout(0);
|
||||
this.socket.setNoDelay(true);
|
||||
this.socket.setEncoding('utf8');
|
||||
} catch (e) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingForNonce) {
|
||||
this.socket.setEncoding('binary');
|
||||
} else if (this.proveReception(headers)) {
|
||||
self.flush();
|
||||
}
|
||||
|
||||
var headBuffer = '';
|
||||
|
||||
this.socket.on('data', function (data) {
|
||||
if (waitingForNonce) {
|
||||
headBuffer += data;
|
||||
|
||||
if (headBuffer.length < 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the connection to utf8 encoding after receiving the nonce
|
||||
self.socket.setEncoding('utf8');
|
||||
waitingForNonce = false;
|
||||
|
||||
// Stuff the nonce into the location where it's expected to be
|
||||
self.req.head = headBuffer.substr(0, 8);
|
||||
headBuffer = '';
|
||||
|
||||
if (self.proveReception(headers)) {
|
||||
self.flush();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self.parser.add(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes to the socket.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.write = function (data) {
|
||||
if (this.open) {
|
||||
this.drained = false;
|
||||
|
||||
if (this.buffer) {
|
||||
this.buffered.push(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
var length = Buffer.byteLength(data)
|
||||
, buffer = new Buffer(2 + length);
|
||||
|
||||
buffer.write('\u0000', 'binary');
|
||||
buffer.write(data, 1, 'utf8');
|
||||
buffer.write('\uffff', 1 + length, 'binary');
|
||||
|
||||
try {
|
||||
if (this.socket.write(buffer)) {
|
||||
this.drained = true;
|
||||
}
|
||||
} catch (e) {
|
||||
this.end();
|
||||
}
|
||||
|
||||
this.log.debug('websocket writing', data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Flushes the internal buffer
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.flush = function () {
|
||||
this.buffer = false;
|
||||
|
||||
for (var i = 0, l = this.buffered.length; i < l; i++) {
|
||||
this.write(this.buffered.splice(0, 1));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finishes the handshake.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.proveReception = function (headers) {
|
||||
var self = this
|
||||
, k1 = this.req.headers['sec-websocket-key1']
|
||||
, k2 = this.req.headers['sec-websocket-key2'];
|
||||
|
||||
if (k1 && k2){
|
||||
var md5 = crypto.createHash('md5');
|
||||
|
||||
[k1, k2].forEach(function (k) {
|
||||
var n = parseInt(k.replace(/[^\d]/g, ''))
|
||||
, spaces = k.replace(/[^ ]/g, '').length;
|
||||
|
||||
if (spaces === 0 || n % spaces !== 0){
|
||||
self.log.warn('Invalid WebSocket key: "' + k + '".');
|
||||
self.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
n /= spaces;
|
||||
|
||||
md5.update(String.fromCharCode(
|
||||
n >> 24 & 0xFF,
|
||||
n >> 16 & 0xFF,
|
||||
n >> 8 & 0xFF,
|
||||
n & 0xFF));
|
||||
});
|
||||
|
||||
md5.update(this.req.head.toString('binary'));
|
||||
|
||||
try {
|
||||
this.socket.write(md5.digest('binary'), 'binary');
|
||||
} catch (e) {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a payload.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.payload = function (msgs) {
|
||||
for (var i = 0, l = msgs.length; i < l; i++) {
|
||||
this.write(msgs[i]);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.doClose = function () {
|
||||
this.socket.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket parser
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Parser () {
|
||||
this.buffer = '';
|
||||
this.i = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
Parser.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Adds data to the buffer.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Parser.prototype.add = function (data) {
|
||||
this.buffer += data;
|
||||
this.parse();
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the buffer.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.parse = function () {
|
||||
for (var i = this.i, chr, l = this.buffer.length; i < l; i++){
|
||||
chr = this.buffer[i];
|
||||
|
||||
if (this.buffer.length == 2 && this.buffer[1] == '\u0000') {
|
||||
this.emit('close');
|
||||
this.buffer = '';
|
||||
this.i = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (i === 0){
|
||||
if (chr != '\u0000')
|
||||
this.error('Bad framing. Expected null byte as first frame');
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chr == '\ufffd'){
|
||||
this.emit('data', this.buffer.substr(1, i - 1));
|
||||
this.buffer = this.buffer.substr(i + 1);
|
||||
this.i = 0;
|
||||
return this.parse();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.error = function (reason) {
|
||||
this.buffer = '';
|
||||
this.i = 0;
|
||||
this.emit('error', reason);
|
||||
return this;
|
||||
else transport = new protocolVersions['default'](mng, data, req);
|
||||
if (typeof this.name !== 'undefined') transport.name = this.name;
|
||||
return transport;
|
||||
};
|
||||
|
||||
358
lib/transports/websocket/default.js
Normal file
358
lib/transports/websocket/default.js
Normal file
@@ -0,0 +1,358 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module requirements.
|
||||
*/
|
||||
|
||||
var Transport = require('../../transport')
|
||||
, EventEmitter = process.EventEmitter
|
||||
, crypto = require('crypto')
|
||||
, parser = require('../../parser');
|
||||
|
||||
/**
|
||||
* Export the constructor.
|
||||
*/
|
||||
|
||||
exports = module.exports = WebSocket;
|
||||
|
||||
/**
|
||||
* HTTP interface constructor. Interface compatible with all transports that
|
||||
* depend on request-response cycles.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function WebSocket (mng, data, req) {
|
||||
// parser
|
||||
var self = this;
|
||||
|
||||
this.parser = new Parser();
|
||||
this.parser.on('data', function (packet) {
|
||||
self.log.debug(self.name + ' received data packet', packet);
|
||||
self.onMessage(parser.decodePacket(packet));
|
||||
});
|
||||
this.parser.on('close', function () {
|
||||
self.end();
|
||||
});
|
||||
this.parser.on('error', function () {
|
||||
self.end();
|
||||
});
|
||||
|
||||
Transport.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from Transport.
|
||||
*/
|
||||
|
||||
WebSocket.prototype.__proto__ = Transport.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.name = 'websocket';
|
||||
|
||||
/**
|
||||
* Websocket draft version
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.protocolVersion = 'hixie-76';
|
||||
|
||||
/**
|
||||
* Called when the socket connects.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.onSocketConnect = function () {
|
||||
var self = this;
|
||||
|
||||
this.socket.setNoDelay(true);
|
||||
|
||||
this.buffer = true;
|
||||
this.buffered = [];
|
||||
|
||||
if (this.req.headers.upgrade !== 'WebSocket') {
|
||||
this.log.warn(this.name + ' connection invalid');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = this.req.headers.origin
|
||||
, location = (this.socket.encrypted ? 'wss' : 'ws')
|
||||
+ '://' + this.req.headers.host + this.req.url
|
||||
, waitingForNonce = false;
|
||||
|
||||
if (this.req.headers['sec-websocket-key1']) {
|
||||
// If we don't have the nonce yet, wait for it (HAProxy compatibility).
|
||||
if (! (this.req.head && this.req.head.length >= 8)) {
|
||||
waitingForNonce = true;
|
||||
}
|
||||
|
||||
var headers = [
|
||||
'HTTP/1.1 101 WebSocket Protocol Handshake'
|
||||
, 'Upgrade: WebSocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'Sec-WebSocket-Origin: ' + origin
|
||||
, 'Sec-WebSocket-Location: ' + location
|
||||
];
|
||||
|
||||
if (this.req.headers['sec-websocket-protocol']){
|
||||
headers.push('Sec-WebSocket-Protocol: '
|
||||
+ this.req.headers['sec-websocket-protocol']);
|
||||
}
|
||||
} else {
|
||||
var headers = [
|
||||
'HTTP/1.1 101 Web Socket Protocol Handshake'
|
||||
, 'Upgrade: WebSocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'WebSocket-Origin: ' + origin
|
||||
, 'WebSocket-Location: ' + location
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
this.socket.write(headers.concat('', '').join('\r\n'));
|
||||
this.socket.setTimeout(0);
|
||||
this.socket.setNoDelay(true);
|
||||
this.socket.setEncoding('utf8');
|
||||
} catch (e) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingForNonce) {
|
||||
this.socket.setEncoding('binary');
|
||||
} else if (this.proveReception(headers)) {
|
||||
self.flush();
|
||||
}
|
||||
|
||||
var headBuffer = '';
|
||||
|
||||
this.socket.on('data', function (data) {
|
||||
if (waitingForNonce) {
|
||||
headBuffer += data;
|
||||
|
||||
if (headBuffer.length < 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the connection to utf8 encoding after receiving the nonce
|
||||
self.socket.setEncoding('utf8');
|
||||
waitingForNonce = false;
|
||||
|
||||
// Stuff the nonce into the location where it's expected to be
|
||||
self.req.head = headBuffer.substr(0, 8);
|
||||
headBuffer = '';
|
||||
|
||||
if (self.proveReception(headers)) {
|
||||
self.flush();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self.parser.add(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes to the socket.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.write = function (data) {
|
||||
if (this.open) {
|
||||
this.drained = false;
|
||||
|
||||
if (this.buffer) {
|
||||
this.buffered.push(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
var length = Buffer.byteLength(data)
|
||||
, buffer = new Buffer(2 + length);
|
||||
|
||||
buffer.write('\x00', 'binary');
|
||||
buffer.write(data, 1, 'utf8');
|
||||
buffer.write('\xff', 1 + length, 'binary');
|
||||
|
||||
try {
|
||||
if (this.socket.write(buffer)) {
|
||||
this.drained = true;
|
||||
}
|
||||
} catch (e) {
|
||||
this.end();
|
||||
}
|
||||
|
||||
this.log.debug(this.name + ' writing', data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Flushes the internal buffer
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.flush = function () {
|
||||
this.buffer = false;
|
||||
|
||||
for (var i = 0, l = this.buffered.length; i < l; i++) {
|
||||
this.write(this.buffered.splice(0, 1)[0]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finishes the handshake.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.proveReception = function (headers) {
|
||||
var self = this
|
||||
, k1 = this.req.headers['sec-websocket-key1']
|
||||
, k2 = this.req.headers['sec-websocket-key2'];
|
||||
|
||||
if (k1 && k2){
|
||||
var md5 = crypto.createHash('md5');
|
||||
|
||||
[k1, k2].forEach(function (k) {
|
||||
var n = parseInt(k.replace(/[^\d]/g, ''))
|
||||
, spaces = k.replace(/[^ ]/g, '').length;
|
||||
|
||||
if (spaces === 0 || n % spaces !== 0){
|
||||
self.log.warn('Invalid ' + self.name + ' key: "' + k + '".');
|
||||
self.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
n /= spaces;
|
||||
|
||||
md5.update(String.fromCharCode(
|
||||
n >> 24 & 0xFF,
|
||||
n >> 16 & 0xFF,
|
||||
n >> 8 & 0xFF,
|
||||
n & 0xFF));
|
||||
});
|
||||
|
||||
md5.update(this.req.head.toString('binary'));
|
||||
|
||||
try {
|
||||
this.socket.write(md5.digest('binary'), 'binary');
|
||||
} catch (e) {
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a payload.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.payload = function (msgs) {
|
||||
for (var i = 0, l = msgs.length; i < l; i++) {
|
||||
this.write(msgs[i]);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.doClose = function () {
|
||||
this.socket.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket parser
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Parser () {
|
||||
this.buffer = '';
|
||||
this.i = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
Parser.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Adds data to the buffer.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Parser.prototype.add = function (data) {
|
||||
this.buffer += data;
|
||||
this.parse();
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the buffer.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.parse = function () {
|
||||
for (var i = this.i, chr, l = this.buffer.length; i < l; i++){
|
||||
chr = this.buffer[i];
|
||||
|
||||
if (this.buffer.length == 2 && this.buffer[1] == '\u0000') {
|
||||
this.emit('close');
|
||||
this.buffer = '';
|
||||
this.i = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (i === 0){
|
||||
if (chr != '\u0000')
|
||||
this.error('Bad framing. Expected null byte as first frame');
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chr == '\ufffd'){
|
||||
this.emit('data', this.buffer.substr(1, i - 1));
|
||||
this.buffer = this.buffer.substr(i + 1);
|
||||
this.i = 0;
|
||||
return this.parse();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.error = function (reason) {
|
||||
this.buffer = '';
|
||||
this.i = 0;
|
||||
this.emit('error', reason);
|
||||
return this;
|
||||
};
|
||||
604
lib/transports/websocket/hybi-07-12.js
Normal file
604
lib/transports/websocket/hybi-07-12.js
Normal file
@@ -0,0 +1,604 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module requirements.
|
||||
*/
|
||||
|
||||
var Transport = require('../../transport')
|
||||
, EventEmitter = process.EventEmitter
|
||||
, crypto = require('crypto')
|
||||
, url = require('url')
|
||||
, parser = require('../../parser')
|
||||
, util = require('../../util');
|
||||
|
||||
/**
|
||||
* Export the constructor.
|
||||
*/
|
||||
|
||||
exports = module.exports = WebSocket;
|
||||
exports.Parser = Parser;
|
||||
|
||||
/**
|
||||
* HTTP interface constructor. Interface compatible with all transports that
|
||||
* depend on request-response cycles.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function WebSocket (mng, data, req) {
|
||||
// parser
|
||||
var self = this;
|
||||
|
||||
this.manager = mng;
|
||||
this.parser = new Parser();
|
||||
this.parser.on('data', function (packet) {
|
||||
self.onMessage(parser.decodePacket(packet));
|
||||
});
|
||||
this.parser.on('ping', function () {
|
||||
// version 8 ping => pong
|
||||
self.socket.write('\u008a\u0000');
|
||||
});
|
||||
this.parser.on('close', function () {
|
||||
self.end();
|
||||
});
|
||||
this.parser.on('error', function (reason) {
|
||||
self.log.warn(self.name + ' parser error: ' + reason);
|
||||
self.end();
|
||||
});
|
||||
|
||||
Transport.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from Transport.
|
||||
*/
|
||||
|
||||
WebSocket.prototype.__proto__ = Transport.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.name = 'websocket';
|
||||
|
||||
/**
|
||||
* Websocket draft version
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.protocolVersion = '07-12';
|
||||
|
||||
/**
|
||||
* Called when the socket connects.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.onSocketConnect = function () {
|
||||
var self = this;
|
||||
|
||||
if (this.req.headers.upgrade !== 'websocket') {
|
||||
this.log.warn(this.name + ' connection invalid');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = this.req.headers['sec-websocket-origin']
|
||||
, location = (this.socket.encrypted ? 'wss' : 'ws')
|
||||
+ '://' + this.req.headers.host + this.req.url;
|
||||
|
||||
if (!this.verifyOrigin(origin)) {
|
||||
this.log.warn(this.name + ' connection invalid: origin mismatch');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.req.headers['sec-websocket-key']) {
|
||||
this.log.warn(this.name + ' connection invalid: received no key');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// calc key
|
||||
var key = this.req.headers['sec-websocket-key'];
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
key = shasum.digest('base64');
|
||||
|
||||
var headers = [
|
||||
'HTTP/1.1 101 Switching Protocols'
|
||||
, 'Upgrade: websocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'Sec-WebSocket-Accept: ' + key
|
||||
];
|
||||
|
||||
try {
|
||||
this.socket.write(headers.concat('', '').join('\r\n'));
|
||||
this.socket.setTimeout(0);
|
||||
this.socket.setNoDelay(true);
|
||||
} catch (e) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on('data', function (data) {
|
||||
self.parser.add(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies the origin of a request.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.verifyOrigin = function (origin) {
|
||||
var origins = this.manager.get('origins');
|
||||
|
||||
if (origin === 'null') origin = '*';
|
||||
|
||||
if (origins.indexOf('*:*') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
try {
|
||||
var parts = url.parse(origin);
|
||||
var ok =
|
||||
~origins.indexOf(parts.hostname + ':' + parts.port) ||
|
||||
~origins.indexOf(parts.hostname + ':*') ||
|
||||
~origins.indexOf('*:' + parts.port);
|
||||
if (!ok) this.log.warn('illegal origin: ' + origin);
|
||||
return ok;
|
||||
} catch (ex) {
|
||||
this.log.warn('error parsing origin');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.log.warn('origin missing from websocket call, yet required by config');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes to the socket.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.write = function (data) {
|
||||
if (this.open) {
|
||||
var buf = this.frame(0x81, data);
|
||||
this.socket.write(buf, 'binary');
|
||||
this.log.debug(this.name + ' writing', data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a payload.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.payload = function (msgs) {
|
||||
for (var i = 0, l = msgs.length; i < l; i++) {
|
||||
this.write(msgs[i]);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Frame server-to-client output as a text packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.frame = function (opcode, str) {
|
||||
var dataBuffer = new Buffer(str)
|
||||
, dataLength = dataBuffer.length
|
||||
, startOffset = 2
|
||||
, secondByte = dataLength;
|
||||
if (dataLength > 65536) {
|
||||
startOffset = 10;
|
||||
secondByte = 127;
|
||||
}
|
||||
else if (dataLength > 125) {
|
||||
startOffset = 4;
|
||||
secondByte = 126;
|
||||
}
|
||||
var outputBuffer = new Buffer(dataLength + startOffset);
|
||||
outputBuffer[0] = opcode;
|
||||
outputBuffer[1] = secondByte;
|
||||
dataBuffer.copy(outputBuffer, startOffset);
|
||||
switch (secondByte) {
|
||||
case 126:
|
||||
outputBuffer[2] = dataLength >>> 8;
|
||||
outputBuffer[3] = dataLength % 256;
|
||||
break;
|
||||
case 127:
|
||||
var l = dataLength;
|
||||
for (var i = 1; i <= 8; ++i) {
|
||||
outputBuffer[startOffset - i] = l & 0xff;
|
||||
l >>>= 8;
|
||||
}
|
||||
}
|
||||
return outputBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.doClose = function () {
|
||||
this.socket.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket parser
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Parser () {
|
||||
this.state = {
|
||||
activeFragmentedOperation: null,
|
||||
lastFragment: false,
|
||||
masked: false,
|
||||
opcode: 0
|
||||
};
|
||||
this.overflow = null;
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.currentMessage = '';
|
||||
|
||||
var self = this;
|
||||
this.opcodeHandlers = {
|
||||
// text
|
||||
'1': function(data) {
|
||||
var finish = function(mask, data) {
|
||||
self.currentMessage += self.unmask(mask, data);
|
||||
if (self.state.lastFragment) {
|
||||
self.emit('data', self.currentMessage);
|
||||
self.currentMessage = '';
|
||||
}
|
||||
self.endPacket();
|
||||
}
|
||||
|
||||
var expectData = function(length) {
|
||||
if (self.state.masked) {
|
||||
self.expect('Mask', 4, function(data) {
|
||||
var mask = data;
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
expectData(firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expect('Length', 2, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expect('Length', 8, function(data) {
|
||||
if (util.unpack(data.slice(0, 4)) != 0) {
|
||||
self.error('packets with length spanning more than 32 bit is currently not supported');
|
||||
return;
|
||||
}
|
||||
var lengthBytes = data.slice(4); // note: cap to 32 bit length
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
},
|
||||
// binary
|
||||
'2': function(data) {
|
||||
var finish = function(mask, data) {
|
||||
if (typeof self.currentMessage == 'string') self.currentMessage = []; // build a buffer list
|
||||
self.currentMessage.push(self.unmask(mask, data, true));
|
||||
if (self.state.lastFragment) {
|
||||
self.emit('binary', self.concatBuffers(self.currentMessage));
|
||||
self.currentMessage = '';
|
||||
}
|
||||
self.endPacket();
|
||||
}
|
||||
|
||||
var expectData = function(length) {
|
||||
if (self.state.masked) {
|
||||
self.expect('Mask', 4, function(data) {
|
||||
var mask = data;
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
expectData(firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expect('Length', 2, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expect('Length', 8, function(data) {
|
||||
if (util.unpack(data.slice(0, 4)) != 0) {
|
||||
self.error('packets with length spanning more than 32 bit is currently not supported');
|
||||
return;
|
||||
}
|
||||
var lengthBytes = data.slice(4); // note: cap to 32 bit length
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
},
|
||||
// close
|
||||
'8': function(data) {
|
||||
self.emit('close');
|
||||
self.reset();
|
||||
},
|
||||
// ping
|
||||
'9': function(data) {
|
||||
if (self.state.lastFragment == false) {
|
||||
self.error('fragmented ping is not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
var finish = function(mask, data) {
|
||||
self.emit('ping', self.unmask(mask, data));
|
||||
self.endPacket();
|
||||
}
|
||||
|
||||
var expectData = function(length) {
|
||||
if (self.state.masked) {
|
||||
self.expect('Mask', 4, function(data) {
|
||||
var mask = data;
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength == 0) {
|
||||
finish(null, null);
|
||||
}
|
||||
else if (firstLength < 126) {
|
||||
expectData(firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expect('Length', 2, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expect('Length', 8, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.expect('Opcode', 2, this.processPacket);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
Parser.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Add new data to the parser.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Parser.prototype.add = function(data) {
|
||||
if (this.expectBuffer == null) {
|
||||
this.addToOverflow(data);
|
||||
return;
|
||||
}
|
||||
var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset);
|
||||
data.copy(this.expectBuffer, this.expectOffset, 0, toRead);
|
||||
this.expectOffset += toRead;
|
||||
if (toRead < data.length) {
|
||||
// at this point the overflow buffer shouldn't at all exist
|
||||
this.overflow = new Buffer(data.length - toRead);
|
||||
data.copy(this.overflow, 0, toRead, toRead + this.overflow.length);
|
||||
}
|
||||
if (this.expectOffset == this.expectBuffer.length) {
|
||||
var bufferForHandler = this.expectBuffer;
|
||||
this.expectBuffer = null;
|
||||
this.expectOffset = 0;
|
||||
this.expectHandler.call(this, bufferForHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a piece of data to the overflow.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.addToOverflow = function(data) {
|
||||
if (this.overflow == null) this.overflow = data;
|
||||
else {
|
||||
var prevOverflow = this.overflow;
|
||||
this.overflow = new Buffer(this.overflow.length + data.length);
|
||||
prevOverflow.copy(this.overflow, 0);
|
||||
data.copy(this.overflow, prevOverflow.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a certain amount of bytes to be available, then fires a callback.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.expect = function(what, length, handler) {
|
||||
this.expectBuffer = new Buffer(length);
|
||||
this.expectOffset = 0;
|
||||
this.expectHandler = handler;
|
||||
if (this.overflow != null) {
|
||||
var toOverflow = this.overflow;
|
||||
this.overflow = null;
|
||||
this.add(toOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start processing a new packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.processPacket = function (data) {
|
||||
if ((data[0] & 0x70) != 0) this.error('reserved fields must be empty');
|
||||
this.state.lastFragment = (data[0] & 0x80) == 0x80;
|
||||
this.state.masked = (data[1] & 0x80) == 0x80;
|
||||
var opcode = data[0] & 0xf;
|
||||
if (opcode == 0) {
|
||||
// continuation frame
|
||||
this.state.opcode = this.state.activeFragmentedOperation;
|
||||
if (!(this.state.opcode == 1 || this.state.opcode == 2)) {
|
||||
this.error('continuation frame cannot follow current opcode')
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.state.opcode = opcode;
|
||||
if (this.state.lastFragment === false) {
|
||||
this.state.activeFragmentedOperation = opcode;
|
||||
}
|
||||
}
|
||||
var handler = this.opcodeHandlers[this.state.opcode];
|
||||
if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode);
|
||||
else handler(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endprocessing a packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.endPacket = function() {
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) {
|
||||
// end current fragmented operation
|
||||
this.state.activeFragmentedOperation = null;
|
||||
}
|
||||
this.state.lastFragment = false;
|
||||
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
|
||||
this.state.masked = false;
|
||||
this.expect('Opcode', 2, this.processPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the parser state.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.reset = function() {
|
||||
this.state = {
|
||||
activeFragmentedOperation: null,
|
||||
lastFragment: false,
|
||||
masked: false,
|
||||
opcode: 0
|
||||
};
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.overflow = null;
|
||||
this.currentMessage = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmask received data.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.unmask = function (mask, buf, binary) {
|
||||
if (mask != null) {
|
||||
for (var i = 0, ll = buf.length; i < ll; i++) {
|
||||
buf[i] ^= mask[i % 4];
|
||||
}
|
||||
}
|
||||
if (binary) return buf;
|
||||
return buf != null ? buf.toString('utf8') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates a list of buffers.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.concatBuffers = function(buffers) {
|
||||
var length = 0;
|
||||
for (var i = 0, l = buffers.length; i < l; ++i) {
|
||||
length += buffers[i].length;
|
||||
}
|
||||
var mergedBuffer = new Buffer(length);
|
||||
var offset = 0;
|
||||
for (var i = 0, l = buffers.length; i < l; ++i) {
|
||||
buffers[i].copy(mergedBuffer, offset);
|
||||
offset += buffers[i].length;
|
||||
}
|
||||
return mergedBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.error = function (reason) {
|
||||
this.reset();
|
||||
this.emit('error', reason);
|
||||
return this;
|
||||
};
|
||||
604
lib/transports/websocket/hybi-16.js
Normal file
604
lib/transports/websocket/hybi-16.js
Normal file
@@ -0,0 +1,604 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module requirements.
|
||||
*/
|
||||
|
||||
var Transport = require('../../transport')
|
||||
, EventEmitter = process.EventEmitter
|
||||
, crypto = require('crypto')
|
||||
, url = require('url')
|
||||
, parser = require('../../parser')
|
||||
, util = require('../../util');
|
||||
|
||||
/**
|
||||
* Export the constructor.
|
||||
*/
|
||||
|
||||
exports = module.exports = WebSocket;
|
||||
exports.Parser = Parser;
|
||||
|
||||
/**
|
||||
* HTTP interface constructor. Interface compatible with all transports that
|
||||
* depend on request-response cycles.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function WebSocket (mng, data, req) {
|
||||
// parser
|
||||
var self = this;
|
||||
|
||||
this.manager = mng;
|
||||
this.parser = new Parser();
|
||||
this.parser.on('data', function (packet) {
|
||||
self.onMessage(parser.decodePacket(packet));
|
||||
});
|
||||
this.parser.on('ping', function () {
|
||||
// version 8 ping => pong
|
||||
self.socket.write('\u008a\u0000');
|
||||
});
|
||||
this.parser.on('close', function () {
|
||||
self.end();
|
||||
});
|
||||
this.parser.on('error', function (reason) {
|
||||
self.log.warn(self.name + ' parser error: ' + reason);
|
||||
self.end();
|
||||
});
|
||||
|
||||
Transport.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from Transport.
|
||||
*/
|
||||
|
||||
WebSocket.prototype.__proto__ = Transport.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.name = 'websocket';
|
||||
|
||||
/**
|
||||
* Websocket draft version
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
WebSocket.prototype.protocolVersion = '16';
|
||||
|
||||
/**
|
||||
* Called when the socket connects.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.onSocketConnect = function () {
|
||||
var self = this;
|
||||
|
||||
if (this.req.headers.upgrade !== 'websocket') {
|
||||
this.log.warn(this.name + ' connection invalid');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = this.req.headers['origin']
|
||||
, location = (this.socket.encrypted ? 'wss' : 'ws')
|
||||
+ '://' + this.req.headers.host + this.req.url;
|
||||
|
||||
if (!this.verifyOrigin(origin)) {
|
||||
this.log.warn(this.name + ' connection invalid: origin mismatch');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.req.headers['sec-websocket-key']) {
|
||||
this.log.warn(this.name + ' connection invalid: received no key');
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// calc key
|
||||
var key = this.req.headers['sec-websocket-key'];
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
key = shasum.digest('base64');
|
||||
|
||||
var headers = [
|
||||
'HTTP/1.1 101 Switching Protocols'
|
||||
, 'Upgrade: websocket'
|
||||
, 'Connection: Upgrade'
|
||||
, 'Sec-WebSocket-Accept: ' + key
|
||||
];
|
||||
|
||||
try {
|
||||
this.socket.write(headers.concat('', '').join('\r\n'));
|
||||
this.socket.setTimeout(0);
|
||||
this.socket.setNoDelay(true);
|
||||
} catch (e) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.on('data', function (data) {
|
||||
self.parser.add(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies the origin of a request.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.verifyOrigin = function (origin) {
|
||||
var origins = this.manager.get('origins');
|
||||
|
||||
if (origin === 'null') origin = '*';
|
||||
|
||||
if (origins.indexOf('*:*') !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
try {
|
||||
var parts = url.parse(origin);
|
||||
var ok =
|
||||
~origins.indexOf(parts.hostname + ':' + parts.port) ||
|
||||
~origins.indexOf(parts.hostname + ':*') ||
|
||||
~origins.indexOf('*:' + parts.port);
|
||||
if (!ok) this.log.warn('illegal origin: ' + origin);
|
||||
return ok;
|
||||
} catch (ex) {
|
||||
this.log.warn('error parsing origin');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.log.warn('origin missing from websocket call, yet required by config');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes to the socket.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.write = function (data) {
|
||||
if (this.open) {
|
||||
var buf = this.frame(0x81, data);
|
||||
this.socket.write(buf, 'binary');
|
||||
this.log.debug(this.name + ' writing', data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a payload.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.payload = function (msgs) {
|
||||
for (var i = 0, l = msgs.length; i < l; i++) {
|
||||
this.write(msgs[i]);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Frame server-to-client output as a text packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.frame = function (opcode, str) {
|
||||
var dataBuffer = new Buffer(str)
|
||||
, dataLength = dataBuffer.length
|
||||
, startOffset = 2
|
||||
, secondByte = dataLength;
|
||||
if (dataLength > 65536) {
|
||||
startOffset = 10;
|
||||
secondByte = 127;
|
||||
}
|
||||
else if (dataLength > 125) {
|
||||
startOffset = 4;
|
||||
secondByte = 126;
|
||||
}
|
||||
var outputBuffer = new Buffer(dataLength + startOffset);
|
||||
outputBuffer[0] = opcode;
|
||||
outputBuffer[1] = secondByte;
|
||||
dataBuffer.copy(outputBuffer, startOffset);
|
||||
switch (secondByte) {
|
||||
case 126:
|
||||
outputBuffer[2] = dataLength >>> 8;
|
||||
outputBuffer[3] = dataLength % 256;
|
||||
break;
|
||||
case 127:
|
||||
var l = dataLength;
|
||||
for (var i = 1; i <= 8; ++i) {
|
||||
outputBuffer[startOffset - i] = l & 0xff;
|
||||
l >>>= 8;
|
||||
}
|
||||
}
|
||||
return outputBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WebSocket.prototype.doClose = function () {
|
||||
this.socket.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket parser
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function Parser () {
|
||||
this.state = {
|
||||
activeFragmentedOperation: null,
|
||||
lastFragment: false,
|
||||
masked: false,
|
||||
opcode: 0
|
||||
};
|
||||
this.overflow = null;
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.currentMessage = '';
|
||||
|
||||
var self = this;
|
||||
this.opcodeHandlers = {
|
||||
// text
|
||||
'1': function(data) {
|
||||
var finish = function(mask, data) {
|
||||
self.currentMessage += self.unmask(mask, data);
|
||||
if (self.state.lastFragment) {
|
||||
self.emit('data', self.currentMessage);
|
||||
self.currentMessage = '';
|
||||
}
|
||||
self.endPacket();
|
||||
}
|
||||
|
||||
var expectData = function(length) {
|
||||
if (self.state.masked) {
|
||||
self.expect('Mask', 4, function(data) {
|
||||
var mask = data;
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
expectData(firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expect('Length', 2, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expect('Length', 8, function(data) {
|
||||
if (util.unpack(data.slice(0, 4)) != 0) {
|
||||
self.error('packets with length spanning more than 32 bit is currently not supported');
|
||||
return;
|
||||
}
|
||||
var lengthBytes = data.slice(4); // note: cap to 32 bit length
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
},
|
||||
// binary
|
||||
'2': function(data) {
|
||||
var finish = function(mask, data) {
|
||||
if (typeof self.currentMessage == 'string') self.currentMessage = []; // build a buffer list
|
||||
self.currentMessage.push(self.unmask(mask, data, true));
|
||||
if (self.state.lastFragment) {
|
||||
self.emit('binary', self.concatBuffers(self.currentMessage));
|
||||
self.currentMessage = '';
|
||||
}
|
||||
self.endPacket();
|
||||
}
|
||||
|
||||
var expectData = function(length) {
|
||||
if (self.state.masked) {
|
||||
self.expect('Mask', 4, function(data) {
|
||||
var mask = data;
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength < 126) {
|
||||
expectData(firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expect('Length', 2, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expect('Length', 8, function(data) {
|
||||
if (util.unpack(data.slice(0, 4)) != 0) {
|
||||
self.error('packets with length spanning more than 32 bit is currently not supported');
|
||||
return;
|
||||
}
|
||||
var lengthBytes = data.slice(4); // note: cap to 32 bit length
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
},
|
||||
// close
|
||||
'8': function(data) {
|
||||
self.emit('close');
|
||||
self.reset();
|
||||
},
|
||||
// ping
|
||||
'9': function(data) {
|
||||
if (self.state.lastFragment == false) {
|
||||
self.error('fragmented ping is not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
var finish = function(mask, data) {
|
||||
self.emit('ping', self.unmask(mask, data));
|
||||
self.endPacket();
|
||||
}
|
||||
|
||||
var expectData = function(length) {
|
||||
if (self.state.masked) {
|
||||
self.expect('Mask', 4, function(data) {
|
||||
var mask = data;
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(mask, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.expect('Data', length, function(data) {
|
||||
finish(null, data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// decode length
|
||||
var firstLength = data[1] & 0x7f;
|
||||
if (firstLength == 0) {
|
||||
finish(null, null);
|
||||
}
|
||||
else if (firstLength < 126) {
|
||||
expectData(firstLength);
|
||||
}
|
||||
else if (firstLength == 126) {
|
||||
self.expect('Length', 2, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
else if (firstLength == 127) {
|
||||
self.expect('Length', 8, function(data) {
|
||||
expectData(util.unpack(data));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.expect('Opcode', 2, this.processPacket);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
Parser.prototype.__proto__ = EventEmitter.prototype;
|
||||
|
||||
/**
|
||||
* Add new data to the parser.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
Parser.prototype.add = function(data) {
|
||||
if (this.expectBuffer == null) {
|
||||
this.addToOverflow(data);
|
||||
return;
|
||||
}
|
||||
var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset);
|
||||
data.copy(this.expectBuffer, this.expectOffset, 0, toRead);
|
||||
this.expectOffset += toRead;
|
||||
if (toRead < data.length) {
|
||||
// at this point the overflow buffer shouldn't at all exist
|
||||
this.overflow = new Buffer(data.length - toRead);
|
||||
data.copy(this.overflow, 0, toRead, toRead + this.overflow.length);
|
||||
}
|
||||
if (this.expectOffset == this.expectBuffer.length) {
|
||||
var bufferForHandler = this.expectBuffer;
|
||||
this.expectBuffer = null;
|
||||
this.expectOffset = 0;
|
||||
this.expectHandler.call(this, bufferForHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a piece of data to the overflow.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.addToOverflow = function(data) {
|
||||
if (this.overflow == null) this.overflow = data;
|
||||
else {
|
||||
var prevOverflow = this.overflow;
|
||||
this.overflow = new Buffer(this.overflow.length + data.length);
|
||||
prevOverflow.copy(this.overflow, 0);
|
||||
data.copy(this.overflow, prevOverflow.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a certain amount of bytes to be available, then fires a callback.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.expect = function(what, length, handler) {
|
||||
this.expectBuffer = new Buffer(length);
|
||||
this.expectOffset = 0;
|
||||
this.expectHandler = handler;
|
||||
if (this.overflow != null) {
|
||||
var toOverflow = this.overflow;
|
||||
this.overflow = null;
|
||||
this.add(toOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start processing a new packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.processPacket = function (data) {
|
||||
if ((data[0] & 0x70) != 0) this.error('reserved fields must be empty');
|
||||
this.state.lastFragment = (data[0] & 0x80) == 0x80;
|
||||
this.state.masked = (data[1] & 0x80) == 0x80;
|
||||
var opcode = data[0] & 0xf;
|
||||
if (opcode == 0) {
|
||||
// continuation frame
|
||||
this.state.opcode = this.state.activeFragmentedOperation;
|
||||
if (!(this.state.opcode == 1 || this.state.opcode == 2)) {
|
||||
this.error('continuation frame cannot follow current opcode')
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.state.opcode = opcode;
|
||||
if (this.state.lastFragment === false) {
|
||||
this.state.activeFragmentedOperation = opcode;
|
||||
}
|
||||
}
|
||||
var handler = this.opcodeHandlers[this.state.opcode];
|
||||
if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode);
|
||||
else handler(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endprocessing a packet.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.endPacket = function() {
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) {
|
||||
// end current fragmented operation
|
||||
this.state.activeFragmentedOperation = null;
|
||||
}
|
||||
this.state.lastFragment = false;
|
||||
this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0;
|
||||
this.state.masked = false;
|
||||
this.expect('Opcode', 2, this.processPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the parser state.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.reset = function() {
|
||||
this.state = {
|
||||
activeFragmentedOperation: null,
|
||||
lastFragment: false,
|
||||
masked: false,
|
||||
opcode: 0
|
||||
};
|
||||
this.expectOffset = 0;
|
||||
this.expectBuffer = null;
|
||||
this.expectHandler = null;
|
||||
this.overflow = null;
|
||||
this.currentMessage = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmask received data.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.unmask = function (mask, buf, binary) {
|
||||
if (mask != null) {
|
||||
for (var i = 0, ll = buf.length; i < ll; i++) {
|
||||
buf[i] ^= mask[i % 4];
|
||||
}
|
||||
}
|
||||
if (binary) return buf;
|
||||
return buf != null ? buf.toString('utf8') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates a list of buffers.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.concatBuffers = function(buffers) {
|
||||
var length = 0;
|
||||
for (var i = 0, l = buffers.length; i < l; ++i) {
|
||||
length += buffers[i].length;
|
||||
}
|
||||
var mergedBuffer = new Buffer(length);
|
||||
var offset = 0;
|
||||
for (var i = 0, l = buffers.length; i < l; ++i) {
|
||||
buffers[i].copy(mergedBuffer, offset);
|
||||
offset += buffers[i].length;
|
||||
}
|
||||
return mergedBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an error
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Parser.prototype.error = function (reason) {
|
||||
this.reset();
|
||||
this.emit('error', reason);
|
||||
return this;
|
||||
};
|
||||
11
lib/transports/websocket/index.js
Normal file
11
lib/transports/websocket/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
/**
|
||||
* Export websocket versions.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
7: require('./hybi-07-12'),
|
||||
8: require('./hybi-07-12'),
|
||||
13: require('./hybi-16'),
|
||||
default: require('./default')
|
||||
};
|
||||
@@ -23,8 +23,8 @@ exports = module.exports = XHRPolling;
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function XHRPolling (mng, data) {
|
||||
HTTPPolling.call(this, mng, data);
|
||||
function XHRPolling (mng, data, req) {
|
||||
HTTPPolling.call(this, mng, data, req);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +33,14 @@ function XHRPolling (mng, data) {
|
||||
|
||||
XHRPolling.prototype.__proto__ = HTTPPolling.prototype;
|
||||
|
||||
/**
|
||||
* Transport name
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
XHRPolling.prototype.name = 'xhr-polling';
|
||||
|
||||
/**
|
||||
* Frames data prior to write.
|
||||
*
|
||||
@@ -60,5 +68,5 @@ XHRPolling.prototype.doWrite = function (data) {
|
||||
|
||||
this.response.writeHead(200, headers);
|
||||
this.response.write(data);
|
||||
this.log.debug('xhr-polling writing', data);
|
||||
this.log.debug(this.name + ' writing', data);
|
||||
};
|
||||
|
||||
25
lib/util.js
25
lib/util.js
@@ -23,3 +23,28 @@ exports.toArray = function (enu) {
|
||||
|
||||
return arr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unpacks a buffer to a number.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.unpack = function (buffer) {
|
||||
var n = 0;
|
||||
for (var i = 0; i < buffer.length; ++i) {
|
||||
n = (i == 0) ? buffer[i] : (n * 256) + buffer[i];
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Left pads a string.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.padl = function (s,n,c) {
|
||||
return new Array(1 + n - s.length).join(c) + s;
|
||||
}
|
||||
|
||||
|
||||
13
package.json
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "socket.io"
|
||||
, "version": "0.7.2"
|
||||
, "description": "Realtime apps made cross-browser & easy with a WebSocket-like API"
|
||||
, "version": "0.8.5"
|
||||
, "description": "Real-time apps made cross-browser & easy with a WebSocket-like API"
|
||||
, "homepage": "http://socket.io"
|
||||
, "keywords": ["websocket", "socket", "realtime", "socket.io", "comet", "ajax"]
|
||||
, "author": "Guillermo Rauch <guillermo@learnboost.com>"
|
||||
@@ -9,18 +9,21 @@
|
||||
{ "name": "Guillermo Rauch", "email": "rauchg@gmail.com" }
|
||||
, { "name": "Arnout Kazemier", "email": "info@3rd-eden.com" }
|
||||
, { "name": "Vladimir Dronnikov", "email": "dronnikov@gmail.com" }
|
||||
, { "name": "Einar Otto Stangvik", "email": "einaros@gmail.com" }
|
||||
]
|
||||
, "repository":{
|
||||
"type": "git"
|
||||
, "url": "https://github.com/LearnBoost/Socket.IO-node.git"
|
||||
, "url": "https://github.com/LearnBoost/socket.io.git"
|
||||
}
|
||||
, "dependencies": {
|
||||
"socket.io-client": "0.7.2"
|
||||
, "policyfile": ">= 0.0.3"
|
||||
"socket.io-client": "0.8.5"
|
||||
, "policyfile": "0.0.4"
|
||||
, "redis": "0.6.6"
|
||||
}
|
||||
, "devDependencies": {
|
||||
"expresso": "0.7.7"
|
||||
, "should": "0.0.4"
|
||||
, "assertvanish": "0.0.3-1"
|
||||
}
|
||||
, "main": "index"
|
||||
, "engines": { "node": ">= 0.4.0" }
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
var io = require('socket.io')
|
||||
, parser = io.parser
|
||||
, http = require('http')
|
||||
, https = require('https');
|
||||
, https = require('https')
|
||||
, WebSocket = require('../support/node-websocket-client/lib/websocket').WebSocket;
|
||||
|
||||
/**
|
||||
* Exports.
|
||||
@@ -178,6 +179,66 @@ client = function (port) {
|
||||
*/
|
||||
|
||||
create = function (cl) {
|
||||
console.error('');
|
||||
return io.listen(cl.port);
|
||||
console.log('');
|
||||
var manager = io.listen(cl.port);
|
||||
manager.set('client store expiration', 0);
|
||||
return manager;
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket socket.io client.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function WSClient (port, sid, transport) {
|
||||
this.sid = sid;
|
||||
this.port = port;
|
||||
this.transportName = transport || 'websocket';
|
||||
WebSocket.call(
|
||||
this
|
||||
, 'ws://localhost:' + port + '/socket.io/'
|
||||
+ io.protocol + '/' + this.transportName + '/' + sid
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherits from WebSocket.
|
||||
*/
|
||||
|
||||
WSClient.prototype.__proto__ = WebSocket.prototype;
|
||||
|
||||
/**
|
||||
* Overrides message event emission.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
WSClient.prototype.emit = function (name) {
|
||||
var args = arguments;
|
||||
|
||||
if (name == 'message' || name == 'data') {
|
||||
args[1] = parser.decodePacket(args[1].toString());
|
||||
}
|
||||
|
||||
return WebSocket.prototype.emit.apply(this, arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a packet
|
||||
*/
|
||||
|
||||
WSClient.prototype.packet = function (pack) {
|
||||
this.write(parser.encodePacket(pack));
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a websocket client.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
websocket = function (cl, sid, transport) {
|
||||
return new WSClient(cl.port, sid, transport);
|
||||
};
|
||||
|
||||
99
test/hybi-common.js
Normal file
99
test/hybi-common.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Returns a Buffer from a "ff 00 ff"-type hex string.
|
||||
*/
|
||||
|
||||
getBufferFromHexString = function(byteStr) {
|
||||
var bytes = byteStr.split(' ');
|
||||
var buf = new Buffer(bytes.length);
|
||||
for (var i = 0; i < bytes.length; ++i) {
|
||||
buf[i] = parseInt(bytes[i], 16);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hex string from a Buffer.
|
||||
*/
|
||||
|
||||
getHexStringFromBuffer = function(data) {
|
||||
var s = '';
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
s += padl(data[i].toString(16), 2, '0') + ' ';
|
||||
}
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a buffer in two parts.
|
||||
*/
|
||||
|
||||
splitBuffer = function(buffer) {
|
||||
var b1 = new Buffer(Math.ceil(buffer.length / 2));
|
||||
buffer.copy(b1, 0, 0, b1.length);
|
||||
var b2 = new Buffer(Math.floor(buffer.length / 2));
|
||||
buffer.copy(b2, 0, b1.length, b1.length + b2.length);
|
||||
return [b1, b2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs hybi07+ type masking on a hex string or buffer.
|
||||
*/
|
||||
|
||||
mask = function(buf, maskString) {
|
||||
if (typeof buf == 'string') buf = new Buffer(buf);
|
||||
var mask = getBufferFromHexString(maskString || '34 83 a8 68');
|
||||
for (var i = 0; i < buf.length; ++i) {
|
||||
buf[i] ^= mask[i % 4];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hex string representing the length of a message
|
||||
*/
|
||||
|
||||
getHybiLengthAsHexString = function(len, masked) {
|
||||
if (len < 126) {
|
||||
var buf = new Buffer(1);
|
||||
buf[0] = (masked ? 0x80 : 0) | len;
|
||||
}
|
||||
else if (len < 65536) {
|
||||
var buf = new Buffer(3);
|
||||
buf[0] = (masked ? 0x80 : 0) | 126;
|
||||
getBufferFromHexString(pack(4, len)).copy(buf, 1);
|
||||
}
|
||||
else {
|
||||
var buf = new Buffer(9);
|
||||
buf[0] = (masked ? 0x80 : 0) | 127;
|
||||
getBufferFromHexString(pack(16, len)).copy(buf, 1);
|
||||
}
|
||||
return getHexStringFromBuffer(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks a Buffer into a number.
|
||||
*/
|
||||
|
||||
unpack = function(buffer) {
|
||||
var n = 0;
|
||||
for (var i = 0; i < buffer.length; ++i) {
|
||||
n = (i == 0) ? buffer[i] : (n * 256) + buffer[i];
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hex string, representing a specific byte count 'length', from a number.
|
||||
*/
|
||||
|
||||
pack = function(length, number) {
|
||||
return padl(number.toString(16), length, '0').replace(/(\d\d)/g, '$1 ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Left pads the string 's' to a total length of 'n' with char 'c'.
|
||||
*/
|
||||
|
||||
padl = function(s, n, c) {
|
||||
return new Array(1 + n - s.length).join(c) + s;
|
||||
}
|
||||
54
test/leaks/socket.leaktest.js
Normal file
54
test/leaks/socket.leaktest.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Test dependencies.
|
||||
*/
|
||||
|
||||
require.paths.unshift(__dirname + '/../../lib');
|
||||
|
||||
var assertvanish = require('assertvanish')
|
||||
, common = require('../common')
|
||||
, ports = 15800;
|
||||
|
||||
function resultCallback (leaks, leakedSocket) {
|
||||
if (leaks) {
|
||||
console.error('Leak detected');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.error('No leaks');
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test.
|
||||
*/
|
||||
|
||||
var cl = client(++ports);
|
||||
var io = create(cl);
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
console.log('connected');
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
console.log("client gone");
|
||||
setTimeout(gc, 1000);
|
||||
assertvanish(socket, 2000, {silent: true, callback: resultCallback});
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
cl.handshake(function (sid) {
|
||||
var ws = websocket(cl, sid);
|
||||
ws.on('open', function () {
|
||||
console.log('open!');
|
||||
setTimeout(function() {
|
||||
ws.close();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
@@ -21,6 +20,22 @@ var sio = require('socket.io')
|
||||
module.exports = {
|
||||
|
||||
'test setting and getting a configuration flag': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(http.createServer());
|
||||
|
||||
io.set('a', 'b');
|
||||
io.get('a').should.eql('b');
|
||||
|
||||
var port = ++ports
|
||||
, io = sio.listen(http.createServer());
|
||||
|
||||
io.configure(function () {
|
||||
io.set('a', 'b');
|
||||
io.enable('tobi');
|
||||
});
|
||||
|
||||
io.get('a').should.eql('b');
|
||||
|
||||
done();
|
||||
},
|
||||
|
||||
@@ -37,11 +52,9 @@ module.exports = {
|
||||
, io = sio.listen(http.createServer());
|
||||
|
||||
io.configure(function () {
|
||||
io.set('a', 'b');
|
||||
io.enable('tobi');
|
||||
});
|
||||
|
||||
io.get('a').should.eql('b');
|
||||
io.enabled('tobi').should.be.true;
|
||||
|
||||
done();
|
||||
@@ -134,215 +147,6 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client etag is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.enable('browser client etag');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the cached client is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
var static = sio.Manager.static;
|
||||
static.cache['/socket.io.js'].content.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the cached client etag is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.enable('browser client etag');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
var static = sio.Manager.static
|
||||
, cache = static.cache['/socket.io.js'];
|
||||
|
||||
cache.content.toString().should.match(/XMLHttpRequest/);
|
||||
Buffer.isBuffer(cache.content).should.be.true;
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that client minification works': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.enable('browser client minification');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
var length = data.length;
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
|
||||
// start a new server with minification enabled and compare lengths
|
||||
var port = ++ports
|
||||
, io2 = sio.listen(port)
|
||||
, cl2 = client(port);
|
||||
|
||||
cl2.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
data.length.should.be.greaterThan(length);
|
||||
|
||||
cl2.end();
|
||||
io2.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the WebSocketMain.swf is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/x-shockwave-flash');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
var static = sio.Manager.static
|
||||
, cache = static.cache['/static/flashsocket/WebSocketMain.swf'];
|
||||
|
||||
Buffer.isBuffer(cache.content).should.be.true;
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the WebSocketMainInsecure.swf is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/static/flashsocket/WebSocketMainInsecure.swf', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/x-shockwave-flash');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
var static = sio.Manager.static
|
||||
, cache = static.cache['/static/flashsocket/WebSocketMain.swf'];
|
||||
|
||||
Buffer.isBuffer(cache.content).should.be.true;
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that you can serve custom clients': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('browser client handler', function (req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/javascript'
|
||||
, 'Content-Length': 13
|
||||
, 'ETag': '1.0'
|
||||
});
|
||||
res.end('custom_client');
|
||||
});
|
||||
});
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.eql(13);
|
||||
res.headers.etag.should.eql('1.0');
|
||||
|
||||
data.should.eql('custom_client');
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that you can disable clients': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
@@ -438,6 +242,111 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
|
||||
'test that a referer is accepted for *:* origin': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('origins', '*:*');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com:82/something' } }, function (res, data) {
|
||||
res.statusCode.should.eql(200);
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that valid referer is accepted for addr:* origin': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('origins', 'foo.bar.com:*');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) {
|
||||
res.statusCode.should.eql(200);
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that erroneous referer is denied for addr:* origin': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('origins', 'foo.bar.com:*');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://baz.bar.com/something' } }, function (res, data) {
|
||||
res.statusCode.should.eql(403);
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that valid referer port is accepted for addr:port origin': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('origins', 'foo.bar.com:81');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com:81/something' } }, function (res, data) {
|
||||
res.statusCode.should.eql(200);
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that erroneous referer port is denied for addr:port origin': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.configure(function () {
|
||||
io.set('origins', 'foo.bar.com:81');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/{protocol}', { headers: { referer: 'http://foo.bar.com/something' } }, function (res, data) {
|
||||
res.statusCode.should.eql(403);
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test handshake cross domain access control': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port)
|
||||
, headers = {
|
||||
Origin: 'http://example.org:1337'
|
||||
, Cookie: 'name=value'
|
||||
};
|
||||
|
||||
cl.get('/socket.io/{protocol}/', { headers:headers }, function (res, data) {
|
||||
res.statusCode.should.eql(200);
|
||||
res.headers['access-control-allow-origin'].should.eql('*');
|
||||
res.headers['access-control-allow-credentials'].should.eql('true');
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test limiting the supported transports for a manager': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
@@ -513,6 +422,100 @@ module.exports = {
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'test disabling heartbeats': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port)
|
||||
, messages = 0
|
||||
, beat = false
|
||||
, ws;
|
||||
|
||||
io.configure(function () {
|
||||
io.disable('heartbeats');
|
||||
io.set('heartbeat interval', .05);
|
||||
io.set('heartbeat timeout', .05);
|
||||
io.set('close timeout', .05);
|
||||
});
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
setTimeout(function () {
|
||||
socket.disconnect();
|
||||
}, io.get('heartbeat timeout') * 1000 + 100);
|
||||
|
||||
socket.on('disconnect', function (reason) {
|
||||
beat.should.be.false;
|
||||
|
||||
cl.end();
|
||||
ws.finishClose();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
cl.get('/socket.io/{protocol}/', function (res, data) {
|
||||
res.statusCode.should.eql(200);
|
||||
data.should.match(/([^:]+)::[\.0-9]+:(.*)/);
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid);
|
||||
ws.on('message', function (packet) {
|
||||
if (++messages == 1) {
|
||||
packet.type.should.eql('connect');
|
||||
} else if (packet.type == 'heartbeat'){
|
||||
beat = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'no duplicate room members': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port);
|
||||
|
||||
Object.keys(io.rooms).length.should.equal(0);
|
||||
|
||||
io.onJoin(123, 'foo');
|
||||
io.rooms.foo.length.should.equal(1);
|
||||
|
||||
io.onJoin(123, 'foo');
|
||||
io.rooms.foo.length.should.equal(1);
|
||||
|
||||
io.onJoin(124, 'foo');
|
||||
io.rooms.foo.length.should.equal(2);
|
||||
|
||||
io.onJoin(124, 'foo');
|
||||
io.rooms.foo.length.should.equal(2);
|
||||
|
||||
io.onJoin(123, 'bar');
|
||||
io.rooms.foo.length.should.equal(2);
|
||||
io.rooms.bar.length.should.equal(1);
|
||||
|
||||
io.onJoin(123, 'bar');
|
||||
io.rooms.foo.length.should.equal(2);
|
||||
io.rooms.bar.length.should.equal(1);
|
||||
|
||||
io.onJoin(124, 'bar');
|
||||
io.rooms.foo.length.should.equal(2);
|
||||
io.rooms.bar.length.should.equal(2);
|
||||
|
||||
io.onJoin(124, 'bar');
|
||||
io.rooms.foo.length.should.equal(2);
|
||||
io.rooms.bar.length.should.equal(2);
|
||||
|
||||
io.server.close();
|
||||
done();
|
||||
},
|
||||
|
||||
'test passing options directly to the Manager through listen': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port, { resource: '/my resource', custom: 'opt' });
|
||||
|
||||
io.get('resource').should.equal('/my resource');
|
||||
io.get('custom').should.equal('opt');
|
||||
io.server.close();
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
247
test/namespace.test.js
Normal file
247
test/namespace.test.js
Normal file
@@ -0,0 +1,247 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Test dependencies.
|
||||
*/
|
||||
|
||||
var sio = require('socket.io')
|
||||
, should = require('./common')
|
||||
, ports = 15700;
|
||||
|
||||
/**
|
||||
* Test.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'namespace pass no authentication': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, ws;
|
||||
|
||||
io.of('/a')
|
||||
.on('connection', function (socket) {
|
||||
cl.end();
|
||||
ws.finishClose();
|
||||
io.server.close()
|
||||
done();
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid);
|
||||
ws.on('open', function () {
|
||||
ws.packet({
|
||||
type: 'connect'
|
||||
, endpoint: '/a'
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
'namespace pass authentication': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, ws;
|
||||
|
||||
io.of('/a')
|
||||
.authorization(function (data, fn) {
|
||||
fn(null, true);
|
||||
})
|
||||
.on('connection', function (socket) {
|
||||
cl.end();
|
||||
ws.finishClose();
|
||||
io.server.close()
|
||||
done();
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid);
|
||||
ws.on('open', function () {
|
||||
ws.packet({
|
||||
type: 'connect'
|
||||
, endpoint: '/a'
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
'namespace authentication handshake data': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, ws;
|
||||
|
||||
io.of('/a')
|
||||
.authorization(function (data, fn) {
|
||||
data.foo = 'bar';
|
||||
fn(null, true);
|
||||
})
|
||||
.on('connection', function (socket) {
|
||||
(!!socket.handshake.address.address).should.be.true;
|
||||
(!!socket.handshake.address.port).should.be.true;
|
||||
socket.handshake.headers.host.should.equal('localhost');
|
||||
socket.handshake.headers.connection.should.equal('keep-alive');
|
||||
socket.handshake.time.should.match(/GMT/);
|
||||
socket.handshake.foo.should.equal('bar');
|
||||
|
||||
cl.end();
|
||||
ws.finishClose();
|
||||
io.server.close()
|
||||
done();
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid);
|
||||
ws.on('open', function () {
|
||||
ws.packet({
|
||||
type: 'connect'
|
||||
, endpoint: '/a'
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
'namespace fail authentication': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, calls = 0
|
||||
, ws;
|
||||
|
||||
io.of('/a')
|
||||
.authorization(function (data, fn) {
|
||||
fn(null, false);
|
||||
})
|
||||
.on('connection', function (socket) {
|
||||
throw new Error('Should not be called');
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid);
|
||||
ws.on('open', function () {
|
||||
ws.packet({
|
||||
type: 'connect'
|
||||
, endpoint: '/a'
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('message', function (data) {
|
||||
if (data.endpoint == '/a') {
|
||||
data.type.should.eql('error');
|
||||
data.reason.should.eql('unauthorized')
|
||||
|
||||
cl.end();
|
||||
ws.finishClose();
|
||||
io.server.close()
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
'broadcasting sends and emits on a namespace': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, calls = 0
|
||||
, connect = 0
|
||||
, message = 0
|
||||
, events = 0
|
||||
, expected = 5
|
||||
, ws1
|
||||
, ws2;
|
||||
|
||||
io.of('a')
|
||||
.on('connection', function (socket){
|
||||
socket.broadcast.emit('b', 'test');
|
||||
socket.broadcast.json.emit('json', {foo:'bar'});
|
||||
socket.broadcast.send('foo');
|
||||
});
|
||||
|
||||
function finish () {
|
||||
connect.should.equal(2);
|
||||
message.should.equal(1);
|
||||
events.should.equal(2);
|
||||
|
||||
cl.end();
|
||||
ws1.finishClose();
|
||||
ws2.finishClose();
|
||||
io.server.close();
|
||||
done();
|
||||
}
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws1 = websocket(cl, sid);
|
||||
|
||||
ws1.on('open', function() {
|
||||
ws1.packet({
|
||||
type: 'connect'
|
||||
, endpoint: 'a'
|
||||
});
|
||||
});
|
||||
|
||||
ws1.on('message', function (data) {
|
||||
if (data.type === 'connect') {
|
||||
++connect;
|
||||
if (++calls === expected) finish();
|
||||
}
|
||||
|
||||
if (data.type === 'message') {
|
||||
++message;
|
||||
if (++calls === expected) finish();
|
||||
}
|
||||
|
||||
if (data.type === 'event') {
|
||||
if (data.name === 'b' || data.name === 'json') ++events;
|
||||
if (++calls === expected) finish();
|
||||
}
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws2 = websocket(cl, sid);
|
||||
|
||||
ws2.on('open', function () {
|
||||
ws2.packet({
|
||||
type: 'connect'
|
||||
, endpoint: 'a'
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
'joining rooms inside a namespace': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, calls = 0
|
||||
, ws;
|
||||
|
||||
io.of('/foo').on('connection', function (socket) {
|
||||
socket.join('foo.bar');
|
||||
this.in('foo.bar').emit('baz', 'pewpew');
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid);
|
||||
|
||||
ws.on('open', function (){
|
||||
ws.packet({
|
||||
type: 'connect'
|
||||
, endpoint: '/foo'
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('message', function (data) {
|
||||
if (data.type === 'event') {
|
||||
data.name.should.equal('baz');
|
||||
|
||||
cl.end();
|
||||
ws.finishClose();
|
||||
io.server.close();
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -343,6 +343,14 @@ module.exports = {
|
||||
parser.encodePacket({ type: 'message', data: '5', endpoint: '' })
|
||||
, parser.encodePacket({ type: 'message', data: '53d', endpoint: '' })
|
||||
]).should.eql('\ufffd5\ufffd3:::5\ufffd7\ufffd3:::53d')
|
||||
},
|
||||
|
||||
'test decoding newline': function () {
|
||||
parser.decodePacket('3:::\n').should.eql({
|
||||
type: 'message'
|
||||
, endpoint: ''
|
||||
, data: '\n'
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
460
test/static.test.js
Normal file
460
test/static.test.js
Normal file
@@ -0,0 +1,460 @@
|
||||
|
||||
/*!
|
||||
* socket.io-node
|
||||
* Copyright(c) 2011 LearnBoost <dev@learnboost.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Test dependencies.
|
||||
*/
|
||||
|
||||
var sio = require('socket.io')
|
||||
, should = require('./common')
|
||||
, ports = 15400;
|
||||
|
||||
/**
|
||||
* Test.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
'test that the default static files are available': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port);
|
||||
|
||||
(!!io.static.has('/socket.io.js')).should.be.true;
|
||||
(!!io.static.has('/socket.io+')).should.be.true;
|
||||
(!!io.static.has('/static/flashsocket/WebSocketMain.swf')).should.be.true;
|
||||
(!!io.static.has('/static/flashsocket/WebSocketMainInsecure.swf')).should.be.true;
|
||||
|
||||
io.server.close();
|
||||
done();
|
||||
},
|
||||
|
||||
'test that static files are correctly looked up': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port);
|
||||
|
||||
(!!io.static.has('/socket.io.js')).should.be.true;
|
||||
(!!io.static.has('/invalidfilehereplease.js')).should.be.false;
|
||||
|
||||
io.server.close();
|
||||
done();
|
||||
},
|
||||
|
||||
'test that the client is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the custom build client is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.enable('browser client etag');
|
||||
|
||||
cl.get('/socket.io/socket.io+websocket.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
data.should.match(/WS\.prototype\.name/);
|
||||
data.should.not.match(/Flashsocket\.prototype\.name/);
|
||||
data.should.not.match(/HTMLFile\.prototype\.name/);
|
||||
data.should.not.match(/JSONPPolling\.prototype\.name/);
|
||||
data.should.not.match(/XHRPolling\.prototype\.name/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client is build with the enabled transports': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.set('transports', ['websocket']);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
data.should.match(/WS\.prototype\.name/);
|
||||
data.should.not.match(/Flashsocket\.prototype\.name/);
|
||||
data.should.not.match(/HTMLFile\.prototype\.name/);
|
||||
data.should.not.match(/JSONPPolling\.prototype\.name/);
|
||||
data.should.not.match(/XHRPolling\.prototype\.name/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client cache is cleared when transports change': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.set('transports', ['websocket']);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
data.should.match(/WS\.prototype\.name/);
|
||||
data.should.not.match(/Flashsocket\.prototype\.name/);
|
||||
data.should.not.match(/HTMLFile\.prototype\.name/);
|
||||
data.should.not.match(/JSONPPolling\.prototype\.name/);
|
||||
data.should.not.match(/XHRPolling\.prototype\.name/);
|
||||
|
||||
io.set('transports', ['xhr-polling']);
|
||||
should.strictEqual(io.static.cache['/socket.io.js'], undefined);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
data.should.match(/XHRPolling\.prototype\.name/);
|
||||
data.should.not.match(/Flashsocket\.prototype\.name/);
|
||||
data.should.not.match(/HTMLFile\.prototype\.name/);
|
||||
data.should.not.match(/JSONPPolling\.prototype\.name/);
|
||||
data.should.not.match(/WS\.prototype\.name/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client etag is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.enable('browser client etag');
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client etag is changed for new transports': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.set('transports', ['websocket']);
|
||||
io.enable('browser client etag');
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
var wsEtag = res.headers.etag;
|
||||
|
||||
io.set('transports', ['xhr-polling']);
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers.etag.should.not.equal(wsEtag);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client is served with gzip': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.enable('browser client gzip');
|
||||
|
||||
cl.get('/socket.io/socket.io.js', {
|
||||
headers: {
|
||||
'accept-encoding': 'deflate, gzip'
|
||||
}
|
||||
}
|
||||
, function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-encoding'].should.eql('gzip');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
'test that the cached client is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
var static = io.static;
|
||||
static.cache['/socket.io.js'].content.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the client is not cached': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.static.add('/random.js', function (path, callback) {
|
||||
var random = Math.floor(Date.now() * Math.random()).toString();
|
||||
callback(null, new Buffer(random));
|
||||
});
|
||||
|
||||
io.disable('browser client cache');
|
||||
|
||||
cl.get('/socket.io/random.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
cl.get('/socket.io/random.js', function (res, random) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.not.equal(random);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the cached client etag is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.enable('browser client etag');
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
var static = io.static
|
||||
, cache = static.cache['/socket.io.js'];
|
||||
|
||||
cache.content.toString().should.match(/XMLHttpRequest/);
|
||||
Buffer.isBuffer(cache.content).should.be.true;
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
res.headers.etag.should.match(/([0-9]+)\.([0-9]+)\.([0-9]+)/);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the cached client sends a 304 header': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.enable('browser client etag');
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
cl.get('/socket.io/socket.io.js', {
|
||||
headers: {
|
||||
'if-none-match': res.headers.etag
|
||||
}
|
||||
}, function (res, data) {
|
||||
res.statusCode.should.eql(304);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
'test that client minification works': function (done) {
|
||||
// server 1
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
// server 2
|
||||
var port = ++ports
|
||||
, io2 = sio.listen(port)
|
||||
, cl2 = client(port);
|
||||
|
||||
io.enable('browser client minification');
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
var length = data.length;
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
|
||||
cl2.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
data.should.match(/XMLHttpRequest/);
|
||||
data.length.should.be.greaterThan(length);
|
||||
|
||||
cl2.end();
|
||||
io2.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test that the WebSocketMain.swf is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/x-shockwave-flash');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
var static = io.static
|
||||
, cache = static.cache['/static/flashsocket/WebSocketMain.swf'];
|
||||
|
||||
Buffer.isBuffer(cache.content).should.be.true;
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that the WebSocketMainInsecure.swf is served': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
cl.get('/socket.io/static/flashsocket/WebSocketMainInsecure.swf', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/x-shockwave-flash');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers.etag, undefined);
|
||||
|
||||
var static = io.static
|
||||
, cache = static.cache['/static/flashsocket/WebSocketMainInsecure.swf'];
|
||||
|
||||
Buffer.isBuffer(cache.content).should.be.true;
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
},
|
||||
|
||||
'test that swf files are not served with gzip': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.enable('browser client gzip');
|
||||
|
||||
cl.get('/socket.io/static/flashsocket/WebSocketMain.swf', {
|
||||
headers: {
|
||||
'accept-encoding': 'deflate, gzip'
|
||||
}
|
||||
}
|
||||
, function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/x-shockwave-flash');
|
||||
res.headers['content-length'].should.match(/([0-9]+)/);
|
||||
should.strictEqual(res.headers['content-encoding'], undefined);
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
'test that you can serve custom clients': function (done) {
|
||||
var port = ++ports
|
||||
, io = sio.listen(port)
|
||||
, cl = client(port);
|
||||
|
||||
io.set('browser client handler', function (req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/javascript'
|
||||
, 'Content-Length': 13
|
||||
, 'ETag': '1.0'
|
||||
});
|
||||
res.end('custom_client');
|
||||
});
|
||||
|
||||
cl.get('/socket.io/socket.io.js', function (res, data) {
|
||||
res.headers['content-type'].should.eql('application/javascript');
|
||||
res.headers['content-length'].should.eql(13);
|
||||
res.headers.etag.should.eql('1.0');
|
||||
|
||||
data.should.eql('custom_client');
|
||||
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
190
test/stores.memory.test.js
Normal file
190
test/stores.memory.test.js
Normal file
@@ -0,0 +1,190 @@
|
||||
|
||||
/**
|
||||
* Test dependencies
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
var sio = require('socket.io')
|
||||
, should = require('should')
|
||||
, MemoryStore = sio.MemoryStore;
|
||||
|
||||
/**
|
||||
* Test.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
'test storing data for a client': function (done) {
|
||||
var store = new MemoryStore
|
||||
, client = store.client('test');
|
||||
|
||||
client.id.should.equal('test');
|
||||
|
||||
client.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.get('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.eql('b');
|
||||
|
||||
client.has('a', function (err, has) {
|
||||
should.strictEqual(err, null);
|
||||
has.should.be.true;
|
||||
|
||||
client.has('b', function (err, has) {
|
||||
should.strictEqual(err, null);
|
||||
has.should.be.false;
|
||||
|
||||
client.del('a', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.has('a', function (err, has) {
|
||||
should.strictEqual(err, null);
|
||||
has.should.be.false;
|
||||
|
||||
client.set('b', 'c', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.set('c', 'd', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.get('b', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('c');
|
||||
|
||||
client.get('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('d');
|
||||
|
||||
store.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test cleaning up clients data': function (done) {
|
||||
var rand1 = Math.abs(Math.random() * Date.now() | 0)
|
||||
, rand2 = Math.abs(Math.random() * Date.now() | 0);
|
||||
|
||||
var store = new MemoryStore()
|
||||
, client1 = store.client(rand1)
|
||||
, client2 = store.client(rand2);
|
||||
|
||||
client1.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client2.set('c', 'd', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
client2.has('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
store.destroy();
|
||||
|
||||
var newstore = new MemoryStore()
|
||||
, newclient1 = newstore.client(rand1)
|
||||
, newclient2 = newstore.client(rand2);
|
||||
|
||||
newclient1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.false;
|
||||
|
||||
newclient2.has('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.false;
|
||||
|
||||
newstore.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test cleaning up a particular client': function (done) {
|
||||
var rand1 = Math.abs(Math.random() * Date.now() | 0)
|
||||
, rand2 = Math.abs(Math.random() * Date.now() | 0);
|
||||
|
||||
var store = new MemoryStore()
|
||||
, client1 = store.client(rand1)
|
||||
, client2 = store.client(rand2);
|
||||
|
||||
client1.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client2.set('c', 'd', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
client2.has('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
store.clients.should.have.property(rand1);
|
||||
store.clients.should.have.property(rand2);
|
||||
store.destroyClient(rand1);
|
||||
|
||||
store.clients.should.not.have.property(rand1);
|
||||
store.clients.should.have.property(rand2);
|
||||
|
||||
client1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal(false);
|
||||
|
||||
store.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test destroy expiration': function (done) {
|
||||
var store = new MemoryStore()
|
||||
, id = Math.abs(Math.random() * Date.now() | 0)
|
||||
, client = store.client(id);
|
||||
|
||||
client.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
store.destroyClient(id, 1);
|
||||
|
||||
setTimeout(function () {
|
||||
client.get('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('b');
|
||||
});
|
||||
}, 500);
|
||||
|
||||
setTimeout(function () {
|
||||
client.get('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
should.strictEqual(val, null);
|
||||
|
||||
store.destroy();
|
||||
done();
|
||||
});
|
||||
}, 1900);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
240
test/stores.redis.test.js
Normal file
240
test/stores.redis.test.js
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
/**
|
||||
* Test dependencies
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
|
||||
var sio = require('socket.io')
|
||||
, redis = require('redis')
|
||||
, should = require('should')
|
||||
, RedisStore = sio.RedisStore;
|
||||
|
||||
/**
|
||||
* Test.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
'test publishing doesnt get caught by the own store subscriber': function (done) {
|
||||
var a = new RedisStore
|
||||
, b = new RedisStore;
|
||||
|
||||
a.subscribe('woot', function (arg) {
|
||||
arg.should.equal('bb');
|
||||
a.destroy();
|
||||
b.destroy();
|
||||
done();
|
||||
}, function () {
|
||||
a.publish('woot', 'aa');
|
||||
b.publish('woot', 'bb');
|
||||
});
|
||||
},
|
||||
|
||||
'test publishing to multiple subscribers': function (done) {
|
||||
var a = new RedisStore
|
||||
, b = new RedisStore
|
||||
, c = new RedisStore
|
||||
, subscriptions = 3
|
||||
, messages = 2;
|
||||
|
||||
a.subscribe('tobi', function () {
|
||||
throw new Error('Shouldnt publish to itself');
|
||||
}, publish);
|
||||
|
||||
function subscription (arg1, arg2, arg3) {
|
||||
arg1.should.equal(1);
|
||||
arg2.should.equal(2);
|
||||
arg3.should.equal(3);
|
||||
--messages || finish();
|
||||
}
|
||||
|
||||
b.subscribe('tobi', subscription, publish);
|
||||
c.subscribe('tobi', subscription, publish);
|
||||
|
||||
function publish () {
|
||||
--subscriptions || a.publish('tobi', 1, 2, 3);
|
||||
}
|
||||
|
||||
function finish () {
|
||||
a.destroy();
|
||||
b.destroy();
|
||||
c.destroy();
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
'test storing data for a client': function (done) {
|
||||
var store = new RedisStore
|
||||
, rand = 'test-' + Date.now()
|
||||
, client = store.client(rand);
|
||||
|
||||
client.id.should.equal(rand);
|
||||
|
||||
client.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.get('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('b');
|
||||
|
||||
client.has('a', function (err, has) {
|
||||
should.strictEqual(err, null);
|
||||
has.should.be.true;
|
||||
|
||||
client.has('b', function (err, has) {
|
||||
should.strictEqual(err, null);
|
||||
has.should.be.false;
|
||||
|
||||
client.del('a', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.has('a', function (err, has) {
|
||||
should.strictEqual(err, null);
|
||||
has.should.be.false;
|
||||
|
||||
client.set('b', 'c', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.set('c', 'd', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client.get('b', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('c');
|
||||
|
||||
client.get('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('d');
|
||||
|
||||
store.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test cleaning up clients data': function (done) {
|
||||
var rand1 = Math.abs(Math.random() * Date.now() | 0)
|
||||
, rand2 = Math.abs(Math.random() * Date.now() | 0);
|
||||
|
||||
var store = new RedisStore()
|
||||
, client1 = store.client(rand1)
|
||||
, client2 = store.client(rand2);
|
||||
|
||||
client1.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client2.set('c', 'd', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
client2.has('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
store.destroy();
|
||||
|
||||
var newstore = new RedisStore()
|
||||
, newclient1 = newstore.client(rand1)
|
||||
, newclient2 = newstore.client(rand2);
|
||||
|
||||
newclient1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.false;
|
||||
|
||||
newclient2.has('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.false;
|
||||
|
||||
newstore.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test cleaning up a particular client': function (done) {
|
||||
var rand1 = Math.abs(Math.random() * Date.now() | 0)
|
||||
, rand2 = Math.abs(Math.random() * Date.now() | 0);
|
||||
|
||||
var store = new RedisStore()
|
||||
, client1 = store.client(rand1)
|
||||
, client2 = store.client(rand2);
|
||||
|
||||
client1.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client2.set('c', 'd', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
|
||||
client1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
client2.has('c', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.be.true;
|
||||
|
||||
store.clients.should.have.property(rand1);
|
||||
store.clients.should.have.property(rand2);
|
||||
store.destroyClient(rand1);
|
||||
|
||||
store.clients.should.not.have.property(rand1);
|
||||
store.clients.should.have.property(rand2);
|
||||
|
||||
client1.has('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal(false);
|
||||
|
||||
store.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test destroy expiration': function (done) {
|
||||
var store = new RedisStore()
|
||||
, id = Math.abs(Math.random() * Date.now() | 0)
|
||||
, client = store.client(id);
|
||||
|
||||
client.set('a', 'b', function (err) {
|
||||
should.strictEqual(err, null);
|
||||
store.destroyClient(id, 1);
|
||||
|
||||
setTimeout(function () {
|
||||
client.get('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
val.should.equal('b');
|
||||
});
|
||||
}, 500);
|
||||
|
||||
setTimeout(function () {
|
||||
client.get('a', function (err, val) {
|
||||
should.strictEqual(err, null);
|
||||
should.strictEqual(val, null);
|
||||
|
||||
store.destroy();
|
||||
done();
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@@ -77,7 +77,7 @@ module.exports = {
|
||||
var io = sio.listen(http.createServer())
|
||||
, port = ++ports;
|
||||
|
||||
io.get('flash policy port').should.eql(843);
|
||||
io.get('flash policy port').should.eql(10843);
|
||||
io.set('flash policy port', port);
|
||||
io.get('flash policy port').should.eql(port);
|
||||
|
||||
@@ -163,6 +163,20 @@ module.exports = {
|
||||
|
||||
io.flashPolicyServer.close();
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
'flashsocket identifies as flashsocket': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
, messages = 0
|
||||
, ws;
|
||||
io.set('transports', ['flashsocket']);
|
||||
io.sockets.on('connection', function (socket) {
|
||||
socket.manager.transports[socket.id].name.should.equal('flashsocket');
|
||||
done();
|
||||
});
|
||||
cl.handshake(function (sid) {
|
||||
ws = websocket(cl, sid, 'flashsocket');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,7 +177,6 @@ module.exports = {
|
||||
--total || finish();
|
||||
});
|
||||
|
||||
// we rely on a small poll duration to close this request quickly
|
||||
cl.get('/socket.io/{protocol}/jsonp-polling/' + sid, function (res, msgs) {
|
||||
res.statusCode.should.eql(200);
|
||||
msgs.should.have.length(1);
|
||||
@@ -244,7 +243,6 @@ module.exports = {
|
||||
msgs.should.have.length(1);
|
||||
msgs[0].type.should.eql('connect');
|
||||
|
||||
// here we close the request instead of relying on a small poll timeout
|
||||
setTimeout(function () {
|
||||
cl.end();
|
||||
}, 10);
|
||||
|
||||
262
test/transports.websocket.hybi07-12.parser.test.js
Normal file
262
test/transports.websocket.hybi07-12.parser.test.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Test dependencies.
|
||||
*/
|
||||
|
||||
var assert = require('assert');
|
||||
var Parser = require('../lib/transports/websocket/hybi-07-12.js').Parser;
|
||||
require('./hybi-common');
|
||||
|
||||
/**
|
||||
* Tests.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'can parse unmasked text message': function() {
|
||||
var p = new Parser();
|
||||
var packet = '81 05 48 65 6c 6c 6f';
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal('Hello', data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse close message': function() {
|
||||
var p = new Parser();
|
||||
var packet = '88 00';
|
||||
|
||||
var gotClose = false;
|
||||
p.on('close', function(data) {
|
||||
gotClose = true;
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotClose);
|
||||
},
|
||||
'can parse masked text message': function() {
|
||||
var p = new Parser();
|
||||
var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5';
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal('5:::{"name":"echo"}', data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a masked text message longer than 125 bytes': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a really long masked text message': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString();
|
||||
var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a fragmented masked text message of 300 bytes': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
var msgpiece1 = message.substr(0, 150);
|
||||
var msgpiece2 = message.substr(150);
|
||||
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
|
||||
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet1));
|
||||
p.add(getBufferFromHexString(packet2));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a ping message': function() {
|
||||
var p = new Parser();
|
||||
var message = 'Hello';
|
||||
var packet = '89 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a ping with no data': function() {
|
||||
var p = new Parser();
|
||||
var packet = '89 00';
|
||||
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a fragmented masked text message of 300 bytes with a ping in the middle': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
|
||||
var msgpiece1 = message.substr(0, 150);
|
||||
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
|
||||
|
||||
var pingMessage = 'Hello';
|
||||
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
|
||||
|
||||
var msgpiece2 = message.substr(150);
|
||||
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
assert.equal(pingMessage, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet1));
|
||||
p.add(getBufferFromHexString(pingPacket));
|
||||
p.add(getBufferFromHexString(packet2));
|
||||
assert.ok(gotData);
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
|
||||
var msgpiece1 = message.substr(0, 150);
|
||||
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
|
||||
|
||||
var pingMessage = 'Hello';
|
||||
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
|
||||
|
||||
var msgpiece2 = message.substr(150);
|
||||
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
assert.equal(pingMessage, data);
|
||||
});
|
||||
|
||||
var buffers = [];
|
||||
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet1)));
|
||||
buffers = buffers.concat(splitBuffer(getBufferFromHexString(pingPacket)));
|
||||
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet2)));
|
||||
for (var i = 0; i < buffers.length; ++i) {
|
||||
p.add(buffers[i]);
|
||||
}
|
||||
assert.ok(gotData);
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a 100 byte long masked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 100;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a 256 byte long masked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 256;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a 200kb long masked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 200 * 1024;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a 200kb long unmasked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 200 * 1024;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, false) + ' ' + getHexStringFromBuffer(message);
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
};
|
||||
|
||||
262
test/transports.websocket.hybi16.parser.test.js
Normal file
262
test/transports.websocket.hybi16.parser.test.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Test dependencies.
|
||||
*/
|
||||
|
||||
var assert = require('assert');
|
||||
var Parser = require('../lib/transports/websocket/hybi-16.js').Parser;
|
||||
require('./hybi-common');
|
||||
|
||||
/**
|
||||
* Tests.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
'can parse unmasked text message': function() {
|
||||
var p = new Parser();
|
||||
var packet = '81 05 48 65 6c 6c 6f';
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal('Hello', data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse close message': function() {
|
||||
var p = new Parser();
|
||||
var packet = '88 00';
|
||||
|
||||
var gotClose = false;
|
||||
p.on('close', function(data) {
|
||||
gotClose = true;
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotClose);
|
||||
},
|
||||
'can parse masked text message': function() {
|
||||
var p = new Parser();
|
||||
var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5';
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal('5:::{"name":"echo"}', data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a masked text message longer than 125 bytes': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a really long masked text message': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString();
|
||||
var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a fragmented masked text message of 300 bytes': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
var msgpiece1 = message.substr(0, 150);
|
||||
var msgpiece2 = message.substr(150);
|
||||
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
|
||||
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet1));
|
||||
p.add(getBufferFromHexString(packet2));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a ping message': function() {
|
||||
var p = new Parser();
|
||||
var message = 'Hello';
|
||||
var packet = '89 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a ping with no data': function() {
|
||||
var p = new Parser();
|
||||
var packet = '89 00';
|
||||
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a fragmented masked text message of 300 bytes with a ping in the middle': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
|
||||
var msgpiece1 = message.substr(0, 150);
|
||||
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
|
||||
|
||||
var pingMessage = 'Hello';
|
||||
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
|
||||
|
||||
var msgpiece2 = message.substr(150);
|
||||
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
assert.equal(pingMessage, data);
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet1));
|
||||
p.add(getBufferFromHexString(pingPacket));
|
||||
p.add(getBufferFromHexString(packet2));
|
||||
assert.ok(gotData);
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets': function() {
|
||||
var p = new Parser();
|
||||
var message = 'A';
|
||||
for (var i = 0; i < 300; ++i) message += (i % 5).toString();
|
||||
|
||||
var msgpiece1 = message.substr(0, 150);
|
||||
var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68'));
|
||||
|
||||
var pingMessage = 'Hello';
|
||||
var pingPacket = '89 FE ' + pack(4, pingMessage.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'));
|
||||
|
||||
var msgpiece2 = message.substr(150);
|
||||
var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('data', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(message, data);
|
||||
});
|
||||
var gotPing = false;
|
||||
p.on('ping', function(data) {
|
||||
gotPing = true;
|
||||
assert.equal(pingMessage, data);
|
||||
});
|
||||
|
||||
var buffers = [];
|
||||
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet1)));
|
||||
buffers = buffers.concat(splitBuffer(getBufferFromHexString(pingPacket)));
|
||||
buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet2)));
|
||||
for (var i = 0; i < buffers.length; ++i) {
|
||||
p.add(buffers[i]);
|
||||
}
|
||||
assert.ok(gotData);
|
||||
assert.ok(gotPing);
|
||||
},
|
||||
'can parse a 100 byte long masked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 100;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a 256 byte long masked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 256;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a 200kb long masked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 200 * 1024;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68'));
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
'can parse a 200kb long unmasked binary message': function() {
|
||||
var p = new Parser();
|
||||
var length = 200 * 1024;
|
||||
var message = new Buffer(length);
|
||||
for (var i = 0; i < length; ++i) message[i] = i % 256;
|
||||
var originalMessage = getHexStringFromBuffer(message);
|
||||
var packet = '82 ' + getHybiLengthAsHexString(length, false) + ' ' + getHexStringFromBuffer(message);
|
||||
|
||||
var gotData = false;
|
||||
p.on('binary', function(data) {
|
||||
gotData = true;
|
||||
assert.equal(originalMessage, getHexStringFromBuffer(data));
|
||||
});
|
||||
|
||||
p.add(getBufferFromHexString(packet));
|
||||
assert.ok(gotData);
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,6 @@ module.exports = {
|
||||
--total || finish();
|
||||
});
|
||||
|
||||
// we rely on a small poll duration to close this request quickly
|
||||
cl.get('/socket.io/{protocol}/xhr-polling/' + sid, function (res, msgs) {
|
||||
res.statusCode.should.eql(200);
|
||||
msgs.should.have.length(1);
|
||||
@@ -180,7 +179,6 @@ module.exports = {
|
||||
msgs.should.have.length(1);
|
||||
msgs[0].type.should.eql('connect');
|
||||
|
||||
// here we close the request instead of relying on a small poll timeout
|
||||
setTimeout(function () {
|
||||
cl.end();
|
||||
}, 10);
|
||||
@@ -952,6 +950,44 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
|
||||
'test that emitting an error event doesnt throw': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
|
||||
io.configure(function () {
|
||||
io.set('polling duration', .05);
|
||||
io.set('close timeout', .05);
|
||||
});
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
socket.on('disconnect', function () {
|
||||
cl.end();
|
||||
io.server.close();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
cl.get('/socket.io/{protocol}/xhr-polling/' + sid, function (res, msgs) {
|
||||
res.statusCode.should.equal(200);
|
||||
msgs.should.have.length(1);
|
||||
msgs[0].should.eql({ type: 'noop', endpoint: '' });
|
||||
|
||||
cl.post(
|
||||
'/socket.io/{protocol}/xhr-polling/' + sid
|
||||
, parser.encodePacket({
|
||||
type: 'event'
|
||||
, name: 'error'
|
||||
})
|
||||
, function (res, data) {
|
||||
res.statusCode.should.eql(200);
|
||||
data.should.equal('1');
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'test emitting an event to the server with data': function (done) {
|
||||
var cl = client(++ports)
|
||||
, io = create(cl)
|
||||
@@ -2637,4 +2673,82 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
|
||||
'test emitting to closed clients': function (done) {
|
||||
var cl = client(++ports)
|
||||
, cl2 = client(ports)
|
||||
, io = create(cl)
|
||||
, connections = 0;
|
||||
|
||||
io.configure(function () {
|
||||
io.set('close timeout', .1);
|
||||
});
|
||||
|
||||
io.sockets.on('connection', function (socket) {
|
||||
socket.send('a');
|
||||
});
|
||||
|
||||
cl.handshake(function (sid) {
|
||||
cl.get('/socket.io/{protocol}/xhr-polling/' + sid, function (res, packs) {
|
||||
res.statusCode.should.equal(200);
|
||||
packs.should.have.length(1);
|
||||
packs[0].should.eql({ type: 'message', endpoint: '', data: 'a' });
|
||||
|
||||
cl2.handshake(function (sid2) {
|
||||
cl2.get(
|
||||
'/socket.io/{protocol}/xhr-polling/' + sid2
|
||||
, function (res, packs) {
|
||||
res.statusCode.should.equal(200);
|
||||
packs.should.have.length(1);
|
||||
packs[0].should.eql({ type: 'message', endpoint: '', data: 'a' });
|
||||
|
||||
io.sockets.emit('woot', 'b');
|
||||
|
||||
var total = 2;
|
||||
|
||||
cl.get(
|
||||
'/socket.io/{protocol}/xhr-polling/' + sid
|
||||
, function (res, packs) {
|
||||
res.statusCode.should.equal(200);
|
||||
packs.should.have.length(1);
|
||||
packs[0].should.eql({
|
||||
type: 'event'
|
||||
, endpoint: ''
|
||||
, name: 'woot'
|
||||
, args: ['b']
|
||||
});
|
||||
|
||||
--total || finish();
|
||||
}
|
||||
);
|
||||
|
||||
cl2.get(
|
||||
'/socket.io/{protocol}/xhr-polling/' + sid2
|
||||
, function (res, packs) {
|
||||
res.statusCode.should.equal(200);
|
||||
packs.should.have.length(1);
|
||||
packs[0].should.eql({
|
||||
type: 'event'
|
||||
, endpoint: ''
|
||||
, name: 'woot'
|
||||
, args: ['b']
|
||||
});
|
||||
|
||||
--total || finish();
|
||||
}
|
||||
);
|
||||
|
||||
function finish () {
|
||||
cl.end();
|
||||
cl2.end();
|
||||
io.server.close();
|
||||
done();
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user