├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── client.js ├── router.js ├── wgram.js ├── wrtc.js └── ws.js ├── package.json ├── server.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | *.ignore 4 | npm-debug.log.* 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | *.ignore 4 | npm-debug.log.* 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | 7 | env: 8 | - DEBUG=peer-relay:* 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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 | # peer-relay 2 | 3 | [![Build Status](https://travis-ci.org/xuset/peer-relay.svg?branch=master)](https://travis-ci.org/xuset/peer-relay) 4 | [![npm version](https://badge.fury.io/js/peer-relay.svg)](https://badge.fury.io/js/peer-relay) 5 | 6 | peer-relay is a p2p message relay that works in nodejs and in the browser by supporting WebSockets and WebRTC as transports. Every peer-relay peer connects to a network of other peers. When a peer wants to send a message to another, it does not have to connect directly to the target because the message will be relayed through the network of peers. This is benificial in a few cases like sending a few messages to a lot peers. Traditionally, this would require connecting directly to every peer you needed to send a message to, but in the web browser this can be very costly because of WebRTC limitations. Instead, it may be better to relay those messages through peers you are already connected to, and this is the problem that peer-relay solves. 7 | 8 | Every peer generates it's own unique and random id that is used to identify itself within the network. To send a message to a peer, all you need to know is the target peer's id. Peer-relay will then take care of the rest by relaying the message through intermediary peers until it reaches it's target. 9 | 10 | ## How it works 11 | 12 | Before a peer can do anything, it first must bootstrap itself onto the network by knowing the WebSocket urls of at least one peer already connected to the network. Whenever a new connection is formed, both peers exchange info about the peers they are already connected to so they can connect to more peers if they need to. When a peer does want to connect to another, which transport used depends on what both peers support. Websockets are straightforward; connect to the WebSocket url. The WebRTC transport is a little different in that the signaling information must be relayed through intermediary peers before the connection can be formed. 13 | 14 | Every peer maintains it's own [k-bucket](https://github.com/tristanls/k-bucket) routing table of peers that it is directly connected to. The message routing used by peer-relay is largely inspired by [kademlia](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) which is why k-bucket is used. When a peer wants to send message to another, it looks at it's routing table for the peer whose id is closest to the target's id, and sends the message to the closest peer. When the receiving peer receives the message, it repeats the same process, and eventually the message will reach it's target. The amount of hops it takes for a message to reach it's target is log(n) where n is the number of peers in the network. 15 | 16 | ## API 17 | 18 | ### `peer = new PeerRelay([opts])` 19 | 20 | Creates a new peer that becomes apart of the relay network 21 | 22 | The following fields can be specified within `opts`: 23 | * port - The port for the web socket server to listen on. If not defined, then a websocket server is not started 24 | * bootstrap - an array of web socket urls to peers already connected to the network 25 | * wrtc - custom nodejs webrtc implementation. Check out [electron-webrtc](https://github.com/mappum/electron-webrtc) or [wrtc](https://github.com/js-platform/node-webrtc) 26 | 27 | `port` can only be specified if the peer is running nodejs since start a WebSocket server is not possible in a browser. Every peer should specify at least on bootstrap peer (unless that peer is the first/only peer in the network) 28 | 29 | ### `peer.id` 30 | 31 | The peer's id. `id` is 160 bit Buffer. This id is used to identify the peer within the network. 32 | 33 | ### `peer.connect(id)` 34 | 35 | Forms a direct connection with the given peer. `id` is the id of the peer to connect to and must be a Buffer. 36 | 37 | Behind the scenes: a message will be relayed to that peer asking it what transports it supports (WebSocket and/or WebRTC). Then the connection will be formed based on this info; if webrtc is chosen then additional signaling info will be relayed before the connection is formed. 38 | 39 | ### `peer.disconnect(id)` 40 | 41 | Disconnect the a currently connected peer with `id`. 42 | 43 | ### `peer.send(id, data)` 44 | 45 | Send `data` to the peer with the `id`. `data` can be anything that is JSON serializable. The peer does not have to be directly connected to because it will be relayed through other peers. Message delivery or order is not guaranteed. 46 | 47 | ### `peer.destroy([cb])` 48 | 49 | Destroy the peer and free it's resources. An optional callback can be specified and will be called when all the resources are freed. 50 | 51 | ## Events 52 | 53 | ### `peer.on('message', function (data, from) {})` 54 | 55 | Fired when a message addressed to the peer was received. `from` is the Buffer id of the peer that sent the message. 56 | 57 | ### `peer.on('peer', function (id) {})` 58 | 59 | Fired when a peer has been directly connected to 60 | 61 | ## PeerRelay.Socket 62 | 63 | ### `var socket = new PeerRelay.Socket([opts])` 64 | 65 | Creates a new [dgram](https://nodejs.org/api/dgram.html) like socket that uses peer-relay to send messages between peers instead of UDP. This allows for peer-relay to be used by programs that expect the dgram socket interface. This method accepts the same arguments as the PeerRelay constructor. The returned object tries to match the interface provided by dgram's [Socket](https://nodejs.org/api/dgram.html#dgram_class_dgram_socket). 66 | 67 | There are a few differences to this socket than dgram's. Mainly, ip addresses are replaced by peer IDs. 68 | 69 | ### `socket.send(buffer, offset, length, port, peerRelayID, [cb])` 70 | 71 | This relays the given buffer to the peer with `peerRelayID` by calling `peer.send(...)`. The signature for this method is similar to dgram's socket.send except the peer's id is used instead of the ip address. Port is also ignored, but is still required for compatibility reasons. 72 | 73 | ### `socket.address()` 74 | 75 | Returns the peer's id instead of ip address: 76 | ``` 77 | { 78 | address: local peer's id 79 | port: random number or whatever port socket.bind([port]) was given 80 | family: a string equal to 'peer-relay' 81 | } 82 | ``` 83 | 84 | ### `socket.close([cb])` 85 | 86 | Destroys the underlying PeerRelay instance and emits the socket's close event 87 | 88 | ### `socket.bind([port], [cb])` 89 | 90 | Doesn't do anything since peer-relay doesn't have the conecept of binding and ports, but this method remains for compatibilty with dgram's socket. 91 | 92 | ### `socket.peer` 93 | 94 | references the underlying PeerRelay instance. 95 | 96 | ### `socket.on('message', function (buffer, rinfo) {})` 97 | 98 | `buffer` is the received message and `rinfo` is the same structure defined by `socket.address()` exept the sender's id is in the address field. 99 | 100 | ### `socket.on('error', function (err) {})` 101 | 102 | If peer-relay experiences an error, it is bubbled up through this event. 103 | 104 | ### `socket.on('close', function () {})` 105 | 106 | Emitted when socket.close is called or when PeerRelay closes. 107 | 108 | ### `socket.on('listening', function () {})` 109 | 110 | Doesn't serve any purpose other than dgram socket compatility. This event is emitted after `socket.bind()` is called. 111 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/client') 2 | module.exports.Socket = require('./lib/wgram') 3 | module.exports.debug = require('debug') 4 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var KBucket = require('k-bucket') 2 | var crypto = require('crypto') 3 | var inherits = require('util').inherits 4 | var EventEmitter = require('events').EventEmitter 5 | var debug = require('debug')('peer-relay:client') 6 | var Router = require('./router') 7 | var WsConnector = require('./ws') 8 | var WrtcConnector = require('./wrtc') 9 | 10 | module.exports = Client 11 | 12 | inherits(Client, EventEmitter) 13 | function Client (opts) { 14 | if (!(this instanceof Client)) return new Client(opts) 15 | if (!opts) opts = {} 16 | 17 | var self = this 18 | 19 | self.id = crypto.randomBytes(20) 20 | self.pending = {} 21 | self.destroyed = false 22 | self.peers = new KBucket({ 23 | localNodeId: self.id, 24 | numberOfNodesPerKBucket: 20 25 | }) 26 | self.peers.on('removed', onRemoved) 27 | self.canidates = new KBucket({ // TODO expire canidates after period 28 | localNodeId: self.id, 29 | numberOfNodesPerKBucket: 20 30 | }) 31 | 32 | self.router = new Router(self.peers, self.id) 33 | self.router.on('message', onMessage) 34 | 35 | self.wsConnector = new WsConnector(self.id, opts.port) 36 | self.wsConnector.on('connection', onConnection) 37 | 38 | self.wrtcConnector = new WrtcConnector(self.id, self.router, opts.wrtc) 39 | self.wrtcConnector.on('connection', onConnection) 40 | 41 | self._debug('Client(%s)', JSON.stringify(opts, ['port', 'bootstrap'])) 42 | 43 | for (var uri of (opts.bootstrap || [])) { 44 | self.wsConnector.connect(uri) 45 | } 46 | 47 | function onConnection (channel) { 48 | self._onConnection(channel) 49 | } 50 | 51 | function onMessage (msg, from) { 52 | self._onMessage(msg, from) 53 | } 54 | 55 | function onRemoved (channel) { 56 | channel.destroy() 57 | } 58 | } 59 | 60 | Client.prototype._onConnection = function (channel) { 61 | var self = this 62 | if (self.destroyed) throw new Error('Cannot setup channel when client is destroyed') 63 | 64 | channel.on('close', onClose) 65 | channel.on('error', onError) 66 | 67 | delete self.pending[channel.id] 68 | self.canidates.add({ id: channel.id }) 69 | 70 | if (self.peers.get(channel.id)) { 71 | if (channel.id.compare(self.id) >= 0) channel.destroy() 72 | return 73 | } 74 | 75 | self.peers.add(channel) 76 | 77 | self.router.send(channel.id, { 78 | type: 'findPeers', 79 | data: self.id.toString('hex') 80 | }) 81 | 82 | self.emit('peer', channel.id) 83 | 84 | function onClose () { 85 | delete self.pending[channel.id] 86 | self.canidates.remove(channel.id) 87 | self.peers.remove(channel.id) 88 | } 89 | 90 | function onError (err) { 91 | self._debug('Error', err) 92 | } 93 | 94 | return channel 95 | } 96 | 97 | Client.prototype.connect = function (id) { 98 | var self = this 99 | if (self.destroyed) return 100 | if (id in self.pending) return 101 | if (self.peers.get(id)) return 102 | if (id.equals(self.id)) return 103 | 104 | self.pending[id] = true 105 | 106 | self._debug('Connecting to id=%s', id.toString('hex', 0, 2)) 107 | 108 | self.router.send(id, { 109 | type: 'handshake-offer' 110 | }) 111 | } 112 | 113 | Client.prototype.disconnect = function (id) { 114 | var self = this 115 | if (self.destroyed) return 116 | if (!self.peers.get(id)) return 117 | 118 | self.peers.get(id).destroy() 119 | } 120 | 121 | Client.prototype.send = function (id, data) { 122 | var self = this 123 | if (self.destroyed) return 124 | 125 | // self._debug('SEND', id.toString('hex', 0, 2), JSON.stringify(data)) 126 | self.router.send(id, { 127 | type: 'user', 128 | data: data 129 | }) 130 | } 131 | 132 | Client.prototype._onMessage = function (msg, from) { 133 | var self = this 134 | if (self.destroyed) return 135 | 136 | if (msg.type === 'user') { 137 | // self._debug('RECV', from.toString('hex', 0, 2), JSON.stringify(msg.data)) 138 | self.emit('message', msg.data, from) 139 | } else if (msg.type === 'findPeers') { 140 | self._onFindPeers(msg, from) 141 | } else if (msg.type === 'foundPeers') { 142 | self._onFoundPeers(msg, from) 143 | } else if (msg.type === 'handshake-offer') { 144 | self._onHandshakeOffer(msg, from) 145 | } else if (msg.type === 'handshake-answer') { 146 | self._onHandshakeAnswer(msg, from) 147 | } 148 | } 149 | 150 | Client.prototype._onFindPeers = function (msg, from) { 151 | var self = this 152 | var target = new Buffer(msg.data, 'hex') 153 | var closest = self.canidates.closest(target, 20) 154 | self.router.send(from, { 155 | type: 'foundPeers', 156 | data: closest.map((e) => e.id.toString('hex')) 157 | }) 158 | } 159 | 160 | Client.prototype._onFoundPeers = function (msg) { 161 | var self = this 162 | for (var canidate of msg.data) { 163 | self.canidates.add({ 164 | id: new Buffer(canidate, 'hex') 165 | }) 166 | } 167 | self._populate() 168 | } 169 | 170 | Client.prototype._onHandshakeOffer = function (msg, from) { 171 | var self = this 172 | if (self.peers.get(from)) return 173 | 174 | if (self.pending[from] == null || from.compare(self.id) < 0) { 175 | self.pending[from] = true 176 | self.router.send(from, { 177 | type: 'handshake-answer', 178 | data: { 179 | ws: self.wsConnector.url, 180 | wrtc: self.wrtcConnector.supported 181 | } 182 | }) 183 | } 184 | } 185 | 186 | Client.prototype._onHandshakeAnswer = function (msg, from) { 187 | var self = this 188 | if (self.peers.get(from)) return 189 | if (msg.data == null) return 190 | 191 | if (msg.data.wrtc && self.wrtcConnector.supported) self.wrtcConnector.connect(from) 192 | else if (msg.data.ws) self.wsConnector.connect(msg.data.ws) 193 | } 194 | 195 | Client.prototype._populate = function () { 196 | var self = this 197 | var optimal = 15 198 | var closest = self.canidates.closest(self.id, optimal) 199 | for (var i = 0; i < closest.length && self.peers.count() + Object.keys(self.pending).length < optimal; i++) { 200 | if (self.peers.get(closest[i].id)) continue 201 | self.connect(closest[i].id) 202 | } 203 | } 204 | 205 | Client.prototype._debug = function () { 206 | var self = this 207 | var prepend = '[' + self.id.toString('hex', 0, 2) + '] ' 208 | arguments[0] = prepend + arguments[0] 209 | debug.apply(null, arguments) 210 | } 211 | 212 | Client.prototype.destroy = function (cb) { 213 | var self = this 214 | if (self.destroyed) return 215 | self.destroyed = true 216 | 217 | self.wsConnector.destroy(cb) 218 | self.wrtcConnector.destroy() 219 | var peers = self.peers.toArray() 220 | for (var i = 0; i < peers.length; i++) { 221 | peers[i].destroy() 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | var inherits = require('util').inherits 3 | var debug = require('debug')('peer-relay:router') 4 | 5 | module.exports = Router 6 | 7 | inherits(Router, EventEmitter) 8 | function Router (channels, id) { 9 | var self = this 10 | self.id = id 11 | self.concurrency = 2 12 | self.maxHops = 20 13 | self._touched = {} 14 | self._channelListeners = {} 15 | self._paths = {} 16 | self._queue = [] 17 | self._channels = channels 18 | self._channels.on('added', onChannelAdded) 19 | self._channels.on('removed', onChannelRemoved) 20 | 21 | // Add listeners for initial channels 22 | for (var c of self._channels.toArray()) self._onChannelAdded(c) 23 | 24 | function onChannelAdded (channel) { 25 | self._onChannelAdded(channel) 26 | } 27 | 28 | function onChannelRemoved (channel) { 29 | self._onChannelRemoved(channel) 30 | } 31 | } 32 | 33 | Router.prototype.send = function (id, data) { 34 | var self = this 35 | 36 | var msg = { 37 | to: id.toString('hex'), 38 | from: self.id.toString('hex'), 39 | path: [], 40 | nonce: '' + Math.floor(1e15 * Math.random()), 41 | data: data 42 | } 43 | 44 | self._touched[msg.nonce] = true 45 | 46 | debugMsg('SEND', self.id, msg) 47 | 48 | self._send(msg) 49 | } 50 | 51 | Router.prototype._send = function (msg) { 52 | var self = this 53 | 54 | if (msg.path.length >= self.maxHops) return // throw new Error('Max hops exceeded nonce=' + msg.nonce) 55 | 56 | if (self._channels.count() === 0) { 57 | self._queue.push(msg) 58 | } 59 | 60 | msg.path.push(self.id.toString('hex')) 61 | 62 | var target = new Buffer(msg.to, 'hex') 63 | var closests = self._channels.closest(target, 20) 64 | .filter((c) => msg.path.indexOf(c.id.toString('hex')) === -1) 65 | .filter((_, index) => index < self.concurrency) 66 | 67 | if (msg.to in self._paths) { 68 | var preferred = self._channels.closest(new Buffer(self._paths[msg.to], 'hex'), 1)[0] 69 | if (preferred != null && closests.indexOf(preferred) === -1) closests.unshift(preferred) 70 | } 71 | 72 | for (var channel of closests) { 73 | // TODO BUG Sometimes the WS on closest in not in the ready state 74 | channel.send(msg) 75 | if (channel.id.toString('hex') === msg.to) break 76 | } 77 | } 78 | 79 | Router.prototype._onMessage = function (msg) { 80 | var self = this 81 | 82 | if (msg.nonce in self._touched) return 83 | self._touched[msg.nonce] = true 84 | 85 | self._paths[msg.from] = msg.path[msg.path.length - 1] 86 | 87 | msg.to = new Buffer(msg.to, 'hex') 88 | msg.from = new Buffer(msg.from, 'hex') 89 | 90 | if (msg.to.equals(self.id)) { 91 | debugMsg('RECV', self.id, msg) 92 | self.emit('message', msg.data, msg.from) 93 | } else { 94 | debugMsg('RELAY', self.id, msg) 95 | self._send(msg) 96 | } 97 | } 98 | 99 | Router.prototype._onChannelAdded = function (channel) { 100 | var self = this 101 | 102 | channel.on('message', listener) 103 | self._channelListeners[channel.id] = listener 104 | 105 | function listener (msg) { 106 | self._onMessage(msg) 107 | } 108 | 109 | while (self._queue.length > 0) self._send(self._queue.shift()) 110 | } 111 | 112 | Router.prototype._onChannelRemoved = function (channel) { 113 | var self = this 114 | var listener = self._channelListeners[channel.id] 115 | channel.removeListener('message', listener) 116 | } 117 | 118 | function debugMsg (verb, localID, msg) { 119 | var to = Buffer.isBuffer(msg.to) ? msg.to.toString('hex') : msg.to 120 | var from = Buffer.isBuffer(msg.from) ? msg.from.toString('hex') : msg.from 121 | verb = (verb + ' ').substr(0, 5) 122 | 123 | debug('[%s] %s (%s->%s) %s', 124 | localID.toString('hex', 0, 2), 125 | verb, 126 | from.substr(0, 4), 127 | to.substr(0, 4), 128 | msg.nonce.substr(0, 4), 129 | JSON.stringify(msg.data)) 130 | } 131 | -------------------------------------------------------------------------------- /lib/wgram.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var EventEmitter = require('events').EventEmitter 4 | var inherits = require('util').inherits 5 | var Client = require('./client') 6 | 7 | module.exports = Socket 8 | 9 | var PORT_MAPPINGS = {} 10 | 11 | var BIND_STATE_UNBOUND = 0 12 | var BIND_STATE_BINDING = 1 13 | var BIND_STATE_BOUND = 2 14 | 15 | inherits(Socket, EventEmitter) 16 | function Socket (opts) { 17 | if (!(this instanceof Socket)) return new Socket(opts) 18 | 19 | var self = this 20 | self._port = Math.floor(Math.random() * 9999) 21 | self._bindState = BIND_STATE_UNBOUND 22 | self.peer = new Client(opts) 23 | self.peer.on('message', onMessage) 24 | self.peer.on('close', onClose) 25 | self.peer.on('error', onError) 26 | 27 | PORT_MAPPINGS[self._port] = self.peer.id.toString('hex') 28 | 29 | function onMessage (msg) { 30 | var rinfo = { 31 | address: msg.address, 32 | port: msg.port, 33 | family: 'peer-relay' 34 | } 35 | self.emit('message', new Buffer(msg.buffer), rinfo) 36 | } 37 | 38 | function onClose () { 39 | self.close() 40 | } 41 | 42 | function onError (err) { 43 | self.emit('error', err) 44 | } 45 | } 46 | 47 | Socket.prototype.bind = function (port, cb) { 48 | var self = this 49 | if (!self.peer) throw new Error('Not running') 50 | if (self._bindState !== BIND_STATE_UNBOUND) throw new Error('Socket is already bound') 51 | 52 | delete PORT_MAPPINGS[self._port] 53 | self._port = port || Math.floor(Math.random() * 9999) 54 | self._bindState = BIND_STATE_BINDING 55 | if (cb) self.once('listening', cb) 56 | 57 | if (self.peer.peers.count() > 0) { 58 | onBind() 59 | } else { 60 | self.peer.once('peer', onBind) 61 | } 62 | 63 | function onBind () { 64 | PORT_MAPPINGS[self._port] = self.peer.id.toString('hex') 65 | self._bindState = BIND_STATE_BOUND 66 | self.emit('listening') 67 | } 68 | } 69 | 70 | // valid combinations 71 | // send(buffer, offset, length, port, address, cb) 72 | // send(buffer, offset, length, port, address) 73 | // send(buffer, offset, length, port) 74 | // send(bufferOrList, port, address, cb) 75 | // send(bufferOrList, port, address) 76 | // send(bufferOrList, port) 77 | Socket.prototype.send = function (buffer, offset, length, port, address, cb) { 78 | var self = this 79 | if (!self.peer) throw new Error('Not running') 80 | 81 | var isIP = address.indexOf('.') !== -1 82 | var isLocal = address === 'localhost' || address === '127.0.0.1' 83 | 84 | var id = isLocal ? PORT_MAPPINGS[port] : address 85 | var msg = { 86 | buffer: buffer.slice(offset, length), 87 | address: isLocal ? address : self.peer.id.toString('hex'), 88 | port: self._port 89 | } 90 | 91 | if (id == null || (isIP && !isLocal)) return 92 | 93 | self.peer.send(new Buffer(id, 'hex'), msg) 94 | if (cb) cb() 95 | } 96 | 97 | Socket.prototype.close = function (cb) { 98 | var self = this 99 | if (!self.peer) throw new Error('Not running') 100 | 101 | self.peer.destroy() 102 | self.peer = null 103 | 104 | delete PORT_MAPPINGS[self._port] 105 | 106 | if (cb) self.on('close', cb) 107 | self.emit('close') 108 | } 109 | 110 | Socket.prototype.address = function () { 111 | var self = this 112 | if (!self.peer) throw new Error('Not running') 113 | 114 | return { 115 | address: self.peer.id.toString('hex'), 116 | port: self._port, 117 | family: 'peer-relay' 118 | } 119 | } 120 | 121 | Socket.prototype.setBroadcast = function () { 122 | throw new Error('setBroadcast not implemented') 123 | } 124 | 125 | Socket.prototype.setTTL = function () { 126 | throw new Error('setTTL not implemented') 127 | } 128 | 129 | Socket.prototype.setMulticastTTL = function () { 130 | throw new Error('setMulticastTTL not implemented') 131 | } 132 | 133 | Socket.prototype.setMulticastLoopback = function () { 134 | throw new Error('setMulticastLoopback not implemented') 135 | } 136 | 137 | Socket.prototype.addMembership = function () { 138 | throw new Error('addMembership not implemented') 139 | } 140 | 141 | Socket.prototype.dropMembership = function () { 142 | throw new Error('dropMembership not implemented') 143 | } 144 | 145 | Socket.prototype.ref = function () { 146 | throw new Error('ref not implemented') 147 | } 148 | 149 | Socket.prototype.unref = function () { 150 | throw new Error('unref not implemented') 151 | } 152 | -------------------------------------------------------------------------------- /lib/wrtc.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits 2 | var EventEmitter = require('events').EventEmitter 3 | var SimplePeer = require('simple-peer') 4 | var debug = require('debug')('peer-relay:wrtc') 5 | 6 | module.exports = WrtcConnector 7 | 8 | inherits(WrtcConnector, EventEmitter) 9 | function WrtcConnector (id, router, wrtc) { 10 | var self = this 11 | 12 | self.id = id 13 | self.destroyed = false 14 | self.supported = wrtc != null || SimplePeer.WEBRTC_SUPPORT 15 | self._wrtc = wrtc 16 | self._pending = {} 17 | self._router = router 18 | self._router.on('message', onMessage) 19 | 20 | function onMessage (msg, from) { 21 | if (msg.type === 'signal') self._onSignal(msg.data, from) 22 | } 23 | } 24 | 25 | WrtcConnector.prototype.connect = function (remoteID) { 26 | var self = this 27 | if (self.destroyed) return 28 | self._setupSimplePeer(remoteID) 29 | } 30 | 31 | WrtcConnector.prototype._onSignal = function (signal, from) { 32 | var self = this 33 | if (self.destroyed) return 34 | var sp = self._pending[from] 35 | if (sp != null) { 36 | sp.signal(signal) 37 | } else { 38 | self._setupSimplePeer(from, signal) 39 | } 40 | } 41 | 42 | WrtcConnector.prototype._setupSimplePeer = function (remoteID, offer) { 43 | var self = this 44 | var sp = new SimplePeer({ 45 | initiator: offer == null, 46 | trickle: true, 47 | wrtc: self._wrtc 48 | }) 49 | 50 | sp.on('signal', onSignal) 51 | sp.on('connect', onConnect) 52 | sp.on('close', onClose) 53 | sp.on('error', onError) 54 | 55 | if (offer != null) sp.signal(offer) 56 | 57 | self._pending[remoteID] = sp 58 | 59 | function onSignal (signal) { 60 | self._debug('SIGNAL', signal) 61 | self._router.send(remoteID, { 62 | type: 'signal', 63 | data: signal 64 | }) 65 | } 66 | 67 | function onConnect () { 68 | self._debug('CONNECT') 69 | delete self._pending[remoteID] 70 | sp.removeListener('signal', onSignal) 71 | sp.removeListener('connect', onConnect) 72 | sp.removeListener('close', onClose) 73 | sp.removeListener('error', onError) 74 | self.emit('connection', new WrtcChannel(sp, remoteID)) 75 | } 76 | 77 | function onClose () { 78 | self._debug('CLOSE') 79 | delete self._pending[remoteID] 80 | sp.removeListener('signal', onSignal) 81 | sp.removeListener('connect', onConnect) 82 | sp.removeListener('close', onClose) 83 | sp.removeListener('error', onError) 84 | } 85 | 86 | function onError (err) { 87 | self._debug('ERROR', err) 88 | } 89 | } 90 | 91 | WrtcConnector.prototype.destroy = function () { 92 | var self = this 93 | if (self.destroyed) return 94 | 95 | self.destroyed = true 96 | 97 | for (var id in self._pending) self._pending[id].destroy() 98 | } 99 | 100 | WrtcConnector.prototype._debug = function () { 101 | var self = this 102 | var prepend = '[' + self.id.toString('hex', 0, 2) + '] ' 103 | arguments[0] = prepend + arguments[0] 104 | debug.apply(null, arguments) 105 | } 106 | 107 | inherits(WrtcChannel, EventEmitter) 108 | function WrtcChannel (sp, id) { 109 | var self = this 110 | 111 | self.destroyed = false 112 | self.id = id 113 | self._sp = sp 114 | self._sp.on('data', onData) 115 | self._sp.on('close', onClose) 116 | self._sp.on('error', onError) 117 | 118 | function onData (data) { 119 | if (self.destroyed) return 120 | self.emit('message', JSON.parse(data)) 121 | } 122 | 123 | function onClose () { 124 | self.destroy() 125 | } 126 | 127 | function onError (err) { 128 | if (self.destroyed) return 129 | self.emit('error', err) 130 | } 131 | } 132 | 133 | WrtcChannel.prototype.send = function (data) { 134 | var self = this 135 | if (self.destroyed) return 136 | 137 | self._sp.send(JSON.stringify(data)) 138 | } 139 | 140 | WrtcChannel.prototype.destroy = function () { 141 | var self = this 142 | if (self.destroyed) return 143 | 144 | self.destroyed = true 145 | self._sp.destroy() 146 | self._sp = null 147 | } 148 | -------------------------------------------------------------------------------- /lib/ws.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits 2 | var EventEmitter = require('events').EventEmitter 3 | var debug = require('debug')('peer-relay:ws') 4 | var WebSocket = getWebSocket() 5 | 6 | module.exports = WsConnector 7 | 8 | inherits(WsConnector, EventEmitter) 9 | function WsConnector (id, port) { 10 | var self = this 11 | 12 | self.id = id 13 | self.destroyed = false 14 | self._wss = null 15 | self.url = null 16 | 17 | if (port != null) { 18 | self._wss = new WebSocket.Server({ port: port }) 19 | self._wss.on('connection', onConnection) 20 | self._wss.on('listening', onListen) 21 | if (port !== 0) self.url = 'ws://localhost:' + port 22 | } 23 | 24 | function onConnection (ws) { 25 | self._onConnection(ws) 26 | } 27 | 28 | function onListen () { 29 | if (self.destroyed) return 30 | self.url = 'ws://localhost:' + self._wss._server.address().port 31 | } 32 | } 33 | 34 | WsConnector.prototype.connect = function (url) { 35 | var self = this 36 | self._onConnection(new WebSocket(url)) 37 | } 38 | 39 | WsConnector.prototype._onConnection = function (ws) { 40 | var self = this 41 | 42 | if (self.destroyed) { 43 | ws.close() 44 | return 45 | } 46 | 47 | var channel = new WsChannel(self.id, ws) 48 | channel.on('open', onOpen) 49 | channel.on('close', onClose) 50 | channel.on('error', onError) 51 | 52 | function onOpen () { 53 | channel.removeListener('open', onOpen) 54 | channel.removeListener('close', onClose) 55 | channel.removeListener('error', onError) 56 | 57 | if (self.destroyed) { 58 | channel.destroy() 59 | return 60 | } 61 | 62 | self.emit('connection', channel) 63 | } 64 | 65 | function onClose () { 66 | channel.removeListener('open', onOpen) 67 | channel.removeListener('close', onClose) 68 | channel.removeListener('error', onError) 69 | } 70 | 71 | function onError (err) { 72 | self._debug(err, err.stack) 73 | } 74 | } 75 | 76 | WsConnector.prototype.destroy = function (cb) { 77 | var self = this 78 | if (self.destroyed) return 79 | 80 | self.destroyed = true 81 | if (self._wss) self._wss.close(cb) 82 | else cb() 83 | self._wss = null 84 | } 85 | 86 | WsConnector.prototype._debug = function () { 87 | var self = this 88 | var prepend = '[' + self.id.toString('hex', 0, 2) + '] ' 89 | arguments[0] = prepend + arguments[0] 90 | debug.apply(null, arguments) 91 | } 92 | 93 | inherits(WsChannel, EventEmitter) 94 | function WsChannel (localID, ws) { 95 | var self = this 96 | 97 | self.localID = localID 98 | self.id = undefined 99 | self.destroyed = false 100 | self.ws = ws 101 | 102 | ws.onopen = onOpen 103 | ws.onmessage = onMessage 104 | ws.onclose = onClose 105 | ws.onerror = onError 106 | 107 | if (ws.readyState === 1) onOpen() // if already open 108 | 109 | function onOpen () { 110 | self._onOpen() 111 | } 112 | 113 | function onMessage (data) { 114 | self._onMessage(data.data) 115 | } 116 | 117 | function onClose () { 118 | self.destroy() 119 | } 120 | 121 | function onError (err) { 122 | self._onError(err) 123 | } 124 | } 125 | 126 | WsChannel.prototype._onOpen = function () { 127 | var self = this 128 | if (self.destroyed) return 129 | 130 | self.ws.send(JSON.stringify(self.localID)) 131 | } 132 | 133 | WsChannel.prototype.send = function (data) { 134 | var self = this 135 | if (self.destroyed) return 136 | if (self.ws.readyState === 2) return // readyState === CLOSING 137 | if (self.ws.readyState !== 1) throw new Error('WebSocket is not ready') 138 | 139 | var str = JSON.stringify(data) 140 | self.ws.send(str) 141 | } 142 | 143 | WsChannel.prototype._onMessage = function (data) { 144 | var self = this 145 | if (self.destroyed) return 146 | 147 | var json = JSON.parse(data) 148 | 149 | if (self.id == null) { 150 | self.id = new Buffer(json, 'hex') 151 | self._debug('OPEN') 152 | self.emit('open') 153 | } else { 154 | self.emit('message', json) 155 | } 156 | } 157 | 158 | WsChannel.prototype._onError = function (err) { 159 | var self = this 160 | if (self.destroyed) return 161 | 162 | self._debug('ERROR', err) 163 | self.emit('error', err) 164 | } 165 | 166 | WsChannel.prototype._debug = function () { 167 | var self = this 168 | var remote = self.id ? self.id.toString('hex', 0, 2) : '?' 169 | var prepend = '[' + self.localID.toString('hex', 0, 2) + '->' + remote + '] ' 170 | arguments[0] = prepend + arguments[0] 171 | debug.apply(null, arguments) 172 | } 173 | 174 | WsChannel.prototype.destroy = function () { 175 | var self = this 176 | if (self.destroyed) return 177 | 178 | self._debug('CLOSE') 179 | self.destroyed = true 180 | self.ws.close() 181 | self.ws = null 182 | 183 | self.emit('close') 184 | } 185 | 186 | function getWebSocket () { 187 | if (typeof window !== 'undefined' && window.WebSocket) return window.WebSocket 188 | return require('ws') 189 | } 190 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peer-relay", 3 | "version": "0.0.2", 4 | "description": "relay messages through peers in a network", 5 | "main": "index.js", 6 | "bin": "server.js", 7 | "scripts": { 8 | "test": "standard && mocha test.js --use-strict --timeout 10000", 9 | "bundle": "browserify index.js -s PeerRelay -d -o bundle.js -i ws", 10 | "prepublish": "npm run -s bundle", 11 | "standard": "standard", 12 | "browserify": "browserify", 13 | "start": "./server.js" 14 | }, 15 | "standard": { 16 | "globals": [ 17 | "it", 18 | "describe" 19 | ] 20 | }, 21 | "repository": "https://github.com/xuset/peer-relay", 22 | "author": "Austin Middleton", 23 | "license": "MIT", 24 | "dependencies": { 25 | "debug": "^2.2.0", 26 | "k-bucket": "^3.0.2", 27 | "simple-peer": "^6.0.4", 28 | "ws": "^1.1.1" 29 | }, 30 | "devDependencies": { 31 | "browserify": "^13.1.0", 32 | "electron-webrtc": "^0.2.7", 33 | "mocha": "^2.5.3", 34 | "standard": "^7.1.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var process = require('process') 4 | var Client = require('./lib/client') 5 | 6 | var needsHelp = process.argv.indexOf('--help') !== -1 || 7 | process.argv.indexOf('-h') !== -1 || 8 | process.argv.length < 3 9 | 10 | if (!needsHelp) { 11 | var opts = { 12 | port: parseInt(process.argv[2]), 13 | bootstrap: process.argv.length === 4 ? [ process.argv[3] ] : [] 14 | } 15 | 16 | var c = new Client(opts) 17 | 18 | c.on('peer', function (id) { 19 | console.error('PEER', id.toString('hex', 0, 2)) 20 | }) 21 | } else { 22 | console.error(`\ 23 | ${process.argv[1]} port [bootstrap_urls...] 24 | 25 | Starts a PeerRelay node and listens for WebSocket connectionso on 'port' 26 | 27 | An optional list of bootstrap urls can be provided as positional arguments. 28 | `) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var Client = require('./lib/client') 3 | var wrtc = require('electron-webrtc')() 4 | 5 | wrtc.on('error', function (err) { console.error(err, err.stack) }) 6 | 7 | describe('End to End', function () { 8 | var clients = [] 9 | 10 | function startClient (opts) { 11 | var c = new Client(opts) 12 | clients.push(c) 13 | return c 14 | } 15 | 16 | this.afterEach(function (done) { 17 | function destroy () { 18 | if (clients.length === 0) { 19 | done() 20 | } else { 21 | clients.pop().destroy(destroy) 22 | } 23 | } 24 | destroy() 25 | }) 26 | 27 | it('two peers connect', function (done) { 28 | var c1 = startClient({ port: 8001, bootstrap: [] }) 29 | var c2 = startClient({ port: 8002, bootstrap: ['ws://localhost:8001'] }) 30 | var count = 0 31 | 32 | c1.on('peer', function (id) { 33 | assert.ok(id.equals(c2.id)) 34 | assert.ok(count <= 2) 35 | count++ 36 | if (count === 2) done() 37 | }) 38 | 39 | c2.on('peer', function (id) { 40 | assert.ok(id.equals(c1.id)) 41 | assert.ok(count <= 2) 42 | count++ 43 | if (count === 2) done() 44 | }) 45 | }) 46 | 47 | it('direct message', function (done) { 48 | var c1 = startClient({ port: 8001, bootstrap: [] }) 49 | var c2 = startClient({ port: 8002, bootstrap: ['ws://localhost:8001'] }) 50 | var count = 0 51 | 52 | c1.on('peer', function (id) { 53 | assert.ok(id.equals(c2.id)) 54 | c1.send(id, 'TEST1') 55 | }) 56 | 57 | c2.on('peer', function (id) { 58 | assert.ok(id.equals(c1.id)) 59 | c2.send(id, 'TEST2') 60 | }) 61 | 62 | c1.on('message', function (msg, id) { 63 | assert.ok(id.equals(c2.id)) 64 | assert.equal(msg, 'TEST2') 65 | assert.ok(count <= 2) 66 | count++ 67 | if (count === 2) done() 68 | }) 69 | 70 | c2.on('message', function (msg, id) { 71 | assert.ok(id.equals(c1.id)) 72 | assert.equal(msg, 'TEST1') 73 | assert.ok(count <= 2) 74 | count++ 75 | if (count === 2) done() 76 | }) 77 | }) 78 | 79 | it('send message before connect', function (done) { 80 | var c1 = startClient({ port: 8001, bootstrap: [] }) 81 | var c2 = startClient({ port: 8002, bootstrap: ['ws://localhost:8001'] }) 82 | var count = 0 83 | 84 | c1.on('message', function (msg, id) { 85 | assert.ok(id.equals(c2.id)) 86 | assert.equal(msg, 'TEST2') 87 | assert.ok(count <= 2) 88 | count++ 89 | if (count === 2) done() 90 | }) 91 | 92 | c2.on('message', function (msg, id) { 93 | assert.ok(id.equals(c1.id)) 94 | assert.equal(msg, 'TEST1') 95 | assert.ok(count <= 2) 96 | count++ 97 | if (count === 2) done() 98 | }) 99 | 100 | c2.send(c1.id, 'TEST2') 101 | c1.send(c2.id, 'TEST1') 102 | }) 103 | 104 | it('relay message', function (done) { 105 | // c1 <-> c2 <-> c3 106 | var c2 = startClient({ port: 8002, bootstrap: [] }) 107 | var c1 = startClient({ port: 8001, bootstrap: ['ws://localhost:8002'] }) 108 | var c3 = startClient({ port: 8003, bootstrap: ['ws://localhost:8002'] }) 109 | 110 | c1.on('peer', function (id) { 111 | assert.ok(id.equals(c2.id)) 112 | c1.send(c3.id, 'TEST') 113 | }) 114 | 115 | c3.on('message', function (msg, id) { 116 | assert.ok(id.equals(c1.id)) 117 | assert.equal(msg, 'TEST') 118 | done() 119 | }) 120 | }) 121 | 122 | it('clients automatically populate', function (done) { 123 | // c1 <-> c2 <-> c3 124 | var c2 = startClient({ port: 8002, bootstrap: [] }) 125 | var c1 = startClient({ port: 8001, bootstrap: ['ws://localhost:8002'] }) 126 | var c3 = startClient({ port: 8003, bootstrap: ['ws://localhost:8002'] }) 127 | 128 | var c1PeerEvent = false 129 | var c3PeerEvent = false 130 | 131 | c1.on('peer', function (id) { 132 | if (id.equals(c2.id)) { 133 | // c1.connect(c3.id) 134 | } else if (id.equals(c3.id)) { 135 | c1PeerEvent = true 136 | c1.disconnect(c2.id) 137 | c1.send(c3.id, 'TEST') 138 | } else { 139 | assert.ok(false) 140 | } 141 | }) 142 | 143 | c3.on('peer', function (id) { 144 | assert.ok(id.equals(c1.id) || id.equals(c2.id)) 145 | if (id.equals(c1.id)) c3PeerEvent = true 146 | }) 147 | 148 | c3.on('message', function (msg, id) { 149 | assert.ok(id.equals(c1.id)) 150 | assert.equal(msg, 'TEST') 151 | assert.ok(c1PeerEvent) 152 | assert.ok(c3PeerEvent) 153 | done() 154 | }) 155 | }) 156 | 157 | // it('webrtc connect and send message', function (done) { 158 | // // c1 <-> c2 <-> c3 159 | // var c2 = startClient({ port: 8002, bootstrap: [] }) 160 | // var c1 = startClient({ wrtc: wrtc, bootstrap: ['ws://localhost:8002'] }) 161 | // var c3 = startClient({ wrtc: wrtc, bootstrap: ['ws://localhost:8002'] }) 162 | 163 | // c1.on('peer', function (id) { 164 | // assert.ok(id.equals(c2.id) || id.equals(c3.id)) 165 | // if (id.equals(c3.id)) c1.send(c3.id, 'TEST') 166 | // }) 167 | 168 | // c3.on('message', function (msg, id) { 169 | // assert.ok(id.equals(c1.id)) 170 | // assert.equal(msg, 'TEST') 171 | // done() 172 | // }) 173 | // }) 174 | 175 | // it('relay chain', function (done) { 176 | // var peers = [] 177 | // for (var i = 0; i < 10; i++) { 178 | // peers.push(startClient({ 179 | // port: 8000 + i, 180 | // bootstrap: i === 0 ? [] : ['ws://localhost:' + (8000 + i - 1)] 181 | // })) 182 | // } 183 | 184 | // var first = peers[0] 185 | // var last = peers[peers.length - 1] 186 | 187 | // last.on('message', function (msg, id) { 188 | // assert.ok(id.equals(first.id)) 189 | // assert.equal('TEST', msg) 190 | // done() 191 | // }) 192 | 193 | // onBootstrap(peers, function () { 194 | // first.send(last.id, 'TEST') 195 | // }) 196 | // }) 197 | }) 198 | 199 | // function onBootstrap (peers, cb) { 200 | // for (var p of peers) { 201 | // p.on('peer', function () { 202 | // if (isBootstrapped()) cb() 203 | // }) 204 | // } 205 | // 206 | // function isBootstrapped () { 207 | // for (var p of peers) { 208 | // if (p.peers.count() === 0) return false 209 | // } 210 | // return true 211 | // } 212 | // } 213 | --------------------------------------------------------------------------------