GHA CI: show installed version
[qBittorrent.git] / src / base / bittorrent / tracker.cpp
blob6da1321cd6df9b427d92b0a86273b8611678560b
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>
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.
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.
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.
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.
31 #include "tracker.h"
33 #include <libtorrent/bencode.hpp>
34 #include <libtorrent/entry.hpp>
36 #include <QHostAddress>
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"
46 namespace
48 // static limits
49 const int MAX_TORRENTS = 10000;
50 const int MAX_PEERS_PER_TORRENT = 200;
51 const int ANNOUNCE_INTERVAL = 1800; // 30min
53 // constants
54 const int PEER_ID_SIZE = 20;
56 const QString ANNOUNCE_REQUEST_PATH = u"/announce"_s;
58 const QString ANNOUNCE_REQUEST_COMPACT = u"compact"_s;
59 const QString ANNOUNCE_REQUEST_INFO_HASH = u"info_hash"_s;
60 const QString ANNOUNCE_REQUEST_IP = u"ip"_s;
61 const QString ANNOUNCE_REQUEST_LEFT = u"left"_s;
62 const QString ANNOUNCE_REQUEST_NO_PEER_ID = u"no_peer_id"_s;
63 const QString ANNOUNCE_REQUEST_NUM_WANT = u"numwant"_s;
64 const QString ANNOUNCE_REQUEST_PEER_ID = u"peer_id"_s;
65 const QString ANNOUNCE_REQUEST_PORT = u"port"_s;
67 const QString ANNOUNCE_REQUEST_EVENT = u"event"_s;
68 const QString ANNOUNCE_REQUEST_EVENT_COMPLETED = u"completed"_s;
69 const QString ANNOUNCE_REQUEST_EVENT_EMPTY = u"empty"_s;
70 const QString ANNOUNCE_REQUEST_EVENT_STARTED = u"started"_s;
71 const QString ANNOUNCE_REQUEST_EVENT_STOPPED = u"stopped"_s;
72 const QString ANNOUNCE_REQUEST_EVENT_PAUSED = u"paused"_s;
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";
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";
86 class TrackerError : public RuntimeError
88 public:
89 using RuntimeError::RuntimeError;
92 QByteArray toBigEndianByteArray(const QHostAddress &addr)
94 // translate IP address to a sequence of bytes in big-endian order
95 switch (addr.protocol())
97 case QAbstractSocket::IPv4Protocol:
98 case QAbstractSocket::AnyIPProtocol:
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;
109 case QAbstractSocket::IPv6Protocol:
111 const Q_IPV6ADDR ipv6 = addr.toIPv6Address();
112 QByteArray ret;
113 for (const quint8 i : ipv6.c)
114 ret.append(i);
115 return ret;
118 case QAbstractSocket::UnknownNetworkLayerProtocol:
119 default:
120 return {};
125 namespace BitTorrent
127 // Peer
128 QByteArray Peer::uniqueID() const
130 return (QByteArray::fromStdString(address) + ':' + QByteArray::number(port));
133 bool operator==(const Peer &left, const Peer &right)
135 return (left.uniqueID() == right.uniqueID());
138 std::size_t qHash(const Peer &key, const std::size_t seed)
140 return qHash(key.uniqueID(), seed);
144 using namespace BitTorrent;
146 // TrackerAnnounceRequest
147 struct Tracker::TrackerAnnounceRequest
149 QHostAddress socketAddress;
150 QByteArray claimedAddress; // self claimed by peer
151 TorrentID torrentID;
152 QString event;
153 Peer peer;
154 int numwant = 50;
155 bool compact = true;
156 bool noPeerId = false;
159 // Tracker::TorrentStats
160 void Tracker::TorrentStats::setPeer(const Peer &peer)
162 // always replace existing peer
163 if (!removePeer(peer))
165 // Too many peers, remove a random one
166 if (peers.size() >= MAX_PEERS_PER_TORRENT)
167 removePeer(*peers.begin());
170 // add peer
171 if (peer.isSeeder)
172 ++seeders;
173 peers.insert(peer);
176 bool Tracker::TorrentStats::removePeer(const Peer &peer)
178 const auto iter = peers.find(peer);
179 if (iter == peers.end())
180 return false;
182 if (iter->isSeeder)
183 --seeders;
184 peers.remove(*iter);
185 return true;
188 // Tracker
189 Tracker::Tracker(QObject *parent)
190 : QObject(parent)
191 , m_server(new Http::Server(this, this))
195 bool Tracker::start()
197 const int port = Preferences::instance()->getTrackerPort();
199 if (m_server->isListening())
201 if (const int oldPort = m_server->serverPort()
202 ; oldPort == port)
204 // Already listening on the right port, just return
205 return true;
208 // Wrong port, closing the server
209 m_server->close();
212 // Listen on port
213 const QHostAddress ip = QHostAddress::Any;
214 const bool listenSuccess = m_server->listen(ip, port);
215 if (listenSuccess)
217 LogMsg(tr("Embedded Tracker: Now listening on IP: %1, port: %2")
218 .arg(ip.toString(), QString::number(port)), Log::INFO);
220 else
222 LogMsg(tr("Embedded Tracker: Unable to bind to IP: %1, port: %2. Reason: %3")
223 .arg(ip.toString(), QString::number(port), m_server->errorString())
224 , Log::WARNING);
227 return listenSuccess;
230 Http::Response Tracker::processRequest(const Http::Request &request, const Http::Environment &env)
232 clear(); // clear response
234 m_request = request;
235 m_env = env;
237 status(200);
241 // Is it a GET request?
242 if (request.method != Http::HEADER_REQUEST_METHOD_GET)
243 throw MethodNotAllowedHTTPError();
245 if (request.path.startsWith(ANNOUNCE_REQUEST_PATH, Qt::CaseInsensitive))
246 processAnnounceRequest();
247 else
248 throw NotFoundHTTPError();
250 catch (const HTTPError &error)
252 status(error.statusCode(), error.statusText());
253 if (!error.message().isEmpty())
254 print(error.message(), Http::CONTENT_TYPE_TXT);
256 catch (const TrackerError &error)
258 clear(); // clear response
259 status(200);
261 const lt::entry::dictionary_type bencodedEntry =
263 {ANNOUNCE_RESPONSE_FAILURE_REASON, {error.message().toStdString()}}
265 QByteArray reply;
266 lt::bencode(std::back_inserter(reply), bencodedEntry);
267 print(reply, Http::CONTENT_TYPE_TXT);
270 return response();
273 void Tracker::processAnnounceRequest()
275 const QHash<QString, QByteArray> &queryParams = m_request.query;
276 TrackerAnnounceRequest announceReq;
278 // ip address
279 announceReq.socketAddress = m_env.clientAddress;
280 announceReq.claimedAddress = queryParams.value(ANNOUNCE_REQUEST_IP);
282 // Enforce using IPv4 if address is indeed IPv4 or if it is an IPv4-mapped IPv6 address
283 bool ok = false;
284 const qint32 decimalIPv4 = announceReq.socketAddress.toIPv4Address(&ok);
285 if (ok)
286 announceReq.socketAddress = QHostAddress(decimalIPv4);
288 // 1. info_hash
289 const auto infoHashIter = queryParams.find(ANNOUNCE_REQUEST_INFO_HASH);
290 if (infoHashIter == queryParams.end())
291 throw TrackerError(u"Missing \"info_hash\" parameter"_s);
293 const auto torrentID = TorrentID::fromString(QString::fromLatin1(infoHashIter->toHex()));
294 if (!torrentID.isValid())
295 throw TrackerError(u"Invalid \"info_hash\" parameter"_s);
297 announceReq.torrentID = torrentID;
299 // 2. peer_id
300 const auto peerIdIter = queryParams.find(ANNOUNCE_REQUEST_PEER_ID);
301 if (peerIdIter == queryParams.end())
302 throw TrackerError(u"Missing \"peer_id\" parameter"_s);
304 if (peerIdIter->size() > PEER_ID_SIZE)
305 throw TrackerError(u"Invalid \"peer_id\" parameter"_s);
307 announceReq.peer.peerId = *peerIdIter;
309 // 3. port
310 const auto portIter = queryParams.find(ANNOUNCE_REQUEST_PORT);
311 if (portIter == queryParams.end())
312 throw TrackerError(u"Missing \"port\" parameter"_s);
314 const ushort portNum = portIter->toUShort();
315 if (portNum == 0)
316 throw TrackerError(u"Invalid \"port\" parameter"_s);
318 announceReq.peer.port = portNum;
320 // 4. numwant
321 const auto numWantIter = queryParams.find(ANNOUNCE_REQUEST_NUM_WANT);
322 if (numWantIter != queryParams.end())
324 const int num = numWantIter->toInt();
325 if (num < 0)
326 throw TrackerError(u"Invalid \"numwant\" parameter"_s);
327 announceReq.numwant = num;
330 // 5. no_peer_id
331 // non-formal extension
332 announceReq.noPeerId = (queryParams.value(ANNOUNCE_REQUEST_NO_PEER_ID) == "1");
334 // 6. left
335 announceReq.peer.isSeeder = (queryParams.value(ANNOUNCE_REQUEST_LEFT) == "0");
337 // 7. compact
338 announceReq.compact = (queryParams.value(ANNOUNCE_REQUEST_COMPACT) != "0");
340 // 8. cache `peers` field so we don't recompute when sending response
341 const QHostAddress claimedIPAddress {QString::fromLatin1(announceReq.claimedAddress)};
342 announceReq.peer.endpoint = toBigEndianByteArray(!claimedIPAddress.isNull() ? claimedIPAddress : announceReq.socketAddress)
343 .append(static_cast<char>((announceReq.peer.port >> 8) & 0xFF))
344 .append(static_cast<char>(announceReq.peer.port & 0xFF))
345 .toStdString();
347 // 9. cache `address` field so we don't recompute when sending response
348 announceReq.peer.address = !announceReq.claimedAddress.isEmpty()
349 ? announceReq.claimedAddress.constData()
350 : announceReq.socketAddress.toString().toLatin1().constData(),
352 // 10. event
353 announceReq.event = QString::fromLatin1(queryParams.value(ANNOUNCE_REQUEST_EVENT));
355 if (announceReq.event.isEmpty()
356 || (announceReq.event == ANNOUNCE_REQUEST_EVENT_EMPTY)
357 || (announceReq.event == ANNOUNCE_REQUEST_EVENT_COMPLETED)
358 || (announceReq.event == ANNOUNCE_REQUEST_EVENT_STARTED)
359 || (announceReq.event == ANNOUNCE_REQUEST_EVENT_PAUSED))
361 // [BEP-21] Extension for partial seeds
362 // (partial support - we don't support BEP-48 so the part that concerns that is not supported)
363 registerPeer(announceReq);
365 else if (announceReq.event == ANNOUNCE_REQUEST_EVENT_STOPPED)
367 unregisterPeer(announceReq);
369 else
371 throw TrackerError(u"Invalid \"event\" parameter"_s);
374 prepareAnnounceResponse(announceReq);
377 void Tracker::registerPeer(const TrackerAnnounceRequest &announceReq)
379 if (!m_torrents.contains(announceReq.torrentID))
381 // Reached max size, remove a random torrent
382 if (m_torrents.size() >= MAX_TORRENTS)
383 m_torrents.erase(m_torrents.begin());
386 m_torrents[announceReq.torrentID].setPeer(announceReq.peer);
389 void Tracker::unregisterPeer(const TrackerAnnounceRequest &announceReq)
391 const auto torrentStatsIter = m_torrents.find(announceReq.torrentID);
392 if (torrentStatsIter == m_torrents.end())
393 return;
395 torrentStatsIter->removePeer(announceReq.peer);
397 if (torrentStatsIter->peers.isEmpty())
398 m_torrents.erase(torrentStatsIter);
401 void Tracker::prepareAnnounceResponse(const TrackerAnnounceRequest &announceReq)
403 const TorrentStats &torrentStats = m_torrents[announceReq.torrentID];
405 lt::entry::dictionary_type replyDict
407 {ANNOUNCE_RESPONSE_INTERVAL, ANNOUNCE_INTERVAL},
408 {ANNOUNCE_RESPONSE_COMPLETE, torrentStats.seeders},
409 {ANNOUNCE_RESPONSE_INCOMPLETE, (torrentStats.peers.size() - torrentStats.seeders)},
411 // [BEP-24] Tracker Returns External IP (partial support - might not work properly for all IPv6 cases)
412 {ANNOUNCE_RESPONSE_EXTERNAL_IP, toBigEndianByteArray(announceReq.socketAddress).toStdString()}
415 // peer list
416 // [BEP-7] IPv6 Tracker Extension (partial support - only the part that concerns BEP-23)
417 // [BEP-23] Tracker Returns Compact Peer Lists
418 if (announceReq.compact)
420 lt::entry::string_type peers;
421 lt::entry::string_type peers6;
423 if (announceReq.event != ANNOUNCE_REQUEST_EVENT_STOPPED)
425 int counter = 0;
426 for (const Peer &peer : asConst(torrentStats.peers))
428 if (counter++ >= announceReq.numwant)
429 break;
431 if (peer.endpoint.size() == 6) // IPv4 + port
432 peers.append(peer.endpoint);
433 else if (peer.endpoint.size() == 18) // IPv6 + port
434 peers6.append(peer.endpoint);
438 replyDict[ANNOUNCE_RESPONSE_PEERS] = peers; // required, even it's empty
439 if (!peers6.empty())
440 replyDict[ANNOUNCE_RESPONSE_PEERS6] = peers6;
442 else
444 lt::entry::list_type peerList;
446 if (announceReq.event != ANNOUNCE_REQUEST_EVENT_STOPPED)
448 int counter = 0;
449 for (const Peer &peer : torrentStats.peers)
451 if (counter++ >= announceReq.numwant)
452 break;
454 lt::entry::dictionary_type peerDict =
456 {ANNOUNCE_RESPONSE_PEERS_IP, peer.address},
457 {ANNOUNCE_RESPONSE_PEERS_PORT, peer.port}
460 if (!announceReq.noPeerId)
461 peerDict[ANNOUNCE_RESPONSE_PEERS_PEER_ID] = peer.peerId.constData();
463 peerList.emplace_back(peerDict);
467 replyDict[ANNOUNCE_RESPONSE_PEERS] = peerList;
470 // bencode
471 QByteArray reply;
472 lt::bencode(std::back_inserter(reply), replyDict);
473 print(reply, Http::CONTENT_TYPE_TXT);