├── src
├── sig-server
│ ├── resources
│ │ ├── peer.js
│ │ └── peer-table.js
│ ├── .DS_Store
│ ├── routes-http
│ │ ├── basic.js
│ │ └── graph.js
│ ├── utils
│ │ ├── ideal-finger.js
│ │ └── finger-best-fit.js
│ ├── bin.js
│ ├── config.js
│ ├── index.js
│ └── routes-ws
│ │ └── index.js
├── .DS_Store
└── explorer
│ ├── config.js
│ ├── message-router.js
│ ├── channel.js
│ ├── connection-switch.js
│ ├── finger-table.js
│ └── index.js
├── examples
├── index.html
└── explorer.js
├── graphs
├── routing.jpg
├── overview.jpg
├── signalling.jpg
├── connection-state.jpg
├── webrtc-explorer-logo.pxm
└── webrtc-explorer-logo-small.png
├── tests
├── sig-server
│ ├── test-lifecycle.js
│ ├── test-http.js
│ ├── index.js
│ ├── test-finger-best-fit.js
│ ├── test-handshake.js
│ ├── test-join.js
│ └── test-update-finger.js
└── explorer
│ ├── index.js
│ ├── scripts
│ └── explorer-peer.js
│ └── test-explorer.js
├── .gitignore
├── LICENSE
├── package.json
└── README.md
/src/sig-server/resources/peer.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/src/.DS_Store
--------------------------------------------------------------------------------
/graphs/routing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/graphs/routing.jpg
--------------------------------------------------------------------------------
/graphs/overview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/graphs/overview.jpg
--------------------------------------------------------------------------------
/graphs/signalling.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/graphs/signalling.jpg
--------------------------------------------------------------------------------
/src/sig-server/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/src/sig-server/.DS_Store
--------------------------------------------------------------------------------
/tests/sig-server/test-lifecycle.js:
--------------------------------------------------------------------------------
1 | // TODO test 10 peers joining, registering, sending the handshakes and all
2 |
--------------------------------------------------------------------------------
/graphs/connection-state.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/graphs/connection-state.jpg
--------------------------------------------------------------------------------
/graphs/webrtc-explorer-logo.pxm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/graphs/webrtc-explorer-logo.pxm
--------------------------------------------------------------------------------
/graphs/webrtc-explorer-logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daviddias/webrtc-explorer/HEAD/graphs/webrtc-explorer-logo-small.png
--------------------------------------------------------------------------------
/src/sig-server/routes-http/basic.js:
--------------------------------------------------------------------------------
1 | const server = require('../').http
2 |
3 | server.route({
4 | method: 'GET',
5 | path: '/',
6 | handler: function (request, reply) {
7 | reply('signaling server')
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/src/explorer/config.js:
--------------------------------------------------------------------------------
1 | const Id = require('webrtc-explorer-peer-id')
2 | const debug = require('debug')
3 | const log = debug('explorer')
4 | log.error = debug('explorer:error')
5 |
6 | module.exports = {
7 | log: log,
8 | peerId: new Id()
9 | }
10 |
--------------------------------------------------------------------------------
/src/sig-server/utils/ideal-finger.js:
--------------------------------------------------------------------------------
1 | const Id = require('webrtc-explorer-peer-id')
2 |
3 | module.exports = (peerId, row) => {
4 | const k = Number(row) + 1
5 | const ideal = (peerId.toDec() + Math.pow(2, k - 1)) % Math.pow(2, 48)
6 | return new Id(ideal)
7 | }
8 |
--------------------------------------------------------------------------------
/src/sig-server/routes-http/graph.js:
--------------------------------------------------------------------------------
1 | const server = require('../').http
2 | const peerTable = require('../resources/peer-table')
3 |
4 | server.route({
5 | method: 'get',
6 | path: '/dht',
7 | handler: function (request, reply) {
8 | reply(peerTable)
9 | }
10 | })
11 |
12 |
--------------------------------------------------------------------------------
/src/sig-server/resources/peer-table.js:
--------------------------------------------------------------------------------
1 | // id : {
2 | // socket:
3 | // fingerTable: {
4 | // row : {
5 | // ideal:
6 | // current:
7 | // }
8 | // }
9 | // predecessorId
10 | // }
11 | const peerTable = {}
12 |
13 | module.exports = peerTable
14 |
--------------------------------------------------------------------------------
/src/sig-server/bin.js:
--------------------------------------------------------------------------------
1 | #!/usr/local/bin/node
2 |
3 | const sigServer = require('./index')
4 |
5 | sigServer.start(() => {
6 | console.log('Signalling Server Started')
7 | })
8 |
9 | process.on('SIGINT', () => {
10 | sigServer.stop(() => {
11 | console.log('Signalling Server Stopped')
12 | process.exit()
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/tests/sig-server/test-http.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 |
3 | const expect = require('chai').expect
4 | const sigServer = require('../../src/sig-server')
5 |
6 | describe('http routes', () => {
7 | it('/', (done) => {
8 | sigServer.http.inject({
9 | method: 'GET',
10 | url: '/'
11 | }, (res) => {
12 | expect(res.payload).to.equal('signaling server')
13 | done()
14 | })
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/examples/explorer.js:
--------------------------------------------------------------------------------
1 | const explorer = require('../src/explorer')
2 |
3 | global.explorer = explorer
4 |
5 | const listener = explorer.createListener((conn) => {
6 | console.log('received conn')
7 | conn.on('data', function (data) {
8 | console.log('received some data', data)
9 | })
10 | })
11 |
12 | listener.listen((err) => {
13 | if (err) {
14 | return console.log('Error listening:', err)
15 | }
16 | console.log('Listening')
17 | })
18 |
--------------------------------------------------------------------------------
/src/sig-server/config.js:
--------------------------------------------------------------------------------
1 | const debug = require('debug')
2 | const log = debug('sig-server')
3 | log.error = debug('sig-server:error')
4 |
5 | module.exports = {
6 | log: log,
7 | hapi: {
8 | port: process.env.PORT || 9000,
9 | options: {
10 | connections: {
11 | routes: {
12 | cors: true
13 | }
14 | }
15 | }
16 | },
17 | explorer: {
18 | fingers: [
19 | 0,
20 | 10,
21 | 12,
22 | 20,
23 | 30,
24 | 47
25 | ],
26 | 'min-peers': 4
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/sig-server/index.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before, after */
2 |
3 | const sigServer = require('../../src/sig-server')
4 | const fs = require('fs')
5 |
6 | describe('sig-server', () => {
7 | before((done) => {
8 | sigServer.start(done)
9 | })
10 |
11 | after((done) => {
12 | sigServer.stop(done)
13 | })
14 |
15 | const tests = fs.readdirSync(__dirname)
16 | tests.filter((file) => {
17 | if (file !== 'index.js') { return true }
18 | return false
19 | }).forEach((file) => {
20 | require('./' + file)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/tests/explorer/index.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before, after */
2 |
3 | const sigServer = require('../../src/sig-server')
4 | const fs = require('fs')
5 |
6 | describe('explorer', () => {
7 | before((done) => {
8 | sigServer.start(done)
9 | })
10 |
11 | after((done) => {
12 | sigServer.stop(done)
13 | })
14 |
15 | const tests = fs.readdirSync(__dirname)
16 | tests.filter((file) => {
17 | if (file === 'index.js' || file === 'scripts') { return false }
18 | return true
19 | }).forEach((file) => {
20 | require('./' + file)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | # Logs
3 | logs
4 | *.log
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # Compiled binary addons (http://nodejs.org/api/addons.html)
21 | build/Release
22 |
23 | # Dependency directory
24 | # Deployed apps should consider commenting this line out:
25 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
26 | node_modules
27 |
--------------------------------------------------------------------------------
/src/sig-server/index.js:
--------------------------------------------------------------------------------
1 | const Hapi = require('Hapi')
2 | const config = require('./config')
3 | const log = config.log
4 |
5 | exports = module.exports
6 |
7 | exports.start = (callback) => {
8 | exports.http = new Hapi.Server(config.hapi.options)
9 |
10 | exports.http.connection({
11 | port: config.hapi.port
12 | })
13 |
14 | require('./routes-http/basic')
15 | // require('./routes-http/graph')
16 |
17 | exports.http.start(() => {
18 | log('signaling server has started on: ' + exports.http.info.uri)
19 | require('./routes-ws')
20 | callback()
21 | })
22 | }
23 |
24 | exports.stop = (callback) => {
25 | exports.http.stop(callback)
26 | }
27 |
--------------------------------------------------------------------------------
/src/sig-server/utils/finger-best-fit.js:
--------------------------------------------------------------------------------
1 | const Id = require('webrtc-explorer-peer-id')
2 |
3 | module.exports = (peerId, idealFingerId, currentFingerId, newFingerId) => {
4 | const p = peerId.toDec()
5 | const i = idealFingerId.toDec()
6 | const c = currentFingerId.toDec()
7 | const n = newFingerId.toDec()
8 |
9 | // does it leap
10 | if (i < p) {
11 | // is it on the left
12 | if (n <= new Id('ffffffffffff').toDec() && n > p && n > c) {
13 | return true
14 | }
15 | // is it on the right
16 | if (n > 0 && n < c) {
17 | return true
18 | }
19 | } else {
20 | if (c < p && n > p) {
21 | return true
22 | }
23 | if (n >= i && n < c) {
24 | return true
25 | }
26 | }
27 | return false
28 | }
29 |
--------------------------------------------------------------------------------
/tests/explorer/scripts/explorer-peer.js:
--------------------------------------------------------------------------------
1 | const explorer = require('./../../../src/explorer/index.js')
2 | const ppc = require('piri-piri').client
3 |
4 | module.exports = function (args) {
5 | ppc.handle('listen', () => {
6 | const listener = explorer.createListener((conn) => {
7 | console.log('received conn')
8 | })
9 |
10 | listener.listen((err) => {
11 | if (err) {
12 | return console.log('Error listening:', err)
13 | }
14 | ppc.send('listening')
15 | })
16 | })
17 |
18 | ppc.handle('get-finger-table', () => {
19 | var ft = explorer.getFingerTable()
20 | ft = Object.keys(ft).map((row) => {
21 | var el = {}
22 | el[row] = ft[row].peerId
23 | return el
24 | })
25 | ppc.send(ft)
26 | })
27 |
28 | // only connect after registering all the handles
29 | ppc.connect((err, socket) => {
30 | if (err) {
31 | return console.log(err)
32 | }
33 | console.log('### connected to piri-piri')
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 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.
22 |
--------------------------------------------------------------------------------
/tests/sig-server/test-finger-best-fit.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it*/
2 |
3 | const expect = require('chai').expect
4 | const Id = require('webrtc-explorer-peer-id')
5 | const fingerBestFit = require('../../src/sig-server/utils/finger-best-fit')
6 |
7 | describe('finger-best-fit', () => {
8 | it('peer id > 0 && < MAX', (done) => {
9 | const randomId = new Id('ffffff000000')
10 |
11 | const idealFingerId = new Id(randomId.toDec() + 200)
12 |
13 | const currentFingerId = new Id(randomId.toDec() + 250)
14 |
15 | const newFingerIdA = new Id(randomId.toDec() + 100)
16 | const newFingerIdB = new Id(randomId.toDec() + 230)
17 | const newFingerIdC = new Id(randomId.toDec() + 280)
18 |
19 | expect(fingerBestFit(randomId, idealFingerId, currentFingerId, newFingerIdA)).to.equal(false)
20 | expect(fingerBestFit(randomId, idealFingerId, currentFingerId, newFingerIdB)).to.equal(true)
21 | expect(fingerBestFit(randomId, idealFingerId, currentFingerId, newFingerIdC)).to.equal(false)
22 | done()
23 | })
24 |
25 | it('when a smaller was in place', (done) => {
26 | const peerId = new Id(366279746573)
27 | const currentId = new Id(343862697473)
28 | const idealId = new Id(366279746574)
29 | const newId = new Id(616263646566)
30 | expect(fingerBestFit(peerId, idealId, currentId, newId)).to.equal(true)
31 | done()
32 | })
33 |
34 | it.skip('peer id === 0', (done) => {
35 | done()
36 | })
37 |
38 | it.skip('peer id === MAX', (done) => {
39 | done()
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webrtc-explorer",
3 | "version": "2.0.0-alpha-v0",
4 | "description": "P2P Network Routing Overlay designed for the Web platform (browsers) ",
5 | "main": "src/explorer/index.js",
6 | "bin": {
7 | "sig-server": "src/sig-server/bin.js"
8 | },
9 | "scripts": {
10 | "lint": "standard",
11 | "test": "npm run test:sig-server && npm run test:explorer",
12 | "test:explorer": "mocha tests/explorer/index.js",
13 | "test:sig-server": "mocha tests/sig-server/index.js",
14 | "sig-server": "node src/sig-server"
15 | },
16 | "standard": {
17 | "ignore": [
18 | "examples/bundle.js"
19 | ]
20 | },
21 | "precommit": [
22 | "lint",
23 | "test"
24 | ],
25 | "keywords": [
26 | "routing",
27 | "webrtc",
28 | "chord",
29 | "peer",
30 | "p2p",
31 | "thesis",
32 | "cloud",
33 | "distributed",
34 | "hashring"
35 | ],
36 | "repository": {
37 | "type": "git",
38 | "url": "https://github.com/diasdavid/webrtc-explorer"
39 | },
40 | "author": "David Dias ",
41 | "license": "MIT",
42 | "dependencies": {
43 | "debug": "^2.2.0",
44 | "hapi": "^13.0.0",
45 | "simple-peer": "^6.0.1",
46 | "socket.io": "^1.4.5",
47 | "socket.io-client": "^1.4.5",
48 | "webrtc-explorer-peer-id": "^1.1.0"
49 | },
50 | "devDependencies": {
51 | "chai": "^3.5.0",
52 | "mocha": "^2.4.5",
53 | "piri-piri": "^0.4.0",
54 | "pre-commit": "^1.1.2",
55 | "standard": "^6.0.5"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/explorer/message-router.js:
--------------------------------------------------------------------------------
1 | const fingerTable = require('./finger-table')
2 | const Id = require('webrtc-explorer-peer-id')
3 | const config = require('./config')
4 | // const log = config.log
5 | const peerId = config.peerId
6 | const connSwitch = require('./connection-switch')
7 |
8 | exports = module.exports
9 |
10 | exports.route = (message) => {
11 | // message struct
12 | // {
13 | // srcId:
14 | // dstId:
15 | // connId:
16 | // leap:
17 | // data:
18 | // }
19 |
20 | // 1. check if it is for me (by routing criteria or leap flag)
21 | // if yes, connSwitch.receiveMessage
22 | // if not, send
23 | message = JSON.parse(message)
24 | if (message.data.type === 'Buffer') {
25 | message.data = new Buffer(message.data.data)
26 | }
27 | const dstId = new Id(message.dstId)
28 |
29 | console.log('router: route -', message)
30 |
31 | if (message.leap || peerId.toDec() >= dstId.toDec()) {
32 | return connSwitch.receiveMessage(message)
33 | }
34 |
35 | send(message)
36 | }
37 |
38 | exports.send = send
39 | function send (message) {
40 | // send message to best next hop
41 | // note: if the nextHop is on the other side of the loop, add leap flag
42 |
43 | const nextHopId = new Id(fingerTable.nextHop(message.dstId))
44 | console.log('next hop is:', nextHopId.toHex())
45 | if (nextHopId.toDec() < peerId.toDec()) {
46 | message.leap = true
47 | }
48 |
49 | var channel
50 |
51 | Object.keys(fingerTable.table).forEach((row) => {
52 | if (fingerTable.table[row].peerId === nextHopId.toHex()) {
53 | channel = fingerTable.table[row].channel
54 | }
55 | })
56 |
57 | console.log('router: send -', message)
58 |
59 | channel.send(JSON.stringify(message))
60 | }
61 |
--------------------------------------------------------------------------------
/tests/sig-server/test-handshake.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it, after */
2 |
3 | const expect = require('chai').expect
4 | const io = require('socket.io-client')
5 |
6 | describe('handshake', () => {
7 | const options = {
8 | transports: ['websocket'],
9 | 'force new connection': true
10 | }
11 |
12 | const url = 'http://localhost:9000'
13 |
14 | var c1
15 | var c2
16 | const c1Id = new Buffer('6bytes').toString('hex')
17 | const c2Id = new Buffer('48bits').toString('hex')
18 |
19 | after((done) => {
20 | c1.disconnect()
21 | c2.disconnect()
22 |
23 | done()
24 | })
25 |
26 | it('io connect', (done) => {
27 | var count = 0
28 |
29 | c1 = io.connect(url, options)
30 | c2 = io.connect(url, options)
31 |
32 | c1.on('connect', connected)
33 | c2.on('connect', connected)
34 |
35 | function connected () {
36 | if (++count === 2) {
37 | done()
38 | }
39 | }
40 | })
41 |
42 | it('2 peers join', (done) => {
43 | var count = 0
44 |
45 | c1.once('we-ready', completed)
46 | c2.once('we-ready', completed)
47 |
48 | c1.emit('ss-join', {
49 | peerId: c1Id
50 | })
51 | c2.emit('ss-join', {
52 | peerId: c2Id
53 | })
54 |
55 | function completed (err) {
56 | expect(err).to.not.exist
57 |
58 | if (++count === 2) {
59 | done()
60 | }
61 | }
62 | })
63 |
64 | it('perform pseudo WebRTC handshake', (done) => {
65 | const originalOffer = {
66 | srcId: c1Id,
67 | dstId: c2Id,
68 | intentId: '1234',
69 | webrtc: 'chicken'
70 | }
71 | c1.once('we-handshake', (offer) => {
72 | expect(offer.webrtc).to.equal('pineapple')
73 | done()
74 | })
75 | c2.once('we-handshake', (offer) => {
76 | expect(offer.webrtc).to.equal('chicken')
77 | offer.webrtc = 'pineapple'
78 | offer.answer = true
79 | c2.emit('ss-handshake', offer)
80 | })
81 |
82 | c1.emit('ss-handshake', originalOffer)
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/src/explorer/channel.js:
--------------------------------------------------------------------------------
1 | const SimplePeer = require('simple-peer')
2 | const config = require('./config')
3 | const router = require('./message-router')
4 |
5 | exports = module.exports
6 |
7 | exports.connect = (io, dstId, callback) => {
8 | const intentId = (~~(Math.random() * 1e9)).toString(36) + Date.now()
9 |
10 | const channel = new SimplePeer({initiator: true, trickle: false})
11 |
12 | channel.on('signal', function (signal) {
13 | // console.log('send offer (src, dst):', config.peerId.toHex(), dstId)
14 | io.emit('ss-handshake', {
15 | intentId: intentId,
16 | srcId: config.peerId.toHex(),
17 | dstId: dstId,
18 | signal: signal
19 | })
20 | })
21 |
22 | io.on('we-handshake', (offer) => {
23 | if (offer.intentId !== intentId || !offer.answer) {
24 | return
25 | }
26 | // console.log('offer was accepted (src, dst):', config.peerId.toHex(), dstId)
27 |
28 | channel.on('connect', function () {
29 | // console.log('channel ready to send')
30 | channel.on('data', function () {
31 | console.log('DEBUG: this channel should be only used to send and not to receive')
32 | })
33 | callback(null, channel)
34 | })
35 |
36 | channel.signal(offer.signal)
37 | })
38 | }
39 |
40 | exports.accept = function (io) {
41 | return (offer) => {
42 | // accept incoming DataChannels request to connect
43 | // pipe the received messages on those sockets to the message router
44 | //
45 | // note: if it says it is an answer, ignore
46 | //
47 | if (offer.answer) { return }
48 |
49 | // console.log('received an offer (src, dst):', offer.srcId, offer.dstId)
50 | const channel = new SimplePeer({trickle: false})
51 |
52 | channel.on('connect', function () {
53 | // console.log('channel ready to listen')
54 | channel.on('data', router.route)
55 | })
56 |
57 | channel.on('signal', function (signal) {
58 | // log('sending back my signal data')
59 | offer.signal = signal
60 | offer.answer = true
61 | io.emit('ss-handshake', offer)
62 | })
63 |
64 | channel.signal(offer.signal)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/explorer/test-explorer.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it, before, after */
2 |
3 | const pp = require('piri-piri')
4 | // const Id = require('webrtc-explorer-peer-id')
5 | const expect = require('chai').expect
6 |
7 | describe('explorer', () => {
8 | var ppId0
9 | var ppId1
10 |
11 | before((done) => {
12 | pp.start((err) => {
13 | expect(err).to.not.exist
14 | done()
15 | })
16 | })
17 |
18 | after((done) => {
19 | Object.keys(pp.clients).forEach((id) => {
20 | pp.browser.send(id, 'exit')
21 | })
22 | done()
23 | })
24 |
25 | it('spawn first browser', (done) => {
26 | expect(Object.keys(pp.clients).length).to.equal(0)
27 | pp.browser.spawn('./tests/explorer/scripts/explorer-peer.js', 1, (err) => {
28 | // this only happens on the after, when browsers are told to exit
29 | expect(err).to.not.exist
30 | })
31 | setTimeout(() => {
32 | expect(Object.keys(pp.clients)[0]).to.exist
33 | ppId0 = Object.keys(pp.clients)[0]
34 | done()
35 | }, 500)
36 | })
37 |
38 | it('browser 0 - join (listen)', (done) => {
39 | pp.browser.send(ppId0, 'listen')
40 | setTimeout(() => {
41 | expect(pp.clients[ppId0].msgs.length).to.equal(1)
42 | const msg = pp.clients[ppId0].msgs.shift()
43 | expect(msg).to.equal('listening')
44 | done()
45 | }, 500)
46 | })
47 |
48 | it('spawn another browser', (done) => {
49 | expect(Object.keys(pp.clients).length).to.equal(1)
50 | pp.browser.spawn('./tests/explorer/scripts/explorer-peer.js', 1, (err) => {
51 | // this only happens on the after, when browsers are told to exit
52 | expect(err).to.not.exist
53 | })
54 | setTimeout(() => {
55 | expect(Object.keys(pp.clients)[1]).to.exist
56 | ppId1 = Object.keys(pp.clients)[1]
57 | done()
58 | }, 500)
59 | })
60 |
61 | it('browser 1 - join (listen)', function (done) {
62 | this.timeout(50000)
63 | pp.browser.send(ppId1, 'listen')
64 | setTimeout(() => {
65 | expect(pp.clients[ppId1].msgs.length).to.equal(1)
66 | var msg = pp.clients[ppId1].msgs.shift()
67 | setTimeout(() => {
68 | pp.browser.send(ppId1, 'get-finger-table')
69 | setTimeout(() => {
70 | expect(pp.clients[ppId1].msgs.length).to.equal(1)
71 | msg = pp.clients[ppId1].msgs.shift()
72 | expect(Object.keys(msg[0])[0]).to.equal('0')
73 | done()
74 | }, 200)
75 | }, 200)
76 | }, 500)
77 | })
78 | })
79 |
--------------------------------------------------------------------------------
/src/explorer/connection-switch.js:
--------------------------------------------------------------------------------
1 | const stream = require('stream')
2 | const PassThrough = stream.PassThrough
3 | const Writable = stream.Writable
4 | const router = require('./message-router')
5 | const config = require('./config')
6 | // const log = config.log
7 | const peerId = config.peerId
8 |
9 | exports = module.exports
10 |
11 | var incConnCB = () => {}
12 |
13 | const connections = {}
14 | // list of connections, by connId, each conn is a duplex stream pair
15 | // { connId: {
16 | // inc: duplex stream
17 | // out: duplex stream (pair of inc)
18 | // }
19 |
20 | exports.setIncConnCB = (func) => {
21 | incConnCB = func
22 | }
23 |
24 | exports.receiveMessage = (message) => {
25 | // message struct
26 | // {
27 | // srcId:
28 | // dstId:
29 | // connId:
30 | // leap:
31 | // data:
32 | // }
33 |
34 | // check if message has indeed my Id
35 | // if yes, check if there is already a conn
36 | // if yes write to that conn (inc)
37 | // if not, create a conn and call incConnCB and send a ACK back
38 | // if not, send message back saying that Id doesn't exist
39 |
40 | if (message.dstId === peerId.toHex()) {
41 | if (connections[message.connId]) {
42 | connections[message.connId].inc.write(message.data)
43 | } else {
44 | console.log('received SYN:', message.data)
45 | // we got to invert srcId and dstId so that messages are routed back
46 | const conn = createConn(message.dstId, message.srcId, message.connId)
47 | incConnCB(conn)
48 | }
49 | } else {
50 | const reply = {
51 | srcId: peerId.toHex(),
52 | dstId: message.srcId,
53 | connId: message.connId,
54 | data: message.dstId + ' does not exist'
55 | }
56 | router.send(reply)
57 | }
58 | }
59 |
60 | exports.createConn = createConn
61 | function createConn (srcId, dstId, connId) {
62 | // create a duplex stream pair
63 | // out.on('data') encapsulate chunk with: connId, srcId, dstId and then router.send(message)
64 | //
65 | console.log('creating conn')
66 |
67 | connId = connId || (~~(Math.random() * 1e9)).toString(36) + Date.now()
68 |
69 | const out = new Writable()
70 | const inc = new PassThrough()
71 |
72 | out._write = (data, enc, cb) => {
73 | const message = {
74 | connId: connId,
75 | data: data,
76 | srcId: srcId,
77 | dstId: dstId
78 | }
79 | router.send(message)
80 | cb()
81 | }
82 |
83 | connections[connId] = {
84 | inc: inc,
85 | out: out
86 | }
87 |
88 | return connections[connId]
89 | }
90 |
--------------------------------------------------------------------------------
/src/explorer/finger-table.js:
--------------------------------------------------------------------------------
1 | const config = require('./config')
2 | const log = config.log
3 | const channelManager = require('./channel')
4 | const Id = require('webrtc-explorer-peer-id')
5 |
6 | exports = module.exports
7 |
8 | // {row: {peerId: <>, channel: <>}}
9 | const table = {}
10 | exports.table = table
11 |
12 | var predecessorId
13 |
14 | exports.updateFinger = function (io) {
15 | return (update) => {
16 | console.log('received an update', update)
17 | log('received an update finger', update)
18 | if (table[update.row] && table[update.row].peerId === update.id) {
19 | return console.log('update is not new')
20 | }
21 |
22 | channelManager.connect(io, update.id, (err, channel) => {
23 | if (err) {
24 | return console.log('could not create the channel', err)
25 | }
26 | console.log(update.id, 'added to finger table on row:', update.row)
27 | table[update.row] = {peerId: update.id, channel: channel}
28 | })
29 | }
30 | }
31 |
32 | exports.updatePredecessor = function (io) {
33 | return (pId) => {
34 | predecessorId = pId
35 | }
36 | }
37 |
38 | exports.forMe = (dstId) => {
39 | var forMe = false
40 |
41 | dstId = new Id(dstId).toDec()
42 |
43 | interval(new Id(predecessorId).toDec(), config.peerId.toDec())
44 | .some((interval) => {
45 | if (isIn(interval, dstId)) {
46 | forMe = true
47 | return true
48 | }
49 | })
50 |
51 | return forMe
52 | }
53 |
54 | // Identify the best candidate to send when sending to destId
55 | exports.nextHop = (dstId) => {
56 | if (typeof dstId === 'object') {
57 | dstId = dstId.toDec()
58 | } else {
59 | dstId = new Id(dstId).toDec()
60 | }
61 | var lower = config.peerId
62 | var next
63 |
64 | const fingers = Object.keys(table)
65 | if (fingers.length === 1) {
66 | return table['0'].peerId
67 | }
68 |
69 | fingers.some((row) => {
70 | const upper = new Id(table[row].fingerId)
71 |
72 | interval(lower.toDec(), upper.toDec())
73 | .some((interval) => {
74 | if (isIn(interval, dstId)) {
75 | next = upper.toHex()
76 | return true
77 | }
78 | })
79 |
80 | // if found
81 | if (next) { return true }
82 | lower = upper
83 | })
84 |
85 | if (!next) { // if we don't know the best, send to best we can
86 | next = lower.toHex()
87 | }
88 |
89 | return next
90 | }
91 |
92 | function interval (a, b) {
93 | const SPIN = '1000000000000'
94 | if (b < a) {
95 | return [
96 | [a, new Id(SPIN).toDec()],
97 | [0, b]
98 | ]
99 | } else {
100 | return [[a, b]]
101 | }
102 | }
103 |
104 | function isIn (interval, dstId) {
105 | if (dstId > interval[0] && dstId <= interval[1]) {
106 | return true
107 | } else {
108 | return false
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/explorer/index.js:
--------------------------------------------------------------------------------
1 | const SocketIO = require('socket.io-client')
2 | const config = require('./config')
3 | const log = config.log
4 | const fingerTable = require('./finger-table')
5 | const connSwitch = require('./connection-switch')
6 | const channel = require('./channel')
7 | const stream = require('stream')
8 | const Duplex = stream.Duplex
9 | const peerId = config.peerId
10 |
11 | console.log('My peerId:', peerId.toHex())
12 |
13 | var io
14 |
15 | exports = module.exports
16 |
17 | exports.dial = (dstId, callback) => {
18 | // create a duplex passthrough stream
19 | // create a conn
20 | // write a SYN to conn.out
21 | // when a ACK arrives
22 | // conn.inc.pipe(ds)
23 | // ds.pipe(conn.out)
24 | // callback(to signal that it is ready)
25 | //
26 | var set = false
27 | var reader
28 |
29 | const conn = connSwitch.createConn(peerId.toHex(), dstId)
30 | console.log('Sending SYN to:', dstId)
31 | conn.out.write('SYN')
32 |
33 | conn.inc.once('data', (data) => {
34 | console.log('received ACK:', data)
35 | conn.inc.on('data', (data) => {
36 | reader.push(data)
37 | })
38 | if (callback) {
39 | callback(ds)
40 | }
41 | })
42 |
43 | const ds = new Duplex({
44 | read: function (n) {
45 | if (set) {
46 | return
47 | }
48 | set = true
49 | reader = this
50 | },
51 | write: function (chunk, enc, next) {
52 | conn.out.write(chunk)
53 | }
54 | })
55 |
56 | return ds
57 | }
58 |
59 | exports.createListener = (options, callback) => {
60 | if (typeof options === 'function') {
61 | callback = options
62 | options = {}
63 | }
64 |
65 | connSwitch.setIncConnCB((conn) => {
66 | // ACK
67 | // create duplexStream
68 | // ds.pipe(conn.out)
69 | // conn.inc.pipe(ds)
70 | // callback(ds)
71 | var set = false
72 | const ds = new Duplex({
73 | read: function (n) {
74 | if (set) {
75 | return
76 | }
77 | set = true
78 | conn.inc.on('data', (data) => {
79 | this.push(data)
80 | })
81 | },
82 | write: function (chunk, enc, next) {
83 | conn.out.write(chunk)
84 | }
85 | })
86 |
87 | conn.out.write('ACK')
88 | callback(ds)
89 | })
90 |
91 | return {
92 | listen: (callback) => {
93 | // connect and join (gen Id first), wait to be established, then go
94 | connect(options.url || 'http://localhost:9000', (err) => {
95 | if (err) {}
96 | join(callback)
97 | })
98 | }
99 | }
100 | }
101 |
102 | // update a finger by asking the sig-server what is the new best
103 | exports.updateFinger = (row) => {
104 | // TODO
105 | // 1. send a request for a finger Update on a specific row
106 | }
107 |
108 | // update every row to a new best
109 | exports.updateFingerTable = () => {
110 | // TODO
111 | // 1. send a request by each Finger Row that I'm already using
112 | }
113 |
114 | exports.getFingerTable = () => {
115 | return fingerTable.table
116 | }
117 |
118 | // connect to the sig-server
119 | function connect (url, callback) {
120 | io = SocketIO.connect(url)
121 | io.on('connect', callback)
122 | }
123 |
124 | // join the peerTable of the sig-server
125 | function join (callback) {
126 | log('connected to sig-server')
127 | io.emit('ss-join', {
128 | peerId: config.peerId.toHex(),
129 | notify: true
130 | })
131 | io.on('we-update-finger', fingerTable.updateFinger(io))
132 | io.on('we-handshake', channel.accept(io))
133 |
134 | io.once('we-ready', callback)
135 | }
136 |
--------------------------------------------------------------------------------
/tests/sig-server/test-join.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it, after */
2 |
3 | const expect = require('chai').expect
4 | const io = require('socket.io-client')
5 |
6 | describe('join', () => {
7 | const options = {
8 | transports: ['websocket'],
9 | 'force new connection': true
10 | }
11 |
12 | const url = 'http://localhost:9000'
13 |
14 | var c1
15 | var c2
16 | var c3
17 |
18 | after((done) => {
19 | c1.disconnect()
20 | c2.disconnect()
21 | c3.disconnect()
22 |
23 | done()
24 | })
25 |
26 | it('io connect', (done) => {
27 | var count = 0
28 |
29 | c1 = io.connect(url, options)
30 | c2 = io.connect(url, options)
31 | c3 = io.connect(url, options)
32 |
33 | c1.on('connect', connected)
34 | c2.on('connect', connected)
35 | c3.on('connect', connected)
36 |
37 | function connected () {
38 | if (++count === 3) {
39 | done()
40 | }
41 | }
42 | })
43 |
44 | it('join with notify off', (done) => {
45 | var count = 0
46 |
47 | c1.once('we-ready', completed)
48 | c2.once('we-ready', completed)
49 |
50 | c1.emit('ss-join', {
51 | peerId: new Buffer('6bytes').toString('hex'),
52 | notify: false
53 | })
54 | c2.emit('ss-join', {
55 | peerId: new Buffer('48bits').toString('hex'),
56 | notify: false
57 | })
58 |
59 | function completed (err) {
60 | expect(err).to.not.exist
61 |
62 | if (++count === 2) {
63 | done()
64 | }
65 | }
66 | })
67 |
68 | it('join with incorrect Id', (done) => {
69 | c3.once('we-ready', completed)
70 |
71 | c3.emit('ss-join', {
72 | peerId: new Buffer('incorrect').toString('hex'),
73 | notify: false
74 | })
75 |
76 | function completed (err) {
77 | expect(err).to.exist
78 | done()
79 | }
80 | })
81 |
82 | it('io disconnect and connect again', (done) => {
83 | var count = 0
84 |
85 | c1.disconnect()
86 | c2.disconnect()
87 | c3.disconnect()
88 |
89 | c1 = io.connect(url, options)
90 | c2 = io.connect(url, options)
91 | c3 = io.connect(url, options)
92 |
93 | c1.on('connect', connected)
94 | c2.on('connect', connected)
95 | c3.on('connect', connected)
96 |
97 | function connected () {
98 | if (++count === 3) {
99 | done()
100 | }
101 | }
102 | })
103 |
104 | it('2 join with notify on', (done) => {
105 | // join with 1 and 2, check that only after 2 joins, one gets the message to connect to it, this way we avoid double call collision
106 |
107 | const c1Id = new Buffer('6bytes').toString('hex')
108 | const c2Id = new Buffer('48bits').toString('hex')
109 |
110 | // console.log('c1', c1Id)
111 | // console.log('c2', c2Id)
112 |
113 | c1.once('we-update-finger', (update) => {
114 | expect(update.id).to.equal(c2Id)
115 | expect(update.row).to.equal('0')
116 | c2.removeListener('we-update-finger')
117 | done()
118 | })
119 | c2.on('we-update-finger', () => {
120 | throw new Error('should not happen')
121 | })
122 |
123 | c1.emit('ss-join', {
124 | peerId: c1Id,
125 | notify: true
126 | })
127 |
128 | c2.emit('ss-join', {
129 | peerId: c2Id
130 | })
131 | })
132 |
133 | it('3rd joins with notify on', (done) => {
134 | var count = 0
135 |
136 | const c3Id = new Buffer('abcdef').toString('hex')
137 | // console.log('c3', c3Id)
138 |
139 | c1.once('we-update-finger', receivedUpdate)
140 | c3.once('we-update-finger', receivedUpdate)
141 |
142 | c3.emit('ss-join', {
143 | peerId: c3Id,
144 | notify: true
145 | })
146 |
147 | function receivedUpdate (update) {
148 | expect(update).to.exist
149 | expect(update.id).to.exist
150 | expect(update.row).to.equal('0')
151 |
152 | if (++count === 2) {
153 | done()
154 | }
155 | }
156 | })
157 | })
158 |
--------------------------------------------------------------------------------
/tests/sig-server/test-update-finger.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it, after */
2 |
3 | const expect = require('chai').expect
4 | const io = require('socket.io-client')
5 |
6 | describe('update-finger', () => {
7 | const options = {
8 | transports: ['websocket'],
9 | 'force new connection': true
10 | }
11 |
12 | const url = 'http://localhost:9000'
13 |
14 | var c1
15 | var c2
16 | var c3
17 | var c4
18 | var c5
19 | const c1Id = new Buffer('48bits').toString('hex')
20 | const c2Id = new Buffer('6bytes').toString('hex')
21 | const c3Id = new Buffer('batata').toString('hex')
22 | const c4Id = new Buffer('cebola').toString('hex')
23 |
24 | // console.log('c1', c1Id)
25 | // console.log('c2', c2Id)
26 | // console.log('c3', c3Id)
27 | // console.log('c4', c4Id)
28 |
29 | after((done) => {
30 | c1.disconnect()
31 | c2.disconnect()
32 | c3.disconnect()
33 | c4.disconnect()
34 | c5.disconnect()
35 |
36 | done()
37 | })
38 |
39 | it('io connect', (done) => {
40 | var count = 0
41 |
42 | c1 = io.connect(url, options)
43 | c2 = io.connect(url, options)
44 | c3 = io.connect(url, options)
45 | c4 = io.connect(url, options)
46 |
47 | c1.on('connect', connected)
48 | c2.on('connect', connected)
49 | c3.on('connect', connected)
50 | c4.on('connect', connected)
51 |
52 | function connected () {
53 | if (++count === 4) {
54 | done()
55 | }
56 | }
57 | })
58 |
59 | it('join four', function (done) {
60 | this.timeout(50000)
61 | var count = 0
62 |
63 | c1.once('we-ready', tick)
64 | c2.once('we-ready', tick)
65 | c3.once('we-ready', tick)
66 | c4.once('we-ready', tick)
67 |
68 | c1.once('we-update-finger', (update) => {
69 | expect(update.row).to.equal('0')
70 | expect(update.id).to.equal(c2Id)
71 | tick()
72 | })
73 | c2.once('we-update-finger', (update) => {
74 | expect(update.row).to.equal('0')
75 | expect(update.id).to.equal(c1Id)
76 | tick()
77 | c2.once('we-update-finger', (update) => {
78 | expect(update.row).to.equal('0')
79 | expect(update.id).to.equal(c3Id)
80 | tick()
81 | })
82 | })
83 | c3.once('we-update-finger', (update) => {
84 | expect(update.row).to.equal('0')
85 | expect(update.id).to.equal(c1Id)
86 | tick()
87 | c3.once('we-update-finger', (update) => {
88 | expect(update.row).to.equal('0')
89 | expect(update.id).to.equal(c4Id)
90 | tick()
91 | })
92 | })
93 | c4.once('we-update-finger', (update) => {
94 | expect(update.row).to.equal('0')
95 | expect(update.id).to.equal(c1Id)
96 | tick()
97 | })
98 |
99 | function tick () {
100 | if (++count === 10) {
101 | done()
102 | // setTimeout(done, 800)
103 | }
104 | }
105 |
106 | c1.emit('ss-join', { peerId: c1Id, notify: true })
107 | c2.emit('ss-join', { peerId: c2Id, notify: true })
108 | c3.emit('ss-join', { peerId: c3Id, notify: true })
109 | c4.emit('ss-join', { peerId: c4Id, notify: true })
110 | })
111 |
112 | it('update-finger c1 row 2', (done) => {
113 | c1.once('we-update-finger', (update) => {
114 | expect(update.row).to.equal('2')
115 | done()
116 | })
117 | c1.emit('ss-update-finger', {peerId: c1Id, row: '2'})
118 | })
119 |
120 | it('update-finger c2 row 10', (done) => {
121 | c2.once('we-update-finger', (update) => {
122 | expect(update.row).to.equal('10')
123 | done()
124 | })
125 | c2.emit('ss-update-finger', {peerId: c2Id, row: '10'})
126 | })
127 |
128 | it('update-finger c3 row 25 and 30', (done) => {
129 | c3.once('we-update-finger', (update) => {
130 | expect(update.row).to.equal('25')
131 | c3.emit('ss-update-finger', {peerId: c3Id, row: '30'})
132 | c3.once('we-update-finger', (update) => {
133 | expect(update.row).to.equal('30')
134 | done()
135 | })
136 | })
137 | c3.emit('ss-update-finger', {peerId: c3Id, row: '25'})
138 | })
139 |
140 | it('join peer c5', (done) => {
141 | const c5Id = new Buffer('fifthe').toString('hex')
142 | // console.log(c5Id)
143 | c5 = io.connect(url, options)
144 | c5.on('connect', () => {
145 | c5.emit('ss-join', { peerId: c5Id, notify: true })
146 | })
147 |
148 | var count = 0
149 |
150 | c4.once('we-update-finger', tick)
151 | c5.once('we-update-finger', tick)
152 |
153 | function tick (update) {
154 | if (++count === 2) {
155 | done()
156 | }
157 | }
158 | })
159 |
160 | it.skip('make a test with even more versatility', (done) => {})
161 | })
162 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://david-dm.org/diasdavid/webrtc-explorer)
4 | [](https://github.com/diasdavid/WebCompute)
5 | [](https://github.com/feross/standard)
6 |
7 | 
8 | 
9 |
10 | > **tl;dr** `webrtc-explorer` is a [Chord](http://pdos.csail.mit.edu/papers/chord:sigcomm01/chord_sigcomm.pdf) inspired, P2P Network Routing Overlay designed for the Web platform (browsers), using WebRTC as its layer of transport between peers and WebSockets for the exchange of signalling data (setting up a connection and NAT traversal). Essentially, webrtc-explorer enables your peers (browsers) to communicate between each other without the need to have a server to be the mediator.
11 |
12 | # Usage
13 |
14 | ## Install
15 |
16 | ```sh
17 | > npm install webrtc-explorer
18 | ```
19 |
20 | If you want to use the Signalling Server that comes with webrtc-explorer, you can use it through your terminal after installing webrtc-explorer globally
21 |
22 | ```sh
23 | > npm install webrtc-explorer --global
24 | # ...
25 | > sig-server
26 | Signalling Server Started
27 | # now the signalling server is running
28 | ```
29 |
30 | Use [browserify](http://browserify.org) to load transpile your JS code that uses webrtc-explorer, so that all of the dependencies are correctly loaded.
31 |
32 | ## API
33 |
34 | ```javascript
35 | const explorer = require('webrtc-explorer')
36 | ```
37 |
38 | #### listen
39 |
40 | Connects your explorer node to the signalling server, and listens for incomming connections from other peers.
41 |
42 | ```javascript
43 | const listener = explorer.createListener((socket) => {
44 | // socket with another peer
45 | })
46 |
47 | listener.listen((err) => {
48 | if (err) {
49 | return console.log('Error listening:', err)
50 | }
51 | console.log('explorer is now listining to incomming connections')
52 | })
53 | ```
54 |
55 | #### dial
56 |
57 | Dials into another peer, using the P2P Overlay Routing.
58 |
59 | ```JavaScript
60 | const socket = explorer.dial( [, ])
61 | ```
62 |
63 | Note: since an explorer node routes messages for other peers and itself, it needs first to be ready to 'listen', in order to be able to use the network to send.
64 |
65 | #### updateFinger
66 |
67 | _not implemented yet_
68 |
69 | updates a finger on the finger table (if no finger was present on that row, it is added).
70 |
71 | ```JavaScript
72 | explorer.updateFinger()
73 | ```
74 |
75 | #### updateFingerTable
76 |
77 | _not implemented yet_
78 |
79 | updates all the rows on the finger table that already had a peer
80 |
81 | ```JavaScript
82 | explorer.updateFingerTable()
83 | ```
84 |
85 | # Architecture
86 |
87 | 
88 |
89 | ## Signalling
90 |
91 | Currently signalling is performed through a central server. The signalling throught the Chord routing is under development.
92 |
93 | 
94 |
95 | ## Routing
96 |
97 | To understand fully webrtc-explorer's core, it is important to be familiar with the [Chord][chord-paper].
98 |
99 | I've delivered a talk before about an earlier version of webrtc-explorer, where I explain the routing scheme, you can find it here: https://youtu.be/fNQGGGE__zI?t=13m33s
100 |
101 | 
102 |
103 | ## Connection State
104 |
105 | Connections in webrtc-explorer are very similar to typical network socket. Before going to the network, the messages are encasulated with srcId and dstId so that they be routed through the Chord routing (parallel to the encasulation with TCP headers, IP headers, etc)
106 |
107 | 
108 |
109 | ## Notes and other properties
110 |
111 | - Ids have 48 bits (so that is a multiple of 4 (for hex notation) and doesn't require importing a big-num lib to handle over 53 bits operations)
112 | - The number of fingers of each peer is flexible, however it is recommended to not pass 16 per node (due to browser resource constraints)
113 | - Each peer is responsible for a segment of the hash ring
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | -----------------------------------------------------------------------------
124 |
125 | ## Initial Development and release was supported by INESC-ID (circa Mar 2015)
126 |
127 | > [David Dias MSc in Peer-to-Peer Networks by Technical University of Lisbon](https://github.com/diasdavid/browserCloudjs#research-and-development)
128 |
129 | [](http://www.gsd.inesc-id.pt/) [](http://tecnico.ulisboa.pt/) [](https://github.com/diasdavid/browserCloudjs)
130 |
131 | This work was developed by David Dias with supervision by Luís Veiga, all in INESC-ID Lisboa (Distributed Systems Group), Instituto Superior Técnico, Universidade de Lisboa, with the support of Fundação para a Ciência e Tecnologia.
132 |
133 | More info on the team's work at:
134 | - http://daviddias.me
135 | - http://www.gsd.inesc-id.pt/~lveiga
136 |
137 | If you use this project, please acknowledge it in your work by referencing the following document:
138 |
139 | David Dias and Luís Veiga. browserCloud.js A federated community cloud served by a P2P overlay network on top of the web platform. INESC-ID Tec. Rep. 14/2015, Apr. 2015
140 |
--------------------------------------------------------------------------------
/src/sig-server/routes-ws/index.js:
--------------------------------------------------------------------------------
1 | const config = require('../config')
2 | const log = config.log
3 | const peerTable = require('../resources/peer-table')
4 | const http = require('../index').http
5 | const SocketIO = require('socket.io')
6 | const Id = require('webrtc-explorer-peer-id')
7 | const idealFinger = require('../utils/ideal-finger')
8 | const fingerBestFit = require('../utils/finger-best-fit')
9 |
10 | const io = new SocketIO(http.listener)
11 | io.on('connection', handle)
12 |
13 | function handle (socket) {
14 | log('received inbound ws conn')
15 |
16 | socket.on('ss-join', join.bind(socket))
17 | socket.on('disconnect', remove.bind(socket)) // socket.io own event
18 | socket.on('ss-handshake', forward.bind(socket))
19 | socket.on('ss-update-finger', (request) => {
20 | updateFinger(request.peerId, request.row)
21 | })
22 | }
23 |
24 | // join this signaling server network
25 | function join (options) {
26 | if (options.peerId.length !== 12) {
27 | return this.emit('we-ready', new Error('Unvalid peerId length, must be 48 bits, received: ' + options.peerId).toString())
28 | }
29 |
30 | if (peerTable[options.peerId]) {
31 | return this.emit('we-ready', new Error('peerId already exists').toString())
32 | }
33 |
34 | peerTable[options.peerId] = {
35 | socket: this,
36 | notify: typeof options.notify === 'boolean' && options.notify === true,
37 | fingers: options.fingers || {
38 | '0': {
39 | ideal: idealFinger(new Id(options.peerId), '0').toHex(),
40 | current: undefined
41 | }}
42 | }
43 |
44 | log('peer joined: ' + options.peerId)
45 | this.emit('we-ready')
46 | if (Object.keys(peerTable).length === 1) {
47 | return log('This was the first peer join, do nothing')
48 | }
49 | notify()
50 | if (peerTable[options.peerId].fingers['0'].current === undefined) {
51 | if (!peerTable[options.peerId].notify) {
52 | return
53 | }
54 | updateFinger(options.peerId, '0')
55 | }
56 |
57 | // notify if to other peers if this new Peer is a best finger for them
58 | function notify () {
59 | const newId = options.peerId
60 | const peerIds = Object.keys(peerTable)
61 | // check for all the peers
62 | // if same id skip
63 | // if notify === false skip
64 | // check the first finger that matches the criteria for ideal or next to ideal
65 | peerIds.forEach((peerId) => {
66 | if (newId === peerId) {
67 | return // skip ourselves
68 | }
69 | if (!peerTable[peerId].notify) {
70 | return // skip if it doesn't want to be notified of available peers
71 | }
72 |
73 | // if it had none, notify to get a successor
74 | if (peerTable[peerId].fingers['0'].current === undefined) {
75 | peerTable[peerId].fingers['0'].current = newId
76 | peerTable[peerId].socket.emit('we-update-finger', {
77 | row: '0',
78 | id: newId
79 | })
80 | return
81 | }
82 |
83 | const rows = Object.keys(peerTable[peerId].fingers)
84 | // find the first row that could use this finger
85 | rows.some((row) => {
86 | const finger = peerTable[peerId].fingers[row]
87 | const bestCandidate = fingerBestFit(
88 | new Id(peerId),
89 | new Id(finger.ideal),
90 | new Id(finger.current),
91 | new Id(newId))
92 | if (bestCandidate) {
93 | peerTable[peerId].fingers[row].current = newId
94 | peerTable[peerId].socket.emit('we-update-finger', {
95 | row: row,
96 | id: newId
97 | })
98 | return true
99 | }
100 | })
101 | })
102 | }
103 | }
104 |
105 | // finds the best new Finger for the peerId's row 'row')
106 | function updateFinger (peerId, row) {
107 | var availablePeers = Object.keys(peerTable)
108 | availablePeers.splice(availablePeers.indexOf(peerId), 1)
109 |
110 | // if row hasn't been checked before
111 | if (!peerTable[peerId].fingers[row]) {
112 | peerTable[peerId].fingers[row] = {
113 | ideal: idealFinger(new Id(peerId), row).toHex(),
114 | current: undefined
115 | }
116 | }
117 |
118 | var best = availablePeers.shift()
119 | availablePeers.forEach((otherId) => {
120 | const isFBT = fingerBestFit(
121 | new Id(peerId),
122 | new Id(peerTable[peerId].fingers[row].ideal),
123 | new Id(best),
124 | new Id(otherId))
125 | if (isFBT) {
126 | best = otherId
127 | }
128 | })
129 | if (best === peerTable[peerId].fingers[row].current) {
130 | return // nevermind then
131 | }
132 | peerTable[peerId].fingers[row].current = best
133 | peerTable[peerId].socket.emit('we-update-finger', {
134 | row: row,
135 | id: best
136 | })
137 | }
138 |
139 | function remove () {
140 | Object.keys(peerTable).forEach((peerId) => {
141 | if (peerTable[peerId].socket.id === this.id) {
142 | delete peerTable[peerId]
143 | log('peer disconnected: ' + peerId)
144 | }
145 | })
146 | }
147 |
148 | // forward an WebRTC offer to another peer
149 | function forward (offer) {
150 | if (offer.answer) {
151 | peerTable[offer.srcId].socket
152 | .emit('we-handshake', offer)
153 | return
154 | }
155 | peerTable[offer.dstId].socket
156 | .emit('we-handshake', offer)
157 | }
158 |
159 | /*
160 | function handle (socket) {
161 | log('received incoming WebSockets conn')
162 |
163 | socket.on('s-register', registerPeer)
164 | socket.on('disconnect', peerRemove) // socket.io own event
165 | socket.on('s-send-offer', sendOffer)
166 | socket.on('s-offer-accepted', offerAccepted)
167 |
168 | function registerPeer () {
169 | console.log('registerPeer')
170 | var peerId = new Id()
171 | peers[peerId.toHex()] = {
172 | socketId: socket.id,
173 | fingerTable: {}
174 | }
175 |
176 | console.log('->', peers)
177 |
178 | sockets[socket.id] = socket
179 |
180 | socket.emit('c-registered', {peerId: peerId.toHex()})
181 |
182 | console.log('registered new peer: ', peerId.toHex())
183 |
184 | calculateIdealFingers(peerId)
185 | updateFingers()
186 | }
187 |
188 | function calculateIdealFingers (peerId) {
189 | var fingers = config.explorer.fingers
190 | var k = 1
191 | while (k <= fingers.length) {
192 | var ideal = (peerId.toDec() + Math.pow(2, fingers[k - 1])) %
193 | Math.pow(2, 48)
194 | peers[peerId.toHex()].fingerTable[k] = {
195 | ideal: new Id(ideal).toHex(),
196 | current: undefined
197 | }
198 | k++
199 | }
200 | }
201 |
202 | function updateFingers () {
203 | if (Object.keys(peers).length < 2) {
204 | return
205 | }
206 |
207 | var sortedPeersId = Object.keys(peers).sort(function (a, b) {
208 | var aId = new Id(a)
209 | var bId = new Id(b)
210 | if (aId.toDec() > bId.toDec()) {
211 | return 1
212 | }
213 | if (aId.toDec() < bId.toDec()) {
214 | return -1
215 | }
216 | if (aId.toDec() === bId.toDec()) {
217 | console.log('error - There should never two identical ids')
218 | process.exit(1)
219 | }
220 | })
221 |
222 | sortedPeersId.forEach(function (peerId) {
223 | // predecessor
224 | var predecessorId = predecessorTo(peerId, sortedPeersId)
225 |
226 | if (peers[peerId].predecessorId !== predecessorId) {
227 | sockets[peers[peerId].socketId].emit('c-predecessor', {
228 | predecessorId: predecessorId
229 | })
230 |
231 | peers[peerId].predecessorId = predecessorId
232 | }
233 |
234 | // sucessors
235 |
236 | Object.keys(peers[peerId].fingerTable).some(function (rowIndex) {
237 | var fingerId = sucessorTo(peers[peerId]
238 | .fingerTable[rowIndex]
239 | .ideal, sortedPeersId)
240 |
241 | if (peers[peerId].fingerTable[rowIndex].current !==
242 | fingerId) {
243 | peers[peerId].fingerTable[rowIndex].current = fingerId
244 |
245 | sockets[peers[peerId].socketId].emit('c-finger-update', {
246 | rowIndex: rowIndex,
247 | fingerId: fingerId
248 | })
249 | }
250 |
251 | if (Object.keys(peers).length < config.explorer['min-peers']) {
252 | return true // stops the loop, calculates only
253 | // for the first position (aka sucessor of the node
254 | }
255 | })
256 | })
257 |
258 | function sucessorTo (pretendedId, sortedIdList) {
259 | pretendedId = new Id(pretendedId).toDec()
260 | sortedIdList = sortedIdList.map(function (inHex) {
261 | return new Id(inHex).toDec()
262 | })
263 |
264 | var sucessorId
265 | sortedIdList.some(function (value, index) {
266 | if (pretendedId === value) {
267 | sucessorId = value
268 | return true
269 | }
270 |
271 | if (pretendedId < value) {
272 | sucessorId = value
273 | return true
274 | }
275 |
276 | if (index + 1 === sortedIdList.length) {
277 | sucessorId = sortedIdList[0]
278 | return true
279 | }
280 | })
281 |
282 | return new Id(sucessorId).toHex()
283 | }
284 |
285 | function predecessorTo (peerId, sortedIdList) {
286 | var index = sortedIdList.indexOf(peerId)
287 |
288 | var predecessorId
289 |
290 | if (index === 0) {
291 | predecessorId = sortedIdList[sortedIdList.length - 1]
292 | } else {
293 | predecessorId = sortedIdList[index - 1]
294 | }
295 |
296 | return new Id(predecessorId).toHex()
297 | }
298 | }
299 |
300 | function peerRemove () {
301 | Object.keys(peers).map(function (peerId) {
302 | if (peers[peerId].socketId === socket.id) {
303 | delete peers[peerId]
304 | delete sockets[socket.id]
305 | console.log('peer with Id: %s has disconnected', peerId)
306 | }
307 | })
308 | }
309 |
310 | // signalling mediation between two peers
311 |
312 | function sendOffer (data) {
313 | sockets[peers[data.offer.dstId].socketId]
314 | .emit('c-accept-offer', data)
315 | }
316 |
317 | function offerAccepted (data) {
318 | sockets[peers[data.offer.srcId].socketId]
319 | .emit('c-offer-accepted', data)
320 | }
321 | }*/
322 |
--------------------------------------------------------------------------------