├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── index.js └── lib │ └── utils.js └── tests └── bootstrap-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/js 3 | bin -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "expr": true, 4 | "loopfunc": true, 5 | "strict": false, 6 | "curly": true, 7 | "evil": true, 8 | "curly": true, 9 | "white": true, 10 | "trailing": true, 11 | "eqeqeq": true, 12 | "immed": true, 13 | "indent": 2, 14 | "debug": true, 15 | "newcap": true, 16 | "undef": true, 17 | "unused": false, 18 | 19 | "quotmark": "single", 20 | "maxdepth": 3, 21 | "maxlen": 80, 22 | 23 | "node": true, 24 | "browser": true, 25 | 26 | "globals": { 27 | "window": true 28 | } 29 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | before_install: 10 | - npm i -g npm 11 | # Workaround for a permissions issue with Travis virtual machine images 12 | script: 13 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 David Dias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | webrtc-chord-signalling-server 2 | ============================== 3 | 4 | Please refer to the [webrtc-chord](https://github.com/diasdavid/webrtc-chord) for documentation. 5 | 6 | # Badgers 7 | 8 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/diasdavid/webrtc-chord?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 9 | [![Dependency Status](https://david-dm.org/diasdavid/webrtc-signalling-server.svg)](https://david-dm.org/diasdavid/webrtc-signalling-server) 10 | 11 | [![](https://cldup.com/pgZbzoshyV-3000x3000.png)](http://www.gsd.inesc-id.pt/) 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-chord-signaling-server", 3 | "version": "0.3.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "big-integer": "^1.3.18", 8 | "hapi": "^16.1.0", 9 | "socket.io": "^1.1.0" 10 | }, 11 | "devDependencies": { 12 | "jscs": "^3.0.7", 13 | "jshint": "^2.5.6", 14 | "lab": "^12.0.0", 15 | "precommit-hook": "^3.0.0", 16 | "socket.io-client": "^1.1.0", 17 | "tap-spec": "^4.1.1", 18 | "webrtc-chord-uuid": "^1.0.0" 19 | }, 20 | "scripts": { 21 | "start": "node src/index.js", 22 | "codestyle": "./node_modules/.bin/jscs -p google src/index.js src/lib/*.js tests/*.js", 23 | "lint": "./node_modules/.bin/jshint .", 24 | "test": "node ./node_modules/.bin/lab -r tap tests/*-test.js | ./node_modules/.bin/tap-spec", 25 | "test-cov": "node ./node_modules/.bin/lab -t 70 tests/*-test.js", 26 | "test-cov-html": "node ./node_modules/.bin/lab -r html -o coverage.html tests/*-test.js" 27 | }, 28 | "precommit": [ 29 | "codestyle", 30 | "lint", 31 | "test", 32 | "test-cov" 33 | ], 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/diasdavid/webrtc-chord-signaling-server.git" 37 | }, 38 | "author": "David Dias ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/diasdavid/webrtc-chord-signaling-server/issues" 42 | }, 43 | "homepage": "https://github.com/diasdavid/webrtc-chord-signaling-server" 44 | } 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var hapi = require('Hapi'); 2 | var io = require('socket.io'); 3 | var bigInt = require('big-integer'); 4 | var utils = require('./lib/utils'); 5 | 6 | var port = parseInt(process.env.PORT) || 9000; 7 | var server = new hapi.Server(port, {cors: true}); 8 | var socketIdpeerId = {}; 9 | var peerTable = {}; 10 | 11 | server.route({ 12 | method: 'GET', 13 | path: '/', 14 | handler: function(request, reply) { 15 | reply('Signaling Server'); 16 | } 17 | }); 18 | 19 | server.start(hapiStarted); 20 | 21 | function hapiStarted() { 22 | io.listen(server.listener).on('connection', ioConnectionHandler); 23 | // console.log('Signaling server has started on:', server.info.uri); 24 | } 25 | 26 | function ioConnectionHandler(socket) { 27 | 28 | socket.on('s-id', registerPeer); 29 | socket.on('s-send-offer', sendOffer); 30 | socket.on('s-offer-accepted', offerAccepted); 31 | socket.on('disconnect', peerRemove); // socket.io own event 32 | 33 | function registerPeer(id) { 34 | // console.log('Registering peer with Id: ', id); 35 | peerTable[id] = { 36 | socket: socket 37 | }; 38 | socketIdpeerId[socket.id] = id; 39 | // console.log('peerTable: ', Object.keys(peerTable)); 40 | 41 | if (Object.keys(peerTable).length > 2) { 42 | // warn a peer of new sucessor 43 | var sorted = utils.sortPeerTable(peerTable); 44 | var predecessorToId = utils.predecessorToId(id, sorted); 45 | peerTable[predecessorToId] 46 | .socket.emit('c-new-sucessor-available', id); 47 | } 48 | } 49 | 50 | function sendOffer(offer) { 51 | // console.log('sendOffer'); 52 | function twoOrMore() { 53 | if (Object.keys(peerTable).length < 2) { 54 | // console.log('--peerTable: ', Object.keys(peerTable)); 55 | return setTimeout(twoOrMore, 1000); 56 | } 57 | 58 | // console.log('sendOffer - flag'); 59 | var sorted = utils.sortPeerTable(peerTable); 60 | 61 | peerTable[utils.sucessorToId(offer.destId, sorted)] 62 | .socket.emit('c-accept-offer', offer); 63 | } 64 | twoOrMore(); 65 | } 66 | 67 | function offerAccepted(offer) { 68 | // console.log('offerAccepted'); 69 | peerTable[offer.srcId].socket.emit('c-offer-accepted', offer); 70 | } 71 | 72 | function peerRemove() { 73 | // console.log('peer disconnected: ', socketIdpeerId[socket.id]); 74 | delete peerTable[socketIdpeerId[socket.id]]; 75 | delete socketIdpeerId[socket.id]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | var bigInt = require('big-integer'); 2 | 3 | exports = module.exports; 4 | 5 | exports.sortPeerTable = function(peerTable) { 6 | var sorted = Object 7 | .keys(peerTable) 8 | .sort(function(a, b) { 9 | var aBig = bigInt(a, 16); 10 | var bBig = bigInt(b, 16); 11 | if (aBig.lesser(bBig)) { 12 | return -1; 13 | } 14 | if (aBig.greater(bBig)) { 15 | return 1; 16 | } 17 | return 0; 18 | }); 19 | return sorted; 20 | }; 21 | 22 | exports.sucessorToId = function(peerId, sortedPeerTable) { 23 | var s = sortedPeerTable; 24 | var r; 25 | var bigPeerId = bigInt(peerId, 16); 26 | var done = false; 27 | 28 | s.forEach(function(value, index) { 29 | if (done) { 30 | return; 31 | } 32 | var bigValue = bigInt(value, 16); 33 | 34 | if (bigPeerId.compare(bigValue) === 0) { 35 | r = value; 36 | done = true; 37 | return; 38 | } 39 | 40 | if (bigPeerId.lesser(bigValue)) { 41 | r = value; 42 | done = true; 43 | return; 44 | } 45 | 46 | if (index + 1 === s.length) { 47 | r = s[0]; 48 | done = true; 49 | return; 50 | } 51 | 52 | }); 53 | return r; 54 | }; 55 | 56 | exports.predecessorToId = function(peerId, sortedPeerTable) { 57 | var s = sortedPeerTable; 58 | var r; 59 | var bigPeerId = bigInt(peerId, 16); 60 | var done = false; 61 | 62 | s.forEach(function(value, index) { 63 | if (done) { 64 | return; 65 | } 66 | 67 | if (index + 1 === s.length && bigPeerId.greater(bigInt(value, 16))) { 68 | r = value; 69 | done = true; 70 | return; 71 | } 72 | 73 | if (bigPeerId.lesser(bigInt(value, 16)) || 74 | bigPeerId.compare(bigInt(value, 16)) === 0) { 75 | if (index === 0) { 76 | r = s[s.length - 1]; 77 | done = true; 78 | return; 79 | } 80 | 81 | r = s[index - 1]; 82 | done = true; 83 | return; 84 | } 85 | }); 86 | return r; 87 | }; 88 | -------------------------------------------------------------------------------- /tests/bootstrap-test.js: -------------------------------------------------------------------------------- 1 | var Lab = require('lab'); 2 | var lab = exports.lab = Lab.script(); 3 | var io = require('socket.io-client'); 4 | var spawn = require('child_process').spawn; 5 | var uuid = require('webrtc-chord-uuid'); 6 | var bigInt = require('big-integer'); 7 | var utils = require('./../src/lib/utils'); 8 | 9 | var experiment = lab.experiment; 10 | var test = lab.test; 11 | var before = lab.before; 12 | var after = lab.after; 13 | var expect = Lab.expect; 14 | 15 | experiment(':', function() { 16 | var signalingServer; 17 | var c1; 18 | var c2; 19 | var c3; 20 | var c4; 21 | var id1; 22 | var id2; 23 | var id3; 24 | var id4; 25 | var peerTable = {}; 26 | 27 | var options = {transports: ['websocket'], 'force new connection': true}; 28 | var socketURL = 'http://localhost:9000'; 29 | 30 | before(function(done) { 31 | signalingServer = spawn('node', ['./src/index.js']); 32 | signalingServer.stdout.on('data', function(data) { 33 | // console.log('stdout: \n' + data); 34 | }); 35 | signalingServer.stderr.on('data', function(data) { 36 | // console.log('stderr: \n' + data); 37 | }); 38 | setTimeout(function() { done(); }, 1000); 39 | }); 40 | 41 | after(function(done) { 42 | c2.disconnect(); 43 | c3.disconnect(); 44 | c4.disconnect(); 45 | 46 | setTimeout(function() { 47 | signalingServer.on('close', function(code) { 48 | // console.log('cp exited: ' + code); 49 | done(); 50 | }); 51 | signalingServer.kill(); 52 | }, 500); 53 | }); 54 | 55 | test('connect 4 clients', function(done) { 56 | var count = 0; 57 | 58 | c1 = io.connect(socketURL, options); 59 | c2 = io.connect(socketURL, options); 60 | c3 = io.connect(socketURL, options); 61 | c4 = io.connect(socketURL, options); 62 | c1.on('connect', connected); 63 | c2.on('connect', connected); 64 | c3.on('connect', connected); 65 | c4.on('connect', connected); 66 | 67 | function connected() { 68 | count += 1; 69 | if (count === 4) { 70 | done(); 71 | } 72 | } 73 | 74 | }); 75 | 76 | test('register 2 clients', function(done) { 77 | id1 = uuid.gen(); 78 | id2 = uuid.gen(); 79 | expect(id1).to.not.equal(id2); 80 | 81 | c1.emit('s-id', id1); 82 | c2.emit('s-id', id2); 83 | setTimeout(done, 500); 84 | }); 85 | 86 | test('boot 2 nodes', function(done) { 87 | var count = 0; 88 | 89 | var offer1 = { 90 | srcId: id1, 91 | destId: idPlusOne(id1), 92 | signal: 'fromPeer1' 93 | }; 94 | 95 | var offer2 = { 96 | srcId: id2, 97 | destId: idPlusOne(id2), 98 | signal: 'fromPeer2' 99 | }; 100 | 101 | c1.once('c-accept-offer', function(offer) { 102 | expect(offer).to.deep.equal(offer2); 103 | offer.destId = id1; 104 | c1.emit('s-offer-accepted', offer); 105 | }); 106 | 107 | c2.once('c-accept-offer', function(offer) { 108 | expect(offer).to.deep.equal(offer1); 109 | offer.destId = id2; 110 | c2.emit('s-offer-accepted', offer); 111 | }); 112 | 113 | c1.once('c-offer-accepted', function(offer) { 114 | expect(offer.destId).to.equal(id2); 115 | 116 | count += 1; 117 | if (count === 2) { 118 | count += 1; // to fire done only once 119 | done(); 120 | } 121 | }); 122 | 123 | c2.once('c-offer-accepted', function(offer) { 124 | expect(offer.destId).to.equal(id1); 125 | 126 | count += 1; 127 | if (count === 2) { 128 | count += 1; // to fire done only once 129 | done(); 130 | } 131 | }); 132 | 133 | c1.emit('s-send-offer', offer1); 134 | c2.emit('s-send-offer', offer2); 135 | 136 | }); 137 | 138 | test('boot a third node', {timeout: 10 * 1000}, function(done) { 139 | id3 = uuid.gen(); 140 | 141 | var offer3 = { 142 | srcId: id3, 143 | destId: idPlusOne(id3), 144 | signal: 'fromPeer3' 145 | }; 146 | 147 | peerTable[id1] = c1; 148 | peerTable[id2] = c2; 149 | peerTable[id3] = c3; 150 | var sorted = utils.sortPeerTable(peerTable); 151 | var sucessorOfId3 = utils.sucessorToId(idPlusOne(id3), sorted); 152 | var predecessoOfId3 = utils.predecessorToId(id3, sorted); 153 | 154 | peerTable[sucessorOfId3].once('c-accept-offer', function(offer) { 155 | expect(offer).to.deep.equal(offer3); 156 | offer.destId = sucessorOfId3; 157 | peerTable[sucessorOfId3].emit('s-offer-accepted', offer); 158 | }); 159 | 160 | c3.once('c-offer-accepted', function(offer) { 161 | expect(offer.destId).to.equal(sucessorOfId3); 162 | setTimeout(done, 500); 163 | }); 164 | 165 | peerTable[predecessoOfId3].once('c-new-sucessor-available', function(id) { 166 | expect(id).to.equal(id3); 167 | }); 168 | 169 | c3.emit('s-id', id3); 170 | setTimeout(function() { 171 | c3.emit('s-send-offer', offer3); 172 | }, 500); 173 | 174 | }); 175 | 176 | test('kill first node', function(done) { 177 | c1.disconnect(); 178 | delete peerTable[id1]; 179 | 180 | done(); 181 | }); 182 | 183 | test('connect fourth node', {timeout: 10 * 1000}, function(done) { 184 | id4 = uuid.gen(); 185 | 186 | var offer4 = { 187 | srcId: id4, 188 | destId: idPlusOne(id4), 189 | signal: 'fromPeer4' 190 | }; 191 | 192 | peerTable.id4 = c4; 193 | var sorted = utils.sortPeerTable(peerTable); 194 | var sucessorOfId4 = utils.sucessorToId(idPlusOne(id3), sorted); 195 | var predecessoOfId4 = utils.predecessorToId(id4, sorted); 196 | 197 | peerTable[sucessorOfId4].once('c-accept-offer', function(offer) { 198 | expect(offer).to.deep.equal(offer4); 199 | offer.destId = sucessorOfId4; 200 | peerTable[sucessorOfId4].emit('s-offer-accepted', offer); 201 | }); 202 | 203 | c4.once('c-offer-accepted', function(offer) { 204 | expect(offer.destId).to.equal(sucessorOfId4); 205 | setTimeout(done, 500); 206 | }); 207 | 208 | peerTable[predecessoOfId4].once('c-new-sucessor-available', function(id) { 209 | expect(id).to.equal(id4); 210 | }); 211 | 212 | c4.emit('s-id', id4); 213 | c4.emit('s-send-offer', offer4); 214 | 215 | done(); 216 | }); 217 | 218 | function idPlusOne(id) { 219 | return (bigInt(id, 16).add(1)).toString(16); 220 | } 221 | 222 | }); 223 | --------------------------------------------------------------------------------