FEATURE: qBittorrent can now act as a tracker

This commit is contained in:
Christophe Dumez 2010-10-15 21:55:56 +00:00
parent f6172f8c77
commit aff27558dd
12 changed files with 437 additions and 6 deletions

View File

@ -1,4 +1,5 @@
* Unreleased - Christophe Dumez <chris@qbittorrent.org> - 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

View File

@ -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() {

View File

@ -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

View File

@ -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");
}

View File

@ -47,6 +47,7 @@
#include <libtorrent/session.hpp>
#include <libtorrent/ip_filter.hpp>
#include "qtracker.h"
#include "qtorrenthandle.h"
#include "trackerinfos.h"
@ -250,6 +251,8 @@ private:
bool geoipDBLoaded;
bool resolve_countries;
#endif
// Tracker
QPointer<QTracker> m_tracker;
};

View File

@ -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 \

37
src/tracker/qpeer.h Normal file
View File

@ -0,0 +1,37 @@
#ifndef QPEER_H
#define QPEER_H
#include <libtorrent/entry.hpp>
#include <QString>
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

236
src/tracker/qtracker.cpp Normal file
View File

@ -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 <QHttpRequestHeader>
#include <QTcpSocket>
#include <libtorrent/bencode.hpp>
#include <libtorrent/entry.hpp>
#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<QTcpSocket*>(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<QPeer> 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<char> 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();
}

72
src/tracker/qtracker.h Normal file
View File

@ -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 <QTcpServer>
#include <QHttpResponseHeader>
#include <QHash>
#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<QString, QPeer> PeerList;
typedef QHash<QString, PeerList> 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

9
src/tracker/tracker.pri Normal file
View File

@ -0,0 +1,9 @@
INCLUDEPATH += $$PWD
HEADERS += \
$$PWD/qtracker.h \
$$PWD/trackerannouncerequest.h \
$$PWD/qpeer.h
SOURCES += \
$$PWD/qtracker.cpp

View File

@ -0,0 +1,15 @@
#ifndef TRACKERANNOUNCEREQUEST_H
#define TRACKERANNOUNCEREQUEST_H
#include <qpeer.h>
struct TrackerAnnounceRequest {
QString info_hash;
QString event;
int numwant;
QPeer peer;
// Extensions
bool no_peer_id;
};
#endif // TRACKERANNOUNCEREQUEST_H

View File

@ -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