From aff27558dd5261115d065153b0c45e939b826f92 Mon Sep 17 00:00:00 2001 From: Christophe Dumez Date: Fri, 15 Oct 2010 21:55:56 +0000 Subject: [PATCH] FEATURE: qBittorrent can now act as a tracker --- Changelog | 1 + src/advancedsettings.h | 27 ++- src/preferences.h | 20 +++ src/qtlibtorrent/qbtsession.cpp | 18 ++ src/qtlibtorrent/qbtsession.h | 3 + src/src.pro | 1 + src/tracker/qpeer.h | 37 +++++ src/tracker/qtracker.cpp | 236 +++++++++++++++++++++++++++ src/tracker/qtracker.h | 72 ++++++++ src/tracker/tracker.pri | 9 + src/tracker/trackerannouncerequest.h | 15 ++ src/webui/httprequestparser.cpp | 4 +- 12 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 src/tracker/qpeer.h create mode 100644 src/tracker/qtracker.cpp create mode 100644 src/tracker/qtracker.h create mode 100644 src/tracker/tracker.pri create mode 100644 src/tracker/trackerannouncerequest.h diff --git a/Changelog b/Changelog index 2ab9161ed..c744135dc 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ * Unreleased - Christophe Dumez - v2.5.0 + - FEATURE: qBittorrent can now act as a tracker - FEATURE: Added feature to shutdown qbittorrent on torrents completion - FEATURE: Added a transfer list column to display the current tracker - COSMETIC: Replaced message box by on-screen notification for download errors diff --git a/src/advancedsettings.h b/src/advancedsettings.h index a8461d12e..0b24bebf1 100644 --- a/src/advancedsettings.h +++ b/src/advancedsettings.h @@ -11,15 +11,15 @@ #include "preferences.h" enum AdvSettingsCols {PROPERTY, VALUE}; -enum AdvSettingsRows {DISK_CACHE, OUTGOING_PORT_MIN, OUTGOING_PORT_MAX, IGNORE_LIMIT_LAN, COUNT_OVERHEAD, RECHECK_COMPLETED, LIST_REFRESH, RESOLVE_COUNTRIES, RESOLVE_HOSTS, MAX_HALF_OPEN, SUPER_SEEDING, NETWORK_IFACE, PROGRAM_NOTIFICATIONS }; -#define ROW_COUNT 13 +enum AdvSettingsRows {DISK_CACHE, OUTGOING_PORT_MIN, OUTGOING_PORT_MAX, IGNORE_LIMIT_LAN, COUNT_OVERHEAD, RECHECK_COMPLETED, LIST_REFRESH, RESOLVE_COUNTRIES, RESOLVE_HOSTS, MAX_HALF_OPEN, SUPER_SEEDING, NETWORK_IFACE, PROGRAM_NOTIFICATIONS, TRACKER_STATUS, TRACKER_PORT }; +#define ROW_COUNT 15 class AdvancedSettings: public QTableWidget { Q_OBJECT private: - QSpinBox *spin_cache, *outgoing_ports_min, *outgoing_ports_max, *spin_list_refresh, *spin_maxhalfopen; - QCheckBox *cb_ignore_limits_lan, *cb_count_overhead, *cb_recheck_completed, *cb_resolve_countries, *cb_resolve_hosts, *cb_super_seeding, *cb_program_notifications; + QSpinBox *spin_cache, *outgoing_ports_min, *outgoing_ports_max, *spin_list_refresh, *spin_maxhalfopen, *spin_tracker_port; + QCheckBox *cb_ignore_limits_lan, *cb_count_overhead, *cb_recheck_completed, *cb_resolve_countries, *cb_resolve_hosts, *cb_super_seeding, *cb_program_notifications, *cb_tracker_status; QComboBox *combo_iface; public: @@ -53,6 +53,8 @@ public: delete cb_super_seeding; delete combo_iface; delete cb_program_notifications; + delete spin_tracker_port; + delete cb_tracker_status; } public slots: @@ -88,6 +90,9 @@ public slots: } // Program notification Preferences::useProgramNotification(cb_program_notifications->isChecked()); + // Tracker + Preferences::setTrackerEnabled(cb_tracker_status->isChecked()); + Preferences::setTrackerPort(spin_tracker_port->value()); } protected slots: @@ -195,6 +200,20 @@ protected slots: connect(cb_program_notifications, SIGNAL(toggled(bool)), this, SLOT(emitSettingsChanged())); cb_program_notifications->setChecked(Preferences::useProgramNotification()); setCellWidget(PROGRAM_NOTIFICATIONS, VALUE, cb_program_notifications); + // Tracker State + setItem(TRACKER_STATUS, PROPERTY, new QTableWidgetItem(tr("Enable embedded tracker"))); + cb_tracker_status = new QCheckBox(); + connect(cb_tracker_status, SIGNAL(toggled(bool)), this, SLOT(emitSettingsChanged())); + cb_tracker_status->setChecked(Preferences::isTrackerEnabled()); + setCellWidget(TRACKER_STATUS, VALUE, cb_tracker_status); + // Tracker port + setItem(TRACKER_PORT, PROPERTY, new QTableWidgetItem(tr("Embedded tracker port"))); + spin_tracker_port = new QSpinBox(); + connect(spin_tracker_port, SIGNAL(valueChanged(int)), this, SLOT(emitSettingsChanged())); + spin_tracker_port->setMinimum(1); + spin_tracker_port->setMaximum(65535); + spin_tracker_port->setValue(Preferences::getTrackerPort()); + setCellWidget(TRACKER_PORT, VALUE, spin_tracker_port); } void emitSettingsChanged() { diff --git a/src/preferences.h b/src/preferences.h index 74cc5bb3c..fbca5981d 100644 --- a/src/preferences.h +++ b/src/preferences.h @@ -1192,6 +1192,26 @@ public: #endif + static bool isTrackerEnabled() { + QIniSettings settings("qBittorrent", "qBittorrent"); + return settings.value(QString::fromUtf8("Preferences/Advanced/trackerEnabled"), false).toBool(); + } + + static void setTrackerEnabled(bool enabled) { + QIniSettings settings("qBittorrent", "qBittorrent"); + settings.setValue(QString::fromUtf8("Preferences/Advanced/trackerEnabled"), enabled); + } + + static int getTrackerPort() { + QIniSettings settings("qBittorrent", "qBittorrent"); + return settings.value(QString::fromUtf8("Preferences/Advanced/trackerPort"), 9000).toInt(); + } + + static void setTrackerPort(int port) { + QIniSettings settings("qBittorrent", "qBittorrent"); + settings.setValue(QString::fromUtf8("Preferences/Advanced/trackerPort"), port); + } + }; #endif // PREFERENCES_H diff --git a/src/qtlibtorrent/qbtsession.cpp b/src/qtlibtorrent/qbtsession.cpp index 965911e55..f2fdd9c3c 100644 --- a/src/qtlibtorrent/qbtsession.cpp +++ b/src/qtlibtorrent/qbtsession.cpp @@ -84,6 +84,7 @@ Bittorrent::Bittorrent() , geoipDBLoaded(false), resolve_countries(false) #endif { + m_tracker = 0; // To avoid some exceptions fs::path::default_name_check(fs::no_check); // For backward compatibility @@ -171,6 +172,8 @@ Bittorrent::~Bittorrent() { delete s; } // Delete our objects + if(m_tracker) + delete m_tracker; delete timerAlerts; if(BigRatioTimer) delete BigRatioTimer; @@ -594,6 +597,21 @@ void Bittorrent::configureSession() { http_proxySettings.type = proxy_settings::none; } setHTTPProxySettings(http_proxySettings); + // Tracker + if(Preferences::isTrackerEnabled()) { + if(!m_tracker) { + m_tracker = new QTracker(this); + } + if(m_tracker->start()) { + addConsoleMessage(tr("Embedded Tracker [ON]"), QString::fromUtf8("blue")); + } else { + addConsoleMessage(tr("Failed to start the embedded tracker!"), QString::fromUtf8("red")); + } + } else { + addConsoleMessage(tr("Embedded Tracker [OFF]")); + if(m_tracker) + delete m_tracker; + } qDebug("Session configured"); } diff --git a/src/qtlibtorrent/qbtsession.h b/src/qtlibtorrent/qbtsession.h index f42212c36..564586c09 100644 --- a/src/qtlibtorrent/qbtsession.h +++ b/src/qtlibtorrent/qbtsession.h @@ -47,6 +47,7 @@ #include #include +#include "qtracker.h" #include "qtorrenthandle.h" #include "trackerinfos.h" @@ -250,6 +251,8 @@ private: bool geoipDBLoaded; bool resolve_countries; #endif + // Tracker + QPointer m_tracker; }; diff --git a/src/src.pro b/src/src.pro index 9f4d932ae..d961a88dd 100644 --- a/src/src.pro +++ b/src/src.pro @@ -355,6 +355,7 @@ contains(DEFINES, USE_SYSTEM_QTSINGLEAPPLICATION) { include(qtlibtorrent/qtlibtorrent.pri) include(webui/webui.pri) include(rss/rss.pri) +include(tracker/tracker.pri) !contains(DEFINES, DISABLE_GUI) { FORMS += ui/mainwindow.ui \ diff --git a/src/tracker/qpeer.h b/src/tracker/qpeer.h new file mode 100644 index 000000000..45c5084a6 --- /dev/null +++ b/src/tracker/qpeer.h @@ -0,0 +1,37 @@ +#ifndef QPEER_H +#define QPEER_H + +#include +#include + +using namespace libtorrent; + +struct QPeer { + + bool operator!=(const QPeer &other) const { + return qhash() != other.qhash(); + } + + bool operator==(const QPeer &other) const { + return qhash() == other.qhash(); + } + + QString qhash() const { + return ip+":"+QString::number(port); + } + + entry toEntry(bool no_peer_id) const { + entry::dictionary_type peer_map; + if(!no_peer_id) + peer_map["id"] = entry(peer_id.toStdString()); + peer_map["ip"] = entry(ip.toStdString()); + peer_map["port"] = entry(port); + return entry(peer_map); + } + + QString ip; + QString peer_id; + int port; +}; + +#endif // QPEER_H diff --git a/src/tracker/qtracker.cpp b/src/tracker/qtracker.cpp new file mode 100644 index 000000000..3b76def31 --- /dev/null +++ b/src/tracker/qtracker.cpp @@ -0,0 +1,236 @@ +/* + * Bittorrent Client using Qt4 and libtorrent. + * Copyright (C) 2006 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * Contact : chris@qbittorrent.org + */ + +#include +#include + +#include +#include + +#include "qtracker.h" +#include "preferences.h" + +using namespace libtorrent; + +QTracker::QTracker(QObject *parent) : + QTcpServer(parent) +{ + Q_ASSERT(Preferences::isTrackerEnabled()); + connect(this, SIGNAL(newConnection()), this, SLOT(handlePeerConnection())); +} + +QTracker::~QTracker() { + if(isListening()) { + qDebug("Shutting down the embedded tracker..."); + close(); + } + // TODO: Store the torrent list +} + +void QTracker::handlePeerConnection() +{ + QTcpSocket *socket; + while((socket = nextPendingConnection())) + { + qDebug("QTracker: New peer connection"); + connect(socket, SIGNAL(readyRead()), SLOT(readRequest())); + } +} + +bool QTracker::start() +{ + const int listen_port = Preferences::getTrackerPort(); + // + if(isListening()) { + if(serverPort() == listen_port) { + // Already listening on the right port, just return + return true; + } + // Wrong port, closing the server + close(); + } + qDebug("Starting the embedded tracker..."); + // Listen on the predefined port + return listen(QHostAddress::Any, listen_port); +} + +void QTracker::readRequest() +{ + QTcpSocket *socket = static_cast(sender()); + QByteArray input = socket->readAll(); + //qDebug("QTracker: Raw request:\n%s", input.data()); + HttpRequestParser parser; + parser.write(input); + if(!parser.isValid()) { + qDebug("QTracker: Invalid HTTP Request:\n %s", qPrintable(parser.toString())); + respondInvalidRequest(socket, 100, "Invalid request type"); + return; + } + //qDebug("QTracker received the following request:\n%s", qPrintable(parser.toString())); + // Request is correct, is it a GET request? + if(parser.method() != "GET") { + qDebug("QTracker: Unsupported HTTP request: %s", qPrintable(parser.method())); + respondInvalidRequest(socket, 100, "Invalid request type"); + return; + } + if(!parser.url().startsWith("/announce", Qt::CaseInsensitive)) { + qDebug("QTracker: Unrecognized path: %s", qPrintable(parser.url())); + respondInvalidRequest(socket, 100, "Invalid request type"); + return; + } + // OK, this is a GET request + respondToAnnounceRequest(socket, parser); +} + +void QTracker::respondInvalidRequest(QTcpSocket *socket, int code, QString msg) +{ + QHttpResponseHeader response; + response.setStatusLine(code, msg); + socket->write(response.toString().toLocal8Bit()); + socket->disconnectFromHost(); +} + +void QTracker::respondToAnnounceRequest(QTcpSocket *socket, const HttpRequestParser& parser) +{ + TrackerAnnounceRequest annonce_req; + // IP + annonce_req.peer.ip = socket->peerAddress().toString(); + // 1. Get info_hash + if(parser.get("info_hash").isNull()) { + qDebug("QTracker: Missing info_hash"); + respondInvalidRequest(socket, 101, "Missing info_hash"); + return; + } + annonce_req.info_hash = parser.get("info_hash"); + // info_hash cannot be longer than 20 bytes + if(annonce_req.info_hash.toAscii().length() > 20) { + qDebug("QTracker: Info_hash is not 20 byte long: %s (%d)", qPrintable(annonce_req.info_hash), annonce_req.info_hash.toAscii().length()); + respondInvalidRequest(socket, 150, "Invalid infohash"); + return; + } + // 2. Get peer ID + if(parser.get("peer_id").isNull()) { + qDebug("QTracker: Missing peer_id"); + respondInvalidRequest(socket, 102, "Missing peer_id"); + return; + } + annonce_req.peer.peer_id = parser.get("peer_id"); + // peer_id cannot be longer than 20 bytes + if(annonce_req.peer.peer_id.length() > 20) { + qDebug("QTracker: peer_id is not 20 byte long: %s", qPrintable(annonce_req.peer.peer_id)); + respondInvalidRequest(socket, 151, "Invalid peerid"); + return; + } + // 3. Get port + if(parser.get("port").isNull()) { + qDebug("QTracker: Missing port"); + respondInvalidRequest(socket, 103, "Missing port"); + return; + } + bool ok = false; + annonce_req.peer.port = parser.get("port").toInt(&ok); + if(!ok || annonce_req.peer.port < 1 || annonce_req.peer.port > 65535) { + qDebug("QTracker: Invalid port number (%d)", annonce_req.peer.port); + respondInvalidRequest(socket, 103, "Missing port"); + return; + } + // 4. Get event + annonce_req.event = ""; + if(!parser.get("event").isNull()) { + annonce_req.event = parser.get("event"); + qDebug("QTracker: event is %s", qPrintable(annonce_req.event)); + } + // 5. Get numwant + annonce_req.numwant = 50; + if(!parser.get("numwant").isNull()) { + int tmp = parser.get("numwant").toInt(); + if(tmp > 0) { + qDebug("QTracker: numwant=%d", tmp); + annonce_req.numwant = tmp; + } + } + // 6. no_peer_id (extension) + annonce_req.no_peer_id = false; + if(parser.hasKey("no_peer_id")) { + annonce_req.no_peer_id = true; + } + // 7. TODO: support "compact" extension + // Done parsing, now let's reply + if(m_torrents.contains(annonce_req.info_hash)) { + if(annonce_req.event == "stopped") { + qDebug("QTracker: Peer stopped downloading, deleting it from the list"); + m_torrents[annonce_req.info_hash].remove(annonce_req.peer.qhash()); + return; + } + } else { + // Unknown torrent + if(m_torrents.size() == MAX_TORRENTS) { + // Reached max size, remove a random torrent + m_torrents.erase(m_torrents.begin()); + } + } + // Register the user + PeerList peers = m_torrents.value(annonce_req.info_hash); + if(peers.size() == MAX_PEERS_PER_TORRENT) { + // Too many peers, remove a random one + peers.erase(peers.begin()); + } + peers[annonce_req.peer.qhash()] = annonce_req.peer; + m_torrents[annonce_req.info_hash] = peers; + // Reply + ReplyWithPeerList(socket, annonce_req); +} + +void QTracker::ReplyWithPeerList(QTcpSocket *socket, const TrackerAnnounceRequest &annonce_req) +{ + // Prepare the entry for bencoding + entry::dictionary_type reply_dict; + reply_dict["interval"] = entry(ANNOUNCE_INTERVAL); + QList peers = m_torrents.value(annonce_req.info_hash).values(); + entry::list_type peer_list; + foreach(const QPeer & p, peers) { + //if(p != annonce_req.peer) + peer_list.push_back(p.toEntry(annonce_req.no_peer_id)); + } + reply_dict["peers"] = entry(peer_list); + entry reply_entry(reply_dict); + // bencode + std::vector buf; + bencode(std::back_inserter(buf), reply_entry); + QByteArray reply(buf.data(), buf.size()); + qDebug("QTracker: reply with the following bencoded data:\n %s", reply.constData()); + // HTTP reply + QHttpResponseHeader response; + response.setStatusLine(200, "OK"); + socket->write(response.toString().toLocal8Bit() + reply); + socket->disconnectFromHost(); +} + + diff --git a/src/tracker/qtracker.h b/src/tracker/qtracker.h new file mode 100644 index 000000000..8b7c26eb4 --- /dev/null +++ b/src/tracker/qtracker.h @@ -0,0 +1,72 @@ +/* + * Bittorrent Client using Qt4 and libtorrent. + * Copyright (C) 2010 Christophe Dumez + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + * + * Contact : chris@qbittorrent.org + */ + +#ifndef QTRACKER_H +#define QTRACKER_H + +#include +#include +#include + +#include "httprequestparser.h" +#include "trackerannouncerequest.h" +#include "qpeer.h" + +// static limits +const int MAX_TORRENTS = 100; +const int MAX_PEERS_PER_TORRENT = 1000; +const int ANNOUNCE_INTERVAL = 1800; // 30min + +typedef QHash PeerList; +typedef QHash TorrentList; + +/* Basic Bittorrent tracker implementation in Qt4 */ +/* Following http://wiki.theory.org/BitTorrent_Tracker_Protocol */ +class QTracker : public QTcpServer +{ + Q_OBJECT +public: + explicit QTracker(QObject *parent = 0); + ~QTracker(); + bool start(); + +protected slots: + void readRequest(); + void handlePeerConnection(); + void respondInvalidRequest(QTcpSocket *socket, int code, QString msg); + void respondToAnnounceRequest(QTcpSocket *socket, const HttpRequestParser& parser); + void ReplyWithPeerList(QTcpSocket *socket, const TrackerAnnounceRequest &annonce_req); + +private: + TorrentList m_torrents; + +}; + +#endif // QTRACKER_H diff --git a/src/tracker/tracker.pri b/src/tracker/tracker.pri new file mode 100644 index 000000000..5b5809a86 --- /dev/null +++ b/src/tracker/tracker.pri @@ -0,0 +1,9 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/qtracker.h \ + $$PWD/trackerannouncerequest.h \ + $$PWD/qpeer.h + +SOURCES += \ + $$PWD/qtracker.cpp diff --git a/src/tracker/trackerannouncerequest.h b/src/tracker/trackerannouncerequest.h new file mode 100644 index 000000000..273ced385 --- /dev/null +++ b/src/tracker/trackerannouncerequest.h @@ -0,0 +1,15 @@ +#ifndef TRACKERANNOUNCEREQUEST_H +#define TRACKERANNOUNCEREQUEST_H + +#include + +struct TrackerAnnounceRequest { + QString info_hash; + QString event; + int numwant; + QPeer peer; + // Extensions + bool no_peer_id; +}; + +#endif // TRACKERANNOUNCEREQUEST_H diff --git a/src/webui/httprequestparser.cpp b/src/webui/httprequestparser.cpp index 59276ef9f..b3cc9f74d 100644 --- a/src/webui/httprequestparser.cpp +++ b/src/webui/httprequestparser.cpp @@ -68,12 +68,12 @@ QByteArray HttpRequestParser::message() const QString HttpRequestParser::get(const QString key) const { - return getMap[key]; + return getMap.value(key); } QString HttpRequestParser::post(const QString key) const { - return postMap[key]; + return postMap.value(key); } QByteArray HttpRequestParser::torrent() const