├── 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 |
39 |
40 |
41 |
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 |
39 |
40 |
41 |
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 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
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 | --------------------------------------------------------------------------------