├── .gitignore ├── index.html ├── cli.js ├── limit-stream.js ├── package.json ├── readme.md ├── demo.js ├── test.js ├── index.js └── versions └── v1.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var lobby = require('./index.js')() 4 | var port = process.argv[2] || process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || 5005 5 | var ip = process.env.OPENSHIFT_NODEJS_IP 6 | 7 | lobby.listen(port, ip, function listening (err) { 8 | if (err) throw err 9 | console.log('Listening on', port) 10 | }) 11 | -------------------------------------------------------------------------------- /limit-stream.js: -------------------------------------------------------------------------------- 1 | var through = require('through2') 2 | 3 | module.exports = function limiter (limit) { 4 | var stream = through(write) 5 | 6 | var len = 0 7 | 8 | return stream 9 | 10 | function write (ch, enc, next) { 11 | if (Buffer.isBuffer(ch)) len += ch.length 12 | else len += 1 13 | 14 | if (len >= limit) this.destroy(new Error('Limit exceeded')) 15 | else this.push(ch) 16 | 17 | next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cat-lobby", 3 | "version": "2.0.0", 4 | "description": "simple http + sse based lobby server with cat themed room names", 5 | "main": "cli.js", 6 | "bin": { 7 | "cat-lobby": "cli.js" 8 | }, 9 | "scripts": { 10 | "test": "standard && node test.js", 11 | "start": "node cli.js" 12 | }, 13 | "dependencies": { 14 | "cat-names": "^1.0.2", 15 | "concat-stream": "^1.4.7", 16 | "corsify": "^2.1.0", 17 | "debug": "^2.1.3", 18 | "duplexify": "^3.2.0", 19 | "http-hash-router": "^1.1.0", 20 | "pumpify": "^1.3.3", 21 | "ssejson": "^1.2.0", 22 | "through2": "^0.6.3" 23 | }, 24 | "author": "max ogden", 25 | "license": "BSD", 26 | "devDependencies": { 27 | "standard": "^3.3.0", 28 | "tape": "^3.5.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cat-lobby 2 | 3 | simple http+sse based lobby server with cat themed room names 4 | 5 | [![RUN Cat-Lobby ON OpenShift](http://launch-shifter.rhcloud.com/launch/RUN Cat-Lobby ON.svg)](https://openshift.redhat.com/app/console/application_type/custom?&cartridges[]=nodejs-0.10&initial_git_url=https://github.com/maxogden/cat-lobby.git&name=cat-lobby) 6 | 7 | ### install + run 8 | 9 | ``` 10 | npm install cat-lobby -g 11 | cat-lobby 12 | ``` 13 | 14 | ### HTTP API 15 | 16 | #### POST `/` 17 | 18 | creates a new lobby. receives response `{name: new lobby name}` 19 | 20 | #### POST `/ping/:name` 21 | #### POST `/pong/:name` 22 | 23 | POST JSON to either the ping or pong channel. upload must be JSON and will be streamed out to anyone listening to pings/pongs. no response body 24 | 25 | #### GET `/pings/:name` 26 | #### GET `/pongs/:name` 27 | 28 | listen to pings or pongs using Server-Sent Events 29 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | /* global EventSource */ 2 | var server = 'http://catlobby.maxogden.com' 3 | 4 | var nets = require('nets') 5 | var url = require('url') 6 | 7 | var params = url.parse(window.location.href, true).query 8 | if (params.room) { 9 | var room = params.room 10 | // listen for pongs 11 | var events = new EventSource(server + '/v1/' + room + '/pongs') 12 | events.onmessage = function onMessage (e) { 13 | var row 14 | try { 15 | row = JSON.parse(e.data) 16 | } catch (e) { 17 | row = {} 18 | } 19 | console.log('pongs onmessage', row) 20 | } 21 | 22 | setInterval(function () { 23 | nets({method: 'POST', url: server + '/v1/' + room + '/ping', json: {time: new Date()}}, function (err, resp, body) { 24 | if (err) console.error(err) 25 | console.log('SENT PING', body) 26 | }) 27 | }, 5000) 28 | 29 | } else { 30 | nets({method: 'POST', url: server + '/v1', json: true}, function (err, resp, body) { 31 | if (err) console.error(err) 32 | var room = body.name 33 | console.log('ROOM', room) 34 | 35 | // listen for pings 36 | var events = new EventSource(server + '/v1/' + room + '/pings') 37 | events.onmessage = function onMessage (e) { 38 | var row 39 | try { 40 | row = JSON.parse(e.data) 41 | } catch (e) { 42 | row = {} 43 | } 44 | console.log('pings onmessage', row) 45 | } 46 | 47 | setInterval(function () { 48 | nets({method: 'POST', url: server + '/v1/' + room + '/pong', json: {time: new Date()}}, function (err, resp, body) { 49 | if (err) console.error(err) 50 | console.log('SENT PONG', body) 51 | }) 52 | }, 5000) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var request = require('request') 3 | var ssejson = require('ssejson') 4 | var CatLobby = require('./') 5 | 6 | test('ping and pong v1 API', function pingPong (t) { 7 | var lobby = CatLobby() 8 | 9 | lobby.listen(5005, function listening (err) { 10 | if (err) return t.err(err) 11 | 12 | var ping = 'hello world' 13 | var pong = 'hi welt' 14 | 15 | request.post({uri: 'http://localhost:5005/v1', json: true}, function response (err, resp, data) { 16 | if (err) return t.err(err) 17 | t.equals(resp.statusCode, 201, 'got 201 created') 18 | t.ok(data.name, 'has name ' + data.name) 19 | var n = data.name 20 | 21 | var pending = 2 22 | var pingReq = request('http://localhost:5005/v1/' + n + '/pongs') 23 | pingReq 24 | .pipe(ssejson.parse()) 25 | .on('data', function data (row) { 26 | if (!row.data) return 27 | t.equal(row.data, pong, 'pong matches') 28 | if (--pending === 0) finish() 29 | }) 30 | 31 | var pongReq = request('http://localhost:5005/v1/' + n + '/pings') 32 | pongReq 33 | .pipe(ssejson.parse()) 34 | .on('data', function data (row) { 35 | if (!row.data) return 36 | t.equal(row.data, ping, 'ping matches') 37 | if (--pending === 0) finish() 38 | }) 39 | 40 | function finish () { 41 | pingReq.abort() 42 | pongReq.abort() 43 | lobby.close(function closed (err) { 44 | if (err) t.ifErr(err) 45 | t.end() 46 | }) 47 | } 48 | 49 | var urls = { 50 | ping: 'http://localhost:5005/v1/' + n + '/ping', 51 | pong: 'http://localhost:5005/v1/' + n + '/pong' 52 | } 53 | 54 | request.post({uri: urls.ping, json: {data: ping}}, function response (err, resp, buff) { 55 | if (err) return t.err(err) 56 | t.equals(resp.statusCode, 200, 'got 200 OK') 57 | t.notOk(buff, 'no resp body') 58 | 59 | request.post({uri: urls.pong, json: {data: pong}}, function response (err, resp, buff) { 60 | if (err) return t.err(err) 61 | t.equals(resp.statusCode, 200, 'got 200 OK') 62 | t.notOk(buff, 'no resp body') 63 | }) 64 | }) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | 3 | var HttpHashRouter = require('http-hash-router') 4 | var catNames = require('cat-names') 5 | var concat = require('concat-stream') 6 | var pumpify = require('pumpify') 7 | var corsify = require('corsify') 8 | var debug = require('debug')('cat-lobby') 9 | 10 | var limitStream = require('./limit-stream.js') 11 | 12 | module.exports = function create (lobbyOpts) { 13 | if (!lobbyOpts) lobbyOpts = {} 14 | 15 | var router = HttpHashRouter() 16 | 17 | var state = { 18 | pings: {}, 19 | pongs: {}, 20 | timeouts: [] 21 | } 22 | 23 | var utils = { 24 | makeName: makeName, 25 | rnd: rnd, 26 | uploadStream: uploadStream 27 | } 28 | 29 | // old alpha API 30 | router.set('/ping', deprecated) 31 | router.set('/pong/:name', deprecated) 32 | router.set('/ping/:name', deprecated) 33 | router.set('/pongs/:name', deprecated) 34 | 35 | var v1API = require('./versions/v1.js')(state, utils) 36 | 37 | router.set('/v1', v1API.create) 38 | router.set('/v1/:name/ping', v1API.ping) 39 | router.set('/v1/:name/pong', v1API.pong) 40 | router.set('/v1/:name/pings', v1API.pings) 41 | router.set('/v1/:name/pongs', v1API.pongs) 42 | 43 | return createServer(router) 44 | 45 | function deprecated (req, res, opts, cb) { 46 | cb(new Error('This version of ScreenCat is unsupported, please upgrade.')) 47 | } 48 | 49 | function uploadStream (cb) { 50 | var limiter = limitStream(1024 * 5) // 5kb max 51 | 52 | var concatter = concat(function concatted (buff) { 53 | cb(buff) 54 | }) 55 | 56 | return pumpify(limiter, concatter) 57 | } 58 | 59 | function makeName () { 60 | var n = [utils.rnd(), utils.rnd(), utils.rnd()].join('-') 61 | if (state.pings[n]) return utils.makeName() 62 | return n 63 | } 64 | 65 | function rnd () { 66 | return catNames.random().toLowerCase().replace(/\s/g, '-') 67 | } 68 | 69 | function createServer (router) { 70 | var cors = corsify({ 71 | 'Access-Control-Allow-Methods': 'POST, GET' 72 | }) 73 | 74 | var server = http.createServer(handler) 75 | 76 | function handler (req, res) { 77 | debug(req.url, 'request/response start') 78 | 79 | // redirect https 80 | if (req.headers['x-forwarded-proto'] === 'http') { 81 | var httpsURL = 'https://' + req.headers.host + req.url 82 | debug('https redirect', httpsURL) 83 | res.writeHead(302, {'Location': httpsURL }) 84 | res.end() 85 | return 86 | } 87 | 88 | req.on('end', function logReqEnd () { 89 | debug(req.url, 'request end') 90 | }) 91 | 92 | res.on('end', function logResEnd () { 93 | debug(req.url, 'response end') 94 | }) 95 | 96 | cors(route)(req, res) 97 | 98 | function route (req, res) { 99 | router(req, res, {}, onError) 100 | } 101 | 102 | function onError (err) { 103 | if (err) { 104 | debug('error', {path: req.url, message: err.message}) 105 | res.statusCode = err.statusCode || 500 106 | res.end(JSON.stringify({name: err.message})) 107 | } 108 | } 109 | } 110 | 111 | server.on('close', function closed () { 112 | // to prevent process from hanging open 113 | state.timeouts.forEach(function each (t) { 114 | clearTimeout(t) 115 | }) 116 | }) 117 | 118 | return server 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /versions/v1.js: -------------------------------------------------------------------------------- 1 | var duplex = require('duplexify') 2 | var ssejson = require('ssejson') 3 | var pumpify = require('pumpify') 4 | var debug = require('debug')('cat-lobby-alpha-api') 5 | 6 | module.exports = function (state, utils) { 7 | return { 8 | create: createHandler, 9 | ping: pingHandler, 10 | pong: pongHandler, 11 | pings: pingsHandler, 12 | pongs: pongsHandler 13 | } 14 | 15 | function createHandler (req, res, opts, cb) { 16 | if (req.method !== 'POST') { 17 | var err = new Error('Only POST is allowed') 18 | err.statusCode = 405 19 | return cb(err) 20 | } 21 | 22 | var room = utils.makeName() 23 | res.setHeader('content-type', 'application/json') 24 | res.statusCode = 201 25 | res.end(JSON.stringify({name: room})) 26 | state.pings[room] = ssejson.serialize({}) 27 | state.pongs[room] = ssejson.serialize({}) 28 | 29 | var tId = setTimeout(function expire () { 30 | // TODO dont delete if being used 31 | destroy(room) 32 | }, 1000 * 60 * 30) // 30 mins 33 | state.timeouts.push(tId) 34 | 35 | debug('create', {name: room}) 36 | } 37 | 38 | function pingHandler (req, res, opts, cb) { 39 | upload('pings', req, res, opts, function uploaded (err, room, data) { 40 | if (err) { 41 | cb(err) 42 | return 43 | } 44 | debug('ping upload', {name: room}) 45 | state.pings[room].write(data) 46 | res.end() 47 | }) 48 | } 49 | 50 | function pongHandler (req, res, opts, cb) { 51 | upload('pongs', req, res, opts, function uploaded (err, room, data) { 52 | if (err) { 53 | cb(err) 54 | return 55 | } 56 | debug('pong upload', {name: room}) 57 | state.pongs[room].write(data) 58 | res.end() 59 | }) 60 | } 61 | 62 | function pingsHandler (req, res, opts, cb) { 63 | var room = opts.params.name 64 | var events = state.pings[room] 65 | debug('pings subscribe', {name: room}) 66 | subscribe(events, req, res, opts, cb) 67 | } 68 | 69 | function pongsHandler (req, res, opts, cb) { 70 | var room = opts.params.name 71 | var events = state.pongs[room] 72 | debug('pongs subscribe', {name: room}) 73 | subscribe(events, req, res, opts, cb) 74 | } 75 | 76 | function upload (type, req, res, opts, cb) { 77 | if (req.method !== 'POST') { 78 | var err = new Error('Only POST is allowed') 79 | err.statusCode = 405 80 | return cb(err) 81 | } 82 | 83 | var room = opts.params.name 84 | if (!room || (Object.keys(state[type]).indexOf(room) === -1)) { 85 | var error = new Error('Doesnt exist or expired') 86 | error.statusCode = 404 87 | return cb(error) 88 | } 89 | 90 | var uploader = utils.uploadStream(function uploaded (buff) { 91 | try { 92 | var data = JSON.parse(buff) 93 | cb(null, room, data) 94 | } catch(e) { 95 | cb(e) 96 | } 97 | }) 98 | 99 | pumpify(req, uploader).on('error', cb) 100 | } 101 | 102 | function subscribe (events, req, res, opts, cb) { 103 | if (!events) { 104 | var err = new Error('Doesnt exist or expired') 105 | err.statusCode = 404 106 | cb(err) 107 | return 108 | } 109 | res.setHeader('content-type', 'text/event-stream') 110 | var readable = duplex() 111 | readable.setReadable(events) 112 | pumpify(readable, res).on('error', cb) 113 | } 114 | 115 | function destroy (room) { 116 | finish(state.pings, room) 117 | finish(state.pongs, room) 118 | 119 | function finish (list, room) { 120 | if (Object.keys(list).indexOf(room) > -1) { 121 | list[room].on('finish', function () { 122 | delete list[room] 123 | }) 124 | list[room].destroy() 125 | } 126 | } 127 | } 128 | } 129 | --------------------------------------------------------------------------------