├── lib
└── .placeholder
├── .gitignore
├── index.js
├── .npmignore
├── examples
├── test_server
│ ├── package.json
│ ├── config.js
│ ├── server.js
│ ├── README.md
│ └── sockjs_app.js
├── echo
│ ├── package.json
│ ├── README.md
│ ├── server.js
│ └── index.html
├── express
│ ├── package.json
│ ├── server.js
│ └── index.html
├── multiplex
│ ├── package.json
│ ├── README.md
│ ├── server.js
│ └── index.html
└── haproxy.cfg
├── COPYING
├── VERSION-GEN
├── src
├── trans-eventsource.coffee
├── iframe.coffee
├── chunking-test.coffee
├── trans-htmlfile.coffee
├── trans-jsonp.coffee
├── trans-xhr.coffee
├── utils.coffee
├── trans-websocket.coffee
├── sockjs.coffee
├── webjs.coffee
└── transport.coffee
├── package.json
├── LICENSE-MIT-SockJS
├── Makefile
├── Changelog
└── README.md
/lib/.placeholder:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .pidfile.pid
2 | node_modules
3 | lib/*.js
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/sockjs');
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | lib/.placeholder
3 | VERSION-GEN
4 | src
5 | node_modules
6 |
--------------------------------------------------------------------------------
/examples/test_server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sockjs-test-server",
3 | "version": "0.0.0-unreleasable",
4 | "dependencies": {
5 | "sockjs": "*"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/echo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sockjs-echo",
3 | "version": "0.0.0-unreleasable",
4 | "dependencies": {
5 | "node-static": "0.5.9",
6 | "sockjs": "*"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sockjs-express",
3 | "version": "0.0.0-unreleasable",
4 | "dependencies": {
5 | "express": "2.5.8",
6 | "sockjs": "*"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/test_server/config.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | server_opts: {
3 | sockjs_url: 'http://localhost:8080/lib/sockjs.js',
4 | websocket: true
5 | },
6 |
7 | port: 8081,
8 | host: '0.0.0.0'
9 | };
10 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Parts of the code are derived from various open source projects.
2 |
3 | For code derived from Socket.IO by Guillermo Rauch see
4 | https://github.com/LearnBoost/socket.io/tree/0.6.17#readme.
5 |
6 | All other code is released on MIT license, see LICENSE-MIT-SockJS.
7 |
--------------------------------------------------------------------------------
/examples/multiplex/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sockjs-multiplex",
3 | "version": "0.0.0-unreleasable",
4 | "dependencies": {
5 | "express": "2.5.8",
6 | "sockjs": "*",
7 | "websocket-multiplex" : "0.1.x"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/VERSION-GEN:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | LF='
4 | '
5 |
6 | VN=$(git describe --match "v[0-9]*" --abbrev=4 HEAD 2>/dev/null)
7 | case "$VN" in
8 | *$LF*) (exit 1) ;;
9 | v[0-9]*)
10 | git update-index -q --refresh
11 | test -z "$(git diff-index --name-only HEAD --)" ||
12 | VN="$VN-dirty" ;;
13 | esac
14 | VN=$(echo "$VN" | sed -e 's/-/./g');
15 | VN=$(expr "$VN" : v*'\(.*\)')
16 |
17 | echo "$VN"
18 |
--------------------------------------------------------------------------------
/examples/echo/README.md:
--------------------------------------------------------------------------------
1 | SockJS-node Echo example
2 | ========================
3 |
4 | To run this example, first install dependencies:
5 |
6 | npm install
7 |
8 | And run a server:
9 |
10 | node server.js
11 |
12 |
13 | That will spawn an http server at http://127.0.0.1:9999/ which will
14 | serve both html (served from the current directory) and also SockJS
15 | server (under the [/echo](http://127.0.0.1:9999/echo) path).
16 |
--------------------------------------------------------------------------------
/examples/test_server/server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var config = require('./config').config;
3 | var sockjs_app = require('./sockjs_app');
4 |
5 |
6 | var server = http.createServer();
7 | server.addListener('request', function(req, res) {
8 | res.setHeader('content-type', 'text/plain');
9 | res.writeHead(404);
10 | res.end('404 - Nothing here (via sockjs-node test_server)');
11 | });
12 | server.addListener('upgrade', function(req, res){
13 | res.end();
14 | });
15 |
16 | sockjs_app.install(config.server_opts, server);
17 |
18 | console.log(" [*] Listening on", config.host + ':' + config.port);
19 | server.listen(config.port, config.host);
20 |
--------------------------------------------------------------------------------
/examples/express/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var sockjs = require('sockjs');
3 |
4 | // 1. Echo sockjs server
5 | var sockjs_opts = {sockjs_url: "http://cdn.sockjs.org/sockjs-0.3.min.js"};
6 |
7 | var sockjs_echo = sockjs.createServer(sockjs_opts);
8 | sockjs_echo.on('connection', function(conn) {
9 | conn.on('data', function(message) {
10 | conn.write(message);
11 | });
12 | });
13 |
14 | // 2. Express server
15 | var app = express.createServer();
16 | sockjs_echo.installHandlers(app, {prefix:'/echo'});
17 |
18 | console.log(' [*] Listening on 0.0.0.0:9999' );
19 | app.listen(9999, '0.0.0.0');
20 |
21 | app.get('/', function (req, res) {
22 | res.sendfile(__dirname + '/index.html');
23 | });
24 |
--------------------------------------------------------------------------------
/examples/multiplex/README.md:
--------------------------------------------------------------------------------
1 | WebSocket-multiplex SockJS example
2 | ==================================
3 |
4 | This example is a copy of example from
5 | [websocket-multiplex](https://github.com/sockjs/websocket-multiplex/)
6 | project:
7 |
8 | * https://github.com/sockjs/websocket-multiplex/
9 |
10 |
11 | To run this example, first install dependencies:
12 |
13 | npm install
14 |
15 | And run a server:
16 |
17 | node server.js
18 |
19 |
20 | That will spawn an http server at http://127.0.0.1:9999/ which will
21 | serve both html (served from the current directory) and also SockJS
22 | service (under the [/multiplex](http://127.0.0.1:9999/multiplex)
23 | path).
24 |
25 | With that set up, WebSocket-multiplex is able to push three virtual
26 | connections over a single SockJS connection. See the code for details.
27 |
--------------------------------------------------------------------------------
/examples/test_server/README.md:
--------------------------------------------------------------------------------
1 | SockJS test server
2 | ==================
3 |
4 | In order to test sockjs server implementation the server needs to
5 | provide a standarized sockjs endpoint that will can used by various
6 | sockjs-related tests. For example by QUnit or
7 | [Sockjs-protocol](https://github.com/sockjs/sockjs-protocol) tests.
8 |
9 | This small code does exactly that - runs a simple server that supports
10 | the following SockJS services:
11 |
12 | * `/echo`
13 | * `/disabled_websocket_echo`
14 | * `/cookie_needed_echo`
15 | * `/close`
16 | * `/ticker`
17 | * `/amplify`
18 | * `/broadcast`
19 |
20 | If you just want to quickly run it:
21 |
22 | npm install
23 | node server.js
24 |
25 |
26 | If you want to run do development it's recommended to run `make
27 | test_server` from the top `sockjs-node` directory:
28 |
29 | cd ../..
30 | make test_server
31 |
--------------------------------------------------------------------------------
/examples/echo/server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var sockjs = require('sockjs');
3 | var node_static = require('node-static');
4 |
5 | // 1. Echo sockjs server
6 | var sockjs_opts = {sockjs_url: "http://cdn.sockjs.org/sockjs-0.3.min.js"};
7 |
8 | var sockjs_echo = sockjs.createServer(sockjs_opts);
9 | sockjs_echo.on('connection', function(conn) {
10 | conn.on('data', function(message) {
11 | conn.write(message);
12 | });
13 | });
14 |
15 | // 2. Static files server
16 | var static_directory = new node_static.Server(__dirname);
17 |
18 | // 3. Usual http stuff
19 | var server = http.createServer();
20 | server.addListener('request', function(req, res) {
21 | static_directory.serve(req, res);
22 | });
23 | server.addListener('upgrade', function(req,res){
24 | res.end();
25 | });
26 |
27 | sockjs_echo.installHandlers(server, {prefix:'/echo'});
28 |
29 | console.log(' [*] Listening on 0.0.0.0:9999' );
30 | server.listen(9999, '0.0.0.0');
31 |
--------------------------------------------------------------------------------
/src/trans-eventsource.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | utils = require('./utils')
8 | transport = require('./transport')
9 |
10 |
11 | class EventSourceReceiver extends transport.ResponseReceiver
12 | protocol: "eventsource"
13 |
14 | doSendFrame: (payload) ->
15 | # Beware of leading whitespace
16 | data = ['data: ',
17 | utils.escape_selected(payload, '\r\n\x00'),
18 | '\r\n\r\n']
19 | super(data.join(''))
20 |
21 | exports.app =
22 | eventsource: (req, res) ->
23 | res.setHeader('Content-Type', 'text/event-stream; charset=UTF-8')
24 | res.writeHead(200)
25 | # Opera needs one more new line at the start.
26 | res.write('\r\n')
27 |
28 | transport.register(req, @, new EventSourceReceiver(res, @options))
29 | return true
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "sockjs",
3 | "author" : "Marek Majkowski",
4 | "version" : "0.3.1",
5 | "description" : "SockJS-node is a server counterpart of SockJS-client a JavaScript library that provides a WebSocket-like object in the browser. SockJS gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication channel between the browser and the web server.",
6 | "keywords" : ["websockets", "websocket"],
7 | "homepage" : "https://github.com/sockjs/sockjs-node",
8 |
9 | "repository": {"type": "git",
10 | "url": "https://github.com/sockjs/sockjs-node.git"},
11 | "dependencies": {
12 | "node-uuid" : "1.3.3",
13 | "faye-websocket" : "0.4.0"
14 | },
15 | "optionalDependencies": {
16 | "rbytes" : "0.0.2"
17 | },
18 | "devDependencies": {
19 | "coffee-script" : "1.2.x"
20 | },
21 | "main": "index"
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE-MIT-SockJS:
--------------------------------------------------------------------------------
1 | Copyright (C) 2011 VMware, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/examples/haproxy.cfg:
--------------------------------------------------------------------------------
1 | # Requires recent Haproxy to work with websockets (for example 1.4.16).
2 | defaults
3 | mode http
4 | # Set timeouts to your needs
5 | timeout client 5s
6 | timeout connect 5s
7 | timeout server 5s
8 |
9 | frontend all 0.0.0.0:8888
10 | mode http
11 | timeout client 120s
12 |
13 | option forwardfor
14 | # Fake connection:close, required in this setup.
15 | option http-server-close
16 | option http-pretend-keepalive
17 |
18 | acl is_sockjs path_beg /echo /broadcast /close
19 | acl is_stats path_beg /stats
20 |
21 | use_backend sockjs if is_sockjs
22 | use_backend stats if is_stats
23 | default_backend static
24 |
25 |
26 | backend sockjs
27 | # Load-balance according to hash created from first two
28 | # directories in url path. For example requests going to /1/
29 | # should be handled by single server (assuming resource prefix is
30 | # one-level deep, like "/echo").
31 | balance uri depth 2
32 | timeout server 120s
33 | server srv_sockjs1 127.0.0.1:9999
34 | # server srv_sockjs2 127.0.0.1:9998
35 |
36 | backend static
37 | balance roundrobin
38 | server srv_static 127.0.0.1:8000
39 |
40 | backend stats
41 | stats uri /stats
42 | stats enable
43 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all serve clean
2 |
3 | COFFEE:=./node_modules/.bin/coffee
4 |
5 | #### General
6 |
7 | all: build
8 |
9 | build: src/*coffee
10 | @$(COFFEE) -v > /dev/null
11 | $(COFFEE) -o lib/ -c src/*.coffee
12 |
13 | clean:
14 | rm -f lib/*.js
15 |
16 |
17 | #### Testing
18 |
19 | test_server:
20 | node examples/test_server/server.js
21 |
22 | serve:
23 | @if [ -e .pidfile.pid ]; then \
24 | kill `cat .pidfile.pid`; \
25 | rm .pidfile.pid; \
26 | fi
27 |
28 | @while [ 1 ]; do \
29 | make build; \
30 | echo " [*] Running http server"; \
31 | make test_server & \
32 | SRVPID=$$!; \
33 | echo $$SRVPID > .pidfile.pid; \
34 | echo " [*] Server pid: $$SRVPID"; \
35 | inotifywait -r -q -e modify .; \
36 | kill `cat .pidfile.pid`; \
37 | rm -f .pidfile.pid; \
38 | sleep 0.1; \
39 | done
40 |
41 | #### Release process
42 | # 1) commit everything
43 | # 2) amend version in package.json
44 | # 3) run 'make tag' and run suggested 'git push' variants
45 | # 4) run 'npm publish'
46 |
47 | RVER:=$(shell grep "version" package.json|tr '\t"' ' \t'|cut -f 4)
48 | VER:=$(shell ./VERSION-GEN)
49 |
50 | .PHONY: tag
51 | tag: all
52 | git commit $(TAG_OPTS) package.json Changelog -m "Release $(RVER)"
53 | git tag -s v$(RVER) -m "Release $(RVER)"
54 | @echo ' [*] Now run'
55 | @echo 'git push; git push --tag'
56 |
--------------------------------------------------------------------------------
/src/iframe.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | utils = require('./utils')
8 |
9 | iframe_template = """
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 | Don't panic!
23 | This is a SockJS hidden iframe. It's used for cross domain magic.
24 |
25 |
26 | """
27 |
28 |
29 | exports.app =
30 | iframe: (req, res) ->
31 | context =
32 | '{{ sockjs_url }}': @options.sockjs_url
33 |
34 | content = iframe_template
35 | for k of context
36 | content = content.replace(k, context[k])
37 |
38 | quoted_md5 = '"' + utils.md5_hex(content) + '"'
39 |
40 | if 'if-none-match' of req.headers and
41 | req.headers['if-none-match'] is quoted_md5
42 | res.statusCode = 304
43 | return ''
44 |
45 | res.setHeader('Content-Type', 'text/html; charset=UTF-8')
46 | res.setHeader('ETag', quoted_md5)
47 | return content
48 |
--------------------------------------------------------------------------------
/examples/multiplex/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var sockjs = require('sockjs');
3 |
4 | var websocket_multiplex = require('websocket-multiplex');
5 |
6 |
7 | // 1. Setup SockJS server
8 | var sockjs_opts = {sockjs_url: "http://cdn.sockjs.org/sockjs-0.3.min.js"};
9 | var service = sockjs.createServer(sockjs_opts);
10 |
11 |
12 | // 2. Setup multiplexing
13 | var multiplexer = new websocket_multiplex.MultiplexServer(service);
14 |
15 | var ann = multiplexer.registerChannel('ann');
16 | ann.on('connection', function(conn) {
17 | conn.write('Ann says hi!');
18 | conn.on('data', function(data) {
19 | conn.write('Ann nods: ' + data);
20 | });
21 | });
22 |
23 | var bob = multiplexer.registerChannel('bob');
24 | bob.on('connection', function(conn) {
25 | conn.write('Bob doesn\'t agree.');
26 | conn.on('data', function(data) {
27 | conn.write('Bob says no to: ' + data);
28 | });
29 | });
30 |
31 | var carl = multiplexer.registerChannel('carl');
32 | carl.on('connection', function(conn) {
33 | conn.write('Carl says goodbye!');
34 | // Explicitly cancel connection
35 | conn.end();
36 | });
37 |
38 |
39 | // 3. Express server
40 | var app = express.createServer();
41 | service.installHandlers(app, {prefix:'/multiplex'});
42 |
43 | console.log(' [*] Listening on 0.0.0.0:9999' );
44 | app.listen(9999, '0.0.0.0');
45 |
46 | app.get('/', function (req, res) {
47 | res.sendfile(__dirname + '/index.html');
48 | });
49 |
50 | app.get('/multiplex.js', function (req, res) {
51 | res.sendfile(__dirname + '/multiplex.js');
52 | });
53 |
--------------------------------------------------------------------------------
/src/chunking-test.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | utils = require('./utils')
8 |
9 | exports.app =
10 | # TODO: remove in next major release
11 | chunking_test: (req, res, _, next_filter) ->
12 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
13 | res.writeHead(200)
14 |
15 | write = (payload) =>
16 | try
17 | res.write(payload + '\n')
18 | catch x
19 | return
20 |
21 | utils.timeout_chain([
22 | # IE requires 2KB prelude
23 | [0, => write('h')],
24 | [1, => write(Array(2049).join(' ') + 'h')],
25 | [5, => write('h')],
26 | [25, => write('h')],
27 | [125, => write('h')],
28 | [625, => write('h')],
29 | [3125, => write('h'); res.end()],
30 | ])
31 | return true
32 |
33 | info: (req, res, _) ->
34 | info = {
35 | websocket: @options.websocket,
36 | origins: @options.origins,
37 | cookie_needed: not not @options.jsessionid,
38 | entropy: utils.random32(),
39 | }
40 | res.setHeader('Content-Type', 'application/json; charset=UTF-8')
41 | res.writeHead(200)
42 | res.end(JSON.stringify(info))
43 |
44 | info_options: (req, res) ->
45 | res.statusCode = 204
46 | res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET')
47 | res.setHeader('Access-Control-Max-Age', res.cache_for)
48 | return ''
49 |
--------------------------------------------------------------------------------
/examples/echo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
35 |
36 | SockJS Echo example
37 |
38 |
42 |
43 |
71 |
72 |
--------------------------------------------------------------------------------
/src/trans-htmlfile.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | utils = require('./utils')
8 | transport = require('./transport')
9 |
10 | # Browsers fail with "Uncaught exception: ReferenceError: Security
11 | # error: attempted to read protected variable: _jp". Set
12 | # document.domain in order to work around that.
13 | iframe_template = """
14 |
15 |
16 |
17 |
18 | Don't panic!
19 |
26 | """
27 | # Safari needs at least 1024 bytes to parse the website. Relevant:
28 | # http://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors
29 | iframe_template += Array(1024 - iframe_template.length + 14).join(' ')
30 | iframe_template += '\r\n\r\n'
31 |
32 |
33 | class HtmlFileReceiver extends transport.ResponseReceiver
34 | protocol: "htmlfile"
35 |
36 | doSendFrame: (payload) ->
37 | super( '\r\n' )
38 |
39 |
40 | exports.app =
41 | htmlfile: (req, res) ->
42 | if not('c' of req.query or 'callback' of req.query)
43 | throw {
44 | status: 500
45 | message: '"callback" parameter required'
46 | }
47 | callback = if 'c' of req.query then req.query['c'] else req.query['callback']
48 | if /[^a-zA-Z0-9-_.]/.test(callback)
49 | throw {
50 | status: 500
51 | message: 'invalid "callback" parameter'
52 | }
53 |
54 |
55 | res.setHeader('Content-Type', 'text/html; charset=UTF-8')
56 | res.writeHead(200)
57 | res.write(iframe_template.replace(/{{ callback }}/g, callback));
58 |
59 | transport.register(req, @, new HtmlFileReceiver(res, @options))
60 | return true
61 |
--------------------------------------------------------------------------------
/examples/express/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
35 |
36 | SockJS Express example
37 |
38 |
42 |
43 |
71 |
72 |
--------------------------------------------------------------------------------
/src/trans-jsonp.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | transport = require('./transport')
8 |
9 | class JsonpReceiver extends transport.ResponseReceiver
10 | protocol: "jsonp-polling"
11 | max_response_size: 1
12 |
13 | constructor: (res, options, @callback) ->
14 | super(res, options)
15 |
16 | doSendFrame: (payload) ->
17 | # Yes, JSONed twice, there isn't a a better way, we must pass
18 | # a string back, and the script, will be evaled() by the
19 | # browser.
20 | super(@callback + "(" + JSON.stringify(payload) + ");\r\n")
21 |
22 |
23 | exports.app =
24 | jsonp: (req, res, _, next_filter) ->
25 | if not('c' of req.query or 'callback' of req.query)
26 | throw {
27 | status: 500
28 | message: '"callback" parameter required'
29 | }
30 |
31 | callback = if 'c' of req.query then req.query['c'] else req.query['callback']
32 | if /[^a-zA-Z0-9-_.]/.test(callback)
33 | throw {
34 | status: 500
35 | message: 'invalid "callback" parameter'
36 | }
37 |
38 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
39 | res.writeHead(200)
40 |
41 | transport.register(req, @, new JsonpReceiver(res, @options, callback))
42 | return true
43 |
44 | jsonp_send: (req, res, query) ->
45 | if not query
46 | throw {
47 | status: 500
48 | message: 'Payload expected.'
49 | }
50 | if typeof query is 'string'
51 | try
52 | d = JSON.parse(query)
53 | catch e
54 | throw {
55 | status: 500
56 | message: 'Broken JSON encoding.'
57 | }
58 | else
59 | d = query.d
60 | if typeof d is 'string' and d
61 | try
62 | d = JSON.parse(d)
63 | catch e
64 | throw {
65 | status: 500
66 | message: 'Broken JSON encoding.'
67 | }
68 |
69 | if not d or d.__proto__.constructor isnt Array
70 | throw {
71 | status: 500
72 | message: 'Payload expected.'
73 | }
74 | jsonp = transport.Session.bySessionId(req.session)
75 | if jsonp is null
76 | throw {status: 404}
77 | for message in d
78 | jsonp.didMessage(message)
79 |
80 | res.setHeader('Content-Length', '2')
81 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8')
82 | res.writeHead(200)
83 | res.end('ok')
84 | return true
85 |
--------------------------------------------------------------------------------
/src/trans-xhr.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | transport = require('./transport')
8 | utils = require('./utils')
9 |
10 | class XhrStreamingReceiver extends transport.ResponseReceiver
11 | protocol: "xhr-streaming"
12 |
13 | doSendFrame: (payload) ->
14 | return super(payload + '\n')
15 |
16 | class XhrPollingReceiver extends XhrStreamingReceiver
17 | protocol: "xhr-polling"
18 | max_response_size: 1
19 |
20 |
21 | exports.app =
22 | xhr_options: (req, res) ->
23 | res.statusCode = 204 # No content
24 | res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST')
25 | res.setHeader('Access-Control-Max-Age', res.cache_for)
26 | return ''
27 |
28 | xhr_send: (req, res, data) ->
29 | if not data
30 | throw {
31 | status: 500
32 | message: 'Payload expected.'
33 | }
34 | try
35 | d = JSON.parse(data)
36 | catch e
37 | throw {
38 | status: 500
39 | message: 'Broken JSON encoding.'
40 | }
41 |
42 | if not d or d.__proto__.constructor isnt Array
43 | throw {
44 | status: 500
45 | message: 'Payload expected.'
46 | }
47 | jsonp = transport.Session.bySessionId(req.session)
48 | if not jsonp
49 | throw {status: 404}
50 | for message in d
51 | jsonp.didMessage(message)
52 |
53 | # FF assumes that the response is XML.
54 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8')
55 | res.writeHead(204)
56 | res.end()
57 | return true
58 |
59 | xhr_cors: (req, res, content) ->
60 | if !req.headers['origin'] or req.headers['origin'] is 'null'
61 | origin = '*'
62 | else
63 | origin = req.headers['origin']
64 | res.setHeader('Access-Control-Allow-Origin', origin)
65 | headers = req.headers['access-control-request-headers']
66 | if headers
67 | res.setHeader('Access-Control-Allow-Headers', headers)
68 | res.setHeader('Access-Control-Allow-Credentials', 'true')
69 | return content
70 |
71 | xhr_poll: (req, res, _, next_filter) ->
72 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
73 | res.writeHead(200)
74 |
75 | transport.register(req, @, new XhrPollingReceiver(res, @options))
76 | return true
77 |
78 | xhr_streaming: (req, res, _, next_filter) ->
79 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
80 | res.writeHead(200)
81 |
82 | # IE requires 2KB prefix:
83 | # http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx
84 | res.write(Array(2049).join('h') + '\n')
85 |
86 | transport.register(req, @, new XhrStreamingReceiver(res, @options) )
87 | return true
88 |
--------------------------------------------------------------------------------
/examples/multiplex/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
36 |
37 | SockJS Multiplex example
38 |
39 |
43 |
44 |
48 |
49 |
53 |
54 |
96 |
97 |
--------------------------------------------------------------------------------
/examples/test_server/sockjs_app.js:
--------------------------------------------------------------------------------
1 | var sockjs = require('sockjs');
2 |
3 | exports.install = function(opts, server) {
4 | var sjs_echo = sockjs.createServer(opts);
5 | sjs_echo.on('connection', function(conn) {
6 | console.log(' [+] echo open ' + conn);
7 | conn.on('close', function() {
8 | console.log(' [-] echo close ' + conn);
9 | });
10 | conn.on('data', function(m) {
11 | var d = JSON.stringify(m);
12 | console.log(' [ ] echo message ' + conn,
13 | d.slice(0,64)+
14 | ((d.length > 64) ? '...' : ''));
15 | conn.write(m);
16 | });
17 | });
18 |
19 | var sjs_close = sockjs.createServer(opts);
20 | sjs_close.on('connection', function(conn) {
21 | console.log(' [+] clos open ' + conn);
22 | conn.close(3000, "Go away!");
23 | conn.on('close', function() {
24 | console.log(' [-] clos close ' + conn);
25 | });
26 | });
27 |
28 | var sjs_ticker = sockjs.createServer(opts);
29 | sjs_ticker.on('connection', function(conn) {
30 | console.log(' [+] ticker open ' + conn);
31 | var tref;
32 | var schedule = function() {
33 | conn.write('tick!');
34 | tref = setTimeout(schedule, 1000);
35 | };
36 | tref = setTimeout(schedule, 1000);
37 | conn.on('close', function() {
38 | clearTimeout(tref);
39 | console.log(' [-] ticker close ' + conn);
40 | });
41 | });
42 |
43 | var broadcast = {};
44 | var sjs_broadcast = sockjs.createServer(opts);
45 | sjs_broadcast.on('connection', function(conn) {
46 | console.log(' [+] broadcast open ' + conn);
47 | broadcast[conn.id] = conn;
48 | conn.on('close', function() {
49 | delete broadcast[conn.id];
50 | console.log(' [-] broadcast close' + conn);
51 | });
52 | conn.on('data', function(m) {
53 | console.log(' [-] broadcast message', m);
54 | for(var id in broadcast) {
55 | broadcast[id].write(m);
56 | }
57 | });
58 | });
59 |
60 | var sjs_amplify = sockjs.createServer(opts);
61 | sjs_amplify.on('connection', function(conn) {
62 | console.log(' [+] amp open ' + conn);
63 | conn.on('close', function() {
64 | console.log(' [-] amp close ' + conn);
65 | });
66 | conn.on('data', function(m) {
67 | var n = Math.floor(Number(m));
68 | n = (n > 0 && n < 19) ? n : 1;
69 | console.log(' [ ] amp message: 2^' + n);
70 | conn.write(Array(Math.pow(2, n)+1).join('x'));
71 | });
72 | });
73 |
74 |
75 | sjs_echo.installHandlers(server, {prefix:'/echo',
76 | response_limit: 4096}),
77 | sjs_echo.installHandlers(server, {prefix:'/disabled_websocket_echo',
78 | websocket: false});
79 | sjs_echo.installHandlers(server, {prefix:'/cookie_needed_echo',
80 | jsessionid: true});
81 | sjs_close.installHandlers(server, {prefix:'/close'});
82 | sjs_ticker.installHandlers(server, {prefix:'/ticker'});
83 | sjs_amplify.installHandlers(server, {prefix:'/amplify'});
84 | sjs_broadcast.installHandlers(server, {prefix:'/broadcast'});
85 | };
86 |
--------------------------------------------------------------------------------
/Changelog:
--------------------------------------------------------------------------------
1 | 0.3.1
2 | =====
3 |
4 | * #58 - websocket transport emitted an array instead of a string
5 | during onmessage event.
6 | * Running under node.js 0.7 caused infinite recursion (Stephan Kochen)
7 | * #59 - restrict characters allowed in callback parameter
8 | * Updated readme - rbytes package is optional
9 | * Updated readme WRT deployments on heroku
10 | * Add minimalistic license block to every source file.
11 |
12 |
13 | 0.3.0
14 | =====
15 |
16 | * Sending JSESSIONID cookie is now *disabled* by default.
17 | * sockjs/sockjs-protocol#46 - introduce new service
18 | required for protocol tests "/cookie_needed_echo"
19 | * Initial work towards better integration with
20 | "connect" (Stephan Kochen). See discusion:
21 | https://github.com/senchalabs/connect/pull/506
22 | * More documentation about the Cookie and Origin headers.
23 | * #51 - expose "readyState" on connection instance
24 | * #53 - expose "protocol" on connection instance
25 | * #52 - Some protocols may not emit 'close' event with IE.
26 | * sockjs/sockjs-client#49 - Support 'null' origin - aka: allow SockJS
27 | client to be served from file:// paths.
28 |
29 |
30 | 0.2.1
31 | =====
32 |
33 | * Bumped "faye-websocket" dependency to 0.4. Updated
34 | code to take advantage of introduced changes.
35 | * Pinned "node-static" and bumped "node-uuid" dependencies.
36 | * Removed "Origin" header list of headers exposed to the user.
37 | This header is not really meaningful in sockjs context.
38 | * Header "Access-Control-Allow-Methods" was misspelled.
39 |
40 |
41 | 0.2.0
42 | =====
43 |
44 | * #36, #3 - Replace a custom WebSocket server implementation
45 | with faye-websocket-node.
46 | * Multiple changes to support SockJS-protocol 0.2.
47 | * The session is now closed on network errors immediately
48 | (instead of waiting 5 seconds)
49 | * Raw websocket interface available - to make it easier
50 | to write command line SockJS clients.
51 | * Support '/info' url.
52 | * The test server got moved from SockJS-client to SockJS-node.
53 | * Dropped deprecated Server API (use createServer method instead).
54 | * Option `websocket` is now used instead of `disabled_transports`.
55 |
56 |
57 | 0.1.2
58 | =====
59 |
60 | * #27 - Allow all unicode characters to be send over SockJS.
61 | * #14 - Make it possible to customize JSESSIONID cookie logic.
62 |
63 |
64 | 0.1.1
65 | =====
66 |
67 | * #32 Expose various request headers on connection.
68 | * #30 Expose request path on connection.
69 |
70 |
71 | 0.1.0
72 | =====
73 |
74 | * The API changed, there is now an idiomatic API, modelled on node.js
75 | Stream API. The old API is deprecated and there is a dummy wrapper
76 | that emulates it. Please do upgrade to the new idiomatic API.
77 | * #22 Initial support for hybi13 (stephank)
78 | * New options accepted by the `Server` constructor: `log`,
79 | `heartbeat_delay` and `disconnect_delay`.
80 | * SockJS is now not able to send rich data structures - all data
81 | passed to `write` is converted to a string.
82 | * #23 `Connection.remoteAddress` property introduced (Stéphan Kochen)
83 | * Loads of small changes in order to adhere to protocol spec.
84 |
85 |
86 | 0.0.5
87 | =====
88 |
89 | * #20: `npm submodule sockjs` didn't work due to outdated github
90 | path.
91 |
92 |
93 | 0.0.4
94 | =====
95 |
96 | * Support for htmlfile transport, used by IE in a deployment
97 | dependent on cookies.
98 | * Added /chunking_test API, used to detect support for HTTP chunking
99 | on client side.
100 | * Unified code logic for all the chunking transports - the same code
101 | is reused for polling versions.
102 | * All the chunking transports are closed by the server after 128K was
103 | send, in order to force client to GC and reconnect.
104 | * Don't distribute source coffeescript with npm.
105 | * Minor fixes in websocket code.
106 | * Dropped jQuery dependency.
107 | * Unicode encoding could been garbled during XHR upload.
108 | * Other minor fixes.
109 |
110 |
111 | 0.0.3
112 | ======
113 |
114 | * EventSource transport didn't emit 'close' event.
115 |
116 |
117 | 0.0.2
118 | =====
119 |
120 | * By default set JSESSIONID cookie, useful for load balancing.
121 |
122 |
123 | 0.0.1
124 | =====
125 |
126 | * Initial release.
127 |
--------------------------------------------------------------------------------
/src/utils.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | crypto = require('crypto')
8 |
9 | try
10 | rbytes = require('rbytes')
11 | catch x
12 | null
13 |
14 | exports.array_intersection = array_intersection = (arr_a, arr_b) ->
15 | r = []
16 | for a in arr_a
17 | if arr_b.indexOf(a) isnt -1
18 | r.push(a)
19 | return r
20 |
21 | # exports.array_contains = (arr, element) ->
22 | # return (arr.indexOf(element) !== -1)
23 |
24 | exports.verify_origin = (origin, list_of_origins) ->
25 | if list_of_origins.indexOf('*:*') isnt -1
26 | return true
27 | if not origin
28 | return false
29 | try
30 | parts = url.parse(origin)
31 | origins = [parts.host + ':' + parts.port,
32 | parts.host + ':*',
33 | '*:' + parts.port]
34 | if array_intersection(origins, list_of_origins).length > 0
35 | return true
36 | catch x
37 | null
38 | return false
39 |
40 | exports.escape_selected = (str, chars) ->
41 | map = {}
42 | chars = '%'+chars
43 | for c in chars
44 | map[c] = escape(c)
45 | r = new RegExp('(['+chars+'])')
46 | parts = str.split(r)
47 | for i in [0...parts.length]
48 | v = parts[i]
49 | if v.length is 1 and v of map
50 | parts[i] = map[v]
51 | return parts.join('')
52 |
53 | # exports.random_string = (letters, max) ->
54 | # chars = 'abcdefghijklmnopqrstuvwxyz0123456789_'
55 | # max or= chars.length
56 | # ret = for i in [0...letters]
57 | # chars[Math.floor(Math.random() * max)]
58 | # return ret.join('')
59 |
60 | exports.buffer_concat = (buf_a, buf_b) ->
61 | dst = new Buffer(buf_a.length + buf_b.length)
62 | buf_a.copy(dst)
63 | buf_b.copy(dst, buf_a.length)
64 | return dst
65 |
66 | exports.md5_hex = (data) ->
67 | return crypto.createHash('md5')
68 | .update(data)
69 | .digest('hex')
70 |
71 | exports.sha1_base64 = (data) ->
72 | return crypto.createHash('sha1')
73 | .update(data)
74 | .digest('base64')
75 |
76 | exports.timeout_chain = (arr) ->
77 | arr = arr.slice(0)
78 | if not arr.length then return
79 | [timeout, user_fun] = arr.shift()
80 | fun = =>
81 | user_fun()
82 | exports.timeout_chain(arr)
83 | setTimeout(fun, timeout)
84 |
85 |
86 | exports.objectExtend = (dst, src) ->
87 | for k of src
88 | if src.hasOwnProperty(k)
89 | dst[k] = src[k]
90 | return dst
91 |
92 | exports.overshadowListeners = (ee, event, handler) ->
93 | # listeners() returns a reference to the internal array of EventEmitter.
94 | # Make a copy, because we're about the replace the actual listeners.
95 | old_listeners = ee.listeners(event).slice(0)
96 |
97 | ee.removeAllListeners(event)
98 | new_handler = () ->
99 | if handler.apply(this, arguments) isnt true
100 | for listener in old_listeners
101 | listener.apply(this, arguments)
102 | return false
103 | return true
104 | ee.addListener(event, new_handler)
105 |
106 |
107 | escapable = /[\x00-\x1f\ud800-\udfff\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufff0-\uffff]/g
108 |
109 | unroll_lookup = (escapable) ->
110 | unrolled = {}
111 | c = for i in [0...65536]
112 | String.fromCharCode(i)
113 | escapable.lastIndex = 0
114 | c.join('').replace escapable, (a) ->
115 | unrolled[ a ] = '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
116 | return unrolled
117 |
118 | lookup = unroll_lookup(escapable)
119 |
120 | exports.quote = (string) ->
121 | quoted = JSON.stringify(string)
122 |
123 | # In most cases normal json encoding fast and enough
124 | escapable.lastIndex = 0
125 | if not escapable.test(quoted)
126 | return quoted
127 |
128 | return quoted.replace escapable, (a) ->
129 | return lookup[a]
130 |
131 | exports.parseCookie = (cookie_header) ->
132 | cookies = {}
133 | if cookie_header
134 | for cookie in cookie_header.split(';')
135 | parts = cookie.split('=')
136 | cookies[ parts[0].trim() ] = ( parts[1] || '' ).trim()
137 | return cookies
138 |
139 | exports.random32 = () ->
140 | if rbytes
141 | x = rbytes.randomBytes(4)
142 | v = [x[0], x[1], x[2], x[3]]
143 | else
144 | foo = -> Math.floor(Math.random()*256)
145 | v = [foo(), foo(), foo(), foo()]
146 |
147 | x = v[0] + (v[1]*256 ) + (v[2]*256*256) + (v[3]*256*256*256)
148 | return x
149 |
--------------------------------------------------------------------------------
/src/trans-websocket.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | FayeWebsocket = require('faye-websocket')
8 |
9 | utils = require('./utils')
10 | transport = require('./transport')
11 |
12 |
13 | exports.app =
14 | _websocket_check: (req, connection, head) ->
15 | # Request via node.js magical 'upgrade' event.
16 | if (req.headers.upgrade || '').toLowerCase() isnt 'websocket'
17 | throw {
18 | status: 400
19 | message: 'Can "Upgrade" only to "WebSocket".'
20 | }
21 | conn = (req.headers.connection || '').toLowerCase()
22 |
23 | if (conn.split(/, */)).indexOf('upgrade') is -1
24 | throw {
25 | status: 400
26 | message: '"Connection" must be "Upgrade".'
27 | }
28 | origin = req.headers.origin
29 | if not utils.verify_origin(origin, @options.origins)
30 | throw {
31 | status: 400
32 | message: 'Unverified origin.'
33 | }
34 |
35 | sockjs_websocket: (req, connection, head) ->
36 | @_websocket_check(req, connection, head)
37 | ws = new FayeWebsocket(req, connection, head)
38 | ws.onopen = =>
39 | # websockets possess no session_id
40 | transport.registerNoSession(req, @,
41 | new WebSocketReceiver(ws, connection))
42 | return true
43 |
44 | raw_websocket: (req, connection, head) ->
45 | @_websocket_check(req, connection, head)
46 | ver = req.headers['sec-websocket-version'] or ''
47 | if ['8', '13'].indexOf(ver) is -1
48 | throw {
49 | status: 400
50 | message: 'Only supported WebSocket protocol is RFC 6455.'
51 | }
52 | ws = new FayeWebsocket(req, connection, head)
53 | ws.onopen = =>
54 | new RawWebsocketSessionReceiver(req, connection, @, ws)
55 | return true
56 |
57 |
58 | class WebSocketReceiver extends transport.GenericReceiver
59 | protocol: "websocket"
60 |
61 | constructor: (@ws, @connection) ->
62 | try
63 | @connection.setKeepAlive(true, 5000)
64 | @connection.setNoDelay(true)
65 | catch x
66 | @ws.addEventListener('message', (m) => @didMessage(m.data))
67 | super @connection
68 |
69 | setUp: ->
70 | super
71 | @ws.addEventListener('close', @thingy_end_cb)
72 |
73 | tearDown: ->
74 | @ws.removeEventListener('close', @thingy_end_cb)
75 | super
76 |
77 | didMessage: (payload) ->
78 | if @ws and @session and payload.length > 0
79 | try
80 | message = JSON.parse(payload)
81 | catch x
82 | return @didClose(1002, 'Broken framing.')
83 | if payload[0] is '['
84 | for msg in message
85 | @session.didMessage(msg)
86 | else
87 | @session.didMessage(message)
88 |
89 | doSendFrame: (payload) ->
90 | if @ws
91 | try
92 | @ws.send(payload)
93 | return true
94 | catch e
95 | return false
96 |
97 | didClose: ->
98 | super
99 | try
100 | @ws.close()
101 | catch x
102 | @ws = null
103 | @connection = null
104 |
105 |
106 |
107 | Transport = transport.Transport
108 |
109 | # Inheritance only for decorateConnection.
110 | class RawWebsocketSessionReceiver extends transport.Session
111 | constructor: (req, conn, server, @ws) ->
112 | @prefix = server.options.prefix
113 | @readyState = Transport.OPEN
114 | @recv = {connection: conn}
115 |
116 | @connection = new transport.SockJSConnection(@)
117 | @decorateConnection(req)
118 | server.emit('connection', @connection)
119 | @_end_cb = => @didClose()
120 | @ws.addEventListener('close', @_end_cb)
121 | @_message_cb = (m) => @didMessage(m)
122 | @ws.addEventListener('message', @_message_cb)
123 |
124 | didMessage: (m) ->
125 | if @readyState is Transport.OPEN
126 | @connection.emit('data', m.data)
127 | return
128 |
129 | send: (payload) ->
130 | if @readyState isnt Transport.OPEN
131 | return false
132 | @ws.send(payload)
133 | return true
134 |
135 | close: (status=1000, reason="Normal closure") ->
136 | if @readyState isnt Transport.OPEN
137 | return false
138 | @readyState = Transport.CLOSING
139 | @ws.close(status, reason)
140 | return true
141 |
142 | didClose: ->
143 | if @ws
144 | return
145 | @ws.removeEventListener('message', @_message_cb)
146 | @ws.removeEventListener('close', @_end_cb)
147 | try
148 | @ws.close()
149 | catch x
150 | @ws = null
151 |
--------------------------------------------------------------------------------
/src/sockjs.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | events = require('events')
8 | fs = require('fs')
9 | webjs = require('./webjs')
10 | utils = require('./utils')
11 |
12 | trans_websocket = require('./trans-websocket')
13 | trans_jsonp = require('./trans-jsonp')
14 | trans_xhr = require('./trans-xhr')
15 | iframe = require('./iframe')
16 | trans_eventsource = require('./trans-eventsource')
17 | trans_htmlfile = require('./trans-htmlfile')
18 | chunking_test = require('./chunking-test')
19 |
20 | sockjsVersion = ->
21 | try
22 | package = fs.readFileSync(__dirname + '/../package.json', 'utf-8')
23 | catch x
24 | return if package then JSON.parse(package).version else null
25 |
26 |
27 | class App extends webjs.GenericApp
28 | welcome_screen: (req, res) ->
29 | res.setHeader('content-type', 'text/plain; charset=UTF-8')
30 | res.writeHead(200)
31 | res.end("Welcome to SockJS!\n")
32 | return true
33 |
34 | handle_404: (req, res) ->
35 | res.setHeader('content-type', 'text/plain; charset=UTF-8')
36 | res.writeHead(404)
37 | res.end('404 Error: Page not found\n')
38 | return true
39 |
40 | disabled_transport: (req, res, data) ->
41 | return @handle_404(req, res, data)
42 |
43 | h_sid: (req, res, data) ->
44 | # Some load balancers do sticky sessions, but only if there is
45 | # a JSESSIONID cookie. If this cookie isn't yet set, we shall
46 | # set it to a dummy value. It doesn't really matter what, as
47 | # session information is usually added by the load balancer.
48 | req.cookies = utils.parseCookie(req.headers.cookie)
49 | if typeof @options.jsessionid is 'function'
50 | # Users can supply a function
51 | @options.jsessionid(req, res)
52 | else if (@options.jsessionid and res.setHeader)
53 | # We need to set it every time, to give the loadbalancer
54 | # opportunity to attach its own cookies.
55 | jsid = req.cookies['JSESSIONID'] or 'dummy'
56 | res.setHeader('Set-Cookie', 'JSESSIONID=' + jsid + '; path=/')
57 | return data
58 |
59 | log: (severity, line) ->
60 | @options.log(severity, line)
61 |
62 |
63 | utils.objectExtend(App.prototype, iframe.app)
64 | utils.objectExtend(App.prototype, chunking_test.app)
65 |
66 | utils.objectExtend(App.prototype, trans_websocket.app)
67 | utils.objectExtend(App.prototype, trans_jsonp.app)
68 | utils.objectExtend(App.prototype, trans_xhr.app)
69 | utils.objectExtend(App.prototype, trans_eventsource.app)
70 | utils.objectExtend(App.prototype, trans_htmlfile.app)
71 |
72 |
73 | generate_dispatcher = (options) ->
74 | p = (s) => new RegExp('^' + options.prefix + s + '[/]?$')
75 | t = (s) => [p('/([^/.]+)/([^/.]+)' + s), 'server', 'session']
76 | opts_filters = (options_filter='xhr_options') ->
77 | return ['h_sid', 'xhr_cors', 'cache_for', options_filter, 'expose']
78 | dispatcher = [
79 | ['GET', p(''), ['welcome_screen']],
80 | ['GET', p('/iframe[0-9-.a-z_]*.html'), ['iframe', 'cache_for', 'expose']],
81 | ['OPTIONS', p('/info'), opts_filters('info_options')],
82 | ['GET', p('/info'), ['xhr_cors', 'h_no_cache', 'info', 'expose']],
83 | ['OPTIONS', p('/chunking_test'), opts_filters()],
84 | ['POST', p('/chunking_test'), ['xhr_cors', 'expect_xhr', 'chunking_test']],
85 | ['GET', p('/websocket'), ['raw_websocket']],
86 | ['GET', t('/jsonp'), ['h_sid', 'h_no_cache', 'jsonp']],
87 | ['POST', t('/jsonp_send'), ['h_sid', 'expect_form', 'jsonp_send']],
88 | ['POST', t('/xhr'), ['h_sid', 'xhr_cors', 'xhr_poll']],
89 | ['OPTIONS', t('/xhr'), opts_filters()],
90 | ['POST', t('/xhr_send'), ['h_sid', 'xhr_cors', 'expect_xhr', 'xhr_send']],
91 | ['OPTIONS', t('/xhr_send'), opts_filters()],
92 | ['POST', t('/xhr_streaming'), ['h_sid', 'xhr_cors', 'xhr_streaming']],
93 | ['OPTIONS', t('/xhr_streaming'), opts_filters()],
94 | ['GET', t('/eventsource'), ['h_sid', 'h_no_cache', 'eventsource']],
95 | ['GET', t('/htmlfile'), ['h_sid', 'h_no_cache', 'htmlfile']],
96 | ]
97 |
98 | # TODO: remove this code on next major release
99 | if options.websocket
100 | dispatcher.push(
101 | ['GET', t('/websocket'), ['sockjs_websocket']])
102 | else
103 | # modify urls to return 404
104 | dispatcher.push(
105 | ['GET', t('/websocket'), ['cache_for', 'disabled_transport']])
106 | return dispatcher
107 |
108 | class Listener
109 | constructor: (@options, emit) ->
110 | @app = new App()
111 | @app.options = options
112 | @app.emit = emit
113 | @app.log('debug', 'SockJS v' + sockjsVersion() + ' ' +
114 | 'bound to ' + JSON.stringify(options.prefix))
115 | @dispatcher = generate_dispatcher(@options)
116 | @webjs_handler = webjs.generateHandler(@app, @dispatcher)
117 | @path_regexp = new RegExp('^' + @options.prefix + '([/].+|[/]?)$')
118 |
119 | handler: (req, res, extra) =>
120 | # All urls that match the prefix must be handled by us.
121 | if not req.url.match(@path_regexp)
122 | return false
123 | @webjs_handler(req, res, extra)
124 | return true
125 |
126 | getHandler: () ->
127 | return (a,b,c) => @handler(a,b,c)
128 |
129 |
130 | class Server extends events.EventEmitter
131 | constructor: (user_options) ->
132 | @options =
133 | prefix: ''
134 | response_limit: 128*1024
135 | origins: ['*:*']
136 | websocket: true
137 | jsessionid: false
138 | heartbeat_delay: 25000
139 | disconnect_delay: 5000
140 | log: (severity, line) -> console.log(line)
141 | sockjs_url: 'http://cdn.sockjs.org/sockjs-0.3.min.js'
142 | if user_options
143 | utils.objectExtend(@options, user_options)
144 |
145 | listener: (handler_options) ->
146 | options = utils.objectExtend({}, @options)
147 | if handler_options
148 | utils.objectExtend(options, handler_options)
149 | return new Listener(options, => @emit.apply(@, arguments))
150 |
151 | installHandlers: (http_server, handler_options) ->
152 | handler = @listener(handler_options).getHandler()
153 | utils.overshadowListeners(http_server, 'request', handler)
154 | utils.overshadowListeners(http_server, 'upgrade', handler)
155 | return true
156 |
157 | middleware: (handler_options) ->
158 | handler = @listener(handler_options).getHandler()
159 | handler.upgrade = handler
160 | return handler
161 |
162 | exports.createServer = (options) ->
163 | return new Server(options)
164 |
165 | exports.listen = (http_server, options) ->
166 | srv = exports.createServer(options)
167 | if http_server
168 | srv.installHandlers(http_server)
169 | return srv
170 |
--------------------------------------------------------------------------------
/src/webjs.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | url = require('url')
8 | querystring = require('querystring')
9 | fs = require('fs')
10 | http = require('http')
11 |
12 | utils = require('./utils')
13 |
14 |
15 | execute_request = (app, funs, req, res, data) ->
16 | try
17 | while funs.length > 0
18 | fun = funs.shift()
19 | req.last_fun = fun
20 | data = app[fun](req, res, data, req.next_filter)
21 | catch x
22 | if typeof x is 'object' and 'status' of x
23 | if x.status is 0
24 | return
25 | else if 'handle_' + x.status of app
26 | app['handle_' + x.status](req, res, x)
27 | else
28 | app['handle_error'](req, res, x)
29 | else
30 | app['handle_error'](req, res, x)
31 | app['log_request'](req, res, true)
32 |
33 |
34 | fake_response = (req, res) ->
35 | # This is quite simplistic, don't expect much.
36 | headers = {'Connection': 'close'}
37 | res.writeHead = (status, user_headers = {}) ->
38 | r = []
39 | r.push('HTTP/' + req.httpVersion + ' ' + status +
40 | ' ' + http.STATUS_CODES[status])
41 | utils.objectExtend(headers, user_headers)
42 | for k of headers
43 | r.push(k + ': ' + headers[k])
44 | r = r.concat(['', ''])
45 | try
46 | res.write(r.join('\r\n'))
47 | catch e
48 | null
49 | try
50 | res.end()
51 | catch e
52 | null
53 | res.setHeader = (k, v) -> headers[k] = v
54 |
55 |
56 | exports.generateHandler = (app, dispatcher) ->
57 | return (req, res, head) ->
58 | if typeof res.writeHead is "undefined"
59 | fake_response(req, res)
60 | utils.objectExtend(req, url.parse(req.url, true))
61 | req.start_date = new Date()
62 |
63 | found = false
64 | allowed_methods = []
65 | for row in dispatcher
66 | [method, path, funs] = row
67 | if path.constructor isnt Array
68 | path = [path]
69 | # path[0] must be a regexp
70 | m = req.pathname.match(path[0])
71 | if not m
72 | continue
73 | if not req.method.match(new RegExp(method))
74 | allowed_methods.push(method)
75 | continue
76 | for i in [1...path.length]
77 | req[path[i]] = m[i]
78 | funs = funs[0..]
79 | funs.push('log_request')
80 | req.next_filter = (data) ->
81 | execute_request(app, funs, req, res, data)
82 | req.next_filter(head)
83 | found = true
84 | break
85 |
86 | if not found
87 | if allowed_methods.length isnt 0
88 | app['handle_405'](req, res, allowed_methods)
89 | else
90 | app['handle_404'](req, res)
91 | app['log_request'](req, res, true)
92 | return
93 |
94 | exports.GenericApp = class GenericApp
95 | handle_404: (req, res, x) ->
96 | if res.finished
97 | return x
98 | res.writeHead(404, {})
99 | res.end()
100 | return true
101 |
102 | handle_405:(req, res, methods) ->
103 | res.writeHead(405, {'Allow': methods.join(', ')})
104 | res.end()
105 | return true
106 |
107 | handle_error: (req, res, x) ->
108 | # console.log('handle_error', x.stack)
109 | if res.finished
110 | return x
111 | if typeof x is 'object' and 'status' of x
112 | res.writeHead(x.status, {})
113 | res.end((x.message or ""))
114 | else
115 | try
116 | res.writeHead(500, {})
117 | res.end("500 - Internal Server Error")
118 | catch y
119 | @log('error', 'Exception on "'+ req.method + ' ' + req.href + '" in filter "' + req.last_fun + '":\n' + (x.stack || x))
120 | return true
121 |
122 | log_request: (req, res, data) ->
123 | td = (new Date()) - req.start_date
124 | @log('info', req.method + ' ' + req.url + ' ' + td + 'ms ' +
125 | (if res.finished then res._header.split('\r')[0].split(' ')[1] \
126 | else '(unfinished)'))
127 | return data
128 |
129 | log: (severity, line) ->
130 | console.log(line)
131 |
132 | expose_html: (req, res, content) ->
133 | if res.finished
134 | return content
135 | if not res.getHeader('Content-Type')
136 | res.setHeader('Content-Type', 'text/html; charset=UTF-8')
137 | return @expose(req, res, content)
138 |
139 | expose_json: (req, res, content) ->
140 | if res.finished
141 | return content
142 | if not res.getHeader('Content-Type')
143 | res.setHeader('Content-Type', 'application/json')
144 | return @expose(req, res, JSON.stringify(content))
145 |
146 | expose: (req, res, content) ->
147 | if res.finished
148 | return content
149 | if content and not res.getHeader('Content-Type')
150 | res.setHeader('Content-Type', 'text/plain')
151 | if content
152 | res.setHeader('Content-Length', content.length)
153 | res.writeHead(res.statusCode)
154 | res.end(content, 'utf8')
155 | return true
156 |
157 | serve_file: (req, res, filename, next_filter) ->
158 | a = (error, content) ->
159 | if error
160 | res.writeHead(500)
161 | res.end("can't read file")
162 | else
163 | res.setHeader('Content-length', content.length)
164 | res.writeHead(res.statusCode, res.headers)
165 | res.end(content, 'utf8')
166 | next_filter(true)
167 | fs.readFile(filename, a)
168 | throw {status:0}
169 |
170 | cache_for: (req, res, content) ->
171 | res.cache_for = res.cache_for or 365 * 24 * 60 * 60 # one year.
172 | # See: http://code.google.com/speed/page-speed/docs/caching.html
173 | res.setHeader('Cache-Control', 'public, max-age=' + res.cache_for)
174 | exp = new Date()
175 | exp.setTime(exp.getTime() + res.cache_for * 1000)
176 | res.setHeader('Expires', exp.toGMTString())
177 | return content
178 |
179 | h_no_cache: (req, res, content) ->
180 | res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
181 | return content
182 |
183 | expect_form: (req, res, _data, next_filter) ->
184 | data = new Buffer(0)
185 | req.on 'data', (d) =>
186 | data = utils.buffer_concat(data, new Buffer(d, 'binary'))
187 | req.on 'end', =>
188 | data = data.toString('utf-8')
189 | switch (req.headers['content-type'] or '').split(';')[0]
190 | when 'application/x-www-form-urlencoded'
191 | q = querystring.parse(data)
192 | when 'text/plain', ''
193 | q = data
194 | else
195 | @log('error', "Unsupported content-type " +
196 | req.headers['content-type'])
197 | q = undefined
198 | next_filter(q)
199 | throw {status:0}
200 |
201 | expect_xhr: (req, res, _data, next_filter) ->
202 | data = new Buffer(0)
203 | req.on 'data', (d) =>
204 | data = utils.buffer_concat(data, new Buffer(d, 'binary'))
205 | req.on 'end', =>
206 | data = data.toString('utf-8')
207 | switch (req.headers['content-type'] or '').split(';')[0]
208 | when 'text/plain', 'T', 'application/json', 'application/xml', '', 'text/xml'
209 | q = data
210 | else
211 | @log('error', 'Unsupported content-type ' +
212 | req.headers['content-type'])
213 | q = undefined
214 | next_filter(q)
215 | throw {status:0}
216 |
--------------------------------------------------------------------------------
/src/transport.coffee:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Copyright (c) 2011-2012 VMware, Inc.
3 | #
4 | # For the license see COPYING.
5 | # ***** END LICENSE BLOCK *****
6 |
7 | stream = require('stream')
8 | uuid = require('node-uuid')
9 | utils = require('./utils')
10 |
11 | class Transport
12 |
13 | Transport.CONNECTING = 0
14 | Transport.OPEN = 1
15 | Transport.CLOSING = 2
16 | Transport.CLOSED = 3
17 |
18 | closeFrame = (status, reason) ->
19 | return 'c' + JSON.stringify([status, reason])
20 |
21 |
22 | class SockJSConnection extends stream.Stream
23 | constructor: (@_session) ->
24 | @id = uuid()
25 | @headers = {}
26 | @prefix = @_session.prefix
27 |
28 | toString: ->
29 | return ''
30 |
31 | write: (string) ->
32 | return @_session.send('' + string)
33 |
34 | end: (string) ->
35 | if string
36 | @write(string)
37 | @close()
38 | return null
39 |
40 | close: (code, reason) ->
41 | @_session.close(code, reason)
42 |
43 | destroy: () ->
44 | @removeAllListeners()
45 | @end()
46 |
47 | destroySoon: () ->
48 | @destroy()
49 |
50 | SockJSConnection.prototype.__defineGetter__ 'readable', ->
51 | @_session.readyState is Transport.OPEN
52 | SockJSConnection.prototype.__defineGetter__ 'writable', ->
53 | @_session.readyState is Transport.OPEN
54 | SockJSConnection.prototype.__defineGetter__ 'readyState', ->
55 | @_session.readyState
56 |
57 |
58 | MAP = {}
59 |
60 | class Session
61 | constructor: (@session_id, server) ->
62 | @heartbeat_delay = server.options.heartbeat_delay
63 | @disconnect_delay = server.options.disconnect_delay
64 | @prefix = server.options.prefix
65 | @send_buffer = []
66 | @is_closing = false
67 | @readyState = Transport.CONNECTING
68 | if @session_id
69 | MAP[@session_id] = @
70 | @timeout_cb = => @didTimeout()
71 | @to_tref = setTimeout(@timeout_cb, @disconnect_delay)
72 | @connection = new SockJSConnection(@)
73 | @emit_open = =>
74 | @emit_open = null
75 | server.emit('connection', @connection)
76 |
77 | register: (req, recv) ->
78 | if @recv
79 | recv.doSendFrame(closeFrame(2010, "Another connection still open"))
80 | recv.didClose()
81 | return
82 | if @to_tref
83 | clearTimeout(@to_tref)
84 | @to_tref = null
85 | if @readyState is Transport.CLOSING
86 | recv.doSendFrame(@close_frame)
87 | recv.didClose()
88 | @to_tref = setTimeout(@timeout_cb, @disconnect_delay)
89 | return
90 | # Registering. From now on 'unregister' is responsible for
91 | # setting the timer.
92 | @recv = recv
93 | @recv.session = @
94 |
95 | # Save parameters from request
96 | @decorateConnection(req)
97 |
98 | # first, send the open frame
99 | if @readyState is Transport.CONNECTING
100 | @recv.doSendFrame('o')
101 | @readyState = Transport.OPEN
102 | # Emit the open event, but not right now
103 | process.nextTick @emit_open
104 |
105 | # At this point the transport might have gotten away (jsonp).
106 | if not @recv
107 | return
108 | @tryFlush()
109 | return
110 |
111 | decorateConnection: (req) ->
112 | # Store the last known address.
113 | unless socket = @recv.connection
114 | socket = @recv.response.connection
115 | @connection.remoteAddress = socket.remoteAddress
116 | @connection.remotePort = socket.remotePort
117 | try
118 | @connection.address = socket.address()
119 | catch e
120 | @connection.address = {}
121 |
122 | @connection.url = req.url
123 | @connection.pathname = req.pathname
124 | @connection.protocol = @recv.protocol
125 |
126 | headers = {}
127 | for key in ['referer', 'x-client-ip', 'x-forwarded-for', \
128 | 'x-cluster-client-ip', 'via', 'x-real-ip']
129 | headers[key] = req.headers[key] if req.headers[key]
130 | if headers
131 | @connection.headers = headers
132 |
133 | unregister: ->
134 | @recv.session = null
135 | @recv = null
136 | if @to_tref
137 | clearTimeout(@to_tref)
138 | @to_tref = setTimeout(@timeout_cb, @disconnect_delay)
139 |
140 | tryFlush: ->
141 | if @send_buffer.length > 0
142 | [sb, @send_buffer] = [@send_buffer, []]
143 | @recv.doSendBulk(sb)
144 | else
145 | if @to_tref
146 | clearTimeout(@to_tref)
147 | x = =>
148 | if @recv
149 | @to_tref = setTimeout(x, @heartbeat_delay)
150 | @recv.doSendFrame("h")
151 | @to_tref = setTimeout(x, @heartbeat_delay)
152 | return
153 |
154 | didTimeout: ->
155 | if @to_tref
156 | clearTimeout(@to_tref)
157 | @to_tref = null
158 | if @readyState isnt Transport.CONNECTING and
159 | @readyState isnt Transport.OPEN and
160 | @readyState isnt Transport.CLOSING
161 | throw Error('INVALID_STATE_ERR')
162 | if @recv
163 | throw Error('RECV_STILL_THERE')
164 | @readyState = Transport.CLOSED
165 | # Node streaming API is broken. Reader defines 'close' and 'end'
166 | # but Writer defines only 'close'. 'End' isn't optional though.
167 | # http://nodejs.org/docs/v0.5.8/api/streams.html#event_close_
168 | @connection.emit('end')
169 | @connection.emit('close')
170 | @connection = null
171 | if @session_id
172 | delete MAP[@session_id]
173 | @session_id = null
174 |
175 | didMessage: (payload) ->
176 | if @readyState is Transport.OPEN
177 | @connection.emit('data', payload)
178 | return
179 |
180 | send: (payload) ->
181 | if @readyState isnt Transport.OPEN
182 | return false
183 | @send_buffer.push('' + payload)
184 | if @recv
185 | @tryFlush()
186 | return true
187 |
188 | close: (status=1000, reason="Normal closure") ->
189 | if @readyState isnt Transport.OPEN
190 | return false
191 | @readyState = Transport.CLOSING
192 | @close_frame = closeFrame(status, reason)
193 | if @recv
194 | # Go away.
195 | @recv.doSendFrame(@close_frame)
196 | @recv.didClose()
197 | if @recv
198 | @unregister()
199 | return true
200 |
201 |
202 |
203 | Session.bySessionId = (session_id) ->
204 | return MAP[session_id] or null
205 |
206 | register = (req, server, session_id, receiver) ->
207 | session = Session.bySessionId(session_id)
208 | if not session
209 | session = new Session(session_id, server)
210 | session.register(req, receiver)
211 | return session
212 |
213 | exports.register = (req, server, receiver) ->
214 | register(req, server, req.session, receiver)
215 | exports.registerNoSession = (req, server, receiver) ->
216 | register(req, server, undefined, receiver)
217 |
218 |
219 |
220 | class GenericReceiver
221 | constructor: (@thingy) ->
222 | @setUp(@thingy)
223 |
224 | setUp: ->
225 | @thingy_end_cb = () => @didAbort(1006, "Connection closed")
226 | @thingy.addListener('close', @thingy_end_cb)
227 | @thingy.addListener('end', @thingy_end_cb)
228 |
229 | tearDown: ->
230 | @thingy.removeListener('close', @thingy_end_cb)
231 | @thingy.removeListener('end', @thingy_end_cb)
232 | @thingy_end_cb = null
233 |
234 | didAbort: (status, reason) ->
235 | session = @session
236 | @didClose(status, reason)
237 | if session
238 | session.didTimeout()
239 |
240 | didClose: (status, reason) ->
241 | if @thingy
242 | @tearDown(@thingy)
243 | @thingy = null
244 | if @session
245 | @session.unregister(status, reason)
246 |
247 | doSendBulk: (messages) ->
248 | q_msgs = for m in messages
249 | utils.quote(m)
250 | @doSendFrame('a' + '[' + q_msgs.join(',') + ']')
251 |
252 |
253 | # Write stuff to response, using chunked encoding if possible.
254 | class ResponseReceiver extends GenericReceiver
255 | max_response_size: undefined
256 |
257 | constructor: (@response, @options) ->
258 | @curr_response_size = 0
259 | try
260 | @response.connection.setKeepAlive(true, 5000)
261 | catch x
262 | super (@response.connection)
263 | if @max_response_size is undefined
264 | @max_response_size = @options.response_limit
265 |
266 | doSendFrame: (payload) ->
267 | @curr_response_size += payload.length
268 | r = false
269 | try
270 | @response.write(payload)
271 | r = true
272 | catch x
273 | if @max_response_size and @curr_response_size >= @max_response_size
274 | @didClose()
275 | return r
276 |
277 | didClose: ->
278 | super
279 | try
280 | @response.end()
281 | catch x
282 | @response = null
283 |
284 |
285 | exports.GenericReceiver = GenericReceiver
286 | exports.Transport = Transport
287 | exports.Session = Session
288 | exports.ResponseReceiver = ResponseReceiver
289 | exports.SockJSConnection = SockJSConnection
290 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SockJS family:
2 |
3 | * [SockJS-client](https://github.com/sockjs/sockjs-client) JavaScript client library
4 | * [SockJS-node](https://github.com/sockjs/sockjs-node) Node.js server
5 | * [SockJS-erlang](https://github.com/sockjs/sockjs-erlang) Erlang server
6 |
7 |
8 | SockJS-node server
9 | ==================
10 |
11 | SockJS-node is a Node.js server side counterpart of
12 | [SockJS-client browser library](https://github.com/sockjs/sockjs-client)
13 | written in CoffeeScript.
14 |
15 | To install `sockjs-node` run:
16 |
17 | npm install sockjs
18 |
19 | (If you see `rbytes` dependecy failing, don't worry, it's optional, SockJS-node will work fine without it.)
20 |
21 | An simplified echo SockJS server could look more or less like:
22 |
23 | ```javascript
24 | var http = require('http');
25 | var sockjs = require('sockjs');
26 |
27 | var echo = sockjs.createServer();
28 | echo.on('connection', function(conn) {
29 | conn.on('data', function(message) {
30 | conn.write(message);
31 | });
32 | conn.on('close', function() {});
33 | });
34 |
35 | var server = http.createServer();
36 | echo.installHandlers(server, {prefix:'/echo'});
37 | server.listen(9999, '0.0.0.0');
38 | ```
39 |
40 | (Take look at
41 | [examples](https://github.com/sockjs/sockjs-node/tree/master/examples/echo)
42 | directory for a complete version.)
43 |
44 | Subscribe to
45 | [SockJS mailing list](https://groups.google.com/forum/#!forum/sockjs) for
46 | discussions and support.
47 |
48 |
49 | Live QUnit tests and smoke tests
50 | --------------------------------
51 |
52 | [SockJS-client](https://github.com/sockjs/sockjs-client) comes with
53 | some QUnit tests and a few smoke tests that are using SockJS-node. At
54 | the moment they are deployed in few places, just click to see if
55 | SockJS is working in your browser:
56 |
57 | * http://sockjs.popcnt.org/ and https://sockjs.popcnt.org/ (hosted in Europe)
58 | * http://sockjs.cloudfoundry.com/ (CloudFoundry, websockets disabled, loadbalanced)
59 | * https://sockjs.cloudfoundry.com/ (CloudFoundry SSL, websockets disabled, loadbalanced)
60 |
61 |
62 | SockJS-node API
63 | ---------------
64 |
65 | The API design is based on the common Node API's like
66 | [Streams API](http://nodejs.org/docs/v0.5.8/api/streams.html) or
67 | [Http.Server API](http://nodejs.org/docs/v0.5.8/api/http.html#http.Server).
68 |
69 | ### Server class
70 |
71 | SockJS module is generating a `Server` class, similar to
72 | [Node.js http.createServer](http://nodejs.org/docs/v0.5.8/api/http.html#http.createServer)
73 | module.
74 |
75 | ```javascript
76 | var sockjs_server = sockjs.createServer(options);
77 | ```
78 |
79 | Where `options` is a hash which can contain:
80 |
81 |
82 | - sockjs_url (string, required)
83 | - Transports which don't support cross-domain communication natively
84 | ('eventsource' to name one) use an iframe trick. A simple page is
85 | served from the SockJS server (using its foreign domain) and is
86 | placed in an invisible iframe. Code run from this iframe doesn't
87 | need to worry about cross-domain issues, as it's being run from
88 | domain local to the SockJS server. This iframe also does need to
89 | load SockJS javascript client library, and this option lets you specify
90 | its url (if you're unsure, point it to
91 |
92 | the latest minified SockJS client release, this is the default).
93 | You must explicitly specify this url on the server side for security
94 | reasons - we don't want the possibility of running any foreign
95 | javascript within the SockJS domain (aka cross site scripting attack).
96 | Also, sockjs javascript library is probably already cached by the
97 | browser - it makes sense to reuse the sockjs url you're using in
98 | normally.
99 |
100 | - prefix (string)
101 | - A url prefix for the server. All http requests which paths begins
102 | with selected prefix will be handled by SockJS. All other requests
103 | will be passed through, to previously registered handlers.
104 |
105 | - response_limit (integer)
106 | - Most streaming transports save responses on the client side and
107 | don't free memory used by delivered messages. Such transports need
108 | to be garbage-collected once in a while. `response_limit` sets
109 | a minimum number of bytes that can be send over a single http streaming
110 | request before it will be closed. After that client needs to open
111 | new request. Setting this value to one effectively disables
112 | streaming and will make streaming transports to behave like polling
113 | transports. The default value is 128K.
114 |
115 | - websocket (boolean)
116 | - Some load balancers don't support websockets. This option can be used
117 | to disable websockets support by the server. By default websockets are
118 | enabled.
119 |
120 | - jsessionid (boolean or function)
121 | - Some hosting providers enable sticky sessions only to requests that
122 | have JSESSIONID cookie set. This setting controls if the server should
123 | set this cookie to a dummy value. By default setting JSESSIONID cookie
124 | is disabled. More sophisticated beaviour can be achieved by supplying
125 | a function.
126 |
127 | - log (function(severity, message))
128 | - It's quite useful, especially for debugging, to see some messages
129 | printed by a SockJS-node library. This is done using this `log`
130 | function, which is by default set to `console.log`. If this
131 | behaviour annoys you for some reason, override `log` setting with a
132 | custom handler. The following `severities` are used: `debug`
133 | (miscellaneous logs), `info` (requests logs), `error` (serious
134 | errors, consider filing an issue).
135 |
136 | - heartbeat_delay (milliseconds)
137 | - In order to keep proxies and load balancers from closing long
138 | running http requests we need to pretend that the connecion is
139 | active and send a heartbeat packet once in a while. This setting
140 | controlls how often this is done. By default a heartbeat packet is
141 | sent every 25 seconds.
142 |
143 | - disconnect_delay (milliseconds)
144 | - The server sends a `close` event when a client receiving
145 | connection have not been seen for a while. This delay is configured
146 | by this setting. By default the `close` event will be emitted when a
147 | receiving connection wasn't seen for 5 seconds.
148 |
149 |
150 |
151 | ### Server instance
152 |
153 | Once you have create `Server` instance you can hook it to the
154 | [http.Server instance](http://nodejs.org/docs/v0.5.8/api/http.html#http.createServer).
155 |
156 | ```javascript
157 | var http_server = http.createServer();
158 | sockjs_server.installHandlers(http_server, options);
159 | http_server.listen(...);
160 | ```
161 |
162 | Where `options` can overshadow options given when creating `Server`
163 | instance.
164 |
165 | `Server` instance is an
166 | [EventEmitter](http://nodejs.org/docs/v0.4.10/api/events.html#events.EventEmitter),
167 | and emits following event:
168 |
169 |
170 | - Event: connection (connection)
171 | - A new connection has been successfully opened.
172 |
173 |
174 | All http requests that don't go under the path selected by `prefix`
175 | will remain unanswered and will be passed to previously registered
176 | handlers. You must install your custom http handlers before calling
177 | `installHandlers`.
178 |
179 | ### Connection instance
180 |
181 | A `Connection` instance supports
182 | [Node Stream API](http://nodejs.org/docs/v0.5.8/api/streams.html) and
183 | has following methods and properties:
184 |
185 |
186 | - Property: readable (boolean)
187 | - Is the stream readable?
188 |
189 | - Property: writable (boolean)
190 | - Is the stream writable?
191 |
192 | - Property: remoteAddress (string)
193 | - Last known IP address of the client.
194 |
195 | - Property: remotePort (number)
196 | - Last known port number of the client.
197 |
198 | - Property: address (object)
199 | - Hash with 'address' and 'port' fields.
200 |
201 | - Property: headers (object)
202 | - Hash containing various headers copied from last receiving request
203 | on that connection. Exposed headers include: `origin`, `referer`
204 | and `x-forwarded-for` (and friends). We expliclty do not grant
205 | access to `cookie` header, as using it may easily lead to security
206 | issues (for details read the section "Authorization").
207 |
208 | - Property: url (string)
209 | - Url
210 | property copied from last request.
211 |
212 | - Property: pathname (string)
213 | - `pathname` from parsed url, for convenience.
214 |
215 | - Property: prefix (string)
216 | - Prefix of the url on which the request was handled.
217 |
218 | - Property: protocol (string)
219 | - Protocol used by the connection. Keep in mind that some protocols
220 | are indistinguishable - for example "xhr-polling" and "xdr-polling".
221 |
222 | - Property: readyState (integer)
223 | - Current state of the connection:
224 | 0-connecting, 1-open, 2-closing, 3-closed.
225 |
226 | - write(message)
227 | - Sends a message over opened connection. A message must be a
228 | non-empty string. It's illegal to send a message after the connection was
229 | closed (either after 'close' or 'end' method or 'close' event).
230 |
231 | - close([code], [reason])
232 | - Asks the remote client to disconnect. 'code' and 'reason'
233 | parameters are optional and can be used to share the reason of
234 | disconnection.
235 |
236 | - end()
237 | - Asks the remote client to disconnect with default 'code' and
238 | 'reason' values.
239 |
240 |
241 |
242 | A `Connection` instance emits the following events:
243 |
244 |
245 | - Event: data (message)
246 | - A message arrived on the connection. Message is a unicode
247 | string.
248 |
249 | - Event: close ()
250 | - Connection was closed. This event is triggered exactly once for
251 | every connection.
252 |
253 |
254 | For example:
255 |
256 | ```javascript
257 | sockjs_server.on('connection', function(conn) {
258 | console.log('connection' + conn);
259 | conn.on('close', function() {
260 | console.log('close ' + conn);
261 | });
262 | conn.on('data', function(message) {
263 | console.log('message ' + conn,
264 | message);
265 | });
266 | });
267 | ```
268 |
269 | ### Footnote
270 |
271 | A fully working echo server does need a bit more boilerplate (to
272 | handle requests unanswered by SockJS), see the
273 | [`echo` example](https://github.com/sockjs/sockjs-node/tree/master/examples/echo)
274 | for a complete code.
275 |
276 | ### Examples
277 |
278 | If you want to see samples of running code, take a look at:
279 |
280 | * [./examples/echo](https://github.com/sockjs/sockjs-node/tree/master/examples/echo)
281 | directory, which contains a full example of a echo server.
282 | * [./examples/test_server](https://github.com/sockjs/sockjs-node/tree/master/examples/test_server) a standard SockJS test server.
283 |
284 |
285 | Connecting to SockJS-node without the client
286 | --------------------------------------------
287 |
288 | Although the main point of SockJS it to enable browser-to-server
289 | connectivity, it is possible to connect to SockJS from an external
290 | application. Any SockJS server complying with 0.3 protocol does
291 | support a raw WebSocket url. The raw WebSocket url for the test server
292 | looks like:
293 |
294 | * ws://localhost:8081/echo/websocket
295 |
296 | You can connect any WebSocket RFC 6455 compliant WebSocket client to
297 | this url. This can be a command line client, external application,
298 | third party code or even a browser (though I don't know why you would
299 | want to do so).
300 |
301 |
302 | Deployment and load balancing
303 | -----------------------------
304 |
305 | There are two issues that needs to be considered when planning a
306 | non-trivial SockJS-node deployment: WebSocket-compatible load balancer
307 | and sticky sessions (aka session affinity).
308 |
309 | ### WebSocket compatible load balancer
310 |
311 | Often WebSockets don't play nicely with proxies and load balancers.
312 | Deploying a SockJS server behind Nginx or Apache could be painful.
313 |
314 | Fortunetely recent versions of an excellent load balancer
315 | [HAProxy](http://haproxy.1wt.eu/) are able to proxy WebSocket
316 | connections. We propose to put HAProxy as a front line load balancer
317 | and use it to split SockJS traffic from normal HTTP data. Take a look
318 | at the sample
319 | [SockJS HAProxy configuration](https://github.com/sockjs/sockjs-node/blob/master/examples/haproxy.cfg).
320 |
321 | The config also shows how to use HAproxy balancing to split traffic
322 | between multiple Node.js servers. You can also do balancing using dns
323 | names.
324 |
325 | ### Sticky sessions
326 |
327 | If you plan depling more than one SockJS server, you must make sure
328 | that all HTTP requests for a single session will hit the same server.
329 | SockJS has two mechanisms that can be usefull to achieve that:
330 |
331 | * Urls are prefixed with server and session id numbers, like:
332 | `/resource///transport`. This is
333 | usefull for load balancers that support prefix-based affinity
334 | (HAProxy does).
335 | * `JSESSIONID` cookie is being set by SockJS-node. Many load
336 | balancers turn on sticky sessions if that cookie is set. This
337 | technique is derived from Java applications, where sticky sessions
338 | are often neccesary. HAProxy does support this method, as well as
339 | some hosting providers, for example CloudFoundry. In order to
340 | enable this method on the client side, please supply a
341 | `cookie:true` option to SockJS constructor.
342 |
343 |
344 | Development and testing
345 | -----------------------
346 |
347 | If you want to work on SockJS-node source code, you need to clone the
348 | git repo and follow these steps. First you need to install
349 | dependencies:
350 |
351 | cd sockjs-node
352 | npm install --dev
353 | ln -s .. node_modules/sockjs
354 |
355 | You're ready to compile CoffeeScript:
356 |
357 | make build
358 |
359 | If compilation succeeds you may want to test if your changes pass all
360 | the tests. Currently, there are two separate test suites. For both of
361 | them you need to start a SockJS-node test server (by default listening
362 | on port 8081):
363 |
364 | make test_server
365 |
366 | ### SockJS-protocol Python tests
367 |
368 | To run it run something like:
369 |
370 | cd sockjs-protocol
371 | make test_deps
372 | ./venv/bin/python sockjs-protocol-0.3.py
373 |
374 | For details see
375 | [SockJS-protocol README](https://github.com/sockjs/sockjs-protocol#readme).
376 |
377 | ### SockJS-client QUnit tests
378 |
379 | You need to start a second web server (by default listening on 8080)
380 | that is serving various static html and javascript files:
381 |
382 | cd sockjs-client
383 | make test
384 |
385 | At that point you should have two web servers running: sockjs-node on
386 | 8081 and sockjs-client on 8080. When you open the browser on
387 | [http://localhost:8080/](http://localhost:8080/) you should be able
388 | run the QUnit tests against your sockjs-node server.
389 |
390 | For details see
391 | [SockJS-client README](https://github.com/sockjs/sockjs-client#readme).
392 |
393 | Additionally, if you're doing more serious development consider using
394 | `make serve`, which will automatically the server when you modify the
395 | source code.
396 |
397 |
398 | Various issues and design considerations
399 | ----------------------------------------
400 |
401 | ### Authorization
402 |
403 | SockJS-node does not expose cookies to the application. This is done
404 | deliberately as using cookie-based authorization with SockJS simply
405 | doesn't make sense and will lead to security issues.
406 |
407 | Cookies are a contract between a browser and an http server, and are
408 | identified by a domain name. If a browser has a cookie set for
409 | particular domain, it will pass it as a part of all http requests to
410 | the host. But to get various transports working, SockJS uses a middleman
411 | - an iframe hosted from target SockJS domain. That means the server
412 | will receive requests from the iframe, and not from the real
413 | domain. The domain of an iframe is the same as the SockJS domain. The
414 | problem is that any website can embedd the iframe and communicate with
415 | it - and request establishing SockJS connection. Using cookies for
416 | authorization in this scenario will result in granting full access to
417 | SockJS communication with your website from any website. This is a
418 | classic CSRF attack.
419 |
420 | Basically - cookies are not suited for SockJS model. If you want to
421 | authorize a session - provide a unique token on a page, send it as a
422 | first thing over SockJS connection and validate it on the server
423 | side. In essence, this is how cookies work.
424 |
425 |
426 | ### Deploying SockJS on Heroku
427 |
428 | Long polling is known to cause problems on Heroku, but
429 | [workaround for SockJS is available](https://github.com/sockjs/sockjs-node/issues/57#issuecomment-5242187).
430 |
--------------------------------------------------------------------------------