Search code examples
node.jsexpressp2pbittorrentbencoding

Having issues with BitTorrent Protocol


I'm trying to make just a simple BitTorrent tracker for a school project. It's totally hacked together right now but I can't find where I'm going wrong. I'm wondering if I have a misunderstanding of what the server response should be. I am using node.js and express.

The server receives /GET requests with the ?info_hash data no problem. And i'm able to save that info into JSON files. The server is also able to respond to the clients using bencoding. The response is a dictionary which has an interval and a peers list. Inside the list is several dictionaries and each dictionary holds the ip and port of a peer.

Currently though none of the peers will connect to each other. I'll have my laptop on a separate network from my desktop and it will see the desktop as a potential peer, correct ip and port (as far as I know) but after a moment it drops off the peer list. I am using deluge and qBitTorrent on each client.

Here's the code for the app:

var express = require('express');
var app = express();
var fs = require("fs");
var contents = fs.readFileSync("data.json");
var data = JSON.parse(contents);

var findTorrent = function(data, hash) {
  for(var i = 0; i < data.length; i++) {
    if(data[i].info_hash === hash) {
      return data[i];
    }
  }
  return false;
}

var findID = function(data, qPort, qip) {
  for(var i = 0; i < data.length; i++) {
    //console.log(data[i].peer_id);
    if(data[i].port == qPort && data[i].ip === qip) {
      return true;
    }
  }
  return false;
}

var findHash = function(data, id) {
  for(var i = 0; i < data.length; i++) {
    if(data[i].peer_id === id) {
      return data[i];
    }
  }
  return false;
}

function hashy (str) {
  var url = str;
  var hexval = '';

  for(var i = 0; i < url.length; i++) {
    if(url[i] !== '%') {
      var code = url.charCodeAt(i);
      var hex = code.toString(16);
      hexval += hex;
    } else {
      hexval += url[i+1] + url[i+2];
      i += 2;
    }
  }
  return hexval;
}

app.get('/', function(req, res) {

  console.log(req.query);
  var info_hash = hashy(req.query.info_hash);
  console.log(info_hash);
  var peer_id = decodeURIComponent(req.query.peer_id);
  var escaped = escape(req.query.peer_id);
  console.log('escaped ' + escaped);
  console.log('decoded ' + peer_id);
  console.log('normal  ' + req.query.peer_id);

  var ip = req.connection.remoteAddress;
  if(ip.substring(0,7) == '::ffff:') {
    ip = ip.substring(7);
  }
  //var port = req.connection.remotePort;
  var port = req.query.port;
  console.log(ip);
  var torrent = findTorrent(data, info_hash);
  var completed;
  if (torrent === false){
    if(req.query.left === '0') {
      completed = true;
    } else {
      completed = false;
    }
    var obj = { "info_hash" : info_hash, "peers" : [{ "peer_id" : peer_id, "ip" : ip, "port" : port, "completed" : completed }]};
    data.push(obj);
    torrent = obj;
    //console.log(obj.peers);
  }
  else {
    //figure out if completed
    if(req.query.left == '0') {
      completed = true;
    } else {
      completed = false;
    }

    var peer = findHash(torrent.peers, peer_id);
    if(peer === false){
      var obj = { "peer_id" : peer_id, "ip" : ip, "port" : port, "completed" : completed };
      torrent.peers.push(obj);
    }
    else {
      peer.ip = ip;
      peer.port = port;
      peer.completed = completed;
    }
  }

  if(torrent) {
    var response = bencode(torrent);
  }
  else {
    response = 'error';
  }

  //console.log(data);

  fs.writeFileSync("data.json", JSON.stringify(data, null, 2), 'utf-8');
  res.send(response);
});

var bencode = function(torrent) {
  var response = 'd8:intervali600e12:min intervali30e'
  var complete = 0;
  var incomplete = 0;
  for(var i = 0; i < torrent.peers.length; i++) {
    if(torrent.peers[i].completed === true) {
      complete++;
    } else {
      incomplete++;
    }
  }
  var response = response.concat('8:completei' + complete + 'e');
  var response = response.concat('10:incompletei' + incomplete + 'e5:peersl');
  for(var i = 0; i < torrent.peers.length; i++) {
    response = response.concat('d');
    response = response.concat('2:ip');
    response = response.concat(torrent.peers[i].ip.length + ':');
    response = response.concat(torrent.peers[i].ip);
    //response = response.concat('7:peer id');
    //response = response.concat(torrent.peers[i].peer_id.length + ':');
    //response = response.concat(torrent.peers[i].peer_id);
    response = response.concat('4:port');
    response = response.concat('i' + torrent.peers[i].port + 'e');
    response = response.concat('e');
  }
  response = response.concat('ee');
  console.log(response);
  return response;
}

app.listen(4000, function() {
  console.log('Example app listening on port 4000!');
});

I'm able to connect to the tracker hosted on Amazon AWS and qBitTorrent reports it as "working". I also can see the GET request going out and the server response coming in via wireshark. The request has the following bencoded string which I believe is all that's necessary:

d8:intervali600e12:min intervali30e8:completei2e10:incompletei3e5:peersld2:ip13:73.66.138.2174:porti8999eed2:ip13:73.66.138.2174:porti63014eed2:ip13:73.66.138.2174:porti8999eed2:ip13:73.25.106.1804:porti6881eed2:ip13:73.66.249.1414:porti8999eeee

According to www.bittorrent.org all that is necessary in the response is an interval and a peer list mapped to a list of peers. Each peer needs id, ip, and port.

I've switched the port to the one that the client is reporting in the request and made sure that my torrent client has it's port forwarded and it seems to be working now. Though I'm still going to continue working on this. Currently I don't have a way to remove peers when they stop seeding/leeching.


Solution

  • This seems to mostly be a issue between the peers and not the tracker. If they are both NATed, at least one of them needs to have the port forwarded throu the NAT for them to be able to connect to each other.

    The port in the tracker response should be the one that the peer reports in the request.

    The bencoded dict in the tracker response is not sorted, the order of the Keys complete, incomplete, interval, min interval, peers should be sorted as raw strings.
    Some clients may have problems if they aren't.

    Another thing is, the tracker response specified in BEP3, while still correct, has been obsoleted by the compact=1 response. All modern clients support 'compact'. While I'm not aware of any client that has dropped support for the legacy way, some trackers has.
    Bram Cohen has said that "... non-support for the 'compact' extension is considered outright malbehavior today." post #5

    A good resource about the BitTorrent protocol is https://wiki.theory.org/BitTorrentSpecification

    This answer is a edited version of what was originaly posted as comments.