@firaenix/bittorrent-protocol
Version:
Simple, robust, BitTorrent peer wire protocol implementation
1,058 lines (1,057 loc) • 272 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventExtension = exports.Extension = void 0;
var tiny_typed_emitter_1 = require("tiny-typed-emitter");
var Extension = /** @class */ (function () {
function Extension(wire) {
var _this = this;
this.sendExtendedMessage = function (data) {
_this.wire.extended(_this.name, data);
};
this.wire = wire;
}
return Extension;
}());
exports.Extension = Extension;
var EventExtension = /** @class */ (function (_super) {
__extends(EventExtension, _super);
function EventExtension(wire) {
var _this = _super.call(this) || this;
_this.sendExtendedMessage = function (data) {
_this.wire.extended(_this.name, data);
};
_this.wire = wire;
return _this;
}
return EventExtension;
}(tiny_typed_emitter_1.TypedEmitter));
exports.EventExtension = EventExtension;
},{"tiny-typed-emitter":40}],2:[function(require,module,exports){
(function (Buffer){
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spread = (this && this.__spread) || function () {
for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i]));
return ar;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Wire = void 0;
var unordered_array_remove_1 = __importDefault(require("unordered-array-remove"));
var bencode_1 = __importDefault(require("bencode"));
var bitfield_1 = __importDefault(require("bitfield"));
var debug_1 = __importDefault(require("debug"));
var randombytes_1 = __importDefault(require("randombytes"));
var speedometer_1 = __importDefault(require("speedometer"));
var readable_stream_1 = __importDefault(require("readable-stream"));
var PeerMessages_1 = require("./models/PeerMessages");
var PieceRequest_1 = require("./models/PieceRequest");
var ParseRequest_1 = require("./models/ParseRequest");
var debug = debug_1.default('firaenix-bittorrent-protocol');
var BITFIELD_GROW = 400000;
var KEEP_ALIVE_TIMEOUT = 55000;
var Wire = /** @class */ (function (_super) {
__extends(Wire, _super);
function Wire(name) {
var _this = _super.call(this) || this;
_this._parseRequests = [];
_this._handshakeSuccess = false;
_this._extendedHandshakeSuccess = false;
_this.parseStream = function () {
var parser = _this._parseRequests.shift();
while (!parser) {
_this._debug('Waiting for parser');
return;
}
_this._debug('Waiting for', parser === null || parser === void 0 ? void 0 : parser.parserSize, 'bytes for', parser === null || parser === void 0 ? void 0 : parser.parserName);
if (_this._buffer.length < (parser === null || parser === void 0 ? void 0 : parser.parserSize)) {
_this._parseRequests.unshift(parser);
return;
}
var buffer = _this._buffer.slice(0, parser === null || parser === void 0 ? void 0 : parser.parserSize);
_this._buffer = _this._buffer.slice(parser === null || parser === void 0 ? void 0 : parser.parserSize);
_this._debug('Sending', parser.parserName, parser === null || parser === void 0 ? void 0 : parser.parserSize, 'bytes');
parser === null || parser === void 0 ? void 0 : parser.callback(buffer);
return _this.parseStream();
};
/**
* Handle the first 4 bytes of a message, to determine the length of bytes that must be
* waited for in order to have the whole message.
* @param {Buffer} buffer
*/
_this._onMessageLength = function (buffer) {
var length = buffer.readUInt32BE(0);
if (length > 0) {
_this._parse(length, 'onMessage', _this._onMessage);
}
else {
_this._onKeepAlive();
_this._parse(4, 'onMessageLength', _this._onMessageLength);
}
};
/**
* Handle a message from the remote peer.
* @param {Buffer} buffer
*/
_this._onMessage = function (buffer) { return __awaiter(_this, void 0, void 0, function () {
var messageFlag, _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
this._parse(4, '_onMessage.onMessageLength', this._onMessageLength);
messageFlag = buffer[0];
_a = messageFlag;
switch (_a) {
case PeerMessages_1.MessageFlags.Choke: return [3 /*break*/, 1];
case PeerMessages_1.MessageFlags.Unchoke: return [3 /*break*/, 3];
case PeerMessages_1.MessageFlags.Interested: return [3 /*break*/, 5];
case PeerMessages_1.MessageFlags.NotInterested: return [3 /*break*/, 7];
case PeerMessages_1.MessageFlags.Have: return [3 /*break*/, 9];
case PeerMessages_1.MessageFlags.Bitfield: return [3 /*break*/, 11];
case PeerMessages_1.MessageFlags.Request: return [3 /*break*/, 13];
case PeerMessages_1.MessageFlags.Piece: return [3 /*break*/, 15];
case PeerMessages_1.MessageFlags.Cancel: return [3 /*break*/, 17];
case 9: return [3 /*break*/, 19];
case PeerMessages_1.MessageFlags.Extended: return [3 /*break*/, 20];
}
return [3 /*break*/, 21];
case 1: return [4 /*yield*/, this._onChoke()];
case 2: return [2 /*return*/, _b.sent()];
case 3: return [4 /*yield*/, this._onUnchoke()];
case 4: return [2 /*return*/, _b.sent()];
case 5: return [4 /*yield*/, this._onInterested()];
case 6: return [2 /*return*/, _b.sent()];
case 7: return [4 /*yield*/, this._onUninterested()];
case 8: return [2 /*return*/, _b.sent()];
case 9: return [4 /*yield*/, this._onHave(buffer.readUInt32BE(1))];
case 10: return [2 /*return*/, _b.sent()];
case 11: return [4 /*yield*/, this._onBitField(buffer.slice(1))];
case 12: return [2 /*return*/, _b.sent()];
case 13: return [4 /*yield*/, this._onRequest(buffer.readUInt32BE(1), buffer.readUInt32BE(5), buffer.readUInt32BE(9))];
case 14: return [2 /*return*/, _b.sent()];
case 15: return [4 /*yield*/, this._onPiece(buffer.readUInt32BE(1), buffer.readUInt32BE(5), buffer.slice(9))];
case 16: return [2 /*return*/, _b.sent()];
case 17: return [4 /*yield*/, this._onCancel(buffer.readUInt32BE(1), buffer.readUInt32BE(5), buffer.readUInt32BE(9))];
case 18: return [2 /*return*/, _b.sent()];
case 19: return [2 /*return*/, this._onPort(buffer.readUInt16BE(1))];
case 20: return [2 /*return*/, this._onExtended(buffer.readUInt8(1), buffer.slice(2))];
case 21:
this._debug('got unknown message');
return [2 /*return*/, this.emit('unknown_message', buffer)];
}
});
}); };
_this.wireName = name;
_this._debugId = name || randombytes_1.default(4).toString('hex');
_this._debug('new wire');
_this.peerId = undefined; // remote peer id (hex string)
_this.peerIdBuffer = undefined; // remote peer id (buffer)
_this.type = null; // connection type ('webrtc', 'tcpIncoming', 'tcpOutgoing', 'webSeed')
_this.amChoking = true; // are we choking the peer?
_this.amInterested = false; // are we interested in the peer?
_this.peerChoking = true; // is the peer choking us?
_this.peerInterested = false; // is the peer interested in us?
// The largest torrent that I know of (the Geocities archive) is ~641 GB and has
// ~41,000 pieces. Therefore, cap bitfield to 10x larger (400,000 bits) to support all
// possible torrents but prevent malicious peers from growing bitfield to fill memory.
_this.peerPieces = new bitfield_1.default(0, { grow: BITFIELD_GROW });
_this.peerExtensions = { dht: false, extended: false };
_this.requests = []; // outgoing
_this.peerRequests = []; // incoming
_this.extendedMapping = {}; // number -> string, ex: 1 -> 'ut_metadata'
_this.peerExtendedMapping = {}; // string -> number, ex: 9 -> 'ut_metadata'
// The extended handshake to send, minus the "m" field, which gets automatically
// filled from `this.extendedMapping`
_this.extendedHandshake = { m: {}, exts: {} };
_this.peerExtendedHandshake = { m: {}, exts: {} }; // remote peer's extended handshake
_this._ext = {}; // string -> function, ex 'ut_metadata' -> ut_metadata()
_this._nextExt = 1;
_this.uploaded = 0;
_this.downloaded = 0;
_this.uploadSpeed = speedometer_1.default();
_this.downloadSpeed = speedometer_1.default();
_this._keepAliveInterval = undefined;
_this._timeout = undefined;
_this._timeoutMs = 0;
_this.destroyed = false; // was the wire ended by calling `destroy`?
_this._finished = false;
_this._buffer = Buffer.alloc(0); // incomplete message data
_this.once('finish', function () { return _this._onFinish(); });
_this._parseHandshake();
return _this;
}
Wire.prototype.once = function (event, listener) {
return _super.prototype.once.call(this, event, listener);
};
Wire.prototype.on = function (event, listener) {
return _super.prototype.on.call(this, event, listener);
};
Wire.prototype.off = function (event, listener) {
return _super.prototype.off.call(this, event, listener);
};
Wire.prototype.emit = function (event) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
return _super.prototype.emit.apply(this, __spread([event], args));
};
/**
* Set whether to send a "keep-alive" ping (sent every 55s)
* @param {boolean} enable
*/
Wire.prototype.setKeepAlive = function (enable) {
var _this = this;
this._debug('setKeepAlive %s', enable);
clearInterval(this._keepAliveInterval);
if (enable === false)
return;
this._keepAliveInterval = setInterval(function () {
_this.keepAlive();
}, KEEP_ALIVE_TIMEOUT);
};
/**
* Set the amount of time to wait before considering a request to be "timed out"
* @param {number} ms
* @param {boolean=} unref (should the timer be unref'd? default: false)
*/
Wire.prototype.setTimeout = function (ms, unref) {
this._debug('setTimeout ms=%d unref=%s', ms, unref);
this._clearTimeout();
this._timeoutMs = ms;
this._timeoutUnref = !!unref;
this._updateTimeout();
};
Wire.prototype.destroy = function () {
if (this.destroyed) {
return this;
}
this.destroyed = true;
this._debug('destroy');
this.emit('close');
this.end();
return this;
};
Wire.prototype.end = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
this._debug('end');
this._onUninterested();
this._onChoke();
_super.prototype.end.apply(this, __spread(args));
};
/**
* Use the specified protocol extension.
* @param {function} Extension
*/
Wire.prototype.use = function (newExtension) {
var ext = this._nextExt;
var handler = newExtension(this);
var name = handler.name;
if (!name) {
throw new Error('Extension class requires a "name" property');
}
this._debug('use extension.name=%s', name);
// eslint-disable-next-line @typescript-eslint/no-empty-function
function noop() { }
if (typeof handler.onHandshake !== 'function') {
handler.onHandshake = noop;
}
if (typeof handler.onExtendedHandshake !== 'function') {
handler.onExtendedHandshake = noop;
}
if (typeof handler.onMessage !== 'function') {
handler.onMessage = noop;
}
this.extendedMapping[ext] = name;
this._ext[name] = handler;
this[name] = handler;
this._nextExt += 1;
};
//
// OUTGOING MESSAGES
//
/**
* Message "keep-alive": <len=0000>
*/
Wire.prototype.keepAlive = function () {
this._debug('keep-alive');
this._push(PeerMessages_1.MessageBuffers.MESSAGE_KEEP_ALIVE);
};
/**
* Message: "handshake" <pstrlen><pstr><reserved><info_hash><peer_id>
* @param {Buffer|string} infoHash (as Buffer or *hex* string)
* @param {Buffer|string} peerId
* @param {Object} extensions
*/
Wire.prototype.handshake = function (infoHash, peerId, extensions) {
var infoHashBuffer;
var peerIdBuffer;
if (typeof infoHash === 'string') {
infoHash = infoHash.toLowerCase();
infoHashBuffer = Buffer.from(infoHash, 'hex');
}
else {
infoHashBuffer = infoHash;
infoHash = infoHashBuffer.toString('hex');
}
if (typeof peerId === 'string') {
peerIdBuffer = Buffer.from(peerId, 'hex');
}
else {
peerIdBuffer = peerId;
peerId = peerIdBuffer.toString('hex');
}
if (!infoHashBuffer || !infoHashBuffer.length) {
var err = new Error('infoHash must be specified');
this.emit('error', err);
throw err;
}
if (infoHashBuffer.length > 255) {
var err = new Error("infoHash must be smaller than 255 bytes long, it is currently " + infoHashBuffer.length);
this.emit('error', err);
throw err;
}
if (peerIdBuffer.length !== 20) {
var err = new Error("peerId MUST have length 20, length is " + peerIdBuffer.length);
this.emit('error', err);
throw err;
}
this._debug('handshake i=%s p=%s exts=%o', infoHash, peerId, extensions);
var reserved = Buffer.from(PeerMessages_1.MessageBuffers.MESSAGE_RESERVED);
// enable extended message
reserved[5] |= 0x10;
if (extensions && extensions.dht)
reserved[7] |= 1;
// Prepend length of infoHash as a single byte - dont care about overflows, for now must be only 255 bits large to represent a 255 byte hash maximum length.
// Going off the assumption that by the time you need 255 bytes for a hash, we will move to a more effective hashing algorithm.
this._push(Buffer.concat([PeerMessages_1.MessageBuffers.MESSAGE_PROTOCOL, reserved, Buffer.from([infoHashBuffer.length]), PeerMessages_1.MessageBuffers.INFOHASH_SPLIT, infoHashBuffer, peerIdBuffer]));
this._handshakeSent = true;
if (this.peerExtensions.extended && !this._extendedHandshakeSent) {
// Peer's handshake indicated support already
// (incoming connection)
this._sendExtendedHandshake();
}
};
/* Peer supports BEP-0010, send extended handshake.
*
* This comes after the 'handshake' event to give the user a chance to populate
* `this.extendedHandshake` and `this.extendedMapping` before the extended handshake
* is sent to the remote peer.
*/
Wire.prototype._sendExtendedHandshake = function () {
var msg = __assign(__assign({}, Object.assign({}, this.extendedHandshake)), { m: {}, exts: {} });
for (var ext in this.extendedMapping) {
var name_1 = this.extendedMapping[ext];
var extension = this._ext[name_1];
msg.m[name_1] = Number(ext);
msg.exts[name_1] = __assign({}, extension.extraFields);
}
// Send extended handshake
this.extended(0, bencode_1.default.encode(msg));
this._extendedHandshakeSent = true;
};
/**
* Message "choke": <len=0001><id=0>
*/
Wire.prototype.choke = function () {
if (this.amChoking)
return;
this.amChoking = true;
this._debug('choke');
while (this.peerRequests.length) {
this.peerRequests.pop();
}
this._push(PeerMessages_1.MessageBuffers.MESSAGE_CHOKE);
};
/**
* Message "unchoke": <len=0001><id=1>
*/
Wire.prototype.unchoke = function () {
if (!this.amChoking)
return;
this.amChoking = false;
this._debug('unchoke');
this._push(PeerMessages_1.MessageBuffers.MESSAGE_UNCHOKE);
};
/**
* Message "interested": <len=0001><id=2>
*/
Wire.prototype.interested = function () {
if (this.amInterested)
return;
this.amInterested = true;
this._debug('interested');
this._push(PeerMessages_1.MessageBuffers.MESSAGE_INTERESTED);
};
/**
* Message "uninterested": <len=0001><id=3>
*/
Wire.prototype.uninterested = function () {
if (!this.amInterested)
return;
this.amInterested = false;
this._debug('uninterested');
this._push(PeerMessages_1.MessageBuffers.MESSAGE_UNINTERESTED);
};
/**
* Message "have": <len=0005><id=4><piece index>
* @param {number} index
*/
Wire.prototype.have = function (index) {
this._debug('have %d', index);
this._message(PeerMessages_1.MessageFlags.Have, [index], null);
};
/**
* Message "bitfield": <len=0001+X><id=5><bitfield>
* @param {BitField|Buffer} bitfield
*/
Wire.prototype.bitfield = function (bitfield) {
this._debug('bitfield');
if (!Buffer.isBuffer(bitfield))
bitfield = bitfield.buffer;
this._message(PeerMessages_1.MessageFlags.Bitfield, [], bitfield);
};
/**
* Callback will be resolved when onPiece(index, offset, length, buffer) is called or something fails when requesting.
*
* NOTE: index,offset,length are used as a key to look up the callback later.
*
* So make sure you specify the length correctly or you will never get your callback.
*
* If the other party sends the same index and offset but a buffer of a different length, you will not recieve your callback.
*
* Message "request": <len=0013><id=6><index><begin><length>
* @param {number} index
* @param {number} offset
* @param {number} length
* @param {function} cb
*/
Wire.prototype.request = function (index, offset, length, cb) {
if (!cb)
cb = function () { };
if (this._finished) {
return cb(new Error('wire is closed'), undefined);
}
if (this.peerChoking) {
return cb(new Error('peer is choking'), undefined);
}
if (this._handshakeSuccess === false) {
return cb(new Error("peer hasn't finished handshaking"), undefined);
}
if (this._nextExt > 1 && this._extendedHandshakeSuccess === false) {
return cb(new Error("peer hasn't finished extended handshaking"), undefined);
}
this._debug('request index=%d offset=%d length=%d', index, offset, length);
this.requests.push(new PieceRequest_1.PieceRequest(index, offset, length, cb));
this._updateTimeout();
this._message(PeerMessages_1.MessageFlags.Request, [index, offset, length], null);
};
/**
* Message "piece": <len=0009+X><id=7><index><begin><block>
* @param {number} index
* @param {number} offset
* @param {Buffer} buffer
*/
Wire.prototype.piece = function (index, offset, buffer) {
this._debug('piece index=%d offset=%d', index, offset);
this.uploaded += buffer.length;
this.uploadSpeed(buffer.length);
this.emit('upload', buffer.length);
this._message(PeerMessages_1.MessageFlags.Piece, [index, offset], buffer);
};
/**
* Message "cancel": <len=0013><id=8><index><begin><length>
* @param {number} index
* @param {number} offset
* @param {number} length
*/
Wire.prototype.cancel = function (index, offset, length) {
this._debug('cancel index=%d offset=%d length=%d', index, offset, length);
this._callback(this._pull(this.requests, index, offset, length), new Error('request was cancelled'), null);
this._message(PeerMessages_1.MessageFlags.Cancel, [index, offset, length], null);
};
/**
* Message: "port" <len=0003><id=9><listen-port>
* @param {Number} port
*/
Wire.prototype.port = function (port) {
this._debug('port %d', port);
var message = Buffer.from(PeerMessages_1.MessageBuffers.MESSAGE_PORT);
message.writeUInt16BE(port, 5);
this._push(message);
};
/**
* Message: "extended" <len=0005+X><id=20><ext-number><payload>
* @param {number|string} ext
* @param {Object} obj
*/
Wire.prototype.extended = function (ext, obj) {
this._debug('extended ext=%s', ext);
if (typeof ext === 'string' && this.peerExtendedMapping[ext]) {
ext = this.peerExtendedMapping[ext];
}
if (typeof ext === 'number') {
var extId = Buffer.from([ext]);
var buf = Buffer.isBuffer(obj) ? obj : bencode_1.default.encode(obj);
this._message(PeerMessages_1.MessageFlags.Extended, [], Buffer.concat([extId, buf]));
}
else {
throw new Error("Unrecognized extension: " + ext);
}
};
/**
* Duplex stream method. Called whenever the remote peer stream wants data. No-op
* since we'll just push data whenever we get it.
*/
Wire.prototype._read = function () { };
/**
* Send a message to the remote peer.
*/
Wire.prototype._message = function (id, numbers, data) {
var dataLength = data ? data.length : 0;
var buffer = Buffer.allocUnsafe(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;
return this.push(data);
};
//
// INCOMING MESSAGES
//
Wire.prototype._onKeepAlive = function () {
this._debug('got keep-alive');
this.emit('keep-alive');
};
Wire.prototype._onHandshake = function (infoHashBuffer, peerIdBuffer, extensions) {
var infoHash = infoHashBuffer.toString('hex');
var peerId = peerIdBuffer.toString('hex');
this._debug('got handshake i=%s p=%s exts=%o', infoHash, peerId, extensions);
this.peerId = peerId;
this.peerIdBuffer = peerIdBuffer;
this.peerExtensions = extensions;
this.emit('handshake', infoHash, peerId, extensions);
for (var name_2 in this._ext) {
this._ext[name_2].onHandshake(infoHash, peerId, extensions);
}
if (extensions.extended && this._handshakeSent && !this._extendedHandshakeSent) {
// outgoing connection
this._sendExtendedHandshake();
}
this._handshakeSuccess = true;
};
Wire.prototype._onChoke = function () {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.peerChoking = true;
this._debug('got choke');
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onChoke) === null || _a === void 0 ? void 0 : _a.call(x); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.emit('choke');
while (this.requests.length) {
this._callback(this.requests.pop(), new Error('peer is choking'), null);
}
return [2 /*return*/];
}
});
});
};
Wire.prototype._onUnchoke = function () {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.peerChoking = false;
this._debug('got unchoke');
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onUnchoke) === null || _a === void 0 ? void 0 : _a.call(x); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.emit('unchoke');
return [2 /*return*/];
}
});
});
};
Wire.prototype._onInterested = function () {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.peerInterested = true;
this._debug('got interested');
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onInterested) === null || _a === void 0 ? void 0 : _a.call(x); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.emit('interested');
return [2 /*return*/];
}
});
});
};
Wire.prototype._onUninterested = function () {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.peerInterested = false;
this._debug('got uninterested');
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onUninterested) === null || _a === void 0 ? void 0 : _a.call(x); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.emit('uninterested');
return [2 /*return*/];
}
});
});
};
Wire.prototype._onHave = function (index) {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (this.peerPieces.get(index))
return [2 /*return*/];
this._debug('got have %d', index);
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onHave) === null || _a === void 0 ? void 0 : _a.call(x, index); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.peerPieces.set(index, true);
this.emit('have', index);
return [2 /*return*/];
}
});
});
};
Wire.prototype._onBitField = function (buffer) {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.peerPieces = new bitfield_1.default(buffer);
this._debug('got bitfield');
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onBitField) === null || _a === void 0 ? void 0 : _a.call(x, buffer); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.emit('bitfield', this.peerPieces);
return [2 /*return*/];
}
});
});
};
Wire.prototype._onRequest = function (index, offset, length) {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls, respond, request;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (this.amChoking) {
return [2 /*return*/];
}
this._debug('got request index=%d offset=%d length=%d', index, offset, length);
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onRequest) === null || _a === void 0 ? void 0 : _a.call(x, index, offset, length); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this._debug('Extensions have resolved');
respond = function (err, buffer) {
// below request var gets hoisted above this function.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (request !== _this._pull(_this.peerRequests, index, offset, length)) {
return;
}
if (err) {
_this._debug('error satisfying request index=%d offset=%d length=%d (%s)', index, offset, length, err.message);
return;
}
if (buffer === null || buffer === undefined) {
_this._debug('the requested piece has no buffer associated with it, return empty buffer index=%d offset=%d length=%d (%s)', index, offset, length);
buffer = Buffer.alloc(0);
}
_this.piece(index, offset, buffer);
};
request = new PieceRequest_1.PieceRequest(index, offset, length, respond);
this.peerRequests.push(request);
this.emit('request', index, offset, length, respond);
return [2 /*return*/];
}
});
});
};
Wire.prototype._onPiece = function (index, offset, buffer) {
return __awaiter(this, void 0, void 0, function () {
var extensionOnPieces, resolvedRequest, error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
extensionOnPieces = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onPiece) === null || _a === void 0 ? void 0 : _a.call(x, index, offset, buffer); });
return [4 /*yield*/, Promise.all(extensionOnPieces)];
case 1:
_a.sent();
this._debug('got piece index=%d offset=%d', index, offset);
resolvedRequest = this._pull(this.requests, index, offset, buffer.length);
this._debug('got resolved request for index=%d offset=%d', index, offset, resolvedRequest);
this._callback(resolvedRequest, null, buffer);
this.downloaded += buffer.length;
this.downloadSpeed(buffer.length);
this.emit('download', buffer.length);
this.emit('piece', index, offset, buffer);
return [3 /*break*/, 3];
case 2:
error_1 = _a.sent();
console.error(error_1);
this._debug('An error occurred when recieving a piece, destroying the connection', error_1);
this.destroy();
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
});
};
Wire.prototype._onCancel = function (index, offset, length) {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this._debug('got cancel index=%d offset=%d length=%d', index, offset, length);
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onCancel) === null || _a === void 0 ? void 0 : _a.call(x, index, offset, length); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this._pull(this.peerRequests, index, offset, length);
this.emit('cancel', index, offset, length);
return [2 /*return*/];
}
});
});
};
Wire.prototype._onPort = function (port) {
this._debug('got port %d', port);
this.emit('port', port);
};
Wire.prototype.onExtendedMessage = function (extensionId, buf) {
if (!this.extendedMapping[extensionId]) {
throw new Error("Could not find extension with the given id: " + extensionId);
}
// get friendly name for extension
var extensionName = this.extendedMapping[extensionId];
if (!this._ext[extensionName]) {
throw new Error("Could not find extension with the given name: " + extensionName);
}
// there is an registered extension handler, so call it
this._ext[extensionName].onMessage(buf);
this._debug('got extended message ext=%s', extensionName);
this.emit('extension_message', extensionName, buf);
};
Wire.prototype.onExtendedHandshake = function (buf) {
var _a;
var info;
try {
info = bencode_1.default.decode(buf);
}
catch (err) {
this._debug('ignoring invalid extended handshake: %s', err.message || err);
return;
}
if (!info) {
return;
}
this.peerExtendedHandshake = info;
// Find any extensions that require the other peer to have it too.
for (var name_3 in this._ext) {
if (this._ext[name_3] && this._ext[name_3].requirePeer && !((_a = this.peerExtendedHandshake.m) === null || _a === void 0 ? void 0 : _a[name_3])) {
this._debug('Destroying connection, peer doesnt have same extension.', name_3);
this.destroy();
this.emit('missing_extension', name_3);
return;
}
}
if (typeof info.m === 'object') {
for (var name_4 in info.m) {
this.peerExtendedMapping[name_4] = Number(info.m[name_4].toString());
}
}
for (var name_5 in this._ext) {
if (this.peerExtendedMapping[name_5]) {
this._ext[name_5].onExtendedHandshake(this.peerExtendedHandshake);
}
}
this._debug('got extended handshake');
this.emit('extended_handshake', 'handshake', this.peerExtendedHandshake);
this._extendedHandshakeSuccess = true;
};
Wire.prototype._onExtended = function (extensionId, buf) {
// Which extension this message came from was not specified, this is a handshake
if (extensionId === 0) {
return this.onExtendedHandshake(buf);
}
return this.onExtendedMessage(extensionId, buf);
};
Wire.prototype._onTimeout = function () {
this._debug('request timed out');
this._callback(this.requests.shift(), new Error('request has timed out'), null);
this.emit('timeout');
};
/**
* Duplex stream method. Called whenever the remote peer has data for us. Data that the
* remote peer sends gets buffered (i.e. not actually processed) until the right number
* of bytes have arrived, determined by the last call to `this._parse(number, callback)`.
* Once enough bytes have arrived to process the message, the callback function
* (i.e. `this._parser`) gets called with the full buffer of data.
* @param {Buffer} data
* @param {string} encoding
* @param {function} cb
*/
Wire.prototype._write = function (data, encoding, cb) {
this._buffer = Buffer.concat([this._buffer, data]);
this._debug('Data pushing', data, data.length);
this.parseStream();
// Keep collecting data for the next request
cb(null);
};
Wire.prototype._callback = function (request, err, buffer) {
this._debug('calling request callback', request);
if (!request) {
this._debug('No request was specified');
return;
}
this._clearTimeout();
if (!this.peerChoking && !this._finished) {
this._updateTimeout();
}
request.callback(err, buffer);
};
Wire.prototype._clearTimeout = function () {
if (!this._timeout)
return;
clearTimeout(this._timeout);
this._timeout = undefined;
};
Wire.prototype._updateTimeout = function () {
var _this = this;
if (!this._timeoutMs || !this.requests.length || this._timeout)
return;
this._timeout = setTimeout(function () { return _this._onTimeout(); }, this._timeoutMs);
if (this._timeoutUnref && this._timeout.unref)
this._timeout.unref();
};
/**
* Takes a number of bytes that the local peer is waiting to receive from the remote peer
* in order to parse a complete message, and a callback function to be called once enough
* bytes have arrived.
* @param {number} size
* @param {function} parser
*/
Wire.prototype._parse = function (size, name, parser) {
this._parseRequests.push(new ParseRequest_1.ParseRequest(size, name, parser));
this._debug('Parse Requests in queue: ', this._parseRequests.length);
};
Wire.prototype._parseHandshake = function () {
var _this = this;
this._parse(1, 'getBittorrentProtocolIdentifier', function (bittorrentProtocolIdent) {
var protocolStringLength = bittorrentProtocolIdent.readUInt8(0);
_this._debug('Is start of Bittorrent Protocol?', protocolStringLength === PeerMessages_1.MessageBuffers.MESSAGE_PROTOCOL[0], bittorrentProtocolIdent.toString());
_this._parse(protocolStringLength, 'getHandshake', function (handshake) {
_this._debug('HANDSHAKE BUFFER', handshake.toString(), handshake);
var protocol = handshake.slice(0, protocolStringLength);
if (protocol.toString() !== 'BitTorrent protocol') {
_this._debug('Error: wire not speaking BitTorrent protocol (%s)', protocol.toString());
_this.end();
return;
}
// GET RESERVED
_this._parse(PeerMessages_1.MessageBuffers.MESSAGE_RESERVED.length, 'getReserved', function (reservedFlags) {
_this._debug('Reserved flags', reservedFlags);
var dht = !!(reservedFlags[7] & 0x01); // see bep_0005
var extended = !!(reservedFlags[5] & 0x10); // see bep_0010
_this._parse(PeerMessages_1.MessageParams.INFOHASH_SIZE_LENGTH, 'getInfoHashSizeBuf', function (infoHashSizeBuf) {
_this._debug('infoHashSizeBuf', infoHashSizeBuf);
var infoHashSize = infoHashSizeBuf.readUInt8(0);
_this._debug('InfoHash Size:', infoHashSize);
// Make sure that the following character is :
_this._parse(PeerMessages_1.MessageBuffers.INFOHASH_SPLIT.length, 'getColonSeparator', function (colonChar) {
if (!colonChar.equals(PeerMessages_1.MessageBuffers.INFOHASH_SPLIT)) {
throw new Error('Invalid handshake, must be infohash_size:infohash. Missing colon');
}
// Infohash size + peerId size
_this._parse(infoHashSize + PeerMessages_1.MessageParams.PEER_ID_LENGTH, 'getPeerIdLength', function (infoHashAndPeerId) {
// handshake = handshake.slice(pstrlen);
_this._onHandshake(infoHashAndPeerId.slice(0, infoHashSize), infoHashAndPeerId.slice(infoHashSize, infoHashSize + PeerMessages_1.MessageParams.PEER_ID_LENGTH), {
dht: dht,
extended: extended
});
_this._parse(4, '_parseHandshake.getMessageLength', _this._onMessageLength);
});
});
});
});
});
});
};
Wire.prototype._onFinish = function () {
return __awaiter(this, void 0, void 0, function () {
var extensionCalls;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this._finished = true;
extensionCalls = Object.values(this._ext).map(function (x) { var _a; return (_a = x.onFinish) === null || _a === void 0 ? void 0 : _a.call(x); });
return [4 /*yield*/, Promise.all(extensionCalls)];
case 1:
_a.sent();
this.push(null); // stream cannot be half open, so signal the end of it
while (this.read()) { } // consume and discard the rest of the stream data
clearInterval(this._keepAliveInterval);
this._parse(Number.MAX_VALUE, 'dispose', function () { });
while (this.peerRequests.length) {
this.peerRequests.pop();
}
while (this.requests.length) {
this._callback(this.requests.pop(), new Error('wire was closed'), null);
}
return [2 /*return*/];
}
});
});
};
Wire.prototype._debug = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
debug.apply(void 0, __spread(["[" + Date.now() + "][" + this._debugId + "]"], args));
};
/**
* Retrieves the first entity from the requests array that matches the index, offset and length given.
*
* Often used in conjunction with _callback to call back to waiting piece requests
* @param requests
* @param pieceIdx
* @param offset
* @param length
*/
Wire.prototype._pull = function (requests, pieceIdx, offset, length) {
for (var i = 0; i < requests.length; i++) {
var req = requests[i];
if (req.piece === pieceIdx && req.offset === offset && req.length === length) {
unordered_array_remove_1.default(requests, i);
ret