├── .gitignore ├── package.json ├── README.md ├── LICENSE └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peer-wire-swarm", 3 | "version": "0.12.2", 4 | "description": "a peer swarm implementation", 5 | "repository": "git://github.com/mafintosh/peer-wire-swarm.git", 6 | "dependencies": { 7 | "buffer-from": "^1.0.0", 8 | "fifo": "^0.1.4", 9 | "once": "^1.1.1", 10 | "peer-wire-protocol": "^0.7.0", 11 | "speedometer": "^0.1.2", 12 | "utp": "0.0.7" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peer-wire-swarm 2 | 3 | Swarm implementation for Bittorrent 4 | 5 | npm install peer-wire-swarm 6 | 7 | # Usage 8 | 9 | ``` js 10 | var wireSwarm = require('peer-wire-swarm'); 11 | var swarm = wireSwarm(myInfoHash, myPeerId); 12 | 13 | swarm.on('wire', function(wire) { 14 | // a relevant peer-wire-protocol as appeared 15 | // see the peer-wire-protocol module for more info 16 | 17 | wire.on('unchoke', function() { 18 | // we are now unchoked 19 | }); 20 | 21 | swarm.wires // <- list of all connected wires 22 | }); 23 | 24 | swarm.add('127.0.0.1:42442'); // add a peer 25 | swarm.remove('127.0.0.1:42244'); // remove a peer 26 | 27 | swarm.pause(); // pause the swarm (stops adding connections) 28 | swarm.resume(); // resume the swarms 29 | 30 | swarm.listen(6881); // listen for incoming connections (optional) 31 | ``` 32 | 33 | ## License 34 | 35 | MIT -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mathias Buus Madsen 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var utp = require('utp'); 2 | var net = require('net'); 3 | var fifo = require('fifo'); 4 | var once = require('once'); 5 | var speedometer = require('speedometer'); 6 | var peerWireProtocol = require('peer-wire-protocol'); 7 | var EventEmitter = require('events').EventEmitter; 8 | var util = require('util'); 9 | var bufferFrom = require('buffer-from') 10 | 11 | var HANDSHAKE_TIMEOUT = 25000; 12 | var CONNECTION_TIMEOUT = 3000; 13 | var RECONNECT_WAIT = [1000, 5000, 15000]; 14 | var DEFAULT_SIZE = 100; 15 | 16 | var toBuffer = function(str, encoding) { 17 | return Buffer.isBuffer(str) ? str : bufferFrom(str, encoding); 18 | }; 19 | 20 | var toAddress = function(wire) { 21 | if (typeof wire === 'string') return wire; 22 | return wire.peerAddress; 23 | }; 24 | 25 | var onwire = function(swarm, connection, onhandshake, isServer) { 26 | var wire = peerWireProtocol(swarm._pwp); 27 | 28 | var destroy = function() { 29 | connection.destroy(); 30 | connection.emit('timeout'); 31 | }; 32 | 33 | var connectTimeout = !isServer && setTimeout(destroy, swarm.connectTimeout); 34 | var handshakeTimeout = setTimeout(destroy, swarm.handshakeTimeout); 35 | 36 | if (handshakeTimeout.unref) handshakeTimeout.unref(); 37 | if (connectTimeout.unref) connectTimeout.unref(); 38 | 39 | connection.on('connect', function() { 40 | clearTimeout(connectTimeout); 41 | }); 42 | 43 | wire.once('handshake', function(infoHash, peerId) { 44 | clearTimeout(handshakeTimeout); 45 | onhandshake(infoHash, peerId); 46 | }); 47 | 48 | connection.on('end', function() { 49 | connection.destroy(); 50 | }); 51 | 52 | connection.on('error', function() { 53 | connection.destroy(); 54 | }); 55 | 56 | connection.on('close', function() { 57 | clearTimeout(connectTimeout); 58 | clearTimeout(handshakeTimeout); 59 | wire.destroy(); 60 | }); 61 | 62 | connection.pipe(wire).pipe(connection); 63 | return wire; 64 | }; 65 | 66 | var pools = {}; 67 | 68 | var leave = function(port, swarm) { 69 | if (!pools[port]) return; 70 | delete pools[port].swarms[swarm.infoHash.toString('hex')]; 71 | 72 | if (Object.keys(pools[port].swarms).length) return; 73 | pools[port].servers.forEach(function(server) { 74 | server.close(); 75 | }); 76 | delete pools[port]; 77 | }; 78 | 79 | var join = function(port, swarm) { 80 | var pool = pools[port]; 81 | 82 | if (!pool) { 83 | var swarms = {}; 84 | var servers = []; 85 | 86 | var onconnection = function(connection) { 87 | var wire = onwire(swarm, connection, function(infoHash, peerId) { 88 | var swarm = swarms[infoHash.toString('hex')]; 89 | if (!swarm) return connection.destroy(); 90 | swarm._onincoming(connection, wire); 91 | }, true); 92 | } 93 | 94 | servers.push(net.createServer(onconnection)); 95 | if (swarm.utp) servers.push(utp.createServer(onconnection)); 96 | 97 | var loop = function(i) { 98 | if (i < servers.length) return servers[i].listen(port, loop.bind(null, i+1)) 99 | pool.listening = true; 100 | Object.keys(swarms).forEach(function(infoHash) { 101 | swarms[infoHash].emit('listening'); 102 | }); 103 | } 104 | 105 | loop(0) 106 | 107 | pool = pools[port] = { 108 | servers: servers, 109 | swarms: swarms, 110 | listening: false 111 | }; 112 | } 113 | 114 | var infoHash = swarm.infoHash.toString('hex'); 115 | 116 | if (pool.listening) { 117 | process.nextTick(function() { 118 | swarm.emit('listening'); 119 | }); 120 | } 121 | if (pool.swarms[infoHash]) { 122 | process.nextTick(function() { 123 | swarm.emit('error', new Error('port and info hash already in use')); 124 | }); 125 | return; 126 | } 127 | 128 | pool.swarms[infoHash] = swarm; 129 | }; 130 | 131 | var Swarm = function(infoHash, peerId, options) { 132 | if (!(this instanceof Swarm)) return new Swarm(infoHash, peerId, options); 133 | EventEmitter.call(this); 134 | 135 | options = options || {}; 136 | this.handshake = options.handshake; 137 | 138 | this.port = 0; 139 | this.size = options.size || DEFAULT_SIZE; 140 | this.utp = options.utp || false; 141 | this.handshakeTimeout = options.handshakeTimeout || HANDSHAKE_TIMEOUT; 142 | this.connectTimeout = options.connectTimeout || CONNECTION_TIMEOUT; 143 | 144 | this.infoHash = toBuffer(infoHash, 'hex'); 145 | this.peerId = toBuffer(peerId, 'utf-8'); 146 | 147 | this.downloaded = 0; 148 | this.uploaded = 0; 149 | this.connections = []; 150 | this.wires = []; 151 | this.paused = false; 152 | 153 | this.uploaded = 0; 154 | this.downloaded = 0; 155 | 156 | this.downloadSpeed = speedometer(); 157 | this.uploadSpeed = speedometer(); 158 | 159 | this._destroyed = false; 160 | this._queues = [fifo()]; 161 | this._peers = {}; 162 | this._pwp = {speed:options.speed}; 163 | }; 164 | 165 | util.inherits(Swarm, EventEmitter); 166 | 167 | Swarm.prototype.__defineGetter__('queued', function() { 168 | return this._queues.reduce(function(prev, queue) { 169 | return prev + queue.length; 170 | }, 0); 171 | }); 172 | 173 | Swarm.prototype.pause = function() { 174 | this.paused = true; 175 | }; 176 | 177 | Swarm.prototype.resume = function() { 178 | this.paused = false; 179 | this._drain(); 180 | }; 181 | 182 | Swarm.prototype.priority = function(addr, level) { 183 | addr = toAddress(addr); 184 | var peer = this._peers[addr]; 185 | 186 | if (!peer) return 0; 187 | if (typeof level !== 'number' || peer.priority === level) return level; 188 | 189 | if (!this._queues[level]) this._queues[level] = fifo(); 190 | 191 | if (peer.node) { 192 | this._queues[peer.priority].remove(peer.node); 193 | peer.node = this._queues[level].push(addr); 194 | } 195 | 196 | return peer.priority = level; 197 | }; 198 | 199 | Swarm.prototype.add = function(addr) { 200 | if (this._destroyed || this._peers[addr]) return; 201 | 202 | var port = Number(addr.split(':')[1]); 203 | if (!(port > 0 && port < 65535)) return; 204 | 205 | this._peers[addr] = { 206 | node: this._queues[0].push(addr), 207 | wire: null, 208 | timeout: null, 209 | reconnect: false, 210 | priority: 0, 211 | retries: 0, 212 | noUtp: false, 213 | }; 214 | 215 | this._drain(); 216 | }; 217 | 218 | Swarm.prototype.remove = function(addr) { 219 | this._remove(toAddress(addr)); 220 | this._drain(); 221 | }; 222 | 223 | Swarm.prototype.listen = function(port, onlistening) { 224 | if (onlistening) this.once('listening', onlistening); 225 | this.port = port; 226 | join(this.port, this); 227 | }; 228 | 229 | Swarm.prototype.destroy = function() { 230 | this._destroyed = true; 231 | 232 | var self = this; 233 | Object.keys(this._peers).forEach(function(addr) { 234 | self._remove(addr); 235 | }); 236 | 237 | this.wires.forEach(function (wire) { 238 | wire.destroy(); 239 | }); 240 | 241 | leave(this.port, this); 242 | process.nextTick(function() { 243 | self.emit('close'); 244 | }); 245 | }; 246 | 247 | Swarm.prototype._remove = function(addr) { 248 | var peer = this._peers[addr]; 249 | if (!peer) return; 250 | delete this._peers[addr]; 251 | if (peer.node) this._queues[peer.priority].remove(peer.node); 252 | if (peer.timeout) clearTimeout(peer.timeout); 253 | if (peer.wire) peer.wire.destroy(); 254 | }; 255 | 256 | Swarm.prototype._drain = function() { 257 | if (this.connections.length >= this.size || this.paused) return; 258 | 259 | var self = this; 260 | var addr = this._shift(); 261 | if (!addr) return; 262 | 263 | var peer = this._peers[addr]; 264 | if (!peer) return; 265 | 266 | var repush = function() { 267 | peer.node = self._queues[peer.priority].push(addr); 268 | self._drain(); 269 | }; 270 | 271 | var parts = addr.split(':'); 272 | var connection; 273 | 274 | if(this.utp && !peer.noUtp) { 275 | connection = utp.connect(parts[1], parts[0]); 276 | connection.on('timeout', function() { 277 | // Unable to connect to peer with uTP 278 | // Assume it doesn't support it. 279 | peer.noUtp = true; 280 | repush(); 281 | }); 282 | } else { 283 | connection = net.connect(parts[1], parts[0]); 284 | } 285 | 286 | if (peer.timeout) clearTimeout(peer.timeout); 287 | 288 | peer.node = null; 289 | peer.timeout = null; 290 | 291 | var wire = onwire(this, connection, function(infoHash) { 292 | if (infoHash.toString('hex') !== self.infoHash.toString('hex')) return connection.destroy(); 293 | peer.reconnect = true; 294 | peer.retries = 0; 295 | self._onwire(connection, wire); 296 | }); 297 | 298 | wire.on('end', function() { 299 | peer.wire = null; 300 | if (!peer.reconnect || self._destroyed || peer.retries >= RECONNECT_WAIT.length) return self._remove(addr); 301 | peer.timeout = setTimeout(repush, RECONNECT_WAIT[peer.retries++]); 302 | }); 303 | 304 | peer.wire = wire; 305 | self._onconnection(connection); 306 | 307 | wire.peerAddress = addr; 308 | wire.handshake(this.infoHash, this.peerId, this.handshake); 309 | }; 310 | 311 | Swarm.prototype._shift = function() { 312 | for (var i = this._queues.length-1; i >= 0; i--) { 313 | if (this._queues[i] && this._queues[i].length) return this._queues[i].shift(); 314 | } 315 | return null; 316 | }; 317 | 318 | Swarm.prototype._onincoming = function(connection, wire) { 319 | wire.peerAddress = connection.address().address + ':' + connection.address().port; 320 | wire.handshake(this.infoHash, this.peerId, this.handshake); 321 | 322 | this._onconnection(connection); 323 | this._onwire(connection, wire); 324 | }; 325 | 326 | Swarm.prototype._onconnection = function(connection) { 327 | var self = this; 328 | 329 | connection.once('close', function() { 330 | self.connections.splice(self.connections.indexOf(connection), 1); 331 | self._drain(); 332 | }); 333 | 334 | this.connections.push(connection); 335 | }; 336 | 337 | Swarm.prototype._onwire = function(connection, wire) { 338 | var self = this; 339 | 340 | wire.on('download', function(downloaded) { 341 | self.downloaded += downloaded; 342 | self.downloadSpeed(downloaded); 343 | self.emit('download', downloaded); 344 | }); 345 | 346 | wire.on('upload', function(uploaded) { 347 | self.uploaded += uploaded; 348 | self.uploadSpeed(uploaded); 349 | self.emit('upload', uploaded); 350 | }); 351 | 352 | var cleanup = once(function() { 353 | self.emit('wire-disconnect', wire, connection); 354 | self.wires.splice(self.wires.indexOf(wire), 1); 355 | connection.destroy(); 356 | }); 357 | 358 | connection.on('close', cleanup); 359 | connection.on('error', cleanup); 360 | connection.on('end', cleanup); 361 | wire.on('end', cleanup); 362 | wire.on('close', cleanup); 363 | wire.on('finish', cleanup); 364 | 365 | this.wires.push(wire); 366 | this.emit('wire', wire, connection); 367 | }; 368 | 369 | module.exports = Swarm; 370 | --------------------------------------------------------------------------------