├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── index.js ├── lib ├── package.json ├── src └── swarm.js └── test ├── browser.sh ├── swarm.js └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | env: 4 | matrix: 5 | - NODE=6 6 | - NODE=5 7 | - NODE=4 8 | - BROWSER=chrome 9 | - BROWSER=firefox 10 | global: 11 | - secure: Wrj0Ajdh20pZk2zFLcWAlc42VFSdLekZSDbJ4ifxMF3uWZ4XA2pWYqkyLicGNRFnnV6YDAKkfAKcK/+dH+SoEru/9CXsx9hfgXG0sCb7qzohi9ShAsfQ9DGT4IdqWLIAEVhsJ7FzzhyvIcbA5zsrgxB5CimYmzhqSFE1ijTKNpGdd6HNN5bNyK8CfjwImpfRNwrlUBrglKWWD+G7QX/o2rTh08yuuzlUgvAM70KzGMD4gal9hhbzCTZRx7q38+K1ytYk29DGZsRmkHywmRWMQk5SRH9Ku8IS1HPNV3IvdC0di6lFvpdvTf1uq8K9Pc+uxNm/yFVfKeA8hDTwRAXFR5R7tKimszrMAmaI8ziAr3rxRjekSaxlc9416/GnyiuvX3PeMjSxp7HEEWOUSb1MFVDxEW/OdGkQHhM3TyphuofASeeZrWlDFzxhkMiZ6cltoWzOrqICv4E+nHb1G5d5oyKB4RTbFY68Xwa5V4cgoyslZnytoblqftzAXAph1bSK0CIEPvBu7Cz/59kY8JavesP/OOIUKv2hH2dTLuaiI0vaBmINZSPKwHIEphQ5MQGJxyefeAuySSPft0lGlcLcL0d3YsmpJxMdOWs4rikF/7CEdnLbR0bNoa7ko0o1GlS/zt0PFTm+Zm+E6RH12KVnQnEsbFPmkMLQQ4HlJiTzN9Y= 12 | - secure: GCtDxggHI/Evin4EuVNve7SHbMDGJi0AOBT2MrVMeV2ZQLOMRen1jzpH/oAd/1SlsJpDCbuONxRdL++XFVL6ycyTB6GS6Y2QjdrnaC4il2w+MRTcQJy1CJxnuHB54pXs0y5aeir8sCoQProIUrVMDgmWQBKLsvJNJpacRD79qlQpNe/6MybUgefhMu+McViK7JeIRa2hC9B6Em6w5Ko2R4xQRUDtHRQZ1gq6LcjG3SQ7Kqa9ZHw/qDUR3R36ZmKpVUZIt00+kaAmymhPE/Fu4WXySJeNCZKPkeAEnuGX9+a80qtlRkoSvtDTQP3jDYtUMT1O7eVPXWA9s2iVJBcXezgfxc+1sz0hlHTw5U6na4ueoKUlmcqJHN1KBxRJOwM78cD9y2G39iBrmwQfF6dpsjN678uwvHW9DzjgH45zq2jkSqrZrI46z2nQF1mf3/o3nzlh2Yp+RKBqVaStGCl743kDq6qJTXRxPGK2Hc5d7wX0x5kSKk8KtGV8RATM8R1Os/X9uiFnbzcYWy1D50/x0pqoxxHc5cQemPiffVRP0mu0eTHvy5nTM4/+hhkUtJpEvZSschplmo64tcXFHbnaDp2ThROUUI92kPzERoa8KHmgV6Bq4okm2yWEtIJTRiC1BoMZ5TW2DwnTgfEi3Sfed9/zgmZrNXxUQsRWkQu+Yso= 13 | addons: 14 | apt: 15 | packages: 16 | - build-essential 17 | - clang 18 | - libdbus-glib-1-dev 19 | - libgtk2.0-dev 20 | - libnotify-dev 21 | - libgnome-keyring-dev 22 | - libgconf2-dev 23 | - libasound2-dev 24 | - libcap-dev 25 | - libcups2-dev 26 | - libxtst-dev 27 | - libxss1 28 | - libnss3-dev 29 | - gcc-multilib 30 | - g++-multilib 31 | - xvfb 32 | install: true 33 | script: ./test/travis.sh 34 | notifications: 35 | email: false 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peer-exchange 2 | 3 | [![npm version](https://img.shields.io/npm/v/peer-exchange.svg)](https://www.npmjs.com/package/peer-exchange) 4 | [![Build Status](https://travis-ci.org/mappum/peer-exchange.svg?branch=master)](https://travis-ci.org/mappum/peer-exchange) 5 | [![Dependency Status](https://david-dm.org/mappum/peer-exchange.svg)](https://david-dm.org/mappum/peer-exchange) 6 | 7 | **Decentralized peer discovery and signaling** 8 | 9 | `peer-exchange` is a client for the Peer Exchange Protocol (PXP), a decentralized protocol for peer discovery and signaling. Rather than using centralized signal hubs, each node in the network exchanges peers and relays signaling data. 10 | 11 | This client uses WebRTC for peer connections, but you may also use any other transport by manually connecting and passing in a duplex stream. 12 | 13 | ## Usage 14 | 15 | `npm install peer-exchange` 16 | 17 | ```js 18 | var Exchange = require('peer-exchange') 19 | 20 | var ex = new Exchange('some-network-id', { wrtc: wrtc }) 21 | // The network id can be any string unique to your network. 22 | // When using Node.js, the `wrtc` option is required. 23 | // (This can come from the 'wrtc' or 'electron-webrtc' packages). 24 | 25 | ex.on('connect', (conn) => { 26 | // `conn` is a duplex stream multiplexed through the PXP connection, 27 | // which can be used for your P2P protocol. 28 | conn.pipe(something).pipe(conn) 29 | 30 | // We can query our current peers for a new peer by calling `getNewPeer()`. 31 | // `peer-exchange` will do the WebRTC signaling and connect to the peer. 32 | if (ex.peers.length < 8) ex.getNewPeer() 33 | }) 34 | 35 | // Bootstrap by connecting to one or more already-known PXP peers. 36 | // You can use any transport that creates a duplex stream (in this case TCP). 37 | var socket = net.connect(8000, '10.0.0.1', () => ex.connect(socket)) 38 | 39 | // You can optionally accept incoming connections using any transport. 40 | var server = net.createServer((socket) => ex.accept(socket)) 41 | server.listen(8000) 42 | ``` 43 | 44 | ### API 45 | 46 | ## Exchange 47 | 48 | ```js 49 | var Exchange = require('peer-exchange') 50 | ``` 51 | 52 | #### Methods 53 | 54 | 55 | #### `var ex = new Exchange(networkId, [opts])` 56 | 57 | Creates a new exchange, which is used to manage connections to peers in a P2P network. After we establish some initial connections, we can query our current peers for new peers. Additionally, we will share peers when we receive queries. 58 | 59 | `networkId` should be a string unique to the network. Nodes can only peer with other nodes that use the same ID. If you need to participate in multiple networks, create multiple `Exchange` instances with different IDs. 60 | 61 | `opts` should contain the following properties: 62 | - `wrtc`, *Object* - A WebRTC implementation for Node.js clients (e.g. [`wrtc`](https://github.com/js-platform/node-webrtc) or [`electron-webrtc`](https://github.com/mappum/electron-webrtc)). In browsers, the built-in implementation is used by default. 63 | 64 | ---- 65 | #### `ex.connect(socket, [callback])` 66 | 67 | Manually adds a peer. This is necessary to bootstrap our exchange with initial peers which we can query for additional peers. This method is for *outgoing* connections, for *incoming* connections use `accept`. 68 | 69 | `socket` should be a duplex stream that represents a connection to a peer which implements the Peer Exchange Protocol. 70 | 71 | `callback` will be called with `callback(err, connection)`, where `connection` is a duplex stream which may be used by your application for your P2P protocol. 72 | 73 | ---- 74 | #### `ex.accept(socket, [callback])` 75 | 76 | Similar to `connect`, but used with *incoming* peer connections. 77 | 78 | ---- 79 | #### `ex.getNewPeer([callback])` 80 | 81 | Queries out current peers for a new peer, then connects to it via WebRTC. The already-connected peer will act as a relay for signaling. 82 | 83 | The `'connect'` event will be emitted once the connection is established. 84 | 85 | This will error if our exchange is not connected to any peers. 86 | 87 | `callback` will be called with `callback(err, connection)`. 88 | 89 | ---- 90 | #### `ex.close([callback])` 91 | 92 | Closes all peer connections in the exchange and prevents adding any new connections. 93 | 94 | `callback` is called with `callback(err)` when the exchange is closed (or when an error occurs). 95 | 96 | ---- 97 | 98 | #### Properties 99 | 100 | #### `ex.peers` 101 | 102 | An array of connected peers. Useful for iterating through peers or getting the number of connections. 103 | 104 | #### `ex.networkId` 105 | 106 | The network ID provided in the constructor. 107 | 108 | ---- 109 | 110 | #### Events 111 | 112 | #### `ex.on('connect', function (conn) { ... })` 113 | 114 | Emitted whenever a new peer connection is established (both incoming and outgoing). 115 | 116 | #### `ex.on('error', function (err) { ... })` 117 | 118 | Emitted when an error occurs. 119 | 120 | ---- 121 | 122 | ## Security Notes 123 | 124 | Some efforts were made to make this module DoS-resistant, but there are probably still some weaknesses. 125 | 126 | It is recommended to use an authenticated transport when possible (e.g. WebSockets over HTTPS) for initial bootstrapping to prevent man-in-the-middle attacks (attackers could control all the peers you connect to, which can be very bad in some applications). 127 | 128 | ## Comparison with `signalhub` 129 | 130 | This module provides functionality similar to [`signalhub`](https://github.com/mafintosh/signalhub), where P2P nodes can get addresses of new peers and establish connections by relaying signaling data. However, this module differs by getting all nodes to provide this "hub" service, rather than a few centralized servers. It also only exchanges currently-connected peer addresses rather than providing general-purpose broadcasting. 131 | 132 | Note that `signalhub` may be better suited for some applications, for instance when connecting to peers in small swarms when no peer addresses are initially known (e.g. BitTorrent swarms). In the future, a DHT could help with finding initial peers for this sort of use case. 133 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/swarm.js') 2 | -------------------------------------------------------------------------------- /lib: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peer-exchange", 3 | "version": "2.2.0", 4 | "description": "Decentralized peer discovery and signaling", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard src/*.js test/*.js index.js && dependency-check package.json && nyc tape test/*.js | faucet && nyc report", 8 | "test-browser": "npm run build && ./test/browser.sh && npm run source", 9 | "build": "rm -rf lib && babel --presets es2015 src -d lib", 10 | "source": "rm -rf lib && ln -s src lib", 11 | "prepublish": "npm run build", 12 | "publish": "npm run source" 13 | }, 14 | "keywords": [ 15 | "p2p", 16 | "peer", 17 | "dicovery", 18 | "exchange", 19 | "seed", 20 | "bootstrap", 21 | "signal", 22 | "hub" 23 | ], 24 | "author": "Matt Bell ", 25 | "license": "MIT", 26 | "dependencies": { 27 | "debug": "^2.2.0", 28 | "duplexify": "^3.4.5", 29 | "get-browser-rtc": "^1.0.2", 30 | "hat": "0.0.3", 31 | "isstream": "^0.1.2", 32 | "multiplex": "^6.6.1", 33 | "ndjson": "^1.4.3", 34 | "object-assign": "^4.1.0", 35 | "old": "^0.1.3", 36 | "on-object": "^1.0.0", 37 | "once": "^1.3.3", 38 | "pump": "^1.0.1", 39 | "pumpify": "^1.3.5", 40 | "pxp": "^1.0.0", 41 | "simple-peer": "^6.0.1", 42 | "through2": "^2.0.1", 43 | "websocket-stream": "^3.1.0" 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "^6.5.1", 47 | "babel-preset-es2015": "^6.5.0", 48 | "dependency-check": "^2.6.0", 49 | "faucet": "0.0.1", 50 | "nyc": "^6.4.4", 51 | "standard": "^8.2.0", 52 | "tape": "^4.5.1", 53 | "wrtc": "0.0.61", 54 | "zuul": "^3.10.1", 55 | "zuul-ngrok": "^4.0.0" 56 | }, 57 | "directories": { 58 | "test": "test" 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/mappum/peer-exchange.git" 63 | }, 64 | "bugs": { 65 | "url": "https://github.com/mappum/peer-exchange/issues" 66 | }, 67 | "homepage": "https://github.com/mappum/peer-exchange#readme", 68 | "browser": { 69 | "electron-webrtc": false 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/swarm.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | const old = require('old') 3 | const RTCPeer = require('simple-peer') 4 | const wrtc = require('get-browser-rtc')() 5 | const onObject = require('on-object') 6 | const assign = require('object-assign') 7 | const debug = require('debug')('peer-exchange') 8 | const once = require('once') 9 | const Peer = require('pxp') 10 | const { floor, random } = Math 11 | 12 | class Swarm extends EventEmitter { 13 | constructor (networkId, opts = {}) { 14 | if (!networkId || typeof networkId !== 'string') { 15 | throw new Error('networkId must be a string') 16 | } 17 | super() 18 | this.networkId = networkId 19 | this._peers = [] 20 | this.closed = false 21 | this.allowIncoming = opts.allowIncoming != null 22 | ? opts.allowIncoming : true 23 | this.wrtc = opts.wrtc || wrtc 24 | if (!this.wrtc) { 25 | throw new Error('No WebRTC implementation found, please pass one in ' + 26 | ' as the "wrtc" option (for example, the "wrtc" or ' + 27 | '"electron-webrtc" packages).') 28 | } 29 | this.rtcConfig = opts.rtcConfig 30 | if (opts.getPeers) this._getPeers = opts.getPeers 31 | if (this.allowIncoming) { 32 | this.connectInfo = { 33 | pxp: true, 34 | relay: true, 35 | webrtc: true 36 | } 37 | } 38 | this._error = this._error.bind(this) 39 | } 40 | 41 | _error (err) { 42 | this.emit('error', err) 43 | } 44 | 45 | _addPeer (peer) { 46 | this._peers.push(peer) 47 | onObject(peer).once({ 48 | disconnect: () => { 49 | var index = this._peers.indexOf(peer) 50 | if (index === -1) return 51 | this._peers.splice(index, 1) 52 | this.emit('disconnect', peer) 53 | }, 54 | error: (err) => this.emit('peerError', err, peer) 55 | }) 56 | if (peer.incoming) { 57 | peer.once(`connect:${this.networkId}`, (stream) => { 58 | this.emit('connect', stream, peer) 59 | }) 60 | } else { 61 | peer.connect(this.networkId, (err, stream) => { 62 | if (err) return this._error(err) 63 | this.emit('connect', stream, peer) 64 | }) 65 | } 66 | if (this.allowIncoming) { 67 | peer.on('incoming', (relay) => { 68 | debug('adding incoming peer') 69 | var incomingPeer = this._createPeer(relay, { 70 | incoming: true, 71 | relayed: true 72 | }) 73 | incomingPeer.once('upgrade', (...args) => 74 | this._onUpgrade(incomingPeer, ...args)) 75 | this._addPeer(incomingPeer) 76 | }) 77 | } 78 | this.emit('peer', peer) 79 | } 80 | 81 | connect (stream, opts, cb) { 82 | if (this.closed) { 83 | return cb(new Error('Swarm is closed')) 84 | } 85 | if (typeof opts === 'function') { 86 | cb = opts 87 | opts = {} 88 | } 89 | var peer = this._createPeer(stream, opts) 90 | if (cb) peer.once('error', cb) 91 | peer.onceReady(() => { 92 | if (cb) peer.removeListener('error', cb) 93 | this._addPeer(peer) 94 | peer.once(`connect:${this.networkId}`, (conn) => { 95 | conn.pxpPeer = peer 96 | if (cb) cb(null, conn) 97 | }) 98 | }) 99 | } 100 | 101 | accept (stream, opts = {}, cb) { 102 | if (this.closed) { 103 | return cb(new Error('Swarm is closed')) 104 | } 105 | if (typeof opts === 'function') { 106 | cb = opts 107 | opts = {} 108 | } 109 | opts.incoming = true 110 | this.connect(stream, opts, cb) 111 | } 112 | 113 | _createPeer (stream, opts = {}) { 114 | var networks = this._getNetworks() 115 | var connectInfo = this._getConnectInfo() 116 | var peer = Peer(stream, networks, connectInfo, { 117 | allowIncoming: this.allowIncoming 118 | }) 119 | assign(peer, opts) 120 | return peer 121 | } 122 | 123 | _getNetworks () { 124 | return { [this.networkId]: this._getPeers.bind(this) } 125 | } 126 | 127 | _getPeers (cb) { 128 | // TODO: limit to random selection 129 | cb(null, this._peers) 130 | } 131 | 132 | _getConnectInfo () { 133 | return this.connectInfo 134 | } 135 | 136 | _onUpgrade (oldPeer, { transport, signal }) { 137 | if (transport !== 'webrtc') { 138 | let err = new Error('Peer requested upgrade via unknown transport: ' + 139 | `"${transport}"`) 140 | return oldPeer.error(err) 141 | } 142 | debug(`upgrading peer: ${transport}`) 143 | var rtcConn = new RTCPeer({ wrtc: this.wrtc, config: this.rtcConfig }) 144 | this._signalRTC(oldPeer, rtcConn, () => { 145 | this.accept(rtcConn, (err) => { 146 | if (err) return this._error(err) 147 | oldPeer.close() 148 | }) 149 | }) 150 | rtcConn.signal(signal) 151 | } 152 | 153 | _upgradePeer (oldPeer, cb) { 154 | var rtcConn = new RTCPeer({ 155 | wrtc: this.wrtc, 156 | initiator: true, 157 | config: this.rtcConfig 158 | }) 159 | this._signalRTC(oldPeer, rtcConn, (err) => { 160 | if (err) return cb(err) 161 | this.connect(rtcConn, (err, newPeer) => { 162 | if (err) return cb(err) 163 | oldPeer.close() 164 | cb(null, newPeer) 165 | }) 166 | }) 167 | } 168 | 169 | _signalRTC (peer, conn, cb) { 170 | cb = once(cb) 171 | conn.once('connect', () => cb(null)) 172 | conn.once('error', (err) => cb(err)) 173 | peer.on('upgrade', ({ signal }) => { 174 | conn.signal(signal) 175 | }) 176 | conn.on('signal', (signal) => { 177 | peer.upgrade({ transport: 'webrtc', signal }) 178 | }) 179 | } 180 | 181 | getNewPeer (cb) { 182 | cb = cb || ((err) => { if (err) this._error(err) }) 183 | if (this.closed) { 184 | return cb(new Error('Swarm is closed')) 185 | } 186 | if (this._peers.length === 0) { 187 | return cb(new Error('Not connected to any peers')) 188 | } 189 | // TODO: smarter selection 190 | var peer = this._peers[floor(random() * this._peers.length)] 191 | peer.getPeers(this.networkId, (err, candidates) => { 192 | if (err) return cb(err) 193 | if (candidates.length === 0) { 194 | return cb(new Error('Peer did not return any candidates')) 195 | } 196 | var candidate = candidates[floor(random() * candidates.length)] 197 | if (candidate.connectInfo.pxp) { 198 | this._relayAndUpgrade(peer, candidate, cb) 199 | } else { 200 | this._relay(peer, candidate, cb) 201 | } 202 | }) 203 | } 204 | 205 | _relayAndUpgrade (peer, dest, cb) { 206 | cb = once(cb) 207 | peer.relay(dest, (err, relay) => { 208 | if (err) return cb(err) 209 | var relayPeer = this._createPeer(relay, { relayed: true }) 210 | relayPeer.once('error', cb) 211 | this._upgradePeer(relayPeer, cb) 212 | }) 213 | } 214 | 215 | _relay (peer, dest, cb) { 216 | peer.relay(dest, (err, relay) => { 217 | if (err) return cb(err) 218 | this.emit('connect', relay) 219 | return cb(null, relay) 220 | }) 221 | } 222 | 223 | close () { 224 | this.closed = true 225 | for (let peer of this._peers) peer.close() 226 | } 227 | 228 | get peers () { 229 | return this._peers.slice(0) 230 | } 231 | } 232 | 233 | module.exports = old(Swarm) 234 | -------------------------------------------------------------------------------- /test/browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $BROWSER ]; then 4 | zuul \ 5 | --browser-name $BROWSER \ 6 | --browser-version latest \ 7 | --ui tape \ 8 | --tunnel ngrok \ 9 | -- test/*.js 10 | else 11 | zuul --local --ui tape -- test/*.js 12 | fi 13 | -------------------------------------------------------------------------------- /test/swarm.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var Swarm = require('../') 3 | var PassThrough = require('stream').PassThrough 4 | var dup = require('duplexify') 5 | var RTCPeer = require('simple-peer') 6 | if (!process.browser) { 7 | var wrtc = require('wrtc') 8 | } else if (!window.localStorage.debug) { 9 | window.localStorage.debug = '*' 10 | window.location.reload() 11 | } 12 | 13 | function nodeTest (t, name, f) { 14 | if (!process.browser) return t.test(name, f) 15 | } 16 | 17 | function browserTest (t, name, f) { 18 | if (process.browser) return t.test(name, f) 19 | } 20 | 21 | function createStreams () { 22 | var conn1 = new PassThrough() 23 | var conn2 = new PassThrough() 24 | return [ 25 | dup(conn1, conn2), 26 | dup(conn2, conn1) 27 | ] 28 | } 29 | 30 | var rtcConfig = { 31 | iceServers: [ 32 | { 33 | url: 'stun:23.21.150.121', 34 | urls: 'stun:23.21.150.121' 35 | }, { 36 | url: 'turn:104.236.185.38:3478?transport=tcp', 37 | urls: 'turn:104.236.185.38:3478?transport=tcp', 38 | username: 'HDCqbgHNTxpbwwdDXlip', 39 | credential: 'Ll0IWHrGsqtLSxjvIFMi' 40 | }, { 41 | url: 'turn:104.236.185.38:3478?transport=udp', 42 | urls: 'turn:104.236.185.38:3478?transport=udp', 43 | username: 'HDCqbgHNTxpbwwdDXlip', 44 | credential: 'Ll0IWHrGsqtLSxjvIFMi' 45 | } 46 | ] 47 | } 48 | 49 | function createRTCStreams (cb) { 50 | var peer1 = RTCPeer({ initiator: true, wrtc: wrtc, config: rtcConfig }) 51 | var peer2 = RTCPeer({ wrtc: wrtc, config: rtcConfig }) 52 | peer1.on('signal', function (data) { peer2.signal(data) }) 53 | peer2.on('signal', function (data) { peer1.signal(data) }) 54 | var maybeDone = function () { 55 | if (!peer1.connected || !peer2.connected) return 56 | cb(null, [ peer1, peer2 ]) 57 | } 58 | peer1.on('connect', maybeDone) 59 | peer2.on('connect', maybeDone) 60 | } 61 | 62 | test('create Swarm', function (t) { 63 | t.test('no networkId', function (t) { 64 | try { 65 | var swarm = Swarm() 66 | t.notOk(swarm, 'should have thrown') 67 | } catch (err) { 68 | t.pass('error thrown') 69 | t.equal(err.message, 'networkId must be a string', 'correct error message') 70 | t.end() 71 | } 72 | }) 73 | 74 | t.test('invalid networkId', function (t) { 75 | try { 76 | var swarm = Swarm(123) 77 | t.notOk(swarm, 'should have thrown') 78 | } catch (err) { 79 | t.pass('error thrown') 80 | t.equal(err.message, 'networkId must be a string', 'correct error message') 81 | t.end() 82 | } 83 | }) 84 | 85 | nodeTest(t, 'no webrtc implementation', function (t) { 86 | try { 87 | var swarm = Swarm('somenet') 88 | t.notOk(swarm, 'should have thrown') 89 | } catch (err) { 90 | t.pass('error thrown') 91 | t.equal(err.message, 'No WebRTC implementation found, please pass one in as the "wrtc" option (for example, the "wrtc" or "electron-webrtc" packages).', 'correct error message') 92 | t.end() 93 | } 94 | }) 95 | 96 | nodeTest(t, 'with webrtc implementation', function (t) { 97 | var swarm = Swarm('somenet', { wrtc: {} }) 98 | t.ok(swarm instanceof Swarm, 'created swarm') 99 | t.end() 100 | }) 101 | 102 | browserTest(t, 'with builtin webrtc implementation', function (t) { 103 | var swarm = Swarm('somenet') 104 | t.ok(swarm instanceof Swarm, 'created swarm') 105 | t.end() 106 | }) 107 | 108 | t.end() 109 | }) 110 | 111 | test('connect', function (t) { 112 | t.test('simple connect', function (t) { 113 | t.plan(18) 114 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 115 | var swarm2 = Swarm('somenet', { wrtc: wrtc }) 116 | var streams = createStreams() 117 | swarm1.on('peer', function (peer) { 118 | t.pass('got "peer" event from swarm1') 119 | t.equal(swarm1.peers.length, 1, 'correct peers length') 120 | t.equal(swarm1.peers[0], peer, 'correct peer object') 121 | }) 122 | swarm2.on('peer', function (peer) { 123 | t.pass('got "peer" event from swarm2') 124 | t.equal(swarm2.peers.length, 1, 'correct peers length') 125 | t.equal(swarm2.peers[0], peer, 'correct peer object') 126 | }) 127 | swarm1.on('connect', function (stream) { 128 | t.pass('received "connect" event from swarm1') 129 | stream.write('123') 130 | stream.once('data', function (data) { 131 | t.pass('received stream data') 132 | t.equal(data.toString(), '456', 'correct data') 133 | }) 134 | }) 135 | swarm2.on('connect', function (stream) { 136 | t.pass('received "connect" event from swarm2') 137 | stream.write('456') 138 | stream.once('data', function (data) { 139 | t.pass('received stream data') 140 | t.equal(data.toString(), '123', 'correct data') 141 | }) 142 | }) 143 | swarm1.connect(streams[0], function (err, peer) { 144 | t.pass('swarm1 connect callback called') 145 | t.error(err, 'no error') 146 | t.ok(peer, 'got peer object') 147 | }) 148 | swarm2.connect(streams[1], { incoming: true }, function (err, peer) { 149 | t.pass('swarm2 connect callback called') 150 | t.error(err, 'no error') 151 | t.ok(peer, 'got peer object') 152 | }) 153 | }) 154 | 155 | t.test('connect with incompatible networks', function (t) { 156 | t.plan(4) 157 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 158 | var swarm2 = Swarm('somenet2', { wrtc: wrtc }) 159 | var streams = createStreams() 160 | swarm1.connect(streams[0], function (err, peer) { 161 | t.ok(err, 'got error') 162 | t.equal(err.message, 'Peer does not have any networks in common.', 'correct error message') 163 | }) 164 | swarm2.connect(streams[1], { incoming: true }, function (err, peer) { 165 | t.ok(err, 'got error') 166 | t.equal(err.message, 'Peer does not have any networks in common.', 'correct error message') 167 | }) 168 | }) 169 | 170 | t.test('connect with 2 outgoing', function (t) { 171 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 172 | var swarm2 = Swarm('somenet', { wrtc: wrtc }) 173 | var streams = createStreams() 174 | swarm2.on('error', function (err) { 175 | t.pass('got "error" event from swarm2') 176 | t.equal(err.message, 'Peer tried to connect to network "somenet" twice', 'correct error message') 177 | t.end() 178 | }) 179 | swarm1.connect(streams[0]) 180 | swarm2.connect(streams[1]) 181 | }) 182 | }) 183 | 184 | test('accept', function (t) { 185 | t.test('simple accept', function (t) { 186 | t.plan(18) 187 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 188 | var swarm2 = Swarm('somenet', { wrtc: wrtc }) 189 | var streams = createStreams() 190 | swarm1.on('peer', function (peer) { 191 | t.pass('got "peer" event from swarm1') 192 | t.equal(swarm1.peers.length, 1, 'correct peers length') 193 | t.equal(swarm1.peers[0], peer, 'correct peer object') 194 | }) 195 | swarm2.on('peer', function (peer) { 196 | t.pass('got "peer" event from swarm2') 197 | t.equal(swarm2.peers.length, 1, 'correct peers length') 198 | t.equal(swarm2.peers[0], peer, 'correct peer object') 199 | }) 200 | swarm1.on('connect', function (stream) { 201 | t.pass('received "connect" event from swarm1') 202 | stream.write('123') 203 | stream.once('data', function (data) { 204 | t.pass('received stream data') 205 | t.equal(data.toString(), '456', 'correct data') 206 | }) 207 | }) 208 | swarm2.on('connect', function (stream) { 209 | t.pass('received "connect" event from swarm2') 210 | stream.write('456') 211 | stream.once('data', function (data) { 212 | t.pass('received stream data') 213 | t.equal(data.toString(), '123', 'correct data') 214 | }) 215 | }) 216 | swarm1.connect(streams[0], function (err, peer) { 217 | t.pass('swarm1 connect callback called') 218 | t.error(err, 'no error') 219 | t.ok(peer, 'got peer object') 220 | }) 221 | swarm2.accept(streams[1], function (err, peer) { 222 | t.pass('swarm2 connect callback called') 223 | t.error(err, 'no error') 224 | t.ok(peer, 'got peer object') 225 | }) 226 | }) 227 | 228 | t.test('accept with opts', function (t) { 229 | t.plan(18) 230 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 231 | var swarm2 = Swarm('somenet', { wrtc: wrtc }) 232 | var streams = createStreams() 233 | swarm1.on('peer', function (peer) { 234 | t.pass('got "peer" event from swarm1') 235 | t.equal(swarm1.peers.length, 1, 'correct peers length') 236 | t.equal(swarm1.peers[0], peer, 'correct peer object') 237 | }) 238 | swarm2.on('peer', function (peer) { 239 | t.pass('got "peer" event from swarm2') 240 | t.equal(swarm2.peers.length, 1, 'correct peers length') 241 | t.equal(swarm2.peers[0], peer, 'correct peer object') 242 | }) 243 | swarm1.on('connect', function (stream) { 244 | t.pass('received "connect" event from swarm1') 245 | stream.write('123') 246 | stream.once('data', function (data) { 247 | t.pass('received stream data') 248 | t.equal(data.toString(), '456', 'correct data') 249 | }) 250 | }) 251 | swarm2.on('connect', function (stream) { 252 | t.pass('received "connect" event from swarm2') 253 | stream.write('456') 254 | stream.once('data', function (data) { 255 | t.pass('received stream data') 256 | t.equal(data.toString(), '123', 'correct data') 257 | }) 258 | }) 259 | swarm1.connect(streams[0], function (err, peer) { 260 | t.pass('swarm1 connect callback called') 261 | t.error(err, 'no error') 262 | t.ok(peer, 'got peer object') 263 | }) 264 | swarm2.accept(streams[1], { foo: true }, function (err, peer) { 265 | t.pass('swarm2 connect callback called') 266 | t.error(err, 'no error') 267 | t.ok(peer, 'got peer object') 268 | }) 269 | }) 270 | }) 271 | 272 | test('disconnect', function (t) { 273 | t.test('end connection with peer.close()', function (t) { 274 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 275 | var swarm2 = Swarm('somenet', { wrtc: wrtc }) 276 | 277 | t.test('setup', function (t) { 278 | t.plan(10) 279 | var streams = createStreams() 280 | swarm1.on('peer', function (peer) { 281 | t.pass('got "peer" event from swarm1') 282 | t.equal(swarm1.peers.length, 1, 'correct peers length') 283 | t.equal(swarm1.peers[0], peer, 'correct peer object') 284 | }) 285 | swarm2.on('peer', function (peer) { 286 | t.pass('got "peer" event from swarm2') 287 | t.equal(swarm2.peers.length, 1, 'correct peers length') 288 | t.equal(swarm2.peers[0], peer, 'correct peer object') 289 | }) 290 | swarm1.connect(streams[0], function (err, peer) { 291 | t.error(err, 'no error') 292 | t.pass('swarm1 connect callback called') 293 | }) 294 | swarm2.accept(streams[1], function (err, peer) { 295 | t.error(err, 'no error') 296 | t.pass('swarm2 connect callback called') 297 | }) 298 | }) 299 | 300 | t.test('disconnect', function (t) { 301 | t.plan(4) 302 | swarm1.on('disconnect', function (peer) { 303 | t.pass('got "disconnect" event from swarm1') 304 | t.equal(swarm1.peers.length, 0, 'swarm1.peers is now empty') 305 | }) 306 | swarm2.on('disconnect', function (peer) { 307 | t.pass('got "disconnect" event from swarm2') 308 | t.equal(swarm2.peers.length, 0, 'swarm2.peers is now empty') 309 | }) 310 | swarm1.peers[0].close() 311 | }) 312 | 313 | t.end() 314 | }) 315 | 316 | t.test('end connection with socket.destroy()', function (t) { 317 | var swarm1 = Swarm('somenet', { wrtc: wrtc }) 318 | var swarm2 = Swarm('somenet', { wrtc: wrtc }) 319 | 320 | t.test('setup', function (t) { 321 | t.plan(11) 322 | createRTCStreams(function (err, streams) { 323 | t.error(err, 'no error') 324 | swarm1.on('peer', function (peer) { 325 | t.pass('got "peer" event from swarm1') 326 | t.equal(swarm1.peers.length, 1, 'correct peers length') 327 | t.equal(swarm1.peers[0], peer, 'correct peer object') 328 | }) 329 | swarm2.on('peer', function (peer) { 330 | t.pass('got "peer" event from swarm2') 331 | t.equal(swarm2.peers.length, 1, 'correct peers length') 332 | t.equal(swarm2.peers[0], peer, 'correct peer object') 333 | }) 334 | swarm1.connect(streams[0], function (err, peer) { 335 | t.error(err, 'no error') 336 | t.pass('swarm1 connect callback called') 337 | }) 338 | swarm2.accept(streams[1], function (err, peer) { 339 | t.error(err, 'no error') 340 | t.pass('swarm2 connect callback called') 341 | }) 342 | }) 343 | }) 344 | 345 | t.test('disconnect', function (t) { 346 | t.plan(4) 347 | swarm1.on('disconnect', function (peer) { 348 | t.pass('got "disconnect" event from swarm1') 349 | t.equal(swarm1.peers.length, 0, 'swarm1.peers is now empty') 350 | }) 351 | swarm2.on('disconnect', function (peer) { 352 | t.pass('got "disconnect" event from swarm2') 353 | t.equal(swarm2.peers.length, 0, 'swarm2.peers is now empty') 354 | }) 355 | swarm1.peers[0].socket.destroy() 356 | }) 357 | 358 | t.end() 359 | }) 360 | }) 361 | 362 | test('getNewPeer', function (t) { 363 | var swarm1 = Swarm('somenet', { wrtc: wrtc, rtcConfig: rtcConfig }) 364 | var swarm2 = Swarm('somenet', { wrtc: wrtc, rtcConfig: rtcConfig }) 365 | var swarm3 = Swarm('somenet', { wrtc: wrtc, rtcConfig: rtcConfig, acceptIncoming: true }) 366 | 367 | t.test('getNewPeer with no other peers', function (t) { 368 | t.test('setup', function (t) { 369 | t.plan(2) 370 | var streams = createStreams() 371 | swarm1.connect(streams[0], function (err, peer) { 372 | t.error(err, 'no error') 373 | }) 374 | swarm2.accept(streams[1], function (err, peer) { 375 | t.error(err, 'no error') 376 | }) 377 | }) 378 | 379 | t.test('getNewPeer', function (t) { 380 | swarm1.getNewPeer(function (err, peer) { 381 | t.pass('getNewPeer callback called') 382 | t.ok(err, 'got error') 383 | t.notOk(peer, 'no peer returned') 384 | t.equal(err.message, 'Peer did not return any candidates', 'correct error message') 385 | t.end() 386 | }) 387 | }) 388 | 389 | t.end() 390 | }) 391 | 392 | t.test('simple getNewPeer', function (t) { 393 | t.test('setup', function (t) { 394 | t.plan(2) 395 | var streams = createStreams() 396 | swarm3.connect(streams[0], function (err, peer) { 397 | t.error(err, 'no error') 398 | }) 399 | swarm2.accept(streams[1], function (err, peer) { 400 | t.error(err, 'no error') 401 | }) 402 | }) 403 | t.test('getNewPeer', function (t) { 404 | t.plan(9) 405 | swarm1.on('connect', function (stream) { 406 | t.pass('swarm1 emitted "connect" event') 407 | stream.write('foo') 408 | stream.on('data', function (data) { 409 | t.pass('got stream data') 410 | t.equal(data.toString(), 'bar', 'correct data') 411 | }) 412 | }) 413 | swarm3.on('connect', function (stream) { 414 | t.pass('swarm2 emitted "connect" event') 415 | stream.write('bar') 416 | stream.on('data', function (data) { 417 | t.pass('got stream data') 418 | t.equal(data.toString(), 'foo', 'correct data') 419 | }) 420 | }) 421 | swarm1.getNewPeer(function (err, stream) { 422 | t.pass('getNewPeer callback called') 423 | t.error(err, 'no error') 424 | t.ok(stream, 'got stream object') 425 | }) 426 | }) 427 | t.end() 428 | }) 429 | 430 | t.test('cleanup', function (t) { 431 | swarm1.close() 432 | swarm2.close() 433 | swarm3.close() 434 | t.end() 435 | }) 436 | 437 | t.end() 438 | }) 439 | -------------------------------------------------------------------------------- /test/travis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z $NODE ]; then 4 | export NODE=6 5 | fi 6 | 7 | echo Installing nvm... 8 | git clone https://github.com/creationix/nvm.git /tmp/.nvm 9 | source /tmp/.nvm/nvm.sh 10 | echo Installed nvm version `nvm --version` 11 | nvm install $NODE 12 | nvm use $NODE 13 | nvm alias default $NODE 14 | echo node version: `node --version` 15 | echo npm version: `npm --version` 16 | npm install 17 | echo Install completed 18 | 19 | if [ $BROWSER ]; then 20 | npm run test-browser 21 | else 22 | HEADLESS=1 npm test 23 | fi 24 | --------------------------------------------------------------------------------