From 39507bf8be3f391abef7c2258b32b6fe5cfcaa27 Mon Sep 17 00:00:00 2001 From: Yoann Ciabaud Date: Mon, 14 Mar 2016 00:15:41 +0100 Subject: [PATCH] Scrape implementation for websocket. Issue #116 --- lib/client/websocket-tracker.js | 55 ++++++++++++++++++++++++++++- lib/server/parse-websocket.js | 62 +++++++++++++++++++++------------ server.js | 12 +++++-- 3 files changed, 103 insertions(+), 26 deletions(-) diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js index 773aa83..04b8590 100644 --- a/lib/client/websocket-tracker.js +++ b/lib/client/websocket-tracker.js @@ -47,6 +47,7 @@ WebSocketTracker.prototype.announce = function (opts) { self._generateOffers(numwant, function (offers) { var params = extend(opts, { + action: 'announce', numwant: numwant, info_hash: self.client._infoHashBinary, peer_id: self.client._peerIdBinary, @@ -61,7 +62,21 @@ WebSocketTracker.prototype.announce = function (opts) { WebSocketTracker.prototype.scrape = function (opts) { var self = this if (self.destroyed || self.reconnecting) return - self._onSocketError(new Error('scrape not supported ' + self.announceUrl)) + if (!self.socket.connected) { + return self.socket.once('connect', self.scrape.bind(self, opts)) + } + + var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) + ? opts.infoHash.map(function (infoHash) { + return infoHash.toString('binary') + }) + : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary + var params = { + action: 'scrape', + info_hash: infoHashes + } + + self._send(params) } WebSocketTracker.prototype.destroy = function (onclose) { @@ -133,6 +148,18 @@ WebSocketTracker.prototype._onSocketData = function (data) { return } + if (data.action === common.ACTIONS.ANNOUNCE || data.offer || data.answer) { + self._onAnnounceResponse(data) + } else if (data.action === common.ACTIONS.SCRAPE) { + self._onScrapeResponse(data) + } else { + throw new Error('invalid action in WS response: ' + data.action) + } +} + +WebSocketTracker.prototype._onAnnounceResponse = function (data) { + var self = this + if (data.info_hash !== self.client._infoHashBinary) { debug( 'ignoring websocket data from %s for %s (looking for %s: reused socket)', @@ -215,6 +242,32 @@ WebSocketTracker.prototype._onSocketData = function (data) { } } +WebSocketTracker.prototype._onScrapeResponse = function (data) { + var self = this + // NOTE: the unofficial spec says to use the 'files' key, 'host' has been + // seen in practice + data = data.files || data.host || {} + + var keys = Object.keys(data) + if (keys.length === 0) { + self.client.emit('warning', new Error('invalid scrape response')) + return + } + + keys.forEach(function (infoHash) { + var response = data[infoHash] + // TODO: optionally handle data.flags.min_request_interval + // (separate from announce interval) + self.client.emit('scrape', { + announce: self.announceUrl, + infoHash: common.binaryToHex(infoHash), + complete: response.complete, + incomplete: response.incomplete, + downloaded: response.downloaded + }) + }) +} + WebSocketTracker.prototype._onSocketClose = function () { var self = this if (self.destroyed) return diff --git a/lib/server/parse-websocket.js b/lib/server/parse-websocket.js index 9847efb..9139964 100644 --- a/lib/server/parse-websocket.js +++ b/lib/server/parse-websocket.js @@ -6,33 +6,51 @@ function parseWebSocketRequest (socket, opts, params) { if (!opts) opts = {} params = JSON.parse(params) // may throw - params.action = common.ACTIONS.ANNOUNCE params.type = 'ws' params.socket = socket + if (params.action === 'announce' || params.answer || params.offers) { + params.action = common.ACTIONS.ANNOUNCE - 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) - - 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) - - 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`)') + if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { + throw new Error('invalid info_hash') } - params.to_peer_id = common.binaryToHex(params.to_peer_id) - } + params.info_hash = common.binaryToHex(params.info_hash) - params.left = Number(params.left) || Infinity - params.numwant = Math.min( - Number(params.offers && params.offers.length) || 0, // no default - explicit only - common.MAX_ANNOUNCE_PEERS - ) - params.compact = -1 // return full peer objects (used for websocket responses) + 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) + + 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.left = Number(params.left) || Infinity + params.numwant = Math.min( + Number(params.offers && params.offers.length) || 0, // no default - explicit only + common.MAX_ANNOUNCE_PEERS + ) + params.compact = -1 // return full peer objects (used for websocket responses) + } else if (params.action === 'scrape') { + params.action = common.ACTIONS.SCRAPE + + if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ] + if (Array.isArray(params.info_hash)) { + params.info_hash = params.info_hash.map(function (binaryInfoHash) { + if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { + throw new Error('invalid info_hash') + } + return common.binaryToHex(binaryInfoHash) + }) + } else { + params.info_hash = common.binaryToHex(params.info_hash) + } + } else { + throw new Error('invalid action in WS request: ' + params.action) + } params.ip = opts.trustProxy ? socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress diff --git a/server.js b/server.js index e60a581..3ee3d5b 100644 --- a/server.js +++ b/server.js @@ -327,6 +327,7 @@ Server.prototype._onWebSocketRequest = function (socket, opts, params) { self._onRequest(params, function (err, response) { if (err) { socket.send(JSON.stringify({ + action: params.action, 'failure reason': err.message, info_hash: common.hexToBinary(params.info_hash) }), socket.onSend) @@ -336,9 +337,14 @@ Server.prototype._onWebSocketRequest = function (socket, opts, params) { } if (self.destroyed) return - if (socket.infoHashes.indexOf(params.info_hash) === -1) { - socket.infoHashes.push(params.info_hash) - } + var hashes + if (typeof params.info_hash === 'string') hashes = [ params.info_hash ] + else hashes = params.info_hash + hashes.forEach(function (info_hash) { + if (socket.infoHashes.indexOf(info_hash) === -1) { + socket.infoHashes.push(info_hash) + } + }) var peers = response.peers delete response.peers