UNPKG

@substrate-system/bittorrent-protocol

Version:

Simple, robust, BitTorrent peer wire protocol implementation

1,294 lines (1,293 loc) 40.6 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import bencode from "@substrate-system/bencode"; import BitField from "@substrate-system/bitfield"; import dh from "diffie-hellman"; import RC4 from "rc4"; import { Duplex } from "streamx"; import { hash, concat, equal, hex2arr, arr2hex, text2arr, arr2text, randomBytes } from "@substrate-system/uint8-util"; import throughput from "@substrate-system/throughput"; import arrayRemove from "unordered-array-remove"; import Debug from "@substrate-system/debug"; const debug = Debug("bittorrent-protocol"); const BITFIELD_GROW = 4e5; const KEEP_ALIVE_TIMEOUT = 55e3; const ALLOWED_FAST_SET_MAX_LENGTH = 100; const DH_PRIME = "ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a63a36210000000000090563"; const DH_GENERATOR = 2; const MESSAGE_PROTOCOL = text2arr("BitTorrent protocol"); const MESSAGE_KEEP_ALIVE = new Uint8Array([0, 0, 0, 0]); const MESSAGE_CHOKE = new Uint8Array([0, 0, 0, 1, 0]); const MESSAGE_UNCHOKE = new Uint8Array([0, 0, 0, 1, 1]); const MESSAGE_INTERESTED = new Uint8Array([0, 0, 0, 1, 2]); const MESSAGE_UNINTERESTED = new Uint8Array([0, 0, 0, 1, 3]); const MESSAGE_RESERVED = [0, 0, 0, 0, 0, 0, 0, 0]; const MESSAGE_PORT = [0, 0, 0, 3, 9, 0, 0]; const MESSAGE_HAVE_ALL = new Uint8Array([0, 0, 0, 1, 14]); const MESSAGE_HAVE_NONE = new Uint8Array([0, 0, 0, 1, 15]); const VC = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); const CRYPTO_PROVIDE = new Uint8Array([0, 0, 1, 2]); const CRYPTO_SELECT = new Uint8Array([0, 0, 0, 2]); class Request { static { __name(this, "Request"); } piece; offset; length; callback; constructor(piece, offset, length, callback) { this.piece = piece; this.offset = offset; this.length = length; this.callback = callback; } } class HaveAllBitField { static { __name(this, "HaveAllBitField"); } buffer; constructor() { this.buffer = new Uint8Array(); } get(_index) { return true; } set(_index) { } } class Wire extends Duplex { static { __name(this, "Wire"); } _debugId; peerId; peerIdBuffer; type; peerChoking; peerInterested; requests; // outgoing peerRequests; // incoming // number -> string, ex: 1 -> 'ut_metadata' extendedMapping; // string -> number, ex: 9 -> 'ut_metadata' peerExtendedMapping; amChoking; amInterested; _dh = null; // _dhKeys:{ keys:CryptoKeyPair, hex, spki } _myPubKey; _setGenerators; _decryptGenerator; _encryptGenerator; _encryptMethod; _peerPubKey; // peer's DH public key _sharedSecret; // shared DH secret _cryptoHandshakeDone; _peerCryptoProvide; // encryption methods provided by peer; we expect this // to always contain 0x02 peerPieces; extensions; peerExtensions; peerExtendedHandshake; // The extended handshake to send, minus the "m" field, which gets // automatically filled from `this.extendedMapping` extendedHandshake = {}; // BEP6 Fast Estension hasFast; // is fast extension enabled? allowedFastSet; // allowed fast set peerAllowedFastSet; // peer's allowed fast set // string -> function, ex 'ut_metadata' -> ut_metadata() _ext; _nextExt; uploaded; downloaded; uploadSpeed; downloadSpeed; _keepAliveInterval; _timeout; _timeoutMs; _timeoutUnref; _timeoutExpiresAt; _finished; _parserSize; // number of needed bytes to parse next message from remote peer // function to call once `this._parserSize` bytes are available _parser; _buffer; // incomplete message data _bufferSize; // cached total length of buffers in `this._buffer` _peEnabled; // the maximum number of bytes resynchronization must occur within _waitMaxBytes; // the pattern to search for when resynchronizing after receiving pe1/pe2 _cryptoSyncPattern; _encryptionMethod; // 1 for plaintext, 2 for RC4 _infoHash; _handshakeSent; _extendedHandshakeSent; constructor(type, retries = 0, peEnabled = false) { super(); this._debugId = arr2hex(randomBytes(4)); this._debug("new wire"); this.peerId = null; this.peerIdBuffer = null; this.type = type; this.amChoking = true; this.amInterested = false; this.peerChoking = true; this.peerInterested = false; this.peerPieces = new BitField(0, { grow: BITFIELD_GROW }); this.extensions = {}; this.peerExtensions = {}; this.requests = []; this.peerRequests = []; this.extendedMapping = {}; this.peerExtendedMapping = {}; this.peerExtendedHandshake = {}; this.hasFast = false; this.allowedFastSet = []; this.peerAllowedFastSet = []; this._ext = {}; this._nextExt = 1; this.uploaded = 0; this.downloaded = 0; this.uploadSpeed = throughput(); this.downloadSpeed = throughput(); this._keepAliveInterval = null; this._timeout = null; this._timeoutMs = 0; this._timeoutExpiresAt = null; this._finished = false; this._parserSize = 0; this._parser = null; this._buffer = []; this._bufferSize = 0; this._peEnabled = peEnabled; if (peEnabled) { this._dh = dh.createDiffieHellman(DH_PRIME, "hex", DH_GENERATOR); this._myPubKey = this._dh.generateKeys("hex"); } else { this._myPubKey = null; } this._peerPubKey = null; this._sharedSecret = null; this._peerCryptoProvide = []; this._cryptoHandshakeDone = false; this._cryptoSyncPattern = null; this._waitMaxBytes = null; this._encryptionMethod = null; this._encryptGenerator = null; this._decryptGenerator = null; this._setGenerators = false; this.on("finish", this._onFinish); this._debug("type:", this.type); if (this.type === "tcpIncoming" && this._peEnabled) { this._determineHandshakeType(); } else if (this.type === "tcpOutgoing" && this._peEnabled && retries === 0) { this._parsePe2(); } else { this._parseHandshake(); } } /** * Factory function b/c async. * * @returns { Wire } A new Wire instance. */ static async create(type = null, retries = 0, peEnabled = false) { const wire = new Wire(type, retries, peEnabled); return wire; } once(event, listener) { return super.once(event, listener); } on(ev, listener) { return super.on(ev, listener); } emit(evName, ...rest) { return super.emit(evName, ...rest); } removeListener(event, listener) { return super.removeListener(event, listener); } /** * Set whether to send a "keep-alive" ping (sent every 55s) * @param {boolean} enable */ setKeepAlive(enable) { this._debug("setKeepAlive %s", enable); clearInterval(this._keepAliveInterval); if (enable === false) return; this._keepAliveInterval = setInterval(() => { 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) */ setTimeout(ms, unref) { this._debug("setTimeout ms=%d unref=%s", ms, unref); this._timeoutMs = ms; this._timeoutUnref = !!unref; this._resetTimeout(true); } destroy() { if (this.destroyed) return; this._debug("destroy"); this.end(); return this; } end(data) { if (this.destroyed || this.destroying) return; this._debug("end"); this._onUninterested(); this._onChoke(); return super.end(data); } /** * Use the specified protocol extension. * * @param {function} Extension */ use(Extension) { const name = Extension.prototype.name; if (!name) { throw new Error('Extension class requires a "name" property on the prototype'); } this._debug("use extension.name=%s", name); const ext = this._nextExt; const handler = new Extension(this); function noop() { } __name(noop, "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> */ keepAlive() { this._debug("keep-alive"); this._push(MESSAGE_KEEP_ALIVE); } sendPe1() { if (this._peEnabled) { const padALen = Math.floor(Math.random() * 513); const padA = randomBytes(padALen); this._push(concat([hex2arr(this._myPubKey), padA])); } } sendPe2() { const padBLen = Math.floor(Math.random() * 513); const padB = randomBytes(padBLen); this._push(concat([hex2arr(this._myPubKey), padB])); } async sendPe3(infoHash) { await this.setEncrypt(this._sharedSecret, infoHash); const hash1Buffer = await hash(hex2arr(this._utfToHex("req1") + this._sharedSecret)); const hash2Buffer = await hash(hex2arr(this._utfToHex("req2") + infoHash)); const hash3Buffer = await hash(hex2arr(this._utfToHex("req3") + this._sharedSecret)); const hashesXorBuffer = xor(hash2Buffer, hash3Buffer); const padCLen = new DataView(randomBytes(2).buffer).getUint16(0) % 512; const padCBuffer = randomBytes(padCLen); let vcAndProvideBuffer = new Uint8Array(8 + 4 + 2 + padCLen + 2); vcAndProvideBuffer.set(VC); vcAndProvideBuffer.set(CRYPTO_PROVIDE, 8); const view = new DataView(vcAndProvideBuffer.buffer); view.setInt16(12, padCLen); vcAndProvideBuffer.set(padCBuffer.slice(), 14); view.setInt16(14 + padCLen, 0); vcAndProvideBuffer = this._encryptHandshake(vcAndProvideBuffer); this._push(concat([hash1Buffer, hashesXorBuffer, vcAndProvideBuffer])); } async sendPe4(infoHash) { await this.setEncrypt(this._sharedSecret, infoHash); const padDLen = new DataView(randomBytes(2).buffer).getUint16(0) % 512; const padDBuffer = randomBytes(padDLen); let vcAndSelectBuffer = new Uint8Array(8 + 4 + 2 + padDLen); const view = new DataView(vcAndSelectBuffer.buffer); vcAndSelectBuffer.set(VC); vcAndSelectBuffer.set(CRYPTO_SELECT, 8); view.setInt16(12, padDLen); vcAndSelectBuffer.set(padDBuffer, 14); vcAndSelectBuffer = this._encryptHandshake(vcAndSelectBuffer); this._push(vcAndSelectBuffer); this._cryptoHandshakeDone = true; this._debug("completed crypto handshake"); } /** * Message: "handshake" <pstrlen><pstr><reserved><info_hash><peer_id> * * @param {Uint8Array|string} infoHash (as Buffer or *hex* string) * @param {Uint8Array|string} peerId * @param {Object} extensions */ handshake(infoHash, peerId, extensions) { let infoHashBuffer; let peerIdBuffer; if (typeof infoHash === "string") { infoHash = infoHash.toLowerCase(); infoHashBuffer = hex2arr(infoHash); } else { infoHashBuffer = infoHash; infoHash = arr2hex(infoHashBuffer); } if (typeof peerId === "string") { peerIdBuffer = hex2arr(peerId); } else { peerIdBuffer = peerId; peerId = arr2hex(peerIdBuffer); } this._infoHash = infoHashBuffer; if (infoHashBuffer.length !== 20 || peerIdBuffer.length !== 20) { throw new Error("infoHash and peerId MUST have length 20"); } this._debug("handshake i=%s p=%s exts=%o", infoHash, peerId, extensions); const reserved = new Uint8Array(MESSAGE_RESERVED); this.extensions = { extended: true, dht: !!(extensions && extensions.dht), fast: !!(extensions && extensions.fast) }; reserved[5] |= 16; if (this.extensions.dht) reserved[7] |= 1; if (this.extensions.fast) reserved[7] |= 4; if (this.extensions.fast && this.peerExtensions.fast) { this._debug("fast extension is enabled"); this.hasFast = true; } this._push(concat([MESSAGE_PROTOCOL, reserved, infoHashBuffer, peerIdBuffer])); this._handshakeSent = true; if (this.peerExtensions.extended && !this._extendedHandshakeSent) { 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. */ _sendExtendedHandshake() { const msg = Object.assign({}, this.extendedHandshake); msg.m = {}; for (const ext in this.extendedMapping) { const name = this.extendedMapping[ext]; msg.m[name] = Number(ext); } this.extended(0, bencode.encode(msg)); this._extendedHandshakeSent = true; } /** * Message "choke": <len=0001><id=0> */ choke() { if (this.amChoking) return; this.amChoking = true; this._debug("choke"); this._push(MESSAGE_CHOKE); if (this.hasFast) { let allowedCount = 0; while (this.peerRequests.length > allowedCount) { const request = this.peerRequests[allowedCount]; if (this.allowedFastSet.includes(request.piece)) { ++allowedCount; } else { this.reject(request.piece, request.offset, request.length); } } } else { while (this.peerRequests.length) { this.peerRequests.pop(); } } } /** * Message "unchoke": <len=0001><id=1> */ unchoke() { if (!this.amChoking) return; this.amChoking = false; this._debug("unchoke"); this._push(MESSAGE_UNCHOKE); } /** * Message "interested": <len=0001><id=2> */ interested() { if (this.amInterested) return; this.amInterested = true; this._debug("interested"); this._push(MESSAGE_INTERESTED); } /** * Message "uninterested": <len=0001><id=3> */ uninterested() { if (!this.amInterested) return; this.amInterested = false; this._debug("uninterested"); this._push(MESSAGE_UNINTERESTED); } /** * Message "have": <len=0005><id=4><piece index> * @param {number} index */ have(index) { this._debug("have %d", index); this._message(4, [index], null); } /** * Message "bitfield": <len=0001+X><id=5><bitfield> * @param {BitField|Buffer} bitfield */ bitfield(bitfield) { this._debug("bitfield"); if (!ArrayBuffer.isView(bitfield)) bitfield = bitfield.buffer; this._message(5, [], bitfield); } /** * Message "request": <len=0013><id=6><index><begin><length> * @param {number} index * @param {number} offset * @param {number} length * @param {function} cb */ request(index, offset, length, cb) { if (!cb) cb = /* @__PURE__ */ __name(() => { }, "cb"); if (this._finished) return cb(new Error("wire is closed")); if (this.peerChoking && !(this.hasFast && this.peerAllowedFastSet.includes(index))) { return cb(new Error("peer is choking")); } this._debug("request index=%d offset=%d length=%d", index, offset, length); this.requests.push(new Request(index, offset, length, cb)); if (!this._timeout) { this._resetTimeout(true); } this._message(6, [index, offset, length], null); } /** * Message "piece": <len=0009+X><id=7><index><begin><block> * @param {number} index * @param {number} offset * @param {Uint8Array} buffer */ piece(index, offset, buffer) { this._debug("piece index=%d offset=%d", index, offset); this._message(7, [index, offset], buffer); this.uploaded += buffer.length; this.uploadSpeed(buffer.length); this.emit("upload", buffer.length); } /** * Message "cancel": <len=0013><id=8><index><begin><length> * @param {number} index * @param {number} offset * @param {number} length */ cancel(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(8, [index, offset, length], null); } /** * Message: "port" <len=0003><id=9><listen-port> * @param {Number} port */ port(port) { this._debug("port %d", port); const message = new Uint8Array(MESSAGE_PORT); const view = new DataView(message.buffer); view.setUint16(5, port); this._push(message); } /** * Message: "suggest" <len=0x0005><id=0x0D><piece index> (BEP6) * @param {number} index */ suggest(index) { if (!this.hasFast) throw Error("fast extension is disabled"); this._debug("suggest %d", index); this._message(13, [index], null); } /** * Message: "have-all" <len=0x0001><id=0x0E> (BEP6) */ haveAll() { if (!this.hasFast) throw Error("fast extension is disabled"); this._debug("have-all"); this._push(MESSAGE_HAVE_ALL); } /** * Message: "have-none" <len=0x0001><id=0x0F> (BEP6) */ haveNone() { if (!this.hasFast) throw Error("fast extension is disabled"); this._debug("have-none"); this._push(MESSAGE_HAVE_NONE); } /** * Message "reject": <len=0x000D><id=0x10><index><offset><length> (BEP6) * @param {number} index * @param {number} offset * @param {number} length */ reject(index, offset, length) { if (!this.hasFast) throw Error("fast extension is disabled"); this._debug("reject index=%d offset=%d length=%d", index, offset, length); this._pull(this.peerRequests, index, offset, length); this._message(16, [index, offset, length], null); } /** * Message: "allowed-fast" <len=0x0005><id=0x11><piece index> (BEP6) * @param {number} index */ allowedFast(index) { if (!this.hasFast) throw Error("fast extension is disabled"); this._debug("allowed-fast %d", index); if (!this.allowedFastSet.includes(index)) this.allowedFastSet.push(index); this._message(17, [index], null); } /** * Message: "extended" <len=0005+X><id=20><ext-number><payload> * @param {number|string} ext * @param {Object} obj */ extended(ext, obj) { this._debug("extended ext=%s", ext); if (typeof ext === "string" && this.peerExtendedMapping[ext]) { ext = this.peerExtendedMapping[ext]; } if (typeof ext === "number") { const extId = new Uint8Array([ext]); const buf = obj instanceof Uint8Array ? obj : bencode.encode(obj); this._message(20, [], concat([extId, buf])); } else { throw new Error(`Unrecognized extension: ${ext}`); } } /** * Sets the encryption method for this wire, as per PSE/ME specification * * @param {string} sharedSecret: A hex-encoded string, which is the shared secret agreed * upon from DH key exchange * @param {string} infoHash: A hex-encoded info hash * @returns boolean, true if encryption setting succeeds, false if it fails. */ async setEncrypt(sharedSecret, infoHash) { let encryptKeyBuf; let encryptKeyIntArray; let decryptKeyBuf; let decryptKeyIntArray; switch (this.type) { case "tcpIncoming": encryptKeyBuf = await hash(hex2arr(this._utfToHex("keyB") + sharedSecret + infoHash)); decryptKeyBuf = await hash(hex2arr(this._utfToHex("keyA") + sharedSecret + infoHash)); encryptKeyIntArray = []; for (const value of encryptKeyBuf.values()) { encryptKeyIntArray.push(value); } decryptKeyIntArray = []; for (const value of decryptKeyBuf.values()) { decryptKeyIntArray.push(value); } this._encryptGenerator = new RC4(encryptKeyIntArray); this._decryptGenerator = new RC4(decryptKeyIntArray); break; case "tcpOutgoing": encryptKeyBuf = await hash(hex2arr(this._utfToHex("keyA") + sharedSecret + infoHash)); decryptKeyBuf = await hash(hex2arr(this._utfToHex("keyB") + sharedSecret + infoHash)); encryptKeyIntArray = []; for (const value of encryptKeyBuf.values()) { encryptKeyIntArray.push(value); } decryptKeyIntArray = []; for (const value of decryptKeyBuf.values()) { decryptKeyIntArray.push(value); } this._encryptGenerator = new RC4(encryptKeyIntArray); this._decryptGenerator = new RC4(decryptKeyIntArray); break; default: return false; } for (let i = 0; i < 1024; i++) { this._encryptGenerator.randomByte(); this._decryptGenerator.randomByte(); } this._setGenerators = true; return true; } /** * Send a message to the remote peer. */ _message(id, numbers, data) { const dataLength = data ? data.length : 0; const buffer = new Uint8Array(5 + 4 * numbers.length); const view = new DataView(buffer.buffer); view.setUint32(0, buffer.length + dataLength - 4); buffer[4] = id; for (let i = 0; i < numbers.length; i++) { view.setUint32(5 + 4 * i, numbers[i]); } this._push(buffer); if (data) this._push(data); } _push(data) { if (this._finished) return; if (this._encryptionMethod === 2 && this._cryptoHandshakeDone) { data = this._encrypt(data); } return this.push(data); } // // INCOMING MESSAGES // _onKeepAlive() { this._debug("got keep-alive"); this.emit("keep-alive"); } _onPe1(pubKeyBuffer) { this._peerPubKey = arr2hex(pubKeyBuffer); this._sharedSecret = this._dh.computeSecret(this._peerPubKey, "hex", "hex"); this.emit("pe1"); } _onPe2(pubKeyBuffer) { this._peerPubKey = arr2hex(pubKeyBuffer); this._sharedSecret = this._dh.computeSecret(this._peerPubKey, "hex", "hex"); this.emit("pe2"); } async _onPe3(hashesXorBuffer) { const hash3 = await hex2arr(this._utfToHex("req3") + this._sharedSecret); const sKeyHash = arr2hex(xor(hash3, hashesXorBuffer)); this.emit("pe3", sKeyHash); } _onPe3Encrypted(vcBuffer, peerProvideBuffer) { if (!equal(vcBuffer, VC)) { this._debug("Error: verification constant did not match"); this.destroy(); return; } for (const provideByte of peerProvideBuffer.values()) { if (provideByte !== 0) { this._peerCryptoProvide.push(provideByte); } } if (this._peerCryptoProvide.includes(2)) { this._encryptionMethod = 2; } else { this._debug("Error: RC4 encryption method not provided by peer"); this.destroy(); } } _onPe4(peerSelectBuffer) { this._encryptionMethod = peerSelectBuffer[3]; if (!CRYPTO_PROVIDE.includes(this._encryptionMethod)) { this._debug("Error: peer selected invalid crypto method"); this.destroy(); } this._cryptoHandshakeDone = true; this._debug("crypto handshake done"); this.emit("pe4"); } _onHandshake(infoHashBuffer, peerIdBuffer, extensions) { const infoHash = arr2hex(infoHashBuffer); const peerId = arr2hex(peerIdBuffer); this._debug("got handshake i=%s p=%s exts=%o", infoHash, peerId, extensions); this.peerId = peerId; this.peerIdBuffer = peerIdBuffer; this.peerExtensions = extensions; if (this.extensions.fast && this.peerExtensions.fast) { this._debug("fast extension is enabled"); this.hasFast = true; } this.emit("handshake", infoHash, peerId, extensions); for (const name in this._ext) { this._ext[name].onHandshake(infoHash, peerId, extensions); } if (extensions.extended && this._handshakeSent && !this._extendedHandshakeSent) { this._sendExtendedHandshake(); } } _onChoke() { this.peerChoking = true; this._debug("got choke"); this.emit("choke"); if (!this.hasFast) { while (this.requests.length) { this._callback(this.requests.pop(), new Error("peer is choking"), null); } } } _onUnchoke() { this.peerChoking = false; this._debug("got unchoke"); this.emit("unchoke"); } _onInterested() { this.peerInterested = true; this._debug("got interested"); this.emit("interested"); } _onUninterested() { this.peerInterested = false; this._debug("got uninterested"); this.emit("uninterested"); } _onHave(index) { if (this.peerPieces.get(index)) return; this._debug("got have %d", index); this.peerPieces.set(index, true); this.emit("have", index); } _onBitField(buffer) { this.peerPieces = new BitField(buffer); this._debug("got bitfield"); this.emit("bitfield", this.peerPieces); } _onRequest(index, offset, length) { if (this.amChoking && !(this.hasFast && this.allowedFastSet.includes(index))) { if (this.hasFast) this.reject(index, offset, length); return; } this._debug("got request index=%d offset=%d length=%d", index, offset, length); const respond = /* @__PURE__ */ __name((err, buffer) => { 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); if (this.hasFast) this.reject(index, offset, length); return; } this.piece(index, offset, buffer); }, "respond"); const request = new Request(index, offset, length, respond); this.peerRequests.push(request); this.emit("request", index, offset, length, respond); } _onPiece(index, offset, buffer) { this._debug("got piece index=%d offset=%d", index, offset); this._callback(this._pull(this.requests, index, offset, buffer.length), null, buffer); this.downloaded += buffer.length; this.downloadSpeed(buffer.length); this.emit("download", buffer.length); this.emit("piece", index, offset, buffer); } _onCancel(index, offset, length) { this._debug("got cancel index=%d offset=%d length=%d", index, offset, length); this._pull(this.peerRequests, index, offset, length); this.emit("cancel", index, offset, length); } _onPort(port) { this._debug("got port %d", port); this.emit("port", port); } _onSuggest(index) { if (!this.hasFast) { this._debug("Error: got suggest whereas fast extension is disabled"); this.destroy(); return; } this._debug("got suggest %d", index); this.emit("suggest", index); } _onHaveAll() { if (!this.hasFast) { this._debug("Error: got have-all whereas fast extension is disabled"); this.destroy(); return; } this._debug("got have-all"); this.peerPieces = new HaveAllBitField(); this.emit("have-all"); } _onHaveNone() { if (!this.hasFast) { this._debug("Error: got have-none whereas fast extension is disabled"); this.destroy(); return; } this._debug("got have-none"); this.emit("have-none"); } _onReject(index, offset, length) { if (!this.hasFast) { this._debug("Error: got reject whereas fast extension is disabled"); this.destroy(); return; } this._debug("got reject index=%d offset=%d length=%d", index, offset, length); this._callback( this._pull(this.requests, index, offset, length), new Error("request was rejected"), null ); this.emit("reject", index, offset, length); } _onAllowedFast(index) { if (!this.hasFast) { this._debug("Error: got allowed-fast whereas fast extension is disabled"); this.destroy(); return; } this._debug("got allowed-fast %d", index); if (!this.peerAllowedFastSet.includes(index)) { this.peerAllowedFastSet.push(index); } if (this.peerAllowedFastSet.length > ALLOWED_FAST_SET_MAX_LENGTH) { this.peerAllowedFastSet.shift(); } this.emit("allowed-fast", index); } _onExtended(ext, buf) { if (ext === 0) { let info; try { info = bencode.decode(buf); } catch (_err) { const err = _err; this._debug( "ignoring invalid extended handshake: %s", err.message || err ); } if (!info) return; this.peerExtendedHandshake = info; if (typeof info.m === "object") { for (const name in info.m) { this.peerExtendedMapping[name] = Number(info.m[name].toString()); } } for (const name in this._ext) { if (this.peerExtendedMapping[name]) { this._ext[name].onExtendedHandshake( this.peerExtendedHandshake ); } } this._debug("got extended handshake"); this.emit("extended", "handshake", this.peerExtendedHandshake); } else { if (this.extendedMapping[ext]) { ext = this.extendedMapping[ext]; if (this._ext[ext]) { this._ext[ext].onMessage(buf); } } this._debug("got extended message ext=%s", ext); this.emit("extended", ext, buf); } } _onTimeout() { 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 {Uint8Array} data * @param {(null):void} cb Signal that we're ready for more data */ _write(data, cb) { if (this._encryptionMethod === 2 && this._cryptoHandshakeDone) { data = this._decrypt(data); } this._bufferSize += data.length; this._buffer.push(data); if (this._buffer.length > 1) { this._buffer = [concat(this._buffer, this._bufferSize)]; } if (this._cryptoSyncPattern) { const index = this._buffer[0].indexOf(this._cryptoSyncPattern); if (index !== -1) { this._buffer[0] = this._buffer[0].slice(index + this._cryptoSyncPattern.length); this._bufferSize -= index + this._cryptoSyncPattern.length; this._cryptoSyncPattern = null; } else if (this._bufferSize + data.length > this._waitMaxBytes + this._cryptoSyncPattern.length) { this._debug("Error: could not resynchronize"); this.destroy(); return; } } while (this._bufferSize >= this._parserSize && !this._cryptoSyncPattern) { if (this._parserSize === 0) { this._parser(new Uint8Array()); } else { const buffer = this._buffer[0]; this._bufferSize -= this._parserSize; this._buffer = this._bufferSize ? [buffer.slice(this._parserSize)] : []; this._parser(buffer.slice(0, this._parserSize)); } } cb(null); } _callback(request, err, buffer) { if (!request) return; this._resetTimeout(!this.peerChoking && !this._finished); request.callback(err, buffer); } _resetTimeout(setAgain) { if (!setAgain || !this._timeoutMs || !this.requests.length) { clearTimeout(this._timeout); this._timeout = null; this._timeoutExpiresAt = null; return; } const timeoutExpiresAt = Date.now() + this._timeoutMs; if (this._timeout) { if (timeoutExpiresAt - this._timeoutExpiresAt < this._timeoutMs * 0.05) { return; } clearTimeout(this._timeout); } this._timeoutExpiresAt = timeoutExpiresAt; this._timeout = setTimeout(() => 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, add a callback function to be * called once enough bytes have arrived. * @param {number} size * @param {function} parser */ _parse(size, parser) { this._parserSize = size; this._parser = parser; } _parseUntil(pattern, maxBytes) { this._cryptoSyncPattern = pattern; this._waitMaxBytes = maxBytes; } /** * 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 {Uint8Array} buffer */ _onMessageLength(buffer) { const length = new DataView( buffer.buffer, buffer.byteOffset, buffer.byteLength ).getUint32(0); if (length > 0) { this._parse(length, this._onMessage); } else { this._onKeepAlive(); this._parse(4, this._onMessageLength); } } /** * Handle a message from the remote peer. * @param {Uint8Array} buffer */ _onMessage(buffer) { this._parse(4, this._onMessageLength); const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); switch (buffer[0]) { case 0: return this._onChoke(); case 1: return this._onUnchoke(); case 2: return this._onInterested(); case 3: return this._onUninterested(); case 4: return this._onHave(view.getUint32(1)); case 5: return this._onBitField(buffer.slice(1)); case 6: return this._onRequest( view.getUint32(1), view.getUint32(5), view.getUint32(9) ); case 7: return this._onPiece( view.getUint32(1), view.getUint32(5), buffer.slice(9) ); case 8: return this._onCancel( view.getUint32(1), view.getUint32(5), view.getUint32(9) ); case 9: return this._onPort(view.getUint16(1)); case 13: return this._onSuggest(view.getUint32(1)); case 14: return this._onHaveAll(); case 15: return this._onHaveNone(); case 16: return this._onReject( view.getUint32(1), view.getUint32(5), view.getUint32(9) ); case 17: return this._onAllowedFast(view.getUint32(1)); case 20: return this._onExtended(buffer[1], buffer.slice(2)); default: this._debug("got unknown message"); return this.emit("unknownmessage", buffer); } } _determineHandshakeType() { this._parse(1, (pstrLenBuffer) => { const pstrlen = pstrLenBuffer[0]; if (pstrlen === 19) { this._parse(pstrlen + 48, this._onHandshakeBuffer); } else { this._parsePe1(pstrLenBuffer); } }); } _parsePe1(pubKeyPrefix) { this._parse(95, (pubKeySuffix) => { this._onPe1(concat([pubKeyPrefix, pubKeySuffix])); this._parsePe3(); }); } _parsePe2() { this._parse(96, (pubKey) => { this._onPe2(pubKey); while (!this._setGenerators) { } this._parsePe4(); }); } // Handles the unencrypted portion of step 4 async _parsePe3() { const hash1Buffer = await hash( hex2arr(this._utfToHex("req1") + this._sharedSecret) ); this._parseUntil(hash1Buffer, 512); this._parse(20, (buffer) => { this._onPe3(buffer); while (!this._setGenerators) { } this._parsePe3Encrypted(); }); } _parsePe3Encrypted() { this._parse(14, (buffer) => { const vcBuffer = this._decryptHandshake(buffer.slice(0, 8)); const peerProvideBuffer = this._decryptHandshake(buffer.slice(8, 12)); const padCLen = new DataView(this._decryptHandshake(buffer.slice(12, 14)).buffer).getUint16(0); this._parse(padCLen, () => { this._parse(2, (iaLenBuf) => { const iaLen = new DataView( this._decryptHandshake(iaLenBuf).buffer ).getUint16(0); this._parse(iaLen, (iaBuffer) => { iaBuffer = this._decryptHandshake(iaBuffer); this._onPe3Encrypted( vcBuffer, peerProvideBuffer ); const pstrlen = iaLen ? iaBuffer[0] : null; const protocol = iaLen ? iaBuffer.slice(1, 20) : null; if (!protocol) throw new Error("Not protocol"); if (pstrlen === 19 && arr2text(protocol) === "BitTorrent protocol") { this._onHandshakeBuffer(iaBuffer.slice(1)); } else { this._parseHandshake(); } }); }); }); }); } _parsePe4() { const vcBufferEncrypted = this._decryptHandshake(VC); this._parseUntil(vcBufferEncrypted, 512); this._parse(6, (buffer) => { const peerSelectBuffer = this._decryptHandshake(buffer.slice(0, 4)); const padDLen = new DataView(this._decryptHandshake(buffer.slice(4, 6)).buffer).getUint16(0); this._parse(padDLen, (padDBuf) => { this._decryptHandshake(padDBuf); this._onPe4(peerSelectBuffer); this._parseHandshake(); }); }); } /** * Reads the handshake as specified by the bittorrent wire protocol. */ _parseHandshake() { this._parse(1, (buffer) => { const pstrlen = buffer[0]; if (pstrlen !== 19) { this._debug("Error: wire not speaking BitTorrent protocol (%s)", pstrlen.toString()); this.end(); return; } this._parse(pstrlen + 48, this._onHandshakeBuffer); }); } _onHandshakeBuffer(handshake) { const protocol = handshake.slice(0, 19); if (arr2text(protocol) !== "BitTorrent protocol") { this._debug("Error: wire not speaking BitTorrent protocol (%s)", arr2text(protocol)); this.end(); return; } handshake = handshake.slice(19); this._onHandshake(handshake.slice(8, 28), handshake.slice(28, 48), { dht: !!(handshake[7] & 1), // see bep_0005 fast: !!(handshake[7] & 4), // see bep_0006 extended: !!(handshake[5] & 16) // see bep_0010 }); this._parse(4, this._onMessageLength); } _onFinish() { this._finished = true; this.push(null); while (this.read()) { } clearInterval(this._keepAliveInterval); this._parse(Number.MAX_VALUE, () => { }); while (this.peerRequests.length) { this.peerRequests.pop(); } while (this.requests.length) { this._callback(this.requests.pop(), new Error("wire was closed"), null); } } _debug(...args) { args[0] = `[${this._debugId}] ${args[0]}`; debug(...args); } _pull(requests, piece, offset, length) { for (let i = 0; i < requests.length; i++) { const req = requests[i]; if (req.piece === piece && req.offset === offset && req.length === length) { arrayRemove(requests, i); return req; } } return null; } _encryptHandshake(buf) { const crypt = new Uint8Array(buf); if (!this._encryptGenerator) { this._debug("Warning: Encrypting without any generator"); return crypt; } for (let i = 0; i < buf.length; i++) { const keystream = this._encryptGenerator.randomByte(); crypt[i] = crypt[i] ^ keystream; } return crypt; } _encrypt(buf) { const crypt = new Uint8Array(buf); if (!this._encryptGenerator || this._encryptionMethod !== 2) { return crypt; } for (let i = 0; i < buf.length; i++) { const keystream = this._encryptGenerator.randomByte(); crypt[i] = crypt[i] ^ keystream; } return crypt; } _decryptHandshake(buf) { const decrypt = new Uint8Array(buf); if (!this._decryptGenerator) { this._debug("Warning: Decrypting without any generator"); return decrypt; } for (let i = 0; i < buf.length; i++) { const keystream = this._decryptGenerator.randomByte(); decrypt[i] = decrypt[i] ^ keystream; } return decrypt; } _decrypt(buf) { const decrypt = new Uint8Array(buf); if (!this._decryptGenerator || this._encryptionMethod !== 2) { return decrypt; } for (let i = 0; i < buf.length; i++) { const keystream = this._decryptGenerator.randomByte(); decrypt[i] = decrypt[i] ^ keystream; } return decrypt; } _utfToHex(str) { return arr2hex(text2arr(str)); } } var src_default = Wire; function xor(a, b) { for (let len = a.length; len--; ) { a[len] ^= b[len]; } return a; } __name(xor, "xor"); export { Wire, src_default as default }; //# sourceMappingURL=index.js.map