bittorrent-protocol
Version:
Simple, robust, BitTorrent peer wire protocol implementation
1,088 lines (945 loc) • 32.2 kB
JavaScript
/*! bittorrent-protocol. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
import bencode from 'bencode'
import BitField from 'bitfield'
import Debug from 'debug'
import { Duplex } from 'streamx'
import { concat, hex2arr, arr2hex, text2arr, arr2text, randomBytes } from 'uint8-util'
import throughput from 'throughput'
import arrayRemove from 'unordered-array-remove'
import { MessageStreamEncryptor, nativeRC4 } from './mse.js'
const debug = Debug('bittorrent-protocol')
const BITFIELD_GROW = 400000
const KEEP_ALIVE_TIMEOUT = 55000
const ALLOWED_FAST_SET_MAX_LENGTH = 100
const MESSAGE_PROTOCOL = text2arr('\u0013BitTorrent protocol')
const MESSAGE_KEEP_ALIVE = new Uint8Array([0x00, 0x00, 0x00, 0x00])
const MESSAGE_CHOKE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x00])
const MESSAGE_UNCHOKE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x01])
const MESSAGE_INTERESTED = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x02])
const MESSAGE_UNINTERESTED = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x03])
const MESSAGE_RESERVED = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
const MESSAGE_PORT = [0x00, 0x00, 0x00, 0x03, 0x09, 0x00, 0x00]
// BEP6 Fast Extension
const MESSAGE_HAVE_ALL = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x0E])
const MESSAGE_HAVE_NONE = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x0F])
/**
* @param {Uint8Array} buffer
* @param {number} at
* @returns number
*/
function getUint32 (buffer, at = 0) {
return (buffer[at] << 24) | (buffer[at + 1] << 16) | (buffer[at + 2] << 8) | buffer[at + 3]
}
/**
* @param {Uint8Array} buffer
* @param {number} at
* @param {number} value
*/
function setUint32 (buffer, at, value) {
buffer[at] = (value >>> 24) & 0xFF
buffer[at + 1] = (value >>> 16) & 0xFF
buffer[at + 2] = (value >>> 8) & 0xFF
buffer[at + 3] = value & 0xFF
}
function indexOfBytes (haystack, needle) {
if (needle.length === 0) return 0
if (haystack.length < needle.length) return -1
const max = haystack.length - needle.length
for (let i = 0; i <= max; i++) {
let match = true
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) { match = false; break }
}
if (match) return i
}
return -1
}
class Request {
constructor (piece, offset, length, callback) {
this.piece = piece
this.offset = offset
this.length = length
this.callback = callback
}
}
class HaveAllBitField {
constructor () {
this.buffer = new Uint8Array() // dummy
}
get (index) {
return true
}
set (index) {}
}
class Wire extends Duplex {
static nativeRC4 = nativeRC4 // true if Node's crypto supports RC4 (avoids slower JS fallback) Node 17+ (requires `--openssl-legacy-provider`)
constructor (type = '', peEnabled = 0) {
super()
this._debugId = arr2hex(randomBytes(4))
this._debug('new wire')
this.peerId = null // remote peer id (hex string)
this.peerIdBuffer = null // remote peer id (buffer)
this.type = type // 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(0, { grow: BITFIELD_GROW })
this.extensions = {}
this.peerExtensions = {}
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 = {}
this.peerExtendedHandshake = {} // remote peer's extended handshake
// BEP6 Fast Estension
this.hasFast = false // is fast extension enabled?
this.allowedFastSet = [] // allowed fast set
this.peerAllowedFastSet = [] // peer's allowed fast set
this._ext = {} // string -> function, ex 'ut_metadata' -> ut_metadata()
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 // number of needed bytes to parse next message from remote peer
this._parser = null // function to call once `this._parserSize` bytes are available
this._buffer = [] // incomplete message data
this._bufferSize = 0 // cached total length of buffers in `this._buffer`
this._peEnabled = peEnabled // 0 = no PE, 1 = PE + plaintext fallback, 2 = PE + RC4 only
this._encryptor = null
this._infoHash = null
this._cryptoSyncPattern = null // the pattern to search for when resynchronizing after receiving pe1/pe2
this._pePending = false // flag set when _parsePe3 callback fires, cleared by setInfoHash
this._waitMaxBytes = null // the maximum number of bytes resynchronization must occur within
this.on('finish', this._onFinish)
this._debug('type:', this.type)
if (this._peEnabled) {
this._encryptor = new MessageStreamEncryptor(this)
if (this.type.includes('Incoming')) {
this._encryptor.handleIncoming()
} else if (this.type.includes('Outgoing')) {
this._encryptor.handleOutgoing()
}
} else {
this._parseHandshake(null)
}
}
get _peState () {
return this._encryptor?.state ?? 'idle'
}
get _encryptionMethod () {
return this._encryptor?.encryptionMethod
}
get _cryptoHandshakeDone () {
return !!this._encryptor?.cryptoHandshakeDone
}
/**
* 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 () {}
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._pushCopy(MESSAGE_KEEP_ALIVE)
}
/**
* Start the PE/MSE handshake as initiator.
* @param {string} infoHash hex-encoded info hash
*/
startEncryption (infoHash) {
if (!this._peEnabled) return
this._infoHash = infoHash
this._encryptor.startAsInitiator(infoHash)
}
/**
* 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] |= 0x10 // enable extended message
if (this.extensions.dht) reserved[7] |= 0x01
if (this.extensions.fast) reserved[7] |= 0x04
// BEP6 Fast Extension: The extension is enabled only if both ends of the connection set this bit.
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) {
// 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.
*/
_sendExtendedHandshake () {
// Create extended message object from registered extensions
const msg = Object.assign({}, this.extendedHandshake)
msg.m = {}
for (const ext in this.extendedMapping) {
const name = this.extendedMapping[ext]
msg.m[name] = Number(ext)
}
// Send extended handshake
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._pushCopy(MESSAGE_CHOKE)
if (this.hasFast) {
// BEP6: If a peer sends a choke, it MUST reject all requests from the peer to whom the choke
// was sent except it SHOULD NOT reject requests for pieces that are in the allowed fast set.
let allowedCount = 0
while (this.peerRequests.length > allowedCount) { // until only allowed requests are left
const request = this.peerRequests[allowedCount] // first non-allowed request
if (this.allowedFastSet.includes(request.piece)) {
++allowedCount // count request as allowed
} else {
this.reject(request.piece, request.offset, request.length) // removes from this.peerRequests
}
}
} 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._pushCopy(MESSAGE_UNCHOKE)
}
/**
* Message "interested": <len=0001><id=2>
*/
interested () {
if (this.amInterested) return
this.amInterested = true
this._debug('interested')
this._pushCopy(MESSAGE_INTERESTED)
}
/**
* Message "uninterested": <len=0001><id=3>
*/
uninterested () {
if (!this.amInterested) return
this.amInterested = false
this._debug('uninterested')
this._pushCopy(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 = () => {}
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(0x0D, [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._pushCopy(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._pushCopy(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(0x10, [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(0x11, [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 = ArrayBuffer.isView(obj) ? 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
* Called by conn-pool when it resolves the infoHash from a crypto-infohash event.
* Initializes the responder's ciphers and resumes pe3 processing.
* @param {string} infoHash hex-encoded info hash
*/
setInfoHash (infoHash) {
this._infoHash = infoHash
if (this._encryptor) this._encryptor.setInfoHash(infoHash)
}
/**
* 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))
setUint32(buffer, 0, buffer.length + dataLength - 4)
buffer[4] = id
for (let i = 0; i < numbers.length; i++) {
setUint32(buffer, 5 + (4 * i), numbers[i])
}
this._push(buffer)
if (data) this._push(data)
}
_pushCopy (data) {
return this._push(new Uint8Array(data))
}
_push (data) {
if (this._finished) return
if (this._encryptor) data = this._encryptor.encrypt(data)
return this.push(data)
}
//
// INCOMING MESSAGES
//
_onKeepAlive () {
this._debug('got keep-alive')
this.emit('keep-alive')
}
_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
// BEP6 Fast Extension: The extension is enabled only if both ends of the connection set this bit.
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) {
// outgoing connection
this._sendExtendedHandshake()
}
}
_onChoke () {
this.peerChoking = true
this._debug('got choke')
this.emit('choke')
if (!this.hasFast) {
// BEP6 Fast Extension: Choke no longer implicitly rejects all pending requests
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))) {
// BEP6: If a peer receives a request from a peer its choking, the peer receiving
// the request SHOULD send a reject unless the piece is in the allowed fast set.
if (this.hasFast) this.reject(index, offset, length)
return
}
this._debug('got request index=%d offset=%d length=%d', index, offset, length)
const respond = (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)
}
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) {
// BEP6: the peer MUST close the connection
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) {
// BEP6: the peer MUST close the connection
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) {
// BEP6: the peer MUST close the connection
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) {
// BEP6: the peer MUST close the connection
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) {
// BEP6: the peer MUST close the connection
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) {
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] // friendly name for extension
if (this._ext[ext]) {
// there is an registered extension handler, so call it
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 {function} cb
*/
_write (data, cb) {
if (this._encryptor) data = this._encryptor.decrypt(data)
this._bufferSize += data.length
this._buffer.push(data)
if (this._buffer.length > 1) {
this._buffer = [concat(this._buffer, this._bufferSize)]
}
// now this._buffer is an array containing a single Buffer
this._processBuffer()
cb(null) // Signal that we're ready for more data
}
_processBuffer () {
if (this._cryptoSyncPattern) {
const index = indexOfBytes(this._buffer[0], 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 > this._waitMaxBytes + this._cryptoSyncPattern.length) {
this._debug('Error: could not resynchronize')
this.destroy()
return
}
if (this._cryptoSyncPattern) return
}
while (this._bufferSize >= this._parserSize) {
if (this._cryptoSyncPattern || this._pePending) break
if (this._parserSize === 0) {
this._parser(new Uint8Array())
} else {
const buffer = this._buffer[0]
this._bufferSize -= this._parserSize
this._buffer = this._bufferSize
? [buffer.subarray(this._parserSize)]
: []
this._parser(buffer.subarray(0, this._parserSize))
}
}
}
_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 existing expiration is already within 5% of correct, it's close enough
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, and 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 = getUint32(buffer)
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)
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(getUint32(buffer, 1))
case 5:
return this._onBitField(buffer.subarray(1))
case 6:
return this._onRequest(
getUint32(buffer, 1),
getUint32(buffer, 5),
getUint32(buffer, 9)
)
case 7:
return this._onPiece(
getUint32(buffer, 1),
getUint32(buffer, 5),
buffer.subarray(9)
)
case 8:
return this._onCancel(
getUint32(buffer, 1),
getUint32(buffer, 5),
getUint32(buffer, 9)
)
case 9:
return this._onPort((buffer[1] << 8) | buffer[2])
case 0x0D:
return this._onSuggest(getUint32(buffer, 1))
case 0x0E:
return this._onHaveAll()
case 0x0F:
return this._onHaveNone()
case 0x10:
return this._onReject(
getUint32(buffer, 1),
getUint32(buffer, 5),
getUint32(buffer, 9)
)
case 0x11:
return this._onAllowedFast(getUint32(buffer, 1))
case 20:
return this._onExtended(buffer[1], buffer.subarray(2))
default:
this._debug('got unknown message')
return this.emit('unknownmessage', buffer)
}
}
/**
* 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] & 0x01), // see bep_0005
fast: !!(handshake[7] & 0x04), // see bep_0006
extended: !!(handshake[5] & 0x10) // see bep_0010
})
this._parse(4, this._onMessageLength)
}
_onFinish () {
this._finished = true
this.push(null) // stream cannot be half open, so signal the end of it
while (this.read()) {
// body intentionally empty
// consume and discard the rest of the stream data
}
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
}
}
export default Wire