├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app.json ├── bin └── server ├── config.example.json ├── package.json ├── public └── index.html ├── src ├── browser.js ├── client.js ├── index.js └── server.js └── test ├── index.js ├── participant └── root /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | package-lock.json 3 | node_modules/** 4 | public/browser.js 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | - "7" 6 | - "6" 7 | - "5" 8 | - "4" 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - g++-4.8 16 | env: 17 | global: 18 | - CXX=g++-4.8 19 | 20 | branches: 21 | only: 22 | - master 23 | notifications: 24 | email: 25 | recipients: 26 | - github@ericklavoie.com 27 | on_success: change 28 | on_failure: always 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Erick Lavoie 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This library simplifies the bootstrapping of WebRTC overlays made with 2 | [Simple Peers](https://github.com/feross/simple-peer) by passing all connection 3 | requests to the same root peer, which may answer the request itself or pass the request 4 | to another peer. 5 | 6 | The handshake between the requester and the answerer are performed over 7 | WebSockets connected to the bootstrap server. After the handshake has been 8 | performed, the websocket connections are closed to conserve resources. 9 | 10 | 11 | # Client 12 | 13 | ## Usage 14 | 15 | // On the root process 16 | 17 | var bootstrap = require('webrtc-bootstrap')('bootstrap-server hostname or ip-address:port') 18 | var newcomers = {} 19 | 20 | // Register to obtain requests 21 | bootstrap.root('secret', function (req) { 22 | console.log('root received: ' + JSON.stringify(req)) 23 | 24 | // Remember previously created peers. 25 | // This way we can route multiple WebRTC handshake signals generated 26 | // by the ICE Trickle protocol to the same peer 27 | if (!newcomers[req.origin]) { 28 | console.log('Creating connection to signaling peer') 29 | newcomers[req.origin] = bootstrap.connect(req) 30 | newcomers[req.origin].on('data', function (data) { 31 | console.log(data) 32 | newcomers[req.origin].send('pong') 33 | }) 34 | } else { 35 | console.log('Passing the signal data') 36 | newcomers[req.origin].signal(req.signal) 37 | } 38 | }) 39 | 40 | // From a different process 41 | 42 | var bootstrap = ... 43 | var p = bootstrap.connect() 44 | p.on('connect', function () { p.send('ping') }) 45 | p.on('data', function (data) { 46 | console.log(data) 47 | }) 48 | 49 | ## API 50 | 51 | ### var bootstrap = new BootstrapClient(host, opts) 52 | 53 | Creates a new bootstrap client that will connect to 'host'. Opts may be one of the followings: 54 | ```` 55 | { 56 | secure: false // if true uses 'wss://' otherwise 'ws://' 57 | } 58 | ```` 59 | 60 | ### bootstrap.root(secret, onRequest(req), cb) 61 | 62 | *secret* is an alphanumeric string that has been set up during the server 63 | configuration (see 64 | [webrtc-bootstrap-server](https://github.com/elavoie/webrtc-bootstrap-server)). 65 | It becomes a route for the root WebSocket connection. It ensures only the the 66 | authorized root will receive requests. 67 | 68 | *onRequest(req)* is a callback that will be called with a request object, 69 | itself with the following properties: 70 | - `req.origin`: the identifier of the originator of the request 71 | (automatically created by the bootstrap server) 72 | - `req.signal`: the SimplePeer signal to establish the WebRTC connection. 73 | Because of the ICE trickle protocol for signaling, the same peer may 74 | trigger multiple calls to *onRequest* (unless `peerOpts.tricke: false`). 75 | All following requests should be routed to the same peer with `peer.signal(req.signal)`. 76 | 77 | *cb(err)* is called after either the connection to the server succeeded or failed. 78 | 79 | ### peer = bootstrap.connect([req, opts]) 80 | 81 | *req* is an optional request object (see `bootstrap.root`). If it is `undefined` or 82 | `falsy`, `initiator: true` will be set on *peerOpts* to initiate the signaling 83 | protocol. All the requests will go to the root. Otherwise, if a valid *req* is 84 | used, then a WebSocket connection will be established with the originator 85 | (`req.origin`) through the bootstrap server to answer the signaling offer and 86 | finish the handshake. 87 | 88 | *opts* are further options: 89 | 90 | `opts.peerOpts` are options to be passed to the [SimplePeer](https://github.com/feross/simple-peer) constructor. Defaults to `{}`. 91 | 92 | `opts.cb(err, peer)` is an optional callback to handle bootstrapping errors. 93 | 94 | Returns *peer*, a [SimplePeer](https://github.com/feross/simple-peer) instance. 95 | 96 | After a connect call if the connection succeeds, *peer* will emit the usual 'connect' event. 97 | 98 | # Server 99 | 100 | The server can be run locally for tests or deployed on any public server 101 | (server with a public IP address) that supports WebSockets. 102 | 103 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 104 | 105 | ## Usage 106 | 107 | Command-line: 108 | 109 | ```` 110 | # Using the configuration file 111 | node bin/server path_to_config.json 112 | 113 | # Or using environment variables 114 | SECRET=12345 node bin/server 115 | ```` 116 | 117 | Library: 118 | 119 | ```` 120 | var Server = require('webrtc-bootstrap').Server 121 | var s = new Server('secret') 122 | ```` 123 | 124 | ## Secret configuration 125 | 126 | Please clone this repository, copy config.example.json to config.json, and 127 | change the secret in the config.json file to ensure only your root node can 128 | connect as root to the bootstrap server. 129 | 130 | ## API 131 | 132 | ### Server(secret, opts) 133 | 134 | `secret` is an alphanumeric string that is used by the client to connect as root. 135 | 136 | `opts` is an optional object with the default values: 137 | 138 | { 139 | public: null, 140 | timeout: 30 * 1000 // ms, 141 | httpServer: null, 142 | port: 5000, 143 | seed: null 144 | } 145 | 146 | `opts.public` is the path to the public directory for serving static content. 147 | 148 | `opts.timeout` is the maximum allowed time for a candidate to successfully join the network. 149 | 150 | `opts.httpServer` is an existing http server. 151 | 152 | `opts.port` is the port used by the http server if none has been provided. 153 | 154 | `opts.seed` is a number to use as a seed for the pseudo-random generation of channel ids. If null, the crypto.randomBytes method is used instead. 155 | 156 | ### Server.upgrade(path, handler) 157 | 158 | `path` is a url ````String```` starting with a '/' 159 | 160 | `handler` is a ````Function````, ````function handler (ws, req) { ... }````, where ````ws```` is the websocket connection that resulted from the upgrade and ````req```` is the original http request. 161 | 162 | # Projects 163 | 164 | This library is used by the the following 165 | [library](https://github.com/elavoie/webrtc-tree-overlay) to organize peers in 166 | a tree. 167 | 168 | Submit a pull-request to add your own! 169 | 170 | MIT. Copyright (c) [Erick Lavoie](http://ericklavoie.com). 171 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bootstrap Server", 3 | "description": "A bootstrap server for creating WebRTC tree overlays", 4 | "repository": "https://github.com/elavoie/webrtc-tree-overlay-bootstrap" 5 | } 6 | -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | 4 | var configFile = process.argv[2] 5 | var secret = configFile 6 | ? JSON.parse(fs.readFileSync(configFile).toString())['secret'] 7 | : process.env.SECRET 8 | 9 | var port = process.env.PORT ? process.env.PORT : 5000 10 | 11 | new require('../').Server(secret, { 12 | public: __dirname + '/../public', 13 | port: port 14 | }) 15 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "EMpGMQADK1oR5n8xffrxYHeCO0RmW42Z46ZcGwTVO5WBzkXBHC6btGt1WBG4" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-bootstrap", 3 | "version": "4.4.1", 4 | "description": "Bootstrap client and server to bootstrap WebRTC connections made with simple-peer", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "DEBUG=webrtc* node bin/server", 8 | "postinstall": "browserify src/browser.js -r -s browser -o public/browser.js;", 9 | "test": "tape test/index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/elavoie/webrtc-bootstrap.git" 14 | }, 15 | "author": "Erick Lavoie", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/elavoie/webrtc-bootstrap/issues" 19 | }, 20 | "homepage": "https://github.com/elavoie/webrtc-bootstrap#readme", 21 | "dependencies": { 22 | "browserify": "^16.2.3", 23 | "debug": "^2.6.9", 24 | "express": "^4.16.3", 25 | "simple-peer": "^6.4.4", 26 | "simple-websocket": "^7.2.0", 27 | "ws": "^6.2.1" 28 | }, 29 | "devDependencies": { 30 | "electron-webrtc": "^0.3.0", 31 | "tape": "^4.10.1", 32 | "wrtc": "^0.3.7" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | WebRTC Bootstrap 22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | var Client = require('..') 2 | var debug = require('debug') 3 | var log = debug('webrtc-bootstrap') 4 | 5 | module.exports = function browser (host, origin, secure) { 6 | var bootstrap = new Client(host, { secure: secure }) 7 | var p = bootstrap.connect(null, { 8 | peerOpts: { 9 | config: { 10 | iceServers: [{ 11 | urls: 'stun:stun.l.google.com:19302' 12 | }] 13 | }, 14 | wrtc: undefined 15 | } 16 | }) 17 | p.on('connect', function () { 18 | console.log('connected to root, sending: ' + origin) 19 | p.send(origin) 20 | }) 21 | p.on('data', function (data) { 22 | console.log('received data from root: ' + String(data)) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | var Socket = require('simple-websocket') 2 | var SimplePeer = require('simple-peer') 3 | var debug = require('debug') 4 | var log = debug('webrtc-bootstrap') 5 | 6 | var HEARTBEAT_INTERVAL = 10000 // ms 7 | 8 | function Client (host, opts) { 9 | if (!(this instanceof Client)) { 10 | throw new Error('Function must be called as a constructor') 11 | } 12 | if (typeof opts === 'undefined') { 13 | opts = {} 14 | } 15 | opts.secure = opts.secure || false 16 | 17 | this.secure = opts.secure 18 | this.host = host 19 | this.rootSocket = null 20 | this.sockets = {} 21 | } 22 | 23 | Client.prototype.root = function (secret, onRequest, cb) { 24 | log('root(' + secret + ')') 25 | var protocol = this.secure ? 'wss://' : 'ws://' 26 | var url = protocol + this.host + '/' + secret + '/webrtc-bootstrap-root' 27 | 28 | var interval = null 29 | 30 | this.rootSocket = new Socket(url) 31 | .on('connect', function () { 32 | log('root(' + secret + ') connected') 33 | if (cb) return cb() 34 | }) 35 | .on('data', function (data) { 36 | var msg = JSON.parse(data) 37 | if (msg === 'heartbeat') { 38 | log('root(' + secret + ') heartbeat') 39 | } else { 40 | log('root(' + secret + ') offer received') 41 | onRequest(msg) 42 | } 43 | }) 44 | .on('close', function () { 45 | log('root(' + secret + ') closing') 46 | clearInterval(interval) 47 | }) 48 | .on('error', function (err) { 49 | log('root(' + secret + ') error') 50 | clearInterval(interval) 51 | if (cb) { return cb(err) } 52 | }) 53 | .on('open', function () { 54 | interval = setInterval(function () { 55 | this.rootSocket.send('heartbeat') 56 | }, HEARTBEAT_INTERVAL) 57 | }) 58 | } 59 | 60 | var connectId = 0 61 | Client.prototype.connect = function (req, opts) { 62 | req = req || {} 63 | opts = opts || {} 64 | opts.timeout = opts.timeout || 30 * 1000 65 | opts.cb = opts.cb || function (err, peer) { 66 | if (err) peer.emit('error', new Error('Bootstrap Timeout')) 67 | } 68 | var peerOpts = opts.peerOpts || {} 69 | 70 | var self = this 71 | var socketId = connectId++ 72 | var log = debug('webrtc-bootstrap:connect ' + socketId) 73 | log('connect(' + JSON.stringify(req) + ',' + peerOpts.toString() + ')') 74 | log('peerOpts:') 75 | log(peerOpts) 76 | 77 | var messageNb = 0 78 | 79 | if (!req.origin) { 80 | var peerOptsCopy = {} 81 | for (var p in peerOpts) { 82 | peerOptsCopy[p] = peerOpts[p] 83 | } 84 | peerOptsCopy.initiator = true 85 | } else { 86 | peerOptsCopy = peerOpts 87 | } 88 | 89 | log('creating SimplePeer() with opts:') 90 | log(JSON.stringify(peerOptsCopy)) 91 | var peer = new SimplePeer(peerOptsCopy) 92 | 93 | var signalQueue = [] 94 | 95 | peer.on('signal', function (data) { 96 | var message = JSON.stringify({ 97 | origin: null, // set by server if null 98 | destination: req.origin || null, // if null, then will be sent to root 99 | signal: data, 100 | rank: messageNb++ 101 | }) 102 | if (!socketConnected || signalQueue.length > 0) { 103 | log('connect() queuing message with signal: ' + JSON.stringify(message)) 104 | signalQueue.push(message) 105 | } else { 106 | log('connect() sending message with signal: ' + JSON.stringify(message)) 107 | socket.send(message) 108 | } 109 | }) 110 | peer.once('connect', function () { 111 | log('bootstrap succeeded, closing signaling websocket connection') 112 | clearTimeout(connectionTimeout) 113 | socket.destroy() 114 | delete self.sockets[socketId] 115 | opts.cb(null, peer) 116 | }) 117 | 118 | if (req.signal) { 119 | log('peer.signal(' + JSON.stringify(req.signal) + ')') 120 | peer.signal(req.signal) 121 | } 122 | 123 | var connectionTimeout = setTimeout(function () { 124 | log('bootstrap timeout, closing signaling websocket connection') 125 | socket.destroy() 126 | delete self.sockets[socketId] 127 | opts.cb(new Error('Bootstrap timeout'), peer) 128 | }, opts.timeout) 129 | 130 | var socketConnected = false 131 | var protocol = this.secure ? 'wss://' : 'ws://' 132 | var socket = new Socket(protocol + this.host + '/join') 133 | .on('connect', function () { 134 | socketConnected = true 135 | log('signaling websocket connected') 136 | 137 | if (signalQueue.length > 0) { 138 | var queue = signalQueue.slice(0) 139 | signalQueue = [] 140 | for (var i = 0; i < queue.length; ++i) { 141 | log('sending queued signal ' + (i+1) + '/' + queue.length + ': ' + JSON.stringify(queue[i])) 142 | socket.send(queue[i]) 143 | } 144 | } 145 | }) 146 | .on('data', function (data) { 147 | log('connect() signal received:') 148 | log(data.toString()) 149 | var message = JSON.parse(data.toString()) 150 | // Optimization to send the subsequent ICE 151 | // messages directly rather than through the tree 152 | // overlay: our next signals will go directly 153 | // to the destination through the bootstrap server 154 | req.origin = req.origin || message.origin 155 | 156 | // Extract our own id from the correspondance 157 | socket.id = message.destination 158 | 159 | log('peer.signal(' + JSON.stringify(message.signal) + ')') 160 | peer.signal(message.signal) 161 | }) 162 | .on('error', function (err) { 163 | log('error()') 164 | log(err) 165 | opts.cb(err) 166 | }) 167 | 168 | this.sockets[socketId] = socket 169 | 170 | return peer 171 | } 172 | 173 | Client.prototype.close = function () { 174 | log('closing') 175 | if (this.rootSocket) { 176 | log('closing root socket') 177 | this.rootSocket.destroy() 178 | } 179 | 180 | log('closing remaining sockets') 181 | for (var id in this.sockets) { 182 | log('closing socket[' + id + ']') 183 | this.sockets[id].destroy() 184 | } 185 | } 186 | 187 | module.exports = Client 188 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var client = require('./client') 2 | 3 | if (typeof window === 'undefined') { 4 | client.Server = require('./server') 5 | } 6 | 7 | module.exports = client 8 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var ws = require('ws') 3 | var debug = require('debug') 4 | var log = debug('webrtc-bootstrap:server') 5 | var express = require('express') 6 | var crypto = require('crypto') 7 | var url = require('url') 8 | 9 | var HEARTBEAT_INTERVAL = 15000 // ms 10 | 11 | var random = { 12 | seed: 49734321, 13 | next: function () { 14 | // Robert Jenkins' 32 bit integer hash function. 15 | random.seed = ((random.seed + 0x7ed55d16) + (random.seed << 12)) & 0xffffffff 16 | random.seed = ((random.seed ^ 0xc761c23c) ^ (random.seed >>> 19)) & 0xffffffff 17 | random.seed = ((random.seed + 0x165667b1) + (random.seed << 5)) & 0xffffffff 18 | random.seed = ((random.seed + 0xd3a2646c) ^ (random.seed << 9)) & 0xffffffff 19 | random.seed = ((random.seed + 0xfd7046c5) + (random.seed << 3)) & 0xffffffff 20 | random.seed = ((random.seed ^ 0xb55a4f09) ^ (random.seed >>> 16)) & 0xffffffff 21 | return random.seed 22 | } 23 | } 24 | 25 | function Server (secret, opts) { 26 | secret = secret || process.env.SECRET 27 | opts = (typeof opts) === 'object' ? opts : {} 28 | opts.public = opts.public || null 29 | opts.timeout = opts.timeout || 30 * 1000 30 | 31 | var root = null 32 | var prospects = this.prospects = {} 33 | this._upgraders = [] 34 | 35 | if (!secret) { 36 | throw new Error('Invalid secret: ' + secret) 37 | } 38 | 39 | if (opts.seed) { 40 | random.seed = Number(opts.seed) 41 | } 42 | 43 | if (!opts.httpServer) { 44 | var app = express() 45 | if (opts.public) { 46 | log('serving files over http at ' + opts.public) 47 | app.use(express.static(opts.public)) 48 | } 49 | var port = opts.port || process.env.PORT || 5000 50 | this.httpServer = http.createServer(app) 51 | this.httpServer.listen(port) 52 | log('http server listening on %d', port) 53 | } else { 54 | this.httpServer = opts.httpServer 55 | } 56 | 57 | function closeProspect (id) { 58 | if (prospects[id]) prospects[id].close() 59 | } 60 | 61 | function messageHandler (id) { 62 | return function incomingMessage (message) { 63 | message = JSON.parse(message) 64 | message.origin = id 65 | log('INCOMING MESSAGE') 66 | log(message) 67 | if (message.destination) { 68 | log('Destination defined') 69 | if (prospects.hasOwnProperty(message.destination)) { 70 | log('Known destination ' + message.destination) 71 | prospects[message.destination].send(JSON.stringify(message), function (err) { 72 | if (err) closeProspect(message.destination) 73 | }) 74 | } else { 75 | log('Unknown destination ' + message.destination + ', ignoring message') 76 | } 77 | } else { 78 | if (root && root.readyState === ws.OPEN) { 79 | root.send(JSON.stringify(message)) 80 | } else { 81 | if (!root) { 82 | log('WARNING: ignoring message because no root is connected') 83 | } else if (root.readyState !== ws.OPEN) { 84 | log('WARNING: ignoring message because root WebSocket channel is not open') 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | log('Opening websocket connection for root on ' + secret) 92 | 93 | var server = this.server = new ws.Server({ noServer: true }) 94 | .on('connection', function (ws) { 95 | function remove () { 96 | log('node ' + id + ' disconnected') 97 | delete prospects[id] 98 | clearTimeout(timeout) 99 | } 100 | var id = null 101 | 102 | if (opts.seed) { 103 | id = crypto.createHash('md5').update(random.next().toString()).digest().hexSlice(0, 16) 104 | } else { 105 | id = crypto.randomBytes(16).hexSlice() 106 | } 107 | ws.id = id 108 | log('node connected with id ' + id) 109 | ws.on('message', messageHandler(id)) 110 | ws.on('close', remove) 111 | prospects[id] = ws 112 | var timeout = setTimeout(function () { 113 | closeProspect(id) 114 | }, opts.timeout) 115 | }) 116 | 117 | var that = this 118 | this.httpServer.on('upgrade', function upgrade(request, socket, head) { 119 | const pathname = url.parse(request.url).pathname; 120 | log('httpServer url: ' + request.url + ', pathname: ' + pathname) 121 | for (var i = 0; i < that._upgraders.length; ++i) { 122 | var upgrader = that._upgraders[i] 123 | if (pathname === upgrader.path) { 124 | log("upgrading connection to '" + upgrader.path + "'") 125 | upgrader.wsServer.handleUpgrade(request, socket, head, function done(ws) { 126 | log("upgraded connection to '" + upgrader.path + "'") 127 | upgrader.wsServer.emit('connection', ws, request); 128 | }); 129 | return 130 | } 131 | } 132 | 133 | // No upgrader found 134 | socket.destroy(); 135 | }) 136 | 137 | this.upgrade('/' + secret + '/webrtc-bootstrap-root', function (ws) { 138 | log('root connected') 139 | var interval = null 140 | ws.on('message', function (data) { 141 | if (JSON.parse(data) === 'heartbeat') { 142 | log('root heartbeat') 143 | } else { 144 | log('WARNING: unexpected message from root: ' + data) 145 | } 146 | }) 147 | ws.on('close', function () { 148 | log('root closed') 149 | clearInterval(interval) 150 | }) 151 | ws.on('error', function (err) { 152 | log('ERROR: root failed with error: ' + err) 153 | clearInterval(interval) 154 | }) 155 | root = ws 156 | interval = setInterval(function () { 157 | ws.send(JSON.stringify('heartbeat')) 158 | }, HEARTBEAT_INTERVAL) 159 | }) 160 | 161 | this.upgrade('/join', function (ws) { 162 | function remove () { 163 | log('node ' + id + ' disconnected') 164 | delete prospects[id] 165 | clearTimeout(timeout) 166 | } 167 | var id = null 168 | 169 | if (opts.seed) { 170 | id = crypto.createHash('md5').update(random.next().toString()).digest().hexSlice(0, 16) 171 | } else { 172 | id = crypto.randomBytes(16).hexSlice() 173 | } 174 | ws.id = id 175 | log('node connected with id ' + id) 176 | ws.on('message', messageHandler(id)) 177 | ws.on('close', remove) 178 | prospects[id] = ws 179 | var timeout = setTimeout(function () { 180 | closeProspect(id) 181 | }, opts.timeout) 182 | }) 183 | 184 | return this 185 | } 186 | 187 | // Handles websocket upgrades 188 | // 189 | // path: String (URL, ex: '/volunteer') 190 | // handler: Function (ws, request) {} 191 | Server.prototype.upgrade = function (path, handler) { 192 | var wsServer = new ws.Server({ noServer: true }) 193 | .on('connection', handler) 194 | this._upgraders.push({ path: path, wsServer: wsServer }) 195 | return this 196 | } 197 | 198 | Server.prototype.close = function () { 199 | log('closing ws servers') 200 | for (var i = 0; i < this._upgraders.length; ++i) { 201 | this._upgraders[i].wsServer.close() 202 | } 203 | this._upgraders = [] 204 | log('closing http server') 205 | this.httpServer.close() 206 | log('closing all prospects') 207 | for (var p in this.prospects) { 208 | log(this.prospects[p].close) 209 | this.prospects[p].close() 210 | } 211 | } 212 | 213 | module.exports = Server 214 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var wrtc = require('wrtc') 3 | var debug = require('debug') 4 | var log = debug('test') 5 | var Server = require('..').Server 6 | var Client = require('../') 7 | 8 | var secret = 'secret' 9 | var port = 5000 10 | 11 | tape('Basic startup shutdown tests', function (t) { 12 | var server = new Server(secret, { port: port }) 13 | t.ok(server) 14 | var bootstrap = new Client('localhost:' + port) 15 | t.ok(bootstrap) 16 | 17 | bootstrap.close() 18 | server.close() 19 | t.end() 20 | }) 21 | 22 | tape('Root connection error to server', function (t) { 23 | var bootstrap = new Client('localhost:' + port) 24 | t.ok(bootstrap) 25 | 26 | bootstrap.root(secret, function (req) { 27 | t.fail('Connection to server should fail') 28 | }, function (err) { 29 | t.ok(err) 30 | bootstrap.close() 31 | t.end() 32 | }) 33 | }) 34 | 35 | tape('Root request on connection', function (t) { 36 | var server = new Server(secret, { port: port, timeout: 5 * 1000 }) 37 | t.ok(server) 38 | var bootstrap = new Client('localhost:' + port) 39 | t.ok(bootstrap) 40 | 41 | log('registering root') 42 | var requestNb = 0 43 | bootstrap.root(secret, function (req) { 44 | log('received request (' + requestNb++ + '): ' + JSON.stringify(req)) 45 | t.ok(req) 46 | t.ok(req.origin) 47 | t.ok(req.signal) 48 | }) 49 | 50 | var p = bootstrap.connect(null, { 51 | peerOpts: { wrtc: wrtc }, 52 | timeout: 3 * 1000, 53 | cb: function (err, peer) { 54 | t.equal(err.message, 'Bootstrap timeout') 55 | bootstrap.close() 56 | server.close() 57 | p.destroy() 58 | t.end() 59 | } 60 | }) 61 | }) 62 | 63 | tape('README example', function (t) { 64 | var server = new Server(secret) 65 | t.ok(server) 66 | 67 | var bootstrap = new Client('localhost:' + port) 68 | t.ok(bootstrap) 69 | var newcomers = {} 70 | 71 | bootstrap.root(secret, function (req) { 72 | log('root received: ' + JSON.stringify(req)) 73 | 74 | if (!newcomers[req.origin]) { 75 | log('Creating connection to signaling peer') 76 | newcomers[req.origin] = bootstrap.connect(req, { 77 | peerOpts: { wrtc: wrtc } 78 | }) 79 | newcomers[req.origin].on('data', function (data) { 80 | log(data) 81 | t.equal(data.toString(), 'ping') 82 | newcomers[req.origin].send('pong') 83 | }) 84 | } else { 85 | log('Passing the signal data') 86 | newcomers[req.origin].signal(req.signal) 87 | } 88 | }) 89 | 90 | // From a different process 91 | var p = bootstrap.connect(null, { 92 | peerOpts: { 93 | wrtc: wrtc 94 | } 95 | }) 96 | p.on('connect', function () { p.send('ping') }) 97 | p.on('data', function (data) { 98 | log(data) 99 | t.equal(data.toString(), 'pong') 100 | p.destroy() 101 | bootstrap.close() 102 | server.close() 103 | t.end() 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /test/participant: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv) 3 | var Client = require('..') 4 | var electronWebRTC = require('electron-webrtc') 5 | var debug = require('debug') 6 | var log = debug('webrtc-bootstrap') 7 | var host = argv.host ? argv.host : 'genet.herokuapp.com' 8 | 9 | if (argv['wrtc']) { 10 | console.error('using wrtc') 11 | var wrtc = require('wrtc') 12 | } else { // including --electron-webrtc 13 | console.error('using electron-webrtc') 14 | var wrtc = electronWebRTC({ headless: false }) 15 | } 16 | var bootstrap = new Client(host) 17 | var p = bootstrap.connect(null, { 18 | peerOpts: { 19 | wrtc: wrtc 20 | } 21 | }) 22 | p.on('connect', function () { 23 | var ping = 'ping' 24 | console.log('connected to root, sending: ' + ping) 25 | p.send(ping) } 26 | ) 27 | p.on('data', function (data) { 28 | console.log('received data from root: ' + String(data)) 29 | }) 30 | -------------------------------------------------------------------------------- /test/root: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv) 3 | var Client = require('..') 4 | var electronWebRTC = require('electron-webrtc') 5 | var debug = require('debug') 6 | var log = debug('webrtc-bootstrap:root') 7 | var secret = argv.secret ? argv.secret : 'SECRET' 8 | var host = argv.host ? argv.host : 'genet.herokuapp.com' 9 | 10 | if (argv['wrtc']) { 11 | console.error('using wrtc') 12 | var wrtc = require('wrtc') 13 | } else { // including --electron-webrtc 14 | console.error('using electron-webrtc') 15 | var wrtc = electronWebRTC({ headless: false }) 16 | } 17 | 18 | var bootstrap = new Client(host) 19 | var newcomers = {} 20 | 21 | bootstrap.root(secret, function (req) { 22 | // Remember previously created peers. 23 | // This way we can route multiple WebRTC handshake signals generated 24 | // by the ICE Trickle protocol to the same peer 25 | if (!newcomers[req.origin]) { 26 | console.log('Creating connection to signaling peer') 27 | newcomers[req.origin] = bootstrap.connect(req, { 28 | peerOpts: { 29 | config: { 30 | iceServers: [{ 31 | urls: 'stun:stun.l.google.com:19302' 32 | }] 33 | }, 34 | wrtc: wrtc 35 | } 36 | }) 37 | newcomers[req.origin].on('data', function (data) { 38 | console.log('received data from ' + req.origin.slice(0,5) + ': ' + String(data)) 39 | newcomers[req.origin].send('pong') 40 | }) 41 | } else { 42 | log('Candidate ' + req.origin.slice(0,5) + ' peer.signal(' + JSON.stringify(req.signal) + ')') 43 | newcomers[req.origin].signal(req.signal) 44 | } 45 | }) 46 | --------------------------------------------------------------------------------