Search code examples
javascriptnode.jsudpbittorrent

UDP tracker scrape in node.js returning zero unexpectedly


I'm trying to perform a UDP scrape from public UDP trackers such as tracker.publicbt.com or tracker.openbittorrent.com using the BitTorrent UDP Tracker Protocol. My app sends a request to the tracker for a connection_id and uses that id to perform a scrape. The scrape response is returned from the tracker, with no errors to indicate a badly formed packet, but no matter what info_hash I use, I get "0" returned for the numbers of seeders, leechers and completed.

I've thoroughly checked that the packet is the right size, that the info_hash starts at the correct offset, and that the data are all correct. As far as I can see, there are no problems creating and sending the packet. This question's been open and unanswered for a few days, so I've updated and edited the code example below in the hope someone can help.

I've hardcoded an info_hash into the following example. When run on the command line this code should connect to the tracker, get a connection_id and then perform a scrape on an Ubuntu torrent info_hash, outputting various bits of info to the console.

The connection_id is split into 2 parts because it is a 64 bit integer.

var dgram = require('dgram'),
    server = dgram.createSocket("udp4"),
    connectionIdHigh = 0x417,
    connectionIdLow = 0x27101980,
    transactionId,
    action,
    trackerHost = "tracker.publicbt.com",
    trackerPort = 80,
    infoHash = "",
    ACTION_CONNECT = 0,
    ACTION_ANNOUNCE = 1,
    ACTION_SCRAPE = 2,
    ACTION_ERROR = 3,
    sendPacket = function (buf, host, port) {
        "use strict";
        server.send(buf, 0, buf.length, port, host, function(err, bytes) {
            if (err) {
                console.log(err.message);
            }
        });
    },
    startConnection = function (host, port) {
        "use strict";
        var buf = new Buffer(16);

        transactionId = Math.floor((Math.random()*100000)+1);

        buf.fill(0);

        buf.writeUInt32BE(connectionIdHigh, 0);
        buf.writeUInt32BE(connectionIdLow, 4);
        buf.writeUInt32BE(ACTION_CONNECT, 8);
        buf.writeUInt32BE(transactionId, 12);

        sendPacket(buf, host, port);
    },
    scrapeTorrent = function (host, port, hash) {
        "use strict";
        var buf = new Buffer(56),
            tmp = '';

        infoHash = hash;

        if (!transactionId) {
            startConnection(host, port);
        } else {

            buf.fill(0);

            buf.writeUInt32BE(connectionIdHigh, 0);
            buf.writeUInt32BE(connectionIdLow, 4);
            buf.writeUInt32BE(ACTION_SCRAPE, 8);
            buf.writeUInt32BE(transactionId, 12);
            buf.write(infoHash, 16, buf.length);

            console.log(infoHash);
            console.log(buf.toString('utf8', 16, buf.length));

            // do scrape
            sendPacket(buf, host, port);

            transactionId = null;
            infoHash = null;
        }

    };

server.on("message", function (msg, rinfo) {
    "use strict";
    var buf = new Buffer(msg),
        seeders,
        completed,
        leechers;

    console.log(rinfo);

    action = buf.readUInt32BE(0, 4);
    transactionId = buf.readUInt32BE(4, 4);

    console.log("returned action: " + action);
    console.log("returned transactionId: " + transactionId);

    if (action === ACTION_CONNECT) {
        console.log("connect response");

        connectionIdHigh = buf.readUInt32BE(8, 4);
        connectionIdLow = buf.readUInt32BE(12, 4);

        scrapeTorrent(trackerHost, trackerPort, infoHash);

    } else if (action === ACTION_SCRAPE) {
        console.log("scrape response");

        seeders = buf.readUInt32BE(8, 4);
        completed = buf.readUInt32BE(12, 4);
        leechers = buf.readUInt32BE(16, 4);

        console.log(seeders);
        console.log(completed);
        console.log(leechers);

    } else if (action === ACTION_ERROR) {
        console.log("error response");
    }
});

server.on("listening", function () {
    "use strict";
    var address = server.address();
    console.log("server listening " + address.address + ":" + address.port);
});

server.bind();

scrapeTorrent(trackerHost, trackerPort, "335990D615594B9BE409CCFEB95864E24EC702C7");

Solution

  • I finally worked this out, and kicked myself for not realising sooner.

    An info_hash is a hex encoded string, so when it's written to the buffer needs to have it's encoding set. For example:

    buf.write(infoHash, 16, buf.length, 'hex');
    

    The UDP tracker protocol doesn't mention the encoding required, it just describes it as a 20 byte string. Hopefully this Q&A might help someone else who encounters the same problem.