|
|
@ -1,5 +1,6 @@ |
|
|
|
/*
|
|
|
|
/*
|
|
|
|
* Bittorrent Client using Qt and libtorrent. |
|
|
|
* Bittorrent Client using Qt and libtorrent. |
|
|
|
|
|
|
|
* Copyright (C) 2019 Mike Tzou (Chocobo1) |
|
|
|
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru> |
|
|
|
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru> |
|
|
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> |
|
|
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> |
|
|
|
* |
|
|
|
* |
|
|
@ -32,235 +33,410 @@ |
|
|
|
#include <libtorrent/bencode.hpp> |
|
|
|
#include <libtorrent/bencode.hpp> |
|
|
|
#include <libtorrent/entry.hpp> |
|
|
|
#include <libtorrent/entry.hpp> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include <QHostAddress> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "base/exceptions.h" |
|
|
|
|
|
|
|
#include "base/global.h" |
|
|
|
|
|
|
|
#include "base/http/httperror.h" |
|
|
|
#include "base/http/server.h" |
|
|
|
#include "base/http/server.h" |
|
|
|
|
|
|
|
#include "base/http/types.h" |
|
|
|
|
|
|
|
#include "base/logger.h" |
|
|
|
#include "base/preferences.h" |
|
|
|
#include "base/preferences.h" |
|
|
|
|
|
|
|
|
|
|
|
// static limits
|
|
|
|
namespace |
|
|
|
static const int MAX_TORRENTS = 100; |
|
|
|
{ |
|
|
|
static const int MAX_PEERS_PER_TORRENT = 1000; |
|
|
|
// static limits
|
|
|
|
static const int ANNOUNCE_INTERVAL = 1800; // 30min
|
|
|
|
const int MAX_TORRENTS = 10000; |
|
|
|
|
|
|
|
const int MAX_PEERS_PER_TORRENT = 200; |
|
|
|
|
|
|
|
const int ANNOUNCE_INTERVAL = 1800; // 30min
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// constants
|
|
|
|
|
|
|
|
const int PEER_ID_SIZE = 20; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_PATH[] = "/announce"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_COMPACT[] = "compact"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_INFO_HASH[] = "info_hash"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_IP[] = "ip"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_LEFT[] = "left"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_NO_PEER_ID[] = "no_peer_id"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_NUM_WANT[] = "numwant"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_PEER_ID[] = "peer_id"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_PORT[] = "port"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_EVENT[] = "event"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_EVENT_COMPLETED[] = "completed"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_EVENT_EMPTY[] = "empty"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_EVENT_STARTED[] = "started"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_EVENT_STOPPED[] = "stopped"; |
|
|
|
|
|
|
|
const char ANNOUNCE_REQUEST_EVENT_PAUSED[] = "paused"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_COMPLETE[] = "complete"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_EXTERNAL_IP[] = "external ip"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_FAILURE_REASON[] = "failure reason"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_INCOMPLETE[] = "incomplete"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_INTERVAL[] = "interval"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_PEERS6[] = "peers6"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_PEERS[] = "peers"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_PEERS_IP[] = "ip"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_PEERS_PEER_ID[] = "peer id"; |
|
|
|
|
|
|
|
const char ANNOUNCE_RESPONSE_PEERS_PORT[] = "port"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TrackerError : public RuntimeError |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
using RuntimeError::RuntimeError; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QByteArray toBigEndianByteArray(const QHostAddress &addr) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
// translate IP address to a sequence of bytes in big-endian order
|
|
|
|
|
|
|
|
switch (addr.protocol()) { |
|
|
|
|
|
|
|
case QAbstractSocket::IPv4Protocol: |
|
|
|
|
|
|
|
case QAbstractSocket::AnyIPProtocol: { |
|
|
|
|
|
|
|
const quint32 ipv4 = addr.toIPv4Address(); |
|
|
|
|
|
|
|
QByteArray ret; |
|
|
|
|
|
|
|
ret.append(static_cast<char>((ipv4 >> 24) & 0xFF)) |
|
|
|
|
|
|
|
.append(static_cast<char>((ipv4 >> 16) & 0xFF)) |
|
|
|
|
|
|
|
.append(static_cast<char>((ipv4 >> 8) & 0xFF)) |
|
|
|
|
|
|
|
.append(static_cast<char>(ipv4 & 0xFF)); |
|
|
|
|
|
|
|
return ret; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
using namespace BitTorrent; |
|
|
|
case QAbstractSocket::IPv6Protocol: { |
|
|
|
|
|
|
|
const Q_IPV6ADDR ipv6 = addr.toIPv6Address(); |
|
|
|
|
|
|
|
QByteArray ret; |
|
|
|
|
|
|
|
for (int i = (sizeof(ipv6.c) - 1); i >= 0; --i) |
|
|
|
|
|
|
|
ret.append(static_cast<char>(ipv6.c[i])); |
|
|
|
|
|
|
|
return ret; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Peer
|
|
|
|
case QAbstractSocket::UnknownNetworkLayerProtocol: |
|
|
|
QString Peer::uid() const |
|
|
|
default: |
|
|
|
{ |
|
|
|
return {}; |
|
|
|
return ip.toString() + ':' + QString::number(port); |
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
lt::entry Peer::toEntry(const bool noPeerId) const |
|
|
|
namespace BitTorrent |
|
|
|
{ |
|
|
|
{ |
|
|
|
lt::entry::dictionary_type peerMap; |
|
|
|
// Peer
|
|
|
|
if (!noPeerId) |
|
|
|
QByteArray Peer::uniqueID() const |
|
|
|
peerMap["id"] = lt::entry(peerId.toStdString()); |
|
|
|
{ |
|
|
|
peerMap["ip"] = lt::entry(ip.toString().toStdString()); |
|
|
|
return (QByteArray::fromStdString(address) + ':' + QByteArray::number(port)); |
|
|
|
peerMap["port"] = lt::entry(port); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool operator==(const Peer &left, const Peer &right) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return (left.uniqueID() == right.uniqueID()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return lt::entry(peerMap); |
|
|
|
bool operator!=(const Peer &left, const Peer &right) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return !(left == right); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uint qHash(const Peer &key, const uint seed) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
return qHash(key.uniqueID(), seed); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool BitTorrent::operator==(const Peer &left, const Peer &right) |
|
|
|
using namespace BitTorrent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TrackerAnnounceRequest
|
|
|
|
|
|
|
|
struct Tracker::TrackerAnnounceRequest |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
QHostAddress socketAddress; |
|
|
|
|
|
|
|
QByteArray claimedAddress; // self claimed by peer
|
|
|
|
|
|
|
|
InfoHash infoHash; |
|
|
|
|
|
|
|
QString event; |
|
|
|
|
|
|
|
Peer peer; |
|
|
|
|
|
|
|
int numwant = 50; |
|
|
|
|
|
|
|
bool compact = true; |
|
|
|
|
|
|
|
bool noPeerId = false; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Tracker::TorrentStats
|
|
|
|
|
|
|
|
void Tracker::TorrentStats::setPeer(const Peer &peer) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return left.uid() == right.uid(); |
|
|
|
// always replace existing peer
|
|
|
|
|
|
|
|
if (!removePeer(peer)) { |
|
|
|
|
|
|
|
// Too many peers, remove a random one
|
|
|
|
|
|
|
|
if (peers.size() >= MAX_PEERS_PER_TORRENT) |
|
|
|
|
|
|
|
removePeer(*peers.begin()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// add peer
|
|
|
|
|
|
|
|
if (peer.isSeeder) |
|
|
|
|
|
|
|
++seeders; |
|
|
|
|
|
|
|
peers.insert(peer); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool BitTorrent::operator!=(const Peer &left, const Peer &right) |
|
|
|
bool Tracker::TorrentStats::removePeer(const Peer &peer) |
|
|
|
{ |
|
|
|
{ |
|
|
|
return !(left == right); |
|
|
|
const auto iter = peers.find(peer); |
|
|
|
|
|
|
|
if (iter == peers.end()) |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (iter->isSeeder) |
|
|
|
|
|
|
|
--seeders; |
|
|
|
|
|
|
|
peers.remove(*iter); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Tracker
|
|
|
|
// Tracker
|
|
|
|
|
|
|
|
|
|
|
|
Tracker::Tracker(QObject *parent) |
|
|
|
Tracker::Tracker(QObject *parent) |
|
|
|
: QObject(parent) |
|
|
|
: QObject(parent) |
|
|
|
, m_server(new Http::Server(this, this)) |
|
|
|
, m_server(new Http::Server(this, this)) |
|
|
|
{ |
|
|
|
{ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Tracker::~Tracker() |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (m_server->isListening()) |
|
|
|
|
|
|
|
qDebug("Shutting down the embedded tracker..."); |
|
|
|
|
|
|
|
// TODO: Store the torrent list
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool Tracker::start() |
|
|
|
bool Tracker::start() |
|
|
|
{ |
|
|
|
{ |
|
|
|
const int listenPort = Preferences::instance()->getTrackerPort(); |
|
|
|
const QHostAddress ip = QHostAddress::Any; |
|
|
|
|
|
|
|
const int port = Preferences::instance()->getTrackerPort(); |
|
|
|
|
|
|
|
|
|
|
|
if (m_server->isListening()) { |
|
|
|
if (m_server->isListening()) { |
|
|
|
if (m_server->serverPort() == listenPort) { |
|
|
|
if (m_server->serverPort() == port) { |
|
|
|
// Already listening on the right port, just return
|
|
|
|
// Already listening on the right port, just return
|
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Wrong port, closing the server
|
|
|
|
// Wrong port, closing the server
|
|
|
|
m_server->close(); |
|
|
|
m_server->close(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
qDebug("Starting the embedded tracker..."); |
|
|
|
|
|
|
|
// Listen on the predefined port
|
|
|
|
// Listen on the predefined port
|
|
|
|
return m_server->listen(QHostAddress::Any, listenPort); |
|
|
|
const bool listenSuccess = m_server->listen(ip, port); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (listenSuccess) { |
|
|
|
|
|
|
|
LogMsg(tr("Embedded Tracker: Now listening on IP: %1, port: %2") |
|
|
|
|
|
|
|
.arg(ip.toString(), QString::number(port)), Log::INFO); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
LogMsg(tr("Embedded Tracker: Unable to bind to IP: %1, port: %2. Reason: %3") |
|
|
|
|
|
|
|
.arg(ip.toString(), QString::number(port), m_server->errorString()) |
|
|
|
|
|
|
|
, Log::WARNING); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return listenSuccess; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Http::Response Tracker::processRequest(const Http::Request &request, const Http::Environment &env) |
|
|
|
Http::Response Tracker::processRequest(const Http::Request &request, const Http::Environment &env) |
|
|
|
{ |
|
|
|
{ |
|
|
|
clear(); // clear response
|
|
|
|
clear(); // clear response
|
|
|
|
|
|
|
|
|
|
|
|
//qDebug("Tracker received the following request:\n%s", qUtf8Printable(parser.toString()));
|
|
|
|
|
|
|
|
// Is request a GET request?
|
|
|
|
|
|
|
|
if (request.method != "GET") { |
|
|
|
|
|
|
|
qDebug("Tracker: Unsupported HTTP request: %s", qUtf8Printable(request.method)); |
|
|
|
|
|
|
|
status(100, "Invalid request type"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (!request.path.startsWith("/announce", Qt::CaseInsensitive)) { |
|
|
|
|
|
|
|
qDebug("Tracker: Unrecognized path: %s", qUtf8Printable(request.path)); |
|
|
|
|
|
|
|
status(100, "Invalid request type"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
// OK, this is a GET request
|
|
|
|
|
|
|
|
m_request = request; |
|
|
|
m_request = request; |
|
|
|
m_env = env; |
|
|
|
m_env = env; |
|
|
|
respondToAnnounceRequest(); |
|
|
|
|
|
|
|
|
|
|
|
status(200); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
// Is it a GET request?
|
|
|
|
|
|
|
|
if (request.method != Http::HEADER_REQUEST_METHOD_GET) |
|
|
|
|
|
|
|
throw MethodNotAllowedHTTPError(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (request.path.toLower().startsWith(ANNOUNCE_REQUEST_PATH)) |
|
|
|
|
|
|
|
processAnnounceRequest(); |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
throw NotFoundHTTPError(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
catch (const HTTPError &error) { |
|
|
|
|
|
|
|
status(error.statusCode(), error.statusText()); |
|
|
|
|
|
|
|
if (!error.message().isEmpty()) |
|
|
|
|
|
|
|
print(error.message(), Http::CONTENT_TYPE_TXT); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
catch (const TrackerError &error) { |
|
|
|
|
|
|
|
clear(); // clear response
|
|
|
|
|
|
|
|
status(200); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const lt::entry::dictionary_type bencodedEntry = { |
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_FAILURE_REASON, {error.what()}} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
QByteArray reply; |
|
|
|
|
|
|
|
lt::bencode(std::back_inserter(reply), bencodedEntry); |
|
|
|
|
|
|
|
print(reply, Http::CONTENT_TYPE_TXT); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return response(); |
|
|
|
return response(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Tracker::respondToAnnounceRequest() |
|
|
|
void Tracker::processAnnounceRequest() |
|
|
|
{ |
|
|
|
{ |
|
|
|
const QHash<QString, QByteArray> &queryParams = m_request.query; |
|
|
|
const QHash<QString, QByteArray> &queryParams = m_request.query; |
|
|
|
TrackerAnnounceRequest announceReq; |
|
|
|
TrackerAnnounceRequest announceReq; |
|
|
|
|
|
|
|
|
|
|
|
// IP
|
|
|
|
// ip address
|
|
|
|
// Use the "ip" parameter provided from tracker request first, then fall back to client IP if invalid
|
|
|
|
announceReq.socketAddress = m_env.clientAddress; |
|
|
|
const QHostAddress paramIP {QString::fromLatin1(queryParams.value("ip"))}; |
|
|
|
announceReq.claimedAddress = queryParams.value(ANNOUNCE_REQUEST_IP); |
|
|
|
announceReq.peer.ip = paramIP.isNull() ? m_env.clientAddress : paramIP; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Get info_hash
|
|
|
|
// 1. info_hash
|
|
|
|
if (!queryParams.contains("info_hash")) { |
|
|
|
const auto infoHashIter = queryParams.find(ANNOUNCE_REQUEST_INFO_HASH); |
|
|
|
qDebug("Tracker: Missing info_hash"); |
|
|
|
if (infoHashIter == queryParams.end()) |
|
|
|
status(101, "Missing info_hash"); |
|
|
|
throw TrackerError("Missing \"info_hash\" parameter"); |
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
announceReq.infoHash = queryParams.value("info_hash"); |
|
|
|
|
|
|
|
// info_hash cannot be longer than 20 bytes
|
|
|
|
|
|
|
|
/*if (annonce_req.info_hash.toLatin1().length() > 20) {
|
|
|
|
|
|
|
|
qDebug("Tracker: Info_hash is not 20 byte long: %s (%d)", qUtf8Printable(annonce_req.info_hash), annonce_req.info_hash.toLatin1().length()); |
|
|
|
|
|
|
|
status(150, "Invalid infohash"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
}*/ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Get peer ID
|
|
|
|
const InfoHash infoHash(infoHashIter->toHex()); |
|
|
|
if (!queryParams.contains("peer_id")) { |
|
|
|
if (!infoHash.isValid()) |
|
|
|
qDebug("Tracker: Missing peer_id"); |
|
|
|
throw TrackerError("Invalid \"info_hash\" parameter"); |
|
|
|
status(102, "Missing peer_id"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
announceReq.peer.peerId = queryParams.value("peer_id"); |
|
|
|
|
|
|
|
// peer_id cannot be longer than 20 bytes
|
|
|
|
|
|
|
|
/*if (annonce_req.peer.peer_id.length() > 20) {
|
|
|
|
|
|
|
|
qDebug("Tracker: peer_id is not 20 byte long: %s", qUtf8Printable(annonce_req.peer.peer_id)); |
|
|
|
|
|
|
|
status(151, "Invalid peerid"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
}*/ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Get port
|
|
|
|
announceReq.infoHash = infoHash; |
|
|
|
if (!queryParams.contains("port")) { |
|
|
|
|
|
|
|
qDebug("Tracker: Missing port"); |
|
|
|
|
|
|
|
status(103, "Missing port"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
bool ok = false; |
|
|
|
|
|
|
|
announceReq.peer.port = queryParams.value("port").toInt(&ok); |
|
|
|
|
|
|
|
if (!ok || (announceReq.peer.port < 0) || (announceReq.peer.port > 65535)) { |
|
|
|
|
|
|
|
qDebug("Tracker: Invalid port number (%d)", announceReq.peer.port); |
|
|
|
|
|
|
|
status(103, "Missing port"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Get event
|
|
|
|
// 2. peer_id
|
|
|
|
announceReq.event = ""; |
|
|
|
const auto peerIdIter = queryParams.find(ANNOUNCE_REQUEST_PEER_ID); |
|
|
|
if (queryParams.contains("event")) { |
|
|
|
if (peerIdIter == queryParams.end()) |
|
|
|
announceReq.event = queryParams.value("event"); |
|
|
|
throw TrackerError("Missing \"peer_id\" parameter"); |
|
|
|
qDebug("Tracker: event is %s", qUtf8Printable(announceReq.event)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Get numwant
|
|
|
|
if (peerIdIter->size() > PEER_ID_SIZE) |
|
|
|
announceReq.numwant = 50; |
|
|
|
throw TrackerError("Invalid \"peer_id\" parameter"); |
|
|
|
if (queryParams.contains("numwant")) { |
|
|
|
|
|
|
|
int tmp = queryParams.value("numwant").toInt(); |
|
|
|
announceReq.peer.peerId = *peerIdIter; |
|
|
|
if (tmp > 0) { |
|
|
|
|
|
|
|
qDebug("Tracker: numwant = %d", tmp); |
|
|
|
|
|
|
|
announceReq.numwant = tmp; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 6. no_peer_id (extension)
|
|
|
|
// 3. port
|
|
|
|
announceReq.noPeerId = false; |
|
|
|
const auto portIter = queryParams.find(ANNOUNCE_REQUEST_PORT); |
|
|
|
if (queryParams.contains("no_peer_id")) |
|
|
|
if (portIter == queryParams.end()) |
|
|
|
announceReq.noPeerId = true; |
|
|
|
throw TrackerError("Missing \"port\" parameter"); |
|
|
|
|
|
|
|
|
|
|
|
// 7. TODO: support "compact" extension
|
|
|
|
const ushort portNum = portIter->toUShort(); |
|
|
|
|
|
|
|
if (portNum == 0) |
|
|
|
|
|
|
|
throw TrackerError("Invalid \"port\" parameter"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
announceReq.peer.port = portNum; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 4. numwant
|
|
|
|
|
|
|
|
const auto numWantIter = queryParams.find(ANNOUNCE_REQUEST_NUM_WANT); |
|
|
|
|
|
|
|
if (numWantIter != queryParams.end()) { |
|
|
|
|
|
|
|
const int num = numWantIter->toInt(); |
|
|
|
|
|
|
|
if (num < 0) |
|
|
|
|
|
|
|
throw TrackerError("Invalid \"numwant\" parameter"); |
|
|
|
|
|
|
|
announceReq.numwant = num; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Done parsing, now let's reply
|
|
|
|
// 5. no_peer_id
|
|
|
|
if (announceReq.event == "stopped") { |
|
|
|
// non-formal extension
|
|
|
|
|
|
|
|
announceReq.noPeerId = (queryParams.value(ANNOUNCE_REQUEST_NO_PEER_ID) == "1"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 6. left
|
|
|
|
|
|
|
|
announceReq.peer.isSeeder = (queryParams.value(ANNOUNCE_REQUEST_LEFT) == "0"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 7. compact
|
|
|
|
|
|
|
|
announceReq.compact = (queryParams.value(ANNOUNCE_REQUEST_COMPACT) != "0"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 8. cache `peers` field so we don't recompute when sending response
|
|
|
|
|
|
|
|
const QHostAddress claimedIPAddress {QString::fromLatin1(announceReq.claimedAddress)}; |
|
|
|
|
|
|
|
announceReq.peer.endpoint = toBigEndianByteArray(!claimedIPAddress.isNull() ? claimedIPAddress : announceReq.socketAddress) |
|
|
|
|
|
|
|
.append(static_cast<char>((announceReq.peer.port >> 8) & 0xFF)) |
|
|
|
|
|
|
|
.append(static_cast<char>(announceReq.peer.port & 0xFF)) |
|
|
|
|
|
|
|
.toStdString(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 9. cache `address` field so we don't recompute when sending response
|
|
|
|
|
|
|
|
announceReq.peer.address = !announceReq.claimedAddress.isEmpty() |
|
|
|
|
|
|
|
? announceReq.claimedAddress.constData() |
|
|
|
|
|
|
|
: announceReq.socketAddress.toString().toLatin1().constData(), |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 10. event
|
|
|
|
|
|
|
|
announceReq.event = queryParams.value(ANNOUNCE_REQUEST_EVENT); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (announceReq.event.isEmpty() |
|
|
|
|
|
|
|
|| (announceReq.event == ANNOUNCE_REQUEST_EVENT_EMPTY) |
|
|
|
|
|
|
|
|| (announceReq.event == ANNOUNCE_REQUEST_EVENT_COMPLETED) |
|
|
|
|
|
|
|
|| (announceReq.event == ANNOUNCE_REQUEST_EVENT_STARTED) |
|
|
|
|
|
|
|
|| (announceReq.event == ANNOUNCE_REQUEST_EVENT_PAUSED)) { |
|
|
|
|
|
|
|
// [BEP-21] Extension for partial seeds (partial support)
|
|
|
|
|
|
|
|
registerPeer(announceReq); |
|
|
|
|
|
|
|
prepareAnnounceResponse(announceReq); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (announceReq.event == ANNOUNCE_REQUEST_EVENT_STOPPED) { |
|
|
|
unregisterPeer(announceReq); |
|
|
|
unregisterPeer(announceReq); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
registerPeer(announceReq); |
|
|
|
throw TrackerError("Invalid \"event\" parameter"); |
|
|
|
replyWithPeerList(announceReq); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Tracker::registerPeer(const TrackerAnnounceRequest &announceReq) |
|
|
|
void Tracker::registerPeer(const TrackerAnnounceRequest &announceReq) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (announceReq.peer.port == 0) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!m_torrents.contains(announceReq.infoHash)) { |
|
|
|
if (!m_torrents.contains(announceReq.infoHash)) { |
|
|
|
// Unknown torrent
|
|
|
|
|
|
|
|
if (m_torrents.size() == MAX_TORRENTS) { |
|
|
|
|
|
|
|
// Reached max size, remove a random torrent
|
|
|
|
// Reached max size, remove a random torrent
|
|
|
|
|
|
|
|
if (m_torrents.size() >= MAX_TORRENTS) |
|
|
|
m_torrents.erase(m_torrents.begin()); |
|
|
|
m_torrents.erase(m_torrents.begin()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Register the user
|
|
|
|
m_torrents[announceReq.infoHash].setPeer(announceReq.peer); |
|
|
|
PeerList &peers = m_torrents[announceReq.infoHash]; |
|
|
|
|
|
|
|
if (!peers.contains(announceReq.peer.uid())) { |
|
|
|
|
|
|
|
// Unknown peer
|
|
|
|
|
|
|
|
if (peers.size() == MAX_PEERS_PER_TORRENT) { |
|
|
|
|
|
|
|
// Too many peers, remove a random one
|
|
|
|
|
|
|
|
peers.erase(peers.begin()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
peers[announceReq.peer.uid()] = announceReq.peer; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Tracker::unregisterPeer(const TrackerAnnounceRequest &announceReq) |
|
|
|
void Tracker::unregisterPeer(const TrackerAnnounceRequest &announceReq) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (announceReq.peer.port == 0) return; |
|
|
|
const auto torrentStatsIter = m_torrents.find(announceReq.infoHash); |
|
|
|
|
|
|
|
if (torrentStatsIter == m_torrents.end()) |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
torrentStatsIter->removePeer(announceReq.peer); |
|
|
|
|
|
|
|
|
|
|
|
if (m_torrents[announceReq.infoHash].remove(announceReq.peer.uid()) > 0) |
|
|
|
if (torrentStatsIter->peers.isEmpty()) |
|
|
|
qDebug("Tracker: Peer stopped downloading, deleting it from the list"); |
|
|
|
m_torrents.erase(torrentStatsIter); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Tracker::replyWithPeerList(const TrackerAnnounceRequest &announceReq) |
|
|
|
void Tracker::prepareAnnounceResponse(const TrackerAnnounceRequest &announceReq) |
|
|
|
{ |
|
|
|
{ |
|
|
|
// Prepare the entry for bencoding
|
|
|
|
const TorrentStats &torrentStats = m_torrents[announceReq.infoHash]; |
|
|
|
lt::entry::dictionary_type replyDict; |
|
|
|
|
|
|
|
replyDict["interval"] = lt::entry(ANNOUNCE_INTERVAL); |
|
|
|
lt::entry::dictionary_type replyDict { |
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_INTERVAL, ANNOUNCE_INTERVAL}, |
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_COMPLETE, torrentStats.seeders}, |
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_INCOMPLETE, (torrentStats.peers.size() - torrentStats.seeders)}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// [BEP-24] Tracker Returns External IP
|
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_EXTERNAL_IP, toBigEndianByteArray(announceReq.socketAddress).toStdString()} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// peer list
|
|
|
|
|
|
|
|
// [BEP-7] IPv6 Tracker Extension (partial support)
|
|
|
|
|
|
|
|
// [BEP-23] Tracker Returns Compact Peer Lists
|
|
|
|
|
|
|
|
if (announceReq.compact) { |
|
|
|
|
|
|
|
lt::entry::list_type peerList; |
|
|
|
|
|
|
|
lt::entry::list_type peer6List; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int counter = 0; |
|
|
|
|
|
|
|
for (const Peer &peer : asConst(torrentStats.peers)) { |
|
|
|
|
|
|
|
if (counter++ >= announceReq.numwant) |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (peer.endpoint.size() == 6) // IPv4
|
|
|
|
|
|
|
|
peerList.emplace_back(peer.endpoint); |
|
|
|
|
|
|
|
else if (peer.endpoint.size() == 18) // IPv6
|
|
|
|
|
|
|
|
peer6List.emplace_back(peer.endpoint); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
replyDict[ANNOUNCE_RESPONSE_PEERS] = peerList; // required, even it's empty
|
|
|
|
|
|
|
|
if (!peer6List.empty()) |
|
|
|
|
|
|
|
replyDict[ANNOUNCE_RESPONSE_PEERS6] = peer6List; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
lt::entry::list_type peerList; |
|
|
|
lt::entry::list_type peerList; |
|
|
|
for (const Peer &p : m_torrents.value(announceReq.infoHash)) |
|
|
|
|
|
|
|
peerList.push_back(p.toEntry(announceReq.noPeerId)); |
|
|
|
|
|
|
|
replyDict["peers"] = lt::entry(peerList); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const lt::entry replyEntry(replyDict); |
|
|
|
int counter = 0; |
|
|
|
|
|
|
|
for (const Peer &peer : torrentStats.peers) { |
|
|
|
|
|
|
|
if (counter++ >= announceReq.numwant) |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lt::entry::dictionary_type peerDict = { |
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_PEERS_IP, peer.address}, |
|
|
|
|
|
|
|
{ANNOUNCE_RESPONSE_PEERS_PORT, peer.port} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!announceReq.noPeerId) |
|
|
|
|
|
|
|
peerDict[ANNOUNCE_RESPONSE_PEERS_PEER_ID] = peer.peerId.constData(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
peerList.emplace_back(peerDict); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
replyDict[ANNOUNCE_RESPONSE_PEERS] = peerList; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// bencode
|
|
|
|
// bencode
|
|
|
|
QByteArray reply; |
|
|
|
QByteArray reply; |
|
|
|
lt::bencode(std::back_inserter(reply), replyEntry); |
|
|
|
lt::bencode(std::back_inserter(reply), replyDict); |
|
|
|
qDebug("Tracker: reply with the following bencoded data:\n %s", reply.constData()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// HTTP reply
|
|
|
|
|
|
|
|
print(reply, Http::CONTENT_TYPE_TXT); |
|
|
|
print(reply, Http::CONTENT_TYPE_TXT); |
|
|
|
} |
|
|
|
} |
|
|
|