diff --git a/client.js b/client.js index c90d7f9..9f36ce1 100644 --- a/client.js +++ b/client.js @@ -4,6 +4,7 @@ import once from 'once' import parallel from 'run-parallel' import Peer from 'simple-peer' import queueMicrotask from 'queue-microtask' +import { hex2arr, hex2bin, text2arr, arr2hex, arr2text } from 'uint8-util' import common from './lib/common.js' import HTTPTracker from './lib/client/http-tracker.js' // empty object in browser @@ -18,8 +19,8 @@ const debug = Debug('bittorrent-tracker:client') * Find torrent peers, to help a torrent client participate in a torrent swarm. * * @param {Object} opts options object - * @param {string|Buffer} opts.infoHash torrent info hash - * @param {string|Buffer} opts.peerId peer id + * @param {string|Uint8Array} opts.infoHash torrent info hash + * @param {string|Uint8Array} opts.peerId peer id * @param {string|Array.} opts.announce announce * @param {number} opts.port torrent client listening port * @param {function} opts.getAnnounceOpts callback to provide data to tracker @@ -39,15 +40,15 @@ class Client extends EventEmitter { this.peerId = typeof opts.peerId === 'string' ? opts.peerId - : opts.peerId.toString('hex') - this._peerIdBuffer = Buffer.from(this.peerId, 'hex') - this._peerIdBinary = this._peerIdBuffer.toString('binary') + : arr2hex(opts.peerId) + this._peerIdBuffer = hex2arr(this.peerId) + this._peerIdBinary = hex2bin(this.peerId) this.infoHash = typeof opts.infoHash === 'string' ? opts.infoHash.toLowerCase() - : opts.infoHash.toString('hex') - this._infoHashBuffer = Buffer.from(this.infoHash, 'hex') - this._infoHashBinary = this._infoHashBuffer.toString('binary') + : arr2hex(opts.infoHash) + this._infoHashBuffer = hex2arr(this.infoHash) + this._infoHashBinary = hex2bin(this.infoHash) debug('new client %s', this.infoHash) @@ -69,7 +70,7 @@ class Client extends EventEmitter { // Remove trailing slash from trackers to catch duplicates announce = announce.map(announceUrl => { - announceUrl = announceUrl.toString() + announceUrl = arr2text(announceUrl) if (announceUrl[announceUrl.length - 1] === '/') { announceUrl = announceUrl.substring(0, announceUrl.length - 1) } @@ -260,7 +261,7 @@ Client.scrape = (opts, cb) => { const clientOpts = Object.assign({}, opts, { infoHash: Array.isArray(opts.infoHash) ? opts.infoHash[0] : opts.infoHash, - peerId: Buffer.from('01234567890123456789'), // dummy value + peerId: text2arr('01234567890123456789'), // dummy value port: 6881 // dummy value }) @@ -284,9 +285,6 @@ Client.scrape = (opts, cb) => { } }) - opts.infoHash = Array.isArray(opts.infoHash) - ? opts.infoHash.map(infoHash => Buffer.from(infoHash, 'hex')) - : Buffer.from(opts.infoHash, 'hex') client.scrape({ infoHash: opts.infoHash }) return client } diff --git a/lib/client/http-tracker.js b/lib/client/http-tracker.js index 07b2990..162c68f 100644 --- a/lib/client/http-tracker.js +++ b/lib/client/http-tracker.js @@ -4,6 +4,7 @@ import clone from 'clone' import Debug from 'debug' import get from 'simple-get' import Socks from 'socks' +import { bin2hex, hex2bin, arr2text } from 'uint8-util' import common from '../common.js' import Tracker from './tracker.js' @@ -65,8 +66,8 @@ class HTTPTracker extends Tracker { } const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(infoHash => infoHash.toString('binary')) - : (opts.infoHash && opts.infoHash.toString('binary')) || this.client._infoHashBinary + ? opts.infoHash.map(infoHash => hex2bin(infoHash)) + : (opts.infoHash && hex2bin(opts.infoHash)) || this.client._infoHashBinary const params = { info_hash: infoHashes } @@ -159,13 +160,13 @@ class HTTPTracker extends Tracker { } catch (err) { return cb(new Error(`Error decoding tracker response: ${err.message}`)) } - const failure = data['failure reason'] && Buffer.from(data['failure reason']).toString() + const failure = data['failure reason'] && arr2text(data['failure reason']) if (failure) { debug(`failure from ${requestUrl} (${failure})`) return cb(new Error(failure)) } - const warning = data['warning message'] && Buffer.from(data['warning message']).toString() + const warning = data['warning message'] && arr2text(data['warning message']) if (warning) { debug(`warning from ${requestUrl} (${warning})`) self.client.emit('warning', new Error(warning)) @@ -189,7 +190,7 @@ class HTTPTracker extends Tracker { const response = Object.assign({}, data, { announce: this.announceUrl, - infoHash: common.binaryToHex(data.info_hash) + infoHash: bin2hex(data.info_hash || String(data.info_hash)) }) this.client.emit('update', response) @@ -248,7 +249,7 @@ class HTTPTracker extends Tracker { // (separate from announce interval) const response = Object.assign(data[infoHash], { announce: this.announceUrl, - infoHash: common.binaryToHex(infoHash) + infoHash: bin2hex(infoHash) }) this.client.emit('scrape', response) }) diff --git a/lib/client/udp-tracker.js b/lib/client/udp-tracker.js index 9162d76..b3218bc 100644 --- a/lib/client/udp-tracker.js +++ b/lib/client/udp-tracker.js @@ -1,10 +1,9 @@ import arrayRemove from 'unordered-array-remove' -import BN from 'bn.js' import clone from 'clone' import Debug from 'debug' import dgram from 'dgram' -import randombytes from 'randombytes' import Socks from 'socks' +import { concat, hex2arr, randomBytes } from 'uint8-util' import common from '../common.js' import Tracker from './tracker.js' @@ -131,7 +130,7 @@ class UDPTracker extends Tracker { }, common.REQUEST_TIMEOUT) if (timeout.unref) timeout.unref() - send(Buffer.concat([ + send(concat([ common.CONNECTION_ID, common.toUInt32(common.ACTIONS.CONNECT), transactionId @@ -175,7 +174,8 @@ class UDPTracker extends Tracker { function onSocketMessage (msg) { if (proxySocket) msg = msg.slice(10) - if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { + const view = new DataView(transactionId.buffer) + if (msg.length < 8 || msg.readUInt32BE(4) !== view.getUint32(0)) { return onError(new Error('tracker sent invalid transaction id')) } @@ -270,14 +270,14 @@ class UDPTracker extends Tracker { function announce (connectionId, opts) { transactionId = genTransactionId() - send(Buffer.concat([ + send(concat([ connectionId, common.toUInt32(common.ACTIONS.ANNOUNCE), transactionId, self.client._infoHashBuffer, self.client._peerIdBuffer, toUInt64(opts.downloaded), - opts.left != null ? toUInt64(opts.left) : Buffer.from('FFFFFFFFFFFFFFFF', 'hex'), + opts.left != null ? toUInt64(opts.left) : hex2arr('ffffffffffffffff'), toUInt64(opts.uploaded), common.toUInt32(common.EVENTS[opts.event] || 0), common.toUInt32(0), // ip address (optional) @@ -291,10 +291,10 @@ class UDPTracker extends Tracker { transactionId = genTransactionId() const infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? Buffer.concat(opts.infoHash) + ? concat(opts.infoHash) : (opts.infoHash || self.client._infoHashBuffer) - send(Buffer.concat([ + send(concat([ connectionId, common.toUInt32(common.ACTIONS.SCRAPE), transactionId, @@ -307,12 +307,13 @@ class UDPTracker extends Tracker { UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes function genTransactionId () { - return randombytes(4) + return randomBytes(4) } function toUInt16 (n) { - const buf = Buffer.allocUnsafe(2) - buf.writeUInt16BE(n, 0) + const buf = new Uint8Array(2) + const view = new DataView(buf.buffer) + view.setUint16(0, n) return buf } @@ -320,13 +321,12 @@ const MAX_UINT = 4294967295 function toUInt64 (n) { if (n > MAX_UINT || typeof n === 'string') { - const bytes = new BN(n).toArray() - while (bytes.length < 8) { - bytes.unshift(0) - } - return Buffer.from(bytes) + const buf = new Uint8Array(8) + const view = new DataView(buf.buffer) + view.setBigUint64(0, n) + return buf } - return Buffer.concat([common.toUInt32(0), common.toUInt32(n)]) + return concat([new Uint8Array(4), common.toUInt32(n)]) } function noop () {} diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js index 75892ec..3889eea 100644 --- a/lib/client/websocket-tracker.js +++ b/lib/client/websocket-tracker.js @@ -1,11 +1,11 @@ import clone from 'clone' import Debug from 'debug' import Peer from 'simple-peer' -import randombytes from 'randombytes' import Socket from '@thaunknown/simple-websocket' import Socks from 'socks' +import { arr2text, arr2hex, hex2bin, bin2hex, randomBytes } from 'uint8-util' -import common from '../common.js' +import { DESTROY_TIMEOUT } from '../common.js' import Tracker from './tracker.js' const debug = Debug('bittorrent-tracker:websocket-tracker') @@ -80,8 +80,8 @@ class WebSocketTracker extends Tracker { } const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(infoHash => infoHash.toString('binary')) - : (opts.infoHash && opts.infoHash.toString('binary')) || this.client._infoHashBinary + ? opts.infoHash.map(infoHash => hex2bin(infoHash)) + : (opts.infoHash && hex2bin(opts.infoHash)) || this.client._infoHashBinary const params = { action: 'scrape', info_hash: infoHashes @@ -138,7 +138,7 @@ class WebSocketTracker extends Tracker { // Otherwise, wait a short time for potential responses to come in from the // server, then force close the socket. - timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) + timeout = setTimeout(destroyCleanup, DESTROY_TIMEOUT) // But, if a response comes from the server before the timeout fires, do cleanup // right away. @@ -214,7 +214,7 @@ class WebSocketTracker extends Tracker { this.expectingResponse = false try { - data = JSON.parse(Buffer.from(data)) + data = JSON.parse(arr2text(data)) } catch (err) { this.client.emit('warning', new Error('Invalid tracker response')) return @@ -233,7 +233,7 @@ class WebSocketTracker extends Tracker { if (data.info_hash !== this.client._infoHashBinary) { debug( 'ignoring websocket data from %s for %s (looking for %s: reused socket)', - this.announceUrl, common.binaryToHex(data.info_hash), this.client.infoHash + this.announceUrl, bin2hex(data.info_hash), this.client.infoHash ) return } @@ -266,7 +266,7 @@ class WebSocketTracker extends Tracker { if (data.complete != null) { const response = Object.assign({}, data, { announce: this.announceUrl, - infoHash: common.binaryToHex(data.info_hash) + infoHash: bin2hex(data.info_hash) }) this.client.emit('update', response) } @@ -275,7 +275,7 @@ class WebSocketTracker extends Tracker { if (data.offer && data.peer_id) { debug('creating peer (from remote offer)') peer = this._createPeer() - peer.id = common.binaryToHex(data.peer_id) + peer.id = bin2hex(data.peer_id) peer.once('signal', answer => { const params = { action: 'announce', @@ -293,10 +293,10 @@ class WebSocketTracker extends Tracker { } if (data.answer && data.peer_id) { - const offerId = common.binaryToHex(data.offer_id) + const offerId = bin2hex(data.offer_id) peer = this.peers[offerId] if (peer) { - peer.id = common.binaryToHex(data.peer_id) + peer.id = bin2hex(data.peer_id) this.client.emit('peer', peer) peer.signal(data.answer) @@ -323,7 +323,7 @@ class WebSocketTracker extends Tracker { // (separate from announce interval) const response = Object.assign(data[infoHash], { announce: this.announceUrl, - infoHash: common.binaryToHex(infoHash) + infoHash: bin2hex(infoHash) }) this.client.emit('scrape', response) }) @@ -376,13 +376,13 @@ class WebSocketTracker extends Tracker { checkDone() function generateOffer () { - const offerId = randombytes(20).toString('hex') + const offerId = arr2hex(randomBytes(20)) debug('creating peer (from _generateOffers)') const peer = self.peers[offerId] = self._createPeer({ initiator: true }) peer.once('signal', offer => { offers.push({ offer, - offer_id: common.hexToBinary(offerId) + offer_id: hex2bin(offerId) }) checkDone() }) diff --git a/lib/common-node.js b/lib/common-node.js index 18af19c..9fefa5c 100644 --- a/lib/common-node.js +++ b/lib/common-node.js @@ -4,12 +4,13 @@ */ import querystring from 'querystring' +import { concat } from 'uint8-util' export const IPV4_RE = /^[\d.]+$/ export const IPV6_RE = /^[\da-fA-F:]+$/ export const REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ -export const CONNECTION_ID = Buffer.concat([toUInt32(0x417), toUInt32(0x27101980)]) +export const CONNECTION_ID = concat([toUInt32(0x417), toUInt32(0x27101980)]) export const ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } export const EVENTS = { update: 0, completed: 1, started: 2, stopped: 3, paused: 4 } export const EVENT_IDS = { @@ -40,8 +41,9 @@ export const REQUEST_TIMEOUT = 15000 export const DESTROY_TIMEOUT = 1000 export function toUInt32 (n) { - const buf = Buffer.allocUnsafe(4) - buf.writeUInt32BE(n, 0) + const buf = new Uint8Array(4) + const view = new DataView(buf.buffer) + view.setUint32(0, n) return buf } diff --git a/lib/common.js b/lib/common.js index d418a32..ef88857 100644 --- a/lib/common.js +++ b/lib/common.js @@ -2,24 +2,11 @@ * Functions/constants needed by both the client and server. */ import * as common from './common-node.js' +export * from './common-node.js' export const DEFAULT_ANNOUNCE_PEERS = 50 export const MAX_ANNOUNCE_PEERS = 82 -export const binaryToHex = str => { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'binary').toString('hex') -} - -export const hexToBinary = str => { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'hex').toString('binary') -} - // HACK: Fix for WHATWG URL object not parsing non-standard URL schemes like // 'udp:'. Just replace it with 'http:' since we only need a few properties. // @@ -49,8 +36,6 @@ export const parseUrl = str => { export default { DEFAULT_ANNOUNCE_PEERS, MAX_ANNOUNCE_PEERS, - binaryToHex, - hexToBinary, parseUrl, ...common } diff --git a/lib/server/parse-http.js b/lib/server/parse-http.js index 1e6fe31..a6ba6a5 100644 --- a/lib/server/parse-http.js +++ b/lib/server/parse-http.js @@ -1,8 +1,8 @@ +import { bin2hex } from 'uint8-util' + import common from '../common.js' -export default parseHttpRequest - -function parseHttpRequest (req, opts) { +export default function (req, opts) { if (!opts) opts = {} const s = req.url.split('?') const params = common.querystringParse(s[1]) @@ -14,12 +14,12 @@ function parseHttpRequest (req, opts) { if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { throw new Error('invalid info_hash') } - params.info_hash = common.binaryToHex(params.info_hash) + params.info_hash = bin2hex(params.info_hash) if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) { throw new Error('invalid peer_id') } - params.peer_id = common.binaryToHex(params.peer_id) + params.peer_id = bin2hex(params.peer_id) params.port = Number(params.port) if (!params.port) throw new Error('invalid port') @@ -56,7 +56,7 @@ function parseHttpRequest (req, opts) { if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { throw new Error('invalid info_hash') } - return common.binaryToHex(binaryInfoHash) + return bin2hex(binaryInfoHash) }) } } else { diff --git a/lib/server/parse-udp.js b/lib/server/parse-udp.js index 935420c..f73b649 100644 --- a/lib/server/parse-udp.js +++ b/lib/server/parse-udp.js @@ -1,9 +1,8 @@ import ipLib from 'ip' import common from '../common.js' +import { equal } from 'uint8-util' -export default parseUdpRequest - -function parseUdpRequest (msg, rinfo) { +export default function (msg, rinfo) { if (msg.length < 16) throw new Error('received packet is too short') const params = { @@ -13,7 +12,7 @@ function parseUdpRequest (msg, rinfo) { type: 'udp' } - if (!common.CONNECTION_ID.equals(params.connectionId)) { + if (!equal(common.CONNECTION_ID, params.connectionId)) { throw new Error('received packet with invalid connection id') } diff --git a/lib/server/parse-websocket.js b/lib/server/parse-websocket.js index 16fea56..744a3c3 100644 --- a/lib/server/parse-websocket.js +++ b/lib/server/parse-websocket.js @@ -1,8 +1,8 @@ +import { bin2hex } from 'uint8-util' + import common from '../common.js' -export default parseWebSocketRequest - -function parseWebSocketRequest (socket, opts, params) { +export default function (socket, opts, params) { if (!opts) opts = {} params = JSON.parse(params) // may throw @@ -14,18 +14,18 @@ function parseWebSocketRequest (socket, opts, params) { if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { throw new Error('invalid info_hash') } - params.info_hash = common.binaryToHex(params.info_hash) + params.info_hash = bin2hex(params.info_hash) if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) { throw new Error('invalid peer_id') } - params.peer_id = common.binaryToHex(params.peer_id) + params.peer_id = bin2hex(params.peer_id) if (params.answer) { if (typeof params.to_peer_id !== 'string' || params.to_peer_id.length !== 20) { throw new Error('invalid `to_peer_id` (required with `answer`)') } - params.to_peer_id = common.binaryToHex(params.to_peer_id) + params.to_peer_id = bin2hex(params.to_peer_id) } params.left = Number(params.left) @@ -45,7 +45,7 @@ function parseWebSocketRequest (socket, opts, params) { if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { throw new Error('invalid info_hash') } - return common.binaryToHex(binaryInfoHash) + return bin2hex(binaryInfoHash) }) } } else { diff --git a/package.json b/package.json index 04b59cf..efe2b4f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "@thaunknown/simple-websocket": "^9.1.0", "bencode": "^3.0.3", "bittorrent-peerid": "^1.3.3", - "bn.js": "^5.2.0", "chrome-dgram": "^3.0.6", "clone": "^2.0.0", "compact2string": "^1.4.1", @@ -41,13 +40,13 @@ "once": "^1.4.0", "queue-microtask": "^1.2.3", "random-iterate": "^1.0.1", - "randombytes": "^2.1.0", "run-parallel": "^1.2.0", "run-series": "^1.1.9", "simple-get": "^4.0.0", "simple-peer": "^9.11.0", "socks": "^2.0.0", "string2compact": "^2.0.0", + "uint8-util": "^2.1.9", "unordered-array-remove": "^1.0.2", "ws": "^8.0.0" }, diff --git a/server.js b/server.js index dd823f4..f7023d9 100644 --- a/server.js +++ b/server.js @@ -7,6 +7,7 @@ import peerid from 'bittorrent-peerid' import series from 'run-series' import string2compact from 'string2compact' import { WebSocketServer } from 'ws' +import { hex2bin } from 'uint8-util' import common from './lib/common.js' import Swarm from './lib/server/swarm.js' @@ -488,7 +489,7 @@ class Server extends EventEmitter { socket.send(JSON.stringify({ action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape', 'failure reason': err.message, - info_hash: common.hexToBinary(params.info_hash) + info_hash: hex2bin(params.info_hash) }), socket.onSend) this.emit('warning', err) @@ -506,7 +507,7 @@ class Server extends EventEmitter { socket.infoHashes.push(params.info_hash) } - response.info_hash = common.hexToBinary(params.info_hash) + response.info_hash = hex2bin(params.info_hash) // WebSocket tracker should have a shorter interval – default: 2 minutes response.interval = Math.ceil(this.intervalMs / 1000 / 5) @@ -526,8 +527,8 @@ class Server extends EventEmitter { action: 'announce', offer: params.offers[i].offer, offer_id: params.offers[i].offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) + peer_id: hex2bin(params.peer_id), + info_hash: hex2bin(params.info_hash) }), peer.socket.onSend) debug('sent offer to %s from %s', peer.peerId, params.peer_id) }) @@ -559,8 +560,8 @@ class Server extends EventEmitter { action: 'announce', answer: params.answer, offer_id: params.offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) + peer_id: hex2bin(params.peer_id), + info_hash: hex2bin(params.info_hash) }), toPeer.socket.onSend) debug('sent answer to %s from %s', toPeer.peerId, params.peer_id) @@ -685,7 +686,7 @@ class Server extends EventEmitter { } else if (params.compact === 0) { // IPv6 peers are not separate for non-compact responses response.peers = response.peers.map(peer => ({ - 'peer id': common.hexToBinary(peer.peerId), + 'peer id': hex2bin(peer.peerId), ip: peer.ip, port: peer.port })) @@ -729,7 +730,7 @@ class Server extends EventEmitter { } results.forEach(result => { - response.files[common.hexToBinary(result.infoHash)] = { + response.files[hex2bin(result.infoHash)] = { complete: result.complete || 0, incomplete: result.incomplete || 0, downloaded: result.complete || 0 // TODO: this only provides a lower-bound diff --git a/test/scrape.js b/test/scrape.js index b702fc6..38af0a4 100644 --- a/test/scrape.js +++ b/test/scrape.js @@ -5,6 +5,7 @@ import commonLib from '../lib/common.js' import fixtures from 'webtorrent-fixtures' import get from 'simple-get' import test from 'tape' +import { hex2bin } from 'uint8-util' const peerId = Buffer.from('01234567890123456789') @@ -152,8 +153,8 @@ test('udp: MULTI scrape using Client.scrape static method', t => { test('server: multiple info_hash scrape (manual http request)', t => { t.plan(13) - const binaryInfoHash1 = commonLib.hexToBinary(fixtures.leaves.parsedTorrent.infoHash) - const binaryInfoHash2 = commonLib.hexToBinary(fixtures.alice.parsedTorrent.infoHash) + const binaryInfoHash1 = hex2bin(fixtures.leaves.parsedTorrent.infoHash) + const binaryInfoHash2 = hex2bin(fixtures.alice.parsedTorrent.infoHash) common.createServer(t, 'http', (server, announceUrl) => { const scrapeUrl = announceUrl.replace('/announce', '/scrape') @@ -189,7 +190,7 @@ test('server: multiple info_hash scrape (manual http request)', t => { test('server: all info_hash scrape (manual http request)', t => { t.plan(10) - const binaryInfoHash = commonLib.hexToBinary(fixtures.leaves.parsedTorrent.infoHash) + const binaryInfoHash = hex2bin(fixtures.leaves.parsedTorrent.infoHash) common.createServer(t, 'http', (server, announceUrl) => { const scrapeUrl = announceUrl.replace('/announce', '/scrape')