qBittorrent
tracker.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2019 Mike Tzou (Chocobo1)
4  * Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
5  * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  * In addition, as a special exception, the copyright holders give permission to
22  * link this program with the OpenSSL project's "OpenSSL" library (or with
23  * modified versions of it that use the same license as the "OpenSSL" library),
24  * and distribute the linked executables. You must obey the GNU General Public
25  * License in all respects for all of the code used other than "OpenSSL". If you
26  * modify file(s), you may extend this exception to your version of the file(s),
27  * but you are not obligated to do so. If you do not wish to do so, delete this
28  * exception statement from your version.
29  */
30 
31 #include "tracker.h"
32 
33 #include <libtorrent/bencode.hpp>
34 #include <libtorrent/entry.hpp>
35 
36 #include <QHostAddress>
37 
38 #include "base/exceptions.h"
39 #include "base/global.h"
40 #include "base/http/httperror.h"
41 #include "base/http/server.h"
42 #include "base/http/types.h"
43 #include "base/logger.h"
44 #include "base/preferences.h"
45 
46 namespace
47 {
48  // static limits
49  const int MAX_TORRENTS = 10000;
50  const int MAX_PEERS_PER_TORRENT = 200;
51  const int ANNOUNCE_INTERVAL = 1800; // 30min
52 
53  // constants
54  const int PEER_ID_SIZE = 20;
55 
56  const char ANNOUNCE_REQUEST_PATH[] = "/announce";
57 
58  const char ANNOUNCE_REQUEST_COMPACT[] = "compact";
59  const char ANNOUNCE_REQUEST_INFO_HASH[] = "info_hash";
60  const char ANNOUNCE_REQUEST_IP[] = "ip";
61  const char ANNOUNCE_REQUEST_LEFT[] = "left";
62  const char ANNOUNCE_REQUEST_NO_PEER_ID[] = "no_peer_id";
63  const char ANNOUNCE_REQUEST_NUM_WANT[] = "numwant";
64  const char ANNOUNCE_REQUEST_PEER_ID[] = "peer_id";
65  const char ANNOUNCE_REQUEST_PORT[] = "port";
66 
67  const char ANNOUNCE_REQUEST_EVENT[] = "event";
68  const char ANNOUNCE_REQUEST_EVENT_COMPLETED[] = "completed";
69  const char ANNOUNCE_REQUEST_EVENT_EMPTY[] = "empty";
70  const char ANNOUNCE_REQUEST_EVENT_STARTED[] = "started";
71  const char ANNOUNCE_REQUEST_EVENT_STOPPED[] = "stopped";
72  const char ANNOUNCE_REQUEST_EVENT_PAUSED[] = "paused";
73 
74  const char ANNOUNCE_RESPONSE_COMPLETE[] = "complete";
75  const char ANNOUNCE_RESPONSE_EXTERNAL_IP[] = "external ip";
76  const char ANNOUNCE_RESPONSE_FAILURE_REASON[] = "failure reason";
77  const char ANNOUNCE_RESPONSE_INCOMPLETE[] = "incomplete";
78  const char ANNOUNCE_RESPONSE_INTERVAL[] = "interval";
79  const char ANNOUNCE_RESPONSE_PEERS6[] = "peers6";
80  const char ANNOUNCE_RESPONSE_PEERS[] = "peers";
81 
82  const char ANNOUNCE_RESPONSE_PEERS_IP[] = "ip";
83  const char ANNOUNCE_RESPONSE_PEERS_PEER_ID[] = "peer id";
84  const char ANNOUNCE_RESPONSE_PEERS_PORT[] = "port";
85 
86  class TrackerError : public RuntimeError
87  {
88  public:
89  using RuntimeError::RuntimeError;
90  };
91 
92  QByteArray toBigEndianByteArray(const QHostAddress &addr)
93  {
94  // translate IP address to a sequence of bytes in big-endian order
95  switch (addr.protocol())
96  {
97  case QAbstractSocket::IPv4Protocol:
98  case QAbstractSocket::AnyIPProtocol:
99  {
100  const quint32 ipv4 = addr.toIPv4Address();
101  QByteArray ret;
102  ret.append(static_cast<char>((ipv4 >> 24) & 0xFF))
103  .append(static_cast<char>((ipv4 >> 16) & 0xFF))
104  .append(static_cast<char>((ipv4 >> 8) & 0xFF))
105  .append(static_cast<char>(ipv4 & 0xFF));
106  return ret;
107  }
108 
109  case QAbstractSocket::IPv6Protocol:
110  {
111  const Q_IPV6ADDR ipv6 = addr.toIPv6Address();
112  QByteArray ret;
113  for (const quint8 i : ipv6.c)
114  ret.append(i);
115  return ret;
116  }
117 
118  case QAbstractSocket::UnknownNetworkLayerProtocol:
119  default:
120  return {};
121  };
122  }
123 }
124 
125 namespace BitTorrent
126 {
127  // Peer
128  QByteArray Peer::uniqueID() const
129  {
130  return (QByteArray::fromStdString(address) + ':' + QByteArray::number(port));
131  }
132 
133  bool operator==(const Peer &left, const Peer &right)
134  {
135  return (left.uniqueID() == right.uniqueID());
136  }
137 
138  bool operator!=(const Peer &left, const Peer &right)
139  {
140  return !(left == right);
141  }
142 
143  uint qHash(const Peer &key, const uint seed)
144  {
145  return qHash(key.uniqueID(), seed);
146  }
147 }
148 
149 using namespace BitTorrent;
150 
151 // TrackerAnnounceRequest
153 {
154  QHostAddress socketAddress;
155  QByteArray claimedAddress; // self claimed by peer
157  QString event;
159  int numwant = 50;
160  bool compact = true;
161  bool noPeerId = false;
162 };
163 
164 // Tracker::TorrentStats
166 {
167  // always replace existing peer
168  if (!removePeer(peer))
169  {
170  // Too many peers, remove a random one
171  if (peers.size() >= MAX_PEERS_PER_TORRENT)
172  removePeer(*peers.begin());
173  }
174 
175  // add peer
176  if (peer.isSeeder)
177  ++seeders;
178  peers.insert(peer);
179 }
180 
182 {
183  const auto iter = peers.find(peer);
184  if (iter == peers.end())
185  return false;
186 
187  if (iter->isSeeder)
188  --seeders;
189  peers.remove(*iter);
190  return true;
191 }
192 
193 // Tracker
194 Tracker::Tracker(QObject *parent)
195  : QObject(parent)
196  , m_server(new Http::Server(this, this))
197 {
198 }
199 
201 {
202  const QHostAddress ip = QHostAddress::Any;
203  const int port = Preferences::instance()->getTrackerPort();
204 
205  if (m_server->isListening())
206  {
207  if (m_server->serverPort() == port)
208  {
209  // Already listening on the right port, just return
210  return true;
211  }
212 
213  // Wrong port, closing the server
214  m_server->close();
215  }
216 
217  // Listen on the predefined port
218  const bool listenSuccess = m_server->listen(ip, port);
219 
220  if (listenSuccess)
221  {
222  LogMsg(tr("Embedded Tracker: Now listening on IP: %1, port: %2")
223  .arg(ip.toString(), QString::number(port)), Log::INFO);
224  }
225  else
226  {
227  LogMsg(tr("Embedded Tracker: Unable to bind to IP: %1, port: %2. Reason: %3")
228  .arg(ip.toString(), QString::number(port), m_server->errorString())
229  , Log::WARNING);
230  }
231 
232  return listenSuccess;
233 }
234 
236 {
237  clear(); // clear response
238 
239  m_request = request;
240  m_env = env;
241 
242  status(200);
243 
244  try
245  {
246  // Is it a GET request?
249 
250  if (request.path.startsWith(ANNOUNCE_REQUEST_PATH, Qt::CaseInsensitive))
252  else
253  throw NotFoundHTTPError();
254  }
255  catch (const HTTPError &error)
256  {
257  status(error.statusCode(), error.statusText());
258  if (!error.message().isEmpty())
260  }
261  catch (const TrackerError &error)
262  {
263  clear(); // clear response
264  status(200);
265 
266  const lt::entry::dictionary_type bencodedEntry =
267  {
268  {ANNOUNCE_RESPONSE_FAILURE_REASON, {error.message().toStdString()}}
269  };
270  QByteArray reply;
271  lt::bencode(std::back_inserter(reply), bencodedEntry);
273  }
274 
275  return response();
276 }
277 
279 {
280  const QHash<QString, QByteArray> &queryParams = m_request.query;
281  TrackerAnnounceRequest announceReq;
282 
283  // ip address
284  announceReq.socketAddress = m_env.clientAddress;
285  announceReq.claimedAddress = queryParams.value(ANNOUNCE_REQUEST_IP);
286 
287  // Enforce using IPv4 if address is indeed IPv4 or if it is an IPv4-mapped IPv6 address
288  bool ok = false;
289  const qint32 decimalIPv4 = announceReq.socketAddress.toIPv4Address(&ok);
290  if (ok)
291  announceReq.socketAddress = QHostAddress(decimalIPv4);
292 
293  // 1. info_hash
294  const auto infoHashIter = queryParams.find(ANNOUNCE_REQUEST_INFO_HASH);
295  if (infoHashIter == queryParams.end())
296  throw TrackerError("Missing \"info_hash\" parameter");
297 
298  const auto torrentID = TorrentID::fromString(infoHashIter->toHex());
299  if (!torrentID.isValid())
300  throw TrackerError("Invalid \"info_hash\" parameter");
301 
302  announceReq.torrentID = torrentID;
303 
304  // 2. peer_id
305  const auto peerIdIter = queryParams.find(ANNOUNCE_REQUEST_PEER_ID);
306  if (peerIdIter == queryParams.end())
307  throw TrackerError("Missing \"peer_id\" parameter");
308 
309  if (peerIdIter->size() > PEER_ID_SIZE)
310  throw TrackerError("Invalid \"peer_id\" parameter");
311 
312  announceReq.peer.peerId = *peerIdIter;
313 
314  // 3. port
315  const auto portIter = queryParams.find(ANNOUNCE_REQUEST_PORT);
316  if (portIter == queryParams.end())
317  throw TrackerError("Missing \"port\" parameter");
318 
319  const ushort portNum = portIter->toUShort();
320  if (portNum == 0)
321  throw TrackerError("Invalid \"port\" parameter");
322 
323  announceReq.peer.port = portNum;
324 
325  // 4. numwant
326  const auto numWantIter = queryParams.find(ANNOUNCE_REQUEST_NUM_WANT);
327  if (numWantIter != queryParams.end())
328  {
329  const int num = numWantIter->toInt();
330  if (num < 0)
331  throw TrackerError("Invalid \"numwant\" parameter");
332  announceReq.numwant = num;
333  }
334 
335  // 5. no_peer_id
336  // non-formal extension
337  announceReq.noPeerId = (queryParams.value(ANNOUNCE_REQUEST_NO_PEER_ID) == "1");
338 
339  // 6. left
340  announceReq.peer.isSeeder = (queryParams.value(ANNOUNCE_REQUEST_LEFT) == "0");
341 
342  // 7. compact
343  announceReq.compact = (queryParams.value(ANNOUNCE_REQUEST_COMPACT) != "0");
344 
345  // 8. cache `peers` field so we don't recompute when sending response
346  const QHostAddress claimedIPAddress {QString::fromLatin1(announceReq.claimedAddress)};
347  announceReq.peer.endpoint = toBigEndianByteArray(!claimedIPAddress.isNull() ? claimedIPAddress : announceReq.socketAddress)
348  .append(static_cast<char>((announceReq.peer.port >> 8) & 0xFF))
349  .append(static_cast<char>(announceReq.peer.port & 0xFF))
350  .toStdString();
351 
352  // 9. cache `address` field so we don't recompute when sending response
353  announceReq.peer.address = !announceReq.claimedAddress.isEmpty()
354  ? announceReq.claimedAddress.constData()
355  : announceReq.socketAddress.toString().toLatin1().constData(),
356 
357  // 10. event
358  announceReq.event = queryParams.value(ANNOUNCE_REQUEST_EVENT);
359 
360  if (announceReq.event.isEmpty()
361  || (announceReq.event == ANNOUNCE_REQUEST_EVENT_EMPTY)
362  || (announceReq.event == ANNOUNCE_REQUEST_EVENT_COMPLETED)
363  || (announceReq.event == ANNOUNCE_REQUEST_EVENT_STARTED)
364  || (announceReq.event == ANNOUNCE_REQUEST_EVENT_PAUSED))
365  {
366  // [BEP-21] Extension for partial seeds
367  // (partial support - we don't support BEP-48 so the part that concerns that is not supported)
368  registerPeer(announceReq);
369  }
370  else if (announceReq.event == ANNOUNCE_REQUEST_EVENT_STOPPED)
371  {
372  unregisterPeer(announceReq);
373  }
374  else
375  {
376  throw TrackerError("Invalid \"event\" parameter");
377  }
378 
379  prepareAnnounceResponse(announceReq);
380 }
381 
383 {
384  if (!m_torrents.contains(announceReq.torrentID))
385  {
386  // Reached max size, remove a random torrent
387  if (m_torrents.size() >= MAX_TORRENTS)
388  m_torrents.erase(m_torrents.begin());
389  }
390 
391  m_torrents[announceReq.torrentID].setPeer(announceReq.peer);
392 }
393 
395 {
396  const auto torrentStatsIter = m_torrents.find(announceReq.torrentID);
397  if (torrentStatsIter == m_torrents.end())
398  return;
399 
400  torrentStatsIter->removePeer(announceReq.peer);
401 
402  if (torrentStatsIter->peers.isEmpty())
403  m_torrents.erase(torrentStatsIter);
404 }
405 
407 {
408  const TorrentStats &torrentStats = m_torrents[announceReq.torrentID];
409 
410  lt::entry::dictionary_type replyDict
411  {
413  {ANNOUNCE_RESPONSE_COMPLETE, torrentStats.seeders},
414  {ANNOUNCE_RESPONSE_INCOMPLETE, (torrentStats.peers.size() - torrentStats.seeders)},
415 
416  // [BEP-24] Tracker Returns External IP (partial support - might not work properly for all IPv6 cases)
418  };
419 
420  // peer list
421  // [BEP-7] IPv6 Tracker Extension (partial support - only the part that concerns BEP-23)
422  // [BEP-23] Tracker Returns Compact Peer Lists
423  if (announceReq.compact)
424  {
425  lt::entry::string_type peers;
426  lt::entry::string_type peers6;
427 
428  if (announceReq.event != ANNOUNCE_REQUEST_EVENT_STOPPED)
429  {
430  int counter = 0;
431  for (const Peer &peer : asConst(torrentStats.peers))
432  {
433  if (counter++ >= announceReq.numwant)
434  break;
435 
436  if (peer.endpoint.size() == 6) // IPv4 + port
437  peers.append(peer.endpoint);
438  else if (peer.endpoint.size() == 18) // IPv6 + port
439  peers6.append(peer.endpoint);
440  }
441  }
442 
443  replyDict[ANNOUNCE_RESPONSE_PEERS] = peers; // required, even it's empty
444  if (!peers6.empty())
445  replyDict[ANNOUNCE_RESPONSE_PEERS6] = peers6;
446  }
447  else
448  {
449  lt::entry::list_type peerList;
450 
451  if (announceReq.event != ANNOUNCE_REQUEST_EVENT_STOPPED)
452  {
453  int counter = 0;
454  for (const Peer &peer : torrentStats.peers)
455  {
456  if (counter++ >= announceReq.numwant)
457  break;
458 
459  lt::entry::dictionary_type peerDict =
460  {
463  };
464 
465  if (!announceReq.noPeerId)
466  peerDict[ANNOUNCE_RESPONSE_PEERS_PEER_ID] = peer.peerId.constData();
467 
468  peerList.emplace_back(peerDict);
469  }
470  }
471 
472  replyDict[ANNOUNCE_RESPONSE_PEERS] = peerList;
473  }
474 
475  // bencode
476  QByteArray reply;
477  lt::bencode(std::back_inserter(reply), replyDict);
479 }
static TorrentID fromString(const QString &hashString)
Definition: infohash.cpp:76
void prepareAnnounceResponse(const TrackerAnnounceRequest &announceReq)
Definition: tracker.cpp:406
void processAnnounceRequest()
Definition: tracker.cpp:278
Http::Environment m_env
Definition: tracker.h:103
Http::Response processRequest(const Http::Request &request, const Http::Environment &env) override
Definition: tracker.cpp:235
void registerPeer(const TrackerAnnounceRequest &announceReq)
Definition: tracker.cpp:382
Http::Request m_request
Definition: tracker.h:102
QHash< TorrentID, TorrentStats > m_torrents
Definition: tracker.h:105
Tracker(QObject *parent=nullptr)
Definition: tracker.cpp:194
Http::Server * m_server
Definition: tracker.h:101
void unregisterPeer(const TrackerAnnounceRequest &announceReq)
Definition: tracker.cpp:394
QString message() const noexcept
Definition: exceptions.cpp:36
QString statusText() const
Definition: httperror.cpp:43
int statusCode() const
Definition: httperror.cpp:38
Response response() const
void print(const QString &text, const QString &type=CONTENT_TYPE_HTML)
void status(uint code=200, const QString &text=QLatin1String("OK"))
static Preferences * instance()
int getTrackerPort() const
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
uint qHash(const TorrentID &key, uint seed)
Definition: infohash.cpp:86
bool operator==(const CategoryOptions::DownloadPathOption &left, const CategoryOptions::DownloadPathOption &right)
bool operator!=(const InfoHash &left, const InfoHash &right)
Definition: infohash.cpp:96
Definition: tracker.h:46
const char HEADER_REQUEST_METHOD_GET[]
Definition: types.h:60
const char CONTENT_TYPE_TXT[]
Definition: types.h:66
@ WARNING
Definition: logger.h:47
@ INFO
Definition: logger.h:46
const char ANNOUNCE_REQUEST_EVENT_STOPPED[]
Definition: tracker.cpp:71
const char ANNOUNCE_REQUEST_EVENT_COMPLETED[]
Definition: tracker.cpp:68
QByteArray toBigEndianByteArray(const QHostAddress &addr)
Definition: tracker.cpp:92
const char ANNOUNCE_REQUEST_NO_PEER_ID[]
Definition: tracker.cpp:62
const char ANNOUNCE_RESPONSE_PEERS6[]
Definition: tracker.cpp:79
const char ANNOUNCE_REQUEST_PATH[]
Definition: tracker.cpp:56
const char ANNOUNCE_REQUEST_EVENT_PAUSED[]
Definition: tracker.cpp:72
const char ANNOUNCE_REQUEST_NUM_WANT[]
Definition: tracker.cpp:63
const char ANNOUNCE_RESPONSE_INCOMPLETE[]
Definition: tracker.cpp:77
const char ANNOUNCE_RESPONSE_INTERVAL[]
Definition: tracker.cpp:78
const char ANNOUNCE_RESPONSE_PEERS_PEER_ID[]
Definition: tracker.cpp:83
const char ANNOUNCE_REQUEST_LEFT[]
Definition: tracker.cpp:61
const char ANNOUNCE_RESPONSE_PEERS_PORT[]
Definition: tracker.cpp:84
const char ANNOUNCE_REQUEST_EVENT_STARTED[]
Definition: tracker.cpp:70
const char ANNOUNCE_REQUEST_PEER_ID[]
Definition: tracker.cpp:64
const char ANNOUNCE_RESPONSE_COMPLETE[]
Definition: tracker.cpp:74
const char ANNOUNCE_RESPONSE_FAILURE_REASON[]
Definition: tracker.cpp:76
const char ANNOUNCE_REQUEST_EVENT_EMPTY[]
Definition: tracker.cpp:69
const char ANNOUNCE_REQUEST_EVENT[]
Definition: tracker.cpp:67
const char ANNOUNCE_REQUEST_COMPACT[]
Definition: tracker.cpp:58
const char ANNOUNCE_RESPONSE_EXTERNAL_IP[]
Definition: tracker.cpp:75
const char ANNOUNCE_REQUEST_PORT[]
Definition: tracker.cpp:65
const char ANNOUNCE_RESPONSE_PEERS[]
Definition: tracker.cpp:80
const char ANNOUNCE_RESPONSE_PEERS_IP[]
Definition: tracker.cpp:82
const char ANNOUNCE_REQUEST_INFO_HASH[]
Definition: tracker.cpp:59
QByteArray peerId
Definition: tracker.h:54
lt::entry::string_type endpoint
Definition: tracker.h:60
ushort port
Definition: tracker.h:55
lt::entry::string_type address
Definition: tracker.h:59
QByteArray uniqueID() const
Definition: tracker.cpp:128
void setPeer(const Peer &peer)
Definition: tracker.cpp:165
bool removePeer(const Peer &peer)
Definition: tracker.cpp:181
QHostAddress clientAddress
Definition: types.h:82
QString path
Definition: types.h:105
QHash< QString, QByteArray > query
Definition: types.h:107
QString method
Definition: types.h:104