peer-wire-protocol
Version:
Stream implementing the peer wire protocol used in bittorrent
366 lines (302 loc) • 10.6 kB
JavaScript
var Duplex = require('stream').Duplex || require('readable-stream').Duplex;
var EventEmitter = require('events').EventEmitter;
var bitfield = require('bitfield');
var util = require('util');
var bncode = require('bncode');
var speedometer = require('speedometer');
var bufferFrom = require('buffer-from');
var bufferAlloc = require('buffer-alloc');
var MESSAGE_PROTOCOL = bufferFrom([0x13,0x42,0x69,0x74,0x54,0x6f,0x72,0x72,0x65,0x6e,0x74,0x20,0x70,0x72,0x6f,0x74,0x6f,0x63,0x6f,0x6c]);
var MESSAGE_KEEP_ALIVE = bufferFrom([0x00,0x00,0x00,0x00]);
var MESSAGE_CHOKE = bufferFrom([0x00,0x00,0x00,0x01,0x00]);
var MESSAGE_UNCHOKE = bufferFrom([0x00,0x00,0x00,0x01,0x01]);
var MESSAGE_INTERESTED = bufferFrom([0x00,0x00,0x00,0x01,0x02]);
var MESSAGE_UNINTERESTED = bufferFrom([0x00,0x00,0x00,0x01,0x03]);
var MESSAGE_RESERVED = [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00];
var MESSAGE_PORT = [0x00,0x00,0x00,0x03,0x09,0x00,0x00];
var noop = function() {};
var pull = function(requests, piece, offset, length) {
for (var i = 0; i < requests.length; i++) {
var req = requests[i];
if (req.piece !== piece || req.offset !== offset || req.length !== length) continue;
if (i === 0) requests.shift();
else requests.splice(i, 1);
return req;
}
return null;
};
var Request = function(piece, offset, length, callback) {
this.piece = piece;
this.offset = offset;
this.length = length;
this.callback = callback;
this.timeout = null;
};
var Wire = function(opts) {
if (!(this instanceof Wire)) return new Wire(opts);
if (!opts) opts = {};
Duplex.call(this);
var self = this;
this.amChoking = true;
this.amInterested = false;
this.peerChoking = true;
this.peerInterested = false;
this.peerPieces = [];
this.peerExtensions = {};
this.peerAddress = null; // external
this.uploaded = 0;
this.downloaded = 0;
this.uploadSpeed = speedometer(opts.speed);
this.downloadSpeed = speedometer(opts.speed);
this.requests = [];
this.peerRequests = [];
this._keepAlive = null;
this._finished = false;
this.on('finish', function() {
self._finished = true;
self.push(null); // cannot be half open
clearInterval(self._keepAlive);
self._parse(Number.MAX_VALUE, noop);
while (self.peerRequests.length) self.peerRequests.pop();
while (self.requests.length) self._callback(self.requests.shift(), new Error('wire is closed'), null);
});
var ontimeout = function() {
self._callback(self.requests.shift(), new Error('request has timed out'), null);
self.emit('timeout');
};
this._timeout = 0;
this._ontimeout = ontimeout;
var onmessagelength = function(buffer) {
var length = buffer.readUInt32BE(0);
if (length) return self._parse(length, onmessage);
self._parse(4, onmessagelength);
self.emit('keep-alive');
};
var onmessage = function(buffer) {
self._parse(4, onmessagelength);
switch (buffer[0]) {
case 0:
return self._onchoke();
case 1:
return self._onunchoke();
case 2:
return self._oninterested();
case 3:
return self._onuninterested();
case 4:
return self._onhave(buffer.readUInt32BE(1));
case 5:
return self._onbitfield(buffer.slice(1));
case 6:
return self._onrequest(buffer.readUInt32BE(1), buffer.readUInt32BE(5), buffer.readUInt32BE(9));
case 7:
return self._onpiece(buffer.readUInt32BE(1), buffer.readUInt32BE(5), buffer.slice(9));
case 8:
return self._oncancel(buffer.readUInt32BE(1), buffer.readUInt32BE(5), buffer.readUInt32BE(9));
case 9:
return self._onport(buffer.readUInt16BE(1));
case 20:
return self._onextended(buffer.readUInt8(1), buffer.slice(2));
}
self.emit('unknownmessage', buffer);
};
this._buffer = [];
this._bufferSize = 0;
this._parser = null;
this._parserSize = 0;
this._parse(1, function(buffer) {
var pstrlen = buffer.readUInt8(0);
self._parse(pstrlen + 48, function(handshake) {
handshake = handshake.slice(pstrlen);
self._onhandshake(handshake.slice(8, 28), handshake.slice(28, 48), {
dht: !!(handshake[7] & 1),
extended: !!(handshake[5] & 0x10)
});
self._parse(4, onmessagelength);
});
});
};
util.inherits(Wire, Duplex);
Wire.prototype.handshake = function(infoHash, peerId, extensions) {
if (typeof infoHash === 'string') infoHash = bufferFrom(infoHash, 'hex');
if (typeof peerId === 'string') peerId = bufferFrom(peerId);
if (infoHash.length !== 20 || peerId.length !== 20) throw new Error('infoHash and peerId MUST have length 20');
var reserved = bufferFrom(MESSAGE_RESERVED);
if (extensions && extensions.dht) reserved[7] |= 1;
reserved[5] |= 0x10; // enable extended message
this._push(Buffer.concat([MESSAGE_PROTOCOL, reserved, infoHash, peerId], MESSAGE_PROTOCOL.length+48));
};
Wire.prototype.choke = function() {
if (this.amChoking) return;
this.amChoking = true;
while (this.peerRequests.length) this.peerRequests.pop();
this._push(MESSAGE_CHOKE);
};
Wire.prototype.unchoke = function() {
if (!this.amChoking) return;
this.amChoking = false;
this._push(MESSAGE_UNCHOKE);
};
Wire.prototype.interested = function() {
if (this.amInterested) return;
this.amInterested = true;
this._push(MESSAGE_INTERESTED);
};
Wire.prototype.uninterested = function() {
if (!this.amInterested) return;
this.amInterested = false;
this._push(MESSAGE_UNINTERESTED);
};
Wire.prototype.have = function(i) {
this._message(4, [i], null);
};
Wire.prototype.bitfield = function(bitfield) {
if (bitfield.buffer) bitfield = bitfield.buffer; // support bitfield objects
this._message(5, [], bitfield);
};
Wire.prototype.request = function(i, offset, length, callback) {
if (!callback) callback = noop;
if (this._finished) return callback(new Error('wire is closed'));
if (this.peerChoking) return callback(new Error('peer is choking'));
this.requests.push(new Request(i, offset, length, callback));
this._updateTimeout();
this._message(6, [i, offset, length], null);
};
Wire.prototype.piece = function(i, offset, buffer) {
this.uploaded += buffer.length;
this.uploadSpeed(buffer.length);
this.emit('upload', buffer.length);
this._message(7, [i, offset], buffer);
};
Wire.prototype.cancel = function(i, offset, length) {
this._callback(pull(this.requests, i, offset, length), new Error('request was cancelled'), null);
this._message(8, [i, offset, length], null);
};
Wire.prototype.extended = function(id, msg) {
this._message(20, [], Buffer.concat([bufferFrom([id]), Buffer.isBuffer(msg) ? msg : bncode.encode(msg)]));
};
Wire.prototype.port = function(port) {
var message = bufferFrom(MESSAGE_PORT);
message.writeUInt16BE(port, 5);
this._push(message);
};
Wire.prototype.setKeepAlive = function(bool) {
clearInterval(this._keepAlive);
if (bool === false) return;
this._keepAlive = setInterval(this._push.bind(this, MESSAGE_KEEP_ALIVE), 60000);
};
Wire.prototype.setTimeout = function(ms, fn) {
if (this.requests.length) clearTimeout(this.requests[0].timeout);
this._timeout = ms;
this._updateTimeout();
if (fn) this.on('timeout', fn);
};
Wire.prototype.destroy = function() {
this.emit('close');
this.end();
};
// inbound
Wire.prototype._onhandshake = function(infoHash, peerId, extensions) {
this.peerExtensions = extensions;
this.emit('handshake', infoHash, peerId, extensions);
};
Wire.prototype._oninterested = function() {
this.peerInterested = true;
this.emit('interested');
};
Wire.prototype._onuninterested = function() {
this.peerInterested = false;
this.emit('uninterested');
};
Wire.prototype._onchoke = function() {
this.peerChoking = true;
this.emit('choke');
while (this.requests.length) this._callback(this.requests.shift(), new Error('peer is choking'), null);
};
Wire.prototype._onunchoke = function() {
this.peerChoking = false;
this.emit('unchoke');
};
Wire.prototype._onbitfield = function(buffer) {
var pieces = bitfield(buffer);
for (var i = 0; i < 8 * buffer.length; i++) {
this.peerPieces[i] = pieces.get(i);
}
this.emit('bitfield', buffer);
};
Wire.prototype._onhave = function(i) {
this.peerPieces[i] = true;
this.emit('have', i);
};
Wire.prototype._onrequest = function(i, offset, length) {
if (this.amChoking) return;
var self = this;
var respond = function(err, buffer) {
if (request !== pull(self.peerRequests, i, offset, length)) return;
if (err) return;
self.piece(i, offset, buffer);
};
var request = new Request(i, offset, length, respond);
this.peerRequests.push(request);
this.emit('request', i, offset, length, respond);
};
Wire.prototype._oncancel = function(i, offset, length) {
pull(this.peerRequests, i, offset, length);
this.emit('cancel', i, offset, length);
};
Wire.prototype._onpiece = function(i, offset, buffer) {
this._callback(pull(this.requests, i, offset, buffer.length), null, buffer);
this.downloaded += buffer.length;
this.downloadSpeed(buffer.length);
this.emit('download', buffer.length);
this.emit('piece', i, offset, buffer);
};
Wire.prototype._onport = function(port) {
this.emit('port', port);
};
Wire.prototype._onextended = function(id, ext) {
this.emit('extended', id, ext);
};
// helpers and streams
Wire.prototype._callback = function(request, err, buffer) {
if (!request) return;
if (request.timeout) clearTimeout(request.timeout);
if (!this.peerChoking && !this._finished) this._updateTimeout();
request.callback(err, buffer);
};
Wire.prototype._updateTimeout = function() {
if (!this._timeout || !this.requests.length || this.requests[0].timeout) return;
this.requests[0].timeout = setTimeout(this._ontimeout, this._timeout);
};
Wire.prototype._message = function(id, numbers, data) {
var dataLength = data ? data.length : 0;
var buffer = bufferAlloc(5 + 4 * numbers.length);
buffer.writeUInt32BE(buffer.length + dataLength - 4, 0);
buffer[4] = id;
for (var i = 0; i < numbers.length; i++) {
buffer.writeUInt32BE(numbers[i], 5 + 4 * i);
}
this._push(buffer);
if (data) this._push(data);
};
Wire.prototype._push = function(data) {
if (this._finished) return;
this.push(data);
};
Wire.prototype._parse = function(size, parser) {
this._parserSize = size;
this._parser = parser;
};
Wire.prototype._write = function(data, encoding, callback) {
this._bufferSize += data.length;
this._buffer.push(data);
while (this._bufferSize >= this._parserSize) {
var buffer = this._buffer.length === 1 ? this._buffer[0] : Buffer.concat(this._buffer, this._bufferSize);
this._bufferSize -= this._parserSize;
this._buffer = this._bufferSize ? [buffer.slice(this._parserSize)] : [];
this._parser(buffer.slice(0, this._parserSize));
}
callback();
};
Wire.prototype._read = noop;
module.exports = Wire;