bittorrent-protocol
Version:
Simple, robust, BitTorrent peer wire protocol implementation
1,343 lines (1,161 loc) • 41.1 kB
JavaScript
/*! bittorrent-protocol. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
import bencode from 'bencode'
import BitField from 'bitfield'
import crypto from 'crypto'
import Debug from 'debug'
import RC4 from 'rc4'
import { Duplex } from 'streamx'
import { hash, concat, equal, hex2arr, arr2hex, text2arr, arr2text, randomBytes } from 'uint8-util'
import throughput from 'throughput'
import arrayRemove from 'unordered-array-remove'
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])
const DH_PRIME = 'ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a63a36210000000000090563'
const DH_GENERATOR = 2
const VC = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
const CRYPTO_PROVIDE = new Uint8Array([0x00, 0x00, 0x01, 0x02])
const CRYPTO_SELECT = new Uint8Array([0x00, 0x00, 0x00, 0x02]) // always try to choose RC4 encryption instead of plaintext
function xor (a, b) {
for (let len = a.length; len--;) a[len] ^= b[len]
return a
}
/**
* @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
}
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 {
constructor (type = null, retries = 0, peEnabled = false) {
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
if (peEnabled) {
this._dh = crypto.createDiffieHellman(DH_PRIME, 'hex', DH_GENERATOR) // crypto object used to generate keys/secret
this._myPubKey = this._dh.generateKeys('hex') // my DH public key
} else {
this._myPubKey = null
}
this._peerPubKey = null // peer's DH public key
this._sharedSecret = null // shared DH secret
this._peerCryptoProvide = [] // encryption methods provided by peer; we expect this to always contain 0x02
this._cryptoHandshakeDone = false
this._cryptoSyncPattern = null // the pattern to search for when resynchronizing after receiving pe1/pe2
this._waitMaxBytes = null // the maximum number of bytes resynchronization must occur within
this._encryptionMethod = null // 1 for plaintext, 2 for RC4
this._encryptGenerator = null // RC4 keystream generator for encryption
this._decryptGenerator = null // RC4 keystream generator for decryption
this._setGenerators = false // a flag for whether setEncrypt() has successfully completed
this.once('finish', () => this._onFinish())
this.on('finish', this._onFinish)
this._debug('type:', this.type)
if (this.type === 'tcpIncoming' && this._peEnabled) {
// If we are not the initiator, we should wait to see if the client begins
// with PE/MSE handshake or the standard bittorrent handshake.
this._determineHandshakeType()
} else if (this.type === 'tcpOutgoing' && this._peEnabled && retries === 0) {
this._parsePe2()
} else {
this._parseHandshake(null)
}
}
/**
* 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._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) // pad C length
padCBuffer.copy(vcAndProvideBuffer, 14)
view.setInt16(14 + padCLen, 0) // IA length
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) // lenD?
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] |= 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._push(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._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 = () => {}
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._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(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
*
* @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) {
if (!this.type.startsWith('tcp')) return false
const outgoing = this.type === 'tcpOutgoing'
const keyAGenerator = new RC4([...await hash(hex2arr(this._utfToHex('keyA') + sharedSecret + infoHash))])
const keyBGenerator = new RC4([...await hash(hex2arr(this._utfToHex('keyB') + sharedSecret + infoHash))])
this._encryptGenerator = outgoing ? keyAGenerator : keyBGenerator
this._decryptGenerator = outgoing ? keyBGenerator : keyAGenerator
// Discard the first 1024 bytes, as per MSE/PE implementation
for (let i = 0; i < 1024; i++) {
this._encryptGenerator.randomByte()
this._decryptGenerator.randomByte()
}
this._setGenerators = true
this.emit('_generators')
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))
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)
}
_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 hash(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
// 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._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)]
}
// now this._buffer is an array containing a single Buffer
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.subarray(this._parserSize)]
: []
this._parser(buffer.subarray(0, this._parserSize))
}
}
cb(null) // Signal that we're ready for more data
}
_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)
}
}
_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, async pubKey => {
this._onPe2(pubKey)
if (!this._setGenerators) {
// Wait until generators have been set
await new Promise(resolve => this.once('_generators', resolve))
}
this._parsePe4()
})
}
// Handles the unencrypted portion of step 4
async _parsePe3 () {
const hash1Buffer = await hash(hex2arr(this._utfToHex('req1') + this._sharedSecret))
// synchronize on HASH('req1', S)
this._parseUntil(hash1Buffer, 512)
this._parse(20, async buffer => {
this._onPe3(buffer)
if (!this._setGenerators) {
// Wait until generators have been set
await new Promise(resolve => this.once('_generators', resolve))
}
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, padCBuffer => {
padCBuffer = this._decryptHandshake(padCBuffer)
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, padCBuffer, iaBuffer)
const pstrlen = iaLen ? iaBuffer[0] : null
const protocol = iaLen ? iaBuffer.slice(1, 20) : null
if (pstrlen === 19 && arr2text(protocol) === 'BitTorrent protocol') {
this._onHandshakeBuffer(iaBuffer.slice(1))
} else {
this._parseHandshake()
}
})
})
})
})
}
_parsePe4 () {
// synchronize on ENCRYPT(VC).
// since we encrypt using bitwise xor, decryption and encryption are the same operation.
// calling _decryptHandshake here advances the decrypt generator keystream forward 8 bytes
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(null)
})
})
}
/**
* 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
}
_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))
}
}
export default Wire