diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 804b68798..a81155322 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -19,6 +19,7 @@ bittorrent/torrentinfo.h bittorrent/tracker.h bittorrent/trackerentry.h http/connection.h +http/httperror.h http/irequesthandler.h http/requestparser.h http/responsebuilder.h @@ -51,6 +52,7 @@ utils/random.h utils/string.h utils/version.h asyncfilestorage.h +exceptions.h filesystemwatcher.h global.h iconprovider.h @@ -84,6 +86,7 @@ bittorrent/torrentinfo.cpp bittorrent/tracker.cpp bittorrent/trackerentry.cpp http/connection.cpp +http/httperror.cpp http/requestparser.cpp http/responsebuilder.cpp http/responsegenerator.cpp @@ -113,6 +116,7 @@ utils/net.cpp utils/random.cpp utils/string.cpp asyncfilestorage.cpp +exceptions.cpp filesystemwatcher.cpp iconprovider.cpp logger.cpp diff --git a/src/base/base.pri b/src/base/base.pri index 5a62fe982..0afca4c40 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -17,9 +17,11 @@ HEADERS += \ $$PWD/bittorrent/torrentinfo.h \ $$PWD/bittorrent/tracker.h \ $$PWD/bittorrent/trackerentry.h \ + $$PWD/exceptions.h \ $$PWD/filesystemwatcher.h \ $$PWD/global.h \ $$PWD/http/connection.h \ + $$PWD/http/httperror.h \ $$PWD/http/irequesthandler.h \ $$PWD/http/requestparser.h \ $$PWD/http/responsebuilder.h \ @@ -82,8 +84,10 @@ SOURCES += \ $$PWD/bittorrent/torrentinfo.cpp \ $$PWD/bittorrent/tracker.cpp \ $$PWD/bittorrent/trackerentry.cpp \ + $$PWD/exceptions.cpp \ $$PWD/filesystemwatcher.cpp \ $$PWD/http/connection.cpp \ + $$PWD/http/httperror.cpp \ $$PWD/http/requestparser.cpp \ $$PWD/http/responsebuilder.cpp \ $$PWD/http/responsegenerator.cpp \ diff --git a/src/base/bittorrent/torrentinfo.cpp b/src/base/bittorrent/torrentinfo.cpp index ca6976b85..950d772c3 100644 --- a/src/base/bittorrent/torrentinfo.cpp +++ b/src/base/bittorrent/torrentinfo.cpp @@ -60,23 +60,32 @@ TorrentInfo &TorrentInfo::operator=(const TorrentInfo &other) return *this; } -TorrentInfo TorrentInfo::loadFromFile(const QString &path, QString &error) +TorrentInfo TorrentInfo::load(const QByteArray &data, QString *error) noexcept { - error.clear(); libt::error_code ec; - TorrentInfo info(NativePtr(new libt::torrent_info(Utils::Fs::toNativePath(path).toStdString(), ec))); - if (ec) { - error = QString::fromUtf8(ec.message().c_str()); - qDebug("Cannot load .torrent file: %s", qUtf8Printable(error)); + TorrentInfo info(NativePtr(new libt::torrent_info(data.constData(), data.size(), ec))); + if (error) { + if (ec) + *error = QString::fromStdString(ec.message()); + else + error->clear(); } return info; } -TorrentInfo TorrentInfo::loadFromFile(const QString &path) +TorrentInfo TorrentInfo::loadFromFile(const QString &path, QString *error) noexcept { - QString error; - return loadFromFile(path, error); + libt::error_code ec; + TorrentInfo info(NativePtr(new libt::torrent_info(Utils::Fs::toNativePath(path).toStdString(), ec))); + if (error) { + if (ec) + *error = QString::fromStdString(ec.message()); + else + error->clear(); + } + + return info; } bool TorrentInfo::isValid() const diff --git a/src/base/bittorrent/torrentinfo.h b/src/base/bittorrent/torrentinfo.h index 6730d2f11..cb40c3927 100644 --- a/src/base/bittorrent/torrentinfo.h +++ b/src/base/bittorrent/torrentinfo.h @@ -63,8 +63,8 @@ namespace BitTorrent explicit TorrentInfo(NativeConstPtr nativeInfo = NativeConstPtr()); TorrentInfo(const TorrentInfo &other); - static TorrentInfo loadFromFile(const QString &path, QString &error); - static TorrentInfo loadFromFile(const QString &path); + static TorrentInfo load(const QByteArray &data, QString *error = nullptr) noexcept; + static TorrentInfo loadFromFile(const QString &path, QString *error = nullptr) noexcept; TorrentInfo &operator=(const TorrentInfo &other); diff --git a/src/base/bittorrent/tracker.cpp b/src/base/bittorrent/tracker.cpp index f914aa8aa..6429016e6 100644 --- a/src/base/bittorrent/tracker.cpp +++ b/src/base/bittorrent/tracker.cpp @@ -74,7 +74,7 @@ libtorrent::entry Peer::toEntry(bool noPeerId) const // Tracker Tracker::Tracker(QObject *parent) - : Http::ResponseBuilder(parent) + : QObject(parent) , m_server(new Http::Server(this, this)) { } diff --git a/src/base/bittorrent/tracker.h b/src/base/bittorrent/tracker.h index 967433445..0f03fb5b2 100644 --- a/src/base/bittorrent/tracker.h +++ b/src/base/bittorrent/tracker.h @@ -31,6 +31,7 @@ #define BITTORRENT_TRACKER_H #include +#include #include "base/http/irequesthandler.h" #include "base/http/responsebuilder.h" @@ -75,7 +76,7 @@ namespace BitTorrent /* Basic Bittorrent tracker implementation in Qt */ /* Following http://wiki.theory.org/BitTorrent_Tracker_Protocol */ - class Tracker : public Http::ResponseBuilder, public Http::IRequestHandler + class Tracker : public QObject, public Http::IRequestHandler, private Http::ResponseBuilder { Q_OBJECT Q_DISABLE_COPY(Tracker) diff --git a/src/base/exceptions.cpp b/src/base/exceptions.cpp new file mode 100644 index 000000000..87c0e0c0a --- /dev/null +++ b/src/base/exceptions.cpp @@ -0,0 +1,40 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "exceptions.h" + +RuntimeError::RuntimeError(const QString &message) + : std::runtime_error {message.toUtf8().data()} + , m_message {message} +{ +} + +QString RuntimeError::message() const +{ + return m_message; +} diff --git a/src/webui/prefjson.h b/src/base/exceptions.h similarity index 80% rename from src/webui/prefjson.h rename to src/base/exceptions.h index fe64e8dbc..3a95f919c 100644 --- a/src/webui/prefjson.h +++ b/src/base/exceptions.h @@ -1,6 +1,6 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006-2012 Ishan Arora and Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,24 +24,19 @@ * 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 PREFJSON_H -#define PREFJSON_H +#pragma once +#include #include -class prefjson +class RuntimeError : public std::runtime_error { -private: - prefjson(); - public: - static QByteArray getPreferences(); - static void setPreferences(const QString& json); + explicit RuntimeError(const QString &message = ""); + QString message() const; +private: + const QString m_message; }; - -#endif // PREFJSON_H diff --git a/src/base/http/httperror.cpp b/src/base/http/httperror.cpp new file mode 100644 index 000000000..d75d4f79a --- /dev/null +++ b/src/base/http/httperror.cpp @@ -0,0 +1,81 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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. + */ + +#include "httperror.h" + +HTTPError::HTTPError(int statusCode, const QString &statusText, const QString &message) + : RuntimeError {message} + , m_statusCode {statusCode} + , m_statusText {statusText} +{ +} + +int HTTPError::statusCode() const +{ + return m_statusCode; +} + +QString HTTPError::statusText() const +{ + return m_statusText; +} + +BadRequestHTTPError::BadRequestHTTPError(const QString &message) + : HTTPError(400, QLatin1String("Bad Request"), message) +{ +} + +ConflictHTTPError::ConflictHTTPError(const QString &message) + : HTTPError(409, QLatin1String("Conflict"), message) +{ +} + +ForbiddenHTTPError::ForbiddenHTTPError(const QString &message) + : HTTPError(403, QLatin1String("Forbidden"), message) +{ +} + +NotFoundHTTPError::NotFoundHTTPError(const QString &message) + : HTTPError(404, QLatin1String("Not Found"), message) +{ +} + +UnsupportedMediaTypeHTTPError::UnsupportedMediaTypeHTTPError(const QString &message) + : HTTPError(415, QLatin1String("Unsupported Media Type"), message) +{ +} + +UnauthorizedHTTPError::UnauthorizedHTTPError(const QString &message) + : HTTPError(401, QLatin1String("Unauthorized"), message) +{ +} + +InternalServerErrorHTTPError::InternalServerErrorHTTPError(const QString &message) + : HTTPError(500, QLatin1String("Internal Server Error"), message) +{ +} diff --git a/src/base/http/httperror.h b/src/base/http/httperror.h new file mode 100644 index 000000000..88fd716d1 --- /dev/null +++ b/src/base/http/httperror.h @@ -0,0 +1,86 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include "base/exceptions.h" + +class HTTPError : public RuntimeError +{ +public: + HTTPError(int statusCode, const QString &statusText, const QString &message = ""); + + int statusCode() const; + QString statusText() const; + +private: + const int m_statusCode; + const QString m_statusText; +}; + +class BadRequestHTTPError : public HTTPError +{ +public: + explicit BadRequestHTTPError(const QString &message = ""); +}; + +class ForbiddenHTTPError : public HTTPError +{ +public: + explicit ForbiddenHTTPError(const QString &message = ""); +}; + +class NotFoundHTTPError : public HTTPError +{ +public: + explicit NotFoundHTTPError(const QString &message = ""); +}; + +class ConflictHTTPError : public HTTPError +{ +public: + explicit ConflictHTTPError(const QString &message = ""); +}; + +class UnsupportedMediaTypeHTTPError : public HTTPError +{ +public: + explicit UnsupportedMediaTypeHTTPError(const QString &message = ""); +}; + +class UnauthorizedHTTPError : public HTTPError +{ +public: + explicit UnauthorizedHTTPError(const QString &message = ""); +}; + +class InternalServerErrorHTTPError : public HTTPError +{ +public: + explicit InternalServerErrorHTTPError(const QString &message = ""); +}; diff --git a/src/base/http/responsebuilder.cpp b/src/base/http/responsebuilder.cpp index 954ed05b5..916391a4c 100644 --- a/src/base/http/responsebuilder.cpp +++ b/src/base/http/responsebuilder.cpp @@ -30,11 +30,6 @@ using namespace Http; -ResponseBuilder::ResponseBuilder(QObject *parent) - : QObject(parent) -{ -} - void ResponseBuilder::status(uint code, const QString &text) { m_response.status = ResponseStatus(code, text); diff --git a/src/base/http/responsebuilder.h b/src/base/http/responsebuilder.h index 4332c06be..f1053cc81 100644 --- a/src/base/http/responsebuilder.h +++ b/src/base/http/responsebuilder.h @@ -29,17 +29,13 @@ #ifndef HTTP_RESPONSEBUILDER_H #define HTTP_RESPONSEBUILDER_H -#include #include "types.h" namespace Http { - class ResponseBuilder : public QObject + class ResponseBuilder { public: - explicit ResponseBuilder(QObject *parent = nullptr); - - protected: void status(uint code = 200, const QString &text = QLatin1String("OK")); void header(const QString &name, const QString &value); void print(const QString &text, const QString &type = CONTENT_TYPE_HTML); diff --git a/src/base/http/types.h b/src/base/http/types.h index 9c8ea535e..b7d5c9586 100644 --- a/src/base/http/types.h +++ b/src/base/http/types.h @@ -37,6 +37,9 @@ namespace Http { + const char METHOD_GET[] = "GET"; + const char METHOD_POST[] = "POST"; + const char HEADER_CACHE_CONTROL[] = "cache-control"; const char HEADER_CONTENT_ENCODING[] = "content-encoding"; const char HEADER_CONTENT_LENGTH[] = "content-length"; @@ -52,13 +55,14 @@ namespace Http const char HEADER_X_FRAME_OPTIONS[] = "x-frame-options"; const char HEADER_X_XSS_PROTECTION[] = "x-xss-protection"; - const char CONTENT_TYPE_CSS[] = "text/css; charset=UTF-8"; - const char CONTENT_TYPE_GIF[] = "image/gif"; - const char CONTENT_TYPE_HTML[] = "text/html; charset=UTF-8"; - const char CONTENT_TYPE_JS[] = "application/javascript; charset=UTF-8"; + const char CONTENT_TYPE_HTML[] = "text/html"; + const char CONTENT_TYPE_JS[] = "application/javascript"; const char CONTENT_TYPE_JSON[] = "application/json"; + const char CONTENT_TYPE_BMP[] = "image/bmp"; + const char CONTENT_TYPE_GIF[] = "image/gif"; + const char CONTENT_TYPE_JPEG[] = "image/jpeg"; const char CONTENT_TYPE_PNG[] = "image/png"; - const char CONTENT_TYPE_TXT[] = "text/plain; charset=UTF-8"; + const char CONTENT_TYPE_TXT[] = "text/plain"; const char CONTENT_TYPE_SVG[] = "image/svg+xml"; // portability: "\r\n" doesn't guarantee mapping to the correct value diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 23f888c3d..03b12e6bb 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -609,6 +609,26 @@ void Preferences::setWebUiHttpsKey(const QByteArray &data) setValue("Preferences/WebUI/HTTPS/Key", data); } +bool Preferences::isAltWebUiEnabled() const +{ + return value("Preferences/WebUI/AlternativeUIEnabled", false).toBool(); +} + +void Preferences::setAltWebUiEnabled(bool enabled) +{ + setValue("Preferences/WebUI/AlternativeUIEnabled", enabled); +} + +QString Preferences::getWebUiRootFolder() const +{ + return value("Preferences/WebUI/RootFolder").toString(); +} + +void Preferences::setWebUiRootFolder(const QString &path) +{ + setValue("Preferences/WebUI/RootFolder", path); +} + bool Preferences::isDynDNSEnabled() const { return value("Preferences/DynDNS/Enabled", false).toBool(); diff --git a/src/base/preferences.h b/src/base/preferences.h index 39e06483a..e01127d7f 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -204,6 +204,10 @@ public: void setWebUiHttpsCertificate(const QByteArray &data); QByteArray getWebUiHttpsKey() const; void setWebUiHttpsKey(const QByteArray &data); + bool isAltWebUiEnabled() const; + void setAltWebUiEnabled(bool enabled); + QString getWebUiRootFolder() const; + void setWebUiRootFolder(const QString &path); // Dynamic DNS bool isDynDNSEnabled() const; diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index 1cdccd087..63a997b49 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -30,6 +30,10 @@ #include "fs.h" +#include +#include +#include + #include #include #include @@ -47,6 +51,7 @@ #include #else #include +#include #endif #include "base/bittorrent/torrenthandle.h" @@ -281,3 +286,17 @@ QString Utils::Fs::tempPath() QDir().mkdir(path); return path; } + +bool Utils::Fs::isRegularFile(const QString &path) +{ + struct ::stat st; + if (::stat(path.toUtf8().constData(), &st) != 0) { + // analyse erno and log the error + const auto err = errno; + qDebug("Could not get file stats for path '%s'. Error: %s" + , qUtf8Printable(path), qUtf8Printable(strerror(err))); + return false; + } + + return (st.st_mode & S_IFMT) == S_IFREG; +} diff --git a/src/base/utils/fs.h b/src/base/utils/fs.h index 769b66a88..14265fc13 100644 --- a/src/base/utils/fs.h +++ b/src/base/utils/fs.h @@ -56,6 +56,7 @@ namespace Utils bool sameFileNames(const QString &first, const QString &second); QString expandPath(const QString &path); QString expandPathAbs(const QString &path); + bool isRegularFile(const QString &path); bool smartRemoveEmptyFolderTree(const QString &path); bool forceRemove(const QString &filePath); diff --git a/src/base/utils/string.cpp b/src/base/utils/string.cpp index c2a592b9c..65430309b 100644 --- a/src/base/utils/string.cpp +++ b/src/base/utils/string.cpp @@ -40,6 +40,8 @@ #include #endif +#include "../tristatebool.h" + namespace { class NaturalCompare @@ -184,3 +186,19 @@ QString Utils::String::wildcardToRegex(const QString &pattern) { return qt_regexp_toCanonical(pattern, QRegExp::Wildcard); } + +bool Utils::String::parseBool(const QString &string, const bool defaultValue) +{ + if (defaultValue) + return (string.compare("false", Qt::CaseInsensitive) == 0) ? false : true; + return (string.compare("true", Qt::CaseInsensitive) == 0) ? true : false; +} + +TriStateBool Utils::String::parseTriStateBool(const QString &string) +{ + if (string.compare("true", Qt::CaseInsensitive) == 0) + return TriStateBool::True; + if (string.compare("false", Qt::CaseInsensitive) == 0) + return TriStateBool::False; + return TriStateBool::Undefined; +} diff --git a/src/base/utils/string.h b/src/base/utils/string.h index 5a8041955..69eb7c020 100644 --- a/src/base/utils/string.h +++ b/src/base/utils/string.h @@ -34,6 +34,7 @@ class QByteArray; class QLatin1String; +class TriStateBool; namespace Utils { @@ -66,6 +67,9 @@ namespace Utils return str; } + + bool parseBool(const QString &string, const bool defaultValue); + TriStateBool parseTriStateBool(const QString &string); } } diff --git a/src/gui/addnewtorrentdialog.cpp b/src/gui/addnewtorrentdialog.cpp index 4f2c7250c..0af079e49 100644 --- a/src/gui/addnewtorrentdialog.cpp +++ b/src/gui/addnewtorrentdialog.cpp @@ -286,7 +286,7 @@ bool AddNewTorrentDialog::loadTorrent(const QString &torrentPath) m_hasMetadata = true; QString error; - m_torrentInfo = BitTorrent::TorrentInfo::loadFromFile(m_filePath, error); + m_torrentInfo = BitTorrent::TorrentInfo::loadFromFile(m_filePath, &error); if (!m_torrentInfo.isValid()) { MessageBoxRaised::critical(this, tr("Invalid torrent"), tr("Failed to load the torrent: %1.\nError: %2", "Don't remove the '\n' characters. They insert a newline.").arg(Utils::Fs::toNativePath(m_filePath)).arg(error)); return false; diff --git a/src/gui/optionsdlg.cpp b/src/gui/optionsdlg.cpp index 1cf5fcf0f..95a43d0dc 100644 --- a/src/gui/optionsdlg.cpp +++ b/src/gui/optionsdlg.cpp @@ -183,6 +183,9 @@ OptionsDialog::OptionsDialog(QWidget *parent) m_ui->groupFileAssociation->setVisible(false); #endif + m_ui->textWebUIRootFolder->setMode(FileSystemPathEdit::Mode::DirectoryOpen); + m_ui->textWebUIRootFolder->setDialogCaption(tr("Choose Alternative UI files location")); + // Connect signals / slots // Shortcuts for frequently used signals that have more than one overload. They would require // type casts and that is why we declare required member pointer here instead. @@ -367,6 +370,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->domainNameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->DNSUsernameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->DNSPasswordTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); + connect(m_ui->groupAltWebUI, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->textWebUIRootFolder, &FileSystemPathLineEdit::selectedPathChanged, this, &ThisType::enableApplyButton); #endif // RSS tab @@ -677,6 +682,9 @@ void OptionsDialog::saveOptions() pref->setDynDomainName(m_ui->domainNameTxt->text()); pref->setDynDNSUsername(m_ui->DNSUsernameTxt->text()); pref->setDynDNSPassword(m_ui->DNSPasswordTxt->text()); + // Alternative UI + pref->setAltWebUiEnabled(m_ui->groupAltWebUI->isChecked()); + pref->setWebUiRootFolder(m_ui->textWebUIRootFolder->selectedPath()); } // End Web UI // End preferences @@ -1069,6 +1077,9 @@ void OptionsDialog::loadOptions() m_ui->domainNameTxt->setText(pref->getDynDomainName()); m_ui->DNSUsernameTxt->setText(pref->getDynDNSUsername()); m_ui->DNSPasswordTxt->setText(pref->getDynDNSPassword()); + + m_ui->groupAltWebUI->setChecked(pref->isAltWebUiEnabled()); + m_ui->textWebUIRootFolder->setSelectedPath(pref->getWebUiRootFolder()); // End Web UI preferences } diff --git a/src/gui/optionsdlg.ui b/src/gui/optionsdlg.ui index f3170a3c8..4319b27c3 100644 --- a/src/gui/optionsdlg.ui +++ b/src/gui/optionsdlg.ui @@ -3014,6 +3014,31 @@ Use ';' to split multiple entries. Can use wildcard '*'. + + + + Use alternative Web UI + + + true + + + false + + + + + + Files location: + + + + + + + + + diff --git a/src/webui/CMakeLists.txt b/src/webui/CMakeLists.txt index b959d5abd..59b34a2ad 100644 --- a/src/webui/CMakeLists.txt +++ b/src/webui/CMakeLists.txt @@ -1,18 +1,31 @@ set(QBT_WEBUI_HEADERS -abstractwebapplication.h -btjson.h +api/apicontroller.h +api/apierror.h +api/appcontroller.h +api/isessionmanager.h +api/authcontroller.h +api/logcontroller.h +api/rsscontroller.h +api/synccontroller.h +api/torrentscontroller.h +api/transfercontroller.h +api/serialize/serialize_torrent.h extra_translations.h -jsonutils.h -prefjson.h webapplication.h -websessiondata.h webui.h ) set(QBT_WEBUI_SOURCES -abstractwebapplication.cpp -btjson.cpp -prefjson.cpp +api/apicontroller.cpp +api/apierror.cpp +api/appcontroller.cpp +api/authcontroller.cpp +api/logcontroller.cpp +api/rsscontroller.cpp +api/synccontroller.cpp +api/torrentscontroller.cpp +api/transfercontroller.cpp +api/serialize/serialize_torrent.cpp webapplication.cpp webui.cpp ) diff --git a/src/webui/abstractwebapplication.cpp b/src/webui/abstractwebapplication.cpp deleted file mode 100644 index d855bab0f..000000000 --- a/src/webui/abstractwebapplication.cpp +++ /dev/null @@ -1,525 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev - * - * 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. - */ - -#include "abstractwebapplication.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "base/logger.h" -#include "base/preferences.h" -#include "base/utils/fs.h" -#include "base/utils/net.h" -#include "base/utils/random.h" -#include "base/utils/string.h" -#include "websessiondata.h" - -// UnbanTimer - -class UnbanTimer: public QTimer -{ -public: - UnbanTimer(const QHostAddress& peer_ip, QObject *parent) - : QTimer(parent), m_peerIp(peer_ip) - { - setSingleShot(true); - setInterval(BAN_TIME); - } - - inline const QHostAddress& peerIp() const { return m_peerIp; } - -private: - QHostAddress m_peerIp; -}; - -// WebSession - -struct WebSession -{ - const QString id; - uint timestamp; - WebSessionData data; - - WebSession(const QString& id) - : id(id) - { - updateTimestamp(); - } - - void updateTimestamp() - { - timestamp = QDateTime::currentDateTime().toTime_t(); - } -}; - -namespace -{ - inline QUrl urlFromHostHeader(const QString &hostHeader) - { - if (!hostHeader.contains(QLatin1String("://"))) - return QUrl(QLatin1String("http://") + hostHeader); - return hostHeader; - } -} - -// AbstractWebApplication - -AbstractWebApplication::AbstractWebApplication(QObject *parent) - : Http::ResponseBuilder(parent) - , session_(0) -{ - QTimer *timer = new QTimer(this); - connect(timer, &QTimer::timeout, this, &AbstractWebApplication::removeInactiveSessions); - timer->start(60 * 1000); // 1 min. - - reloadDomainList(); - connect(Preferences::instance(), &Preferences::changed, this, &AbstractWebApplication::reloadDomainList); -} - -AbstractWebApplication::~AbstractWebApplication() -{ - // cleanup sessions data - qDeleteAll(sessions_); -} - -Http::Response AbstractWebApplication::processRequest(const Http::Request &request, const Http::Environment &env) -{ - session_ = 0; - request_ = request; - env_ = env; - - // clear response - clear(); - - // avoid clickjacking attacks - header(Http::HEADER_X_FRAME_OPTIONS, "SAMEORIGIN"); - header(Http::HEADER_X_XSS_PROTECTION, "1; mode=block"); - header(Http::HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff"); - header(Http::HEADER_CONTENT_SECURITY_POLICY, "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none';"); - - // block cross-site requests - if (isCrossSiteRequest(request_) || !validateHostHeader(domainList)) { - status(401, "Unauthorized"); - return response(); - } - - sessionInitialize(); - if (!sessionActive() && !isAuthNeeded()) - sessionStart(); - - if (isBanned()) { - status(403, "Forbidden"); - print(QObject::tr("Your IP address has been banned after too many failed authentication attempts."), Http::CONTENT_TYPE_TXT); - } - else { - doProcessRequest(); - } - - return response(); -} - -void AbstractWebApplication::UnbanTimerEvent() -{ - UnbanTimer* ubantimer = static_cast(sender()); - qDebug("Ban period has expired for %s", qUtf8Printable(ubantimer->peerIp().toString())); - clientFailedAttempts_.remove(ubantimer->peerIp()); - ubantimer->deleteLater(); -} - -void AbstractWebApplication::removeInactiveSessions() -{ - const uint now = QDateTime::currentDateTime().toTime_t(); - - foreach (const QString &id, sessions_.keys()) { - if ((now - sessions_[id]->timestamp) > INACTIVE_TIME) - delete sessions_.take(id); - } -} - -void AbstractWebApplication::reloadDomainList() -{ - domainList = Preferences::instance()->getServerDomains().split(';', QString::SkipEmptyParts); - std::for_each(domainList.begin(), domainList.end(), [](QString &entry){ entry = entry.trimmed(); }); -} - -bool AbstractWebApplication::sessionInitialize() -{ - if (session_ == 0) - { - const QString sessionId = parseCookie(request_).value(C_SID); - - // TODO: Additional session check - - if (!sessionId.isEmpty()) { - if (sessions_.contains(sessionId)) { - session_ = sessions_[sessionId]; - session_->updateTimestamp(); - return true; - } - else { - qDebug() << Q_FUNC_INFO << "session does not exist!"; - } - } - } - - return false; -} - -bool AbstractWebApplication::readFile(const QString& path, QByteArray &data, QString &type) -{ - QString ext = ""; - int index = path.lastIndexOf('.') + 1; - if (index > 0) - ext = path.mid(index); - - // find translated file in cache - if (translatedFiles_.contains(path)) { - data = translatedFiles_[path]; - } - else { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qDebug("File %s was not found!", qUtf8Printable(path)); - return false; - } - - data = file.readAll(); - file.close(); - - // Translate the file - if ((ext == "html") || ((ext == "js") && !path.endsWith("excanvas-compressed.js"))) { - QString dataStr = QString::fromUtf8(data.constData()); - translateDocument(dataStr); - - if (path.endsWith("about.html") || path.endsWith("index.html") || path.endsWith("client.js")) - dataStr.replace("${VERSION}", QBT_VERSION); - - data = dataStr.toUtf8(); - translatedFiles_[path] = data; // cashing translated file - } - } - - type = CONTENT_TYPE_BY_EXT[ext]; - return true; -} - -WebSessionData *AbstractWebApplication::session() -{ - Q_ASSERT(session_ != 0); - return &session_->data; -} - - -QString AbstractWebApplication::generateSid() -{ - QString sid; - - do { - const size_t size = 6; - quint32 tmp[size]; - - for (size_t i = 0; i < size; ++i) - tmp[i] = Utils::Random::rand(); - - sid = QByteArray::fromRawData(reinterpret_cast(tmp), sizeof(quint32) * size).toBase64(); - } - while (sessions_.contains(sid)); - - return sid; -} - -void AbstractWebApplication::translateDocument(QString& data) -{ - const QRegExp regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR(\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\])"); - const QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?"); - int i = 0; - bool found = true; - - const QString locale = Preferences::instance()->getLocale(); - bool isTranslationNeeded = !locale.startsWith("en") || locale.startsWith("en_AU") || locale.startsWith("en_GB"); - - while(i < data.size() && found) { - i = regex.indexIn(data, i); - if (i >= 0) { - //qDebug("Found translatable string: %s", regex.cap(1).toUtf8().data()); - QByteArray word = regex.cap(1).toUtf8(); - - QString translation = word; - if (isTranslationNeeded) { - QString context = regex.cap(4); - translation = qApp->translate(context.toUtf8().constData(), word.constData(), 0, 1); - } - // Remove keyboard shortcuts - translation.replace(mnemonic, ""); - - // Use HTML code for quotes to prevent issues with JS - translation.replace("'", "'"); - translation.replace("\"", """); - - data.replace(i, regex.matchedLength(), translation); - i += translation.length(); - } - else { - found = false; // no more translatable strings - } - } -} - -bool AbstractWebApplication::isBanned() const -{ - return clientFailedAttempts_.value(env_.clientAddress, 0) >= MAX_AUTH_FAILED_ATTEMPTS; -} - -int AbstractWebApplication::failedAttempts() const -{ - return clientFailedAttempts_.value(env_.clientAddress, 0); -} - -void AbstractWebApplication::resetFailedAttempts() -{ - clientFailedAttempts_.remove(env_.clientAddress); -} - -void AbstractWebApplication::increaseFailedAttempts() -{ - const int nb_fail = clientFailedAttempts_.value(env_.clientAddress, 0) + 1; - - clientFailedAttempts_[env_.clientAddress] = nb_fail; - if (nb_fail == MAX_AUTH_FAILED_ATTEMPTS) { - // Max number of failed attempts reached - // Start ban period - UnbanTimer* ubantimer = new UnbanTimer(env_.clientAddress, this); - connect(ubantimer, SIGNAL(timeout()), SLOT(UnbanTimerEvent())); - ubantimer->start(); - } -} - -bool AbstractWebApplication::isAuthNeeded() -{ - qDebug("Checking auth rules against client address %s", qPrintable(env().clientAddress.toString())); - const Preferences *pref = Preferences::instance(); - if (!pref->isWebUiLocalAuthEnabled() && Utils::Net::isLoopbackAddress(env().clientAddress)) - return false; - if (pref->isWebUiAuthSubnetWhitelistEnabled() && Utils::Net::isIPInRange(env().clientAddress, pref->getWebUiAuthSubnetWhitelist())) - return false; - return true; -} - -void AbstractWebApplication::printFile(const QString& path) -{ - QByteArray data; - QString type; - - if (!readFile(path, data, type)) { - status(404, "Not Found"); - return; - } - - print(data, type); -} - -bool AbstractWebApplication::sessionStart() -{ - if (session_ == 0) { - session_ = new WebSession(generateSid()); - sessions_[session_->id] = session_; - - QNetworkCookie cookie(C_SID, session_->id.toUtf8()); - cookie.setHttpOnly(true); - cookie.setPath(QLatin1String("/")); - header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); - - return true; - } - - return false; -} - -bool AbstractWebApplication::sessionEnd() -{ - if ((session_ != 0) && (sessions_.contains(session_->id))) { - QNetworkCookie cookie(C_SID); - cookie.setPath(QLatin1String("/")); - cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1)); - - sessions_.remove(session_->id); - delete session_; - session_ = 0; - - header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); - return true; - } - - return false; -} - -QString AbstractWebApplication::saveTmpFile(const QByteArray &data) -{ - QTemporaryFile tmpfile(Utils::Fs::tempPath() + "XXXXXX.torrent"); - tmpfile.setAutoRemove(false); - if (tmpfile.open()) { - tmpfile.write(data); - tmpfile.close(); - return tmpfile.fileName(); - } - - qWarning() << "I/O Error: Could not create temporary file"; - return QString(); -} - -bool AbstractWebApplication::isCrossSiteRequest(const Http::Request &request) const -{ - // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers - - const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool - { - // [rfc6454] 5. Comparing Origins - return ((left.port() == right.port()) - // && (left.scheme() == right.scheme()) // not present in this context - && (left.host() == right.host())); - }; - - const QString targetOrigin = request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST)); - const QString originValue = request.headers.value(Http::HEADER_ORIGIN); - const QString refererValue = request.headers.value(Http::HEADER_REFERER); - - if (originValue.isEmpty() && refererValue.isEmpty()) { - // owasp.org recommends to block this request, but doing so will inevitably lead Web API users to spoof headers - // so lets be permissive here - return false; - } - - // sent with CORS requests, as well as with POST requests - if (!originValue.isEmpty()) { - const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue); - if (isInvalid) - Logger::instance()->addMessage(tr("WebUI: Origin header & Target origin mismatch!") + "\n" - + tr("Source IP: '%1'. Origin header: '%2'. Target origin: '%3'") - .arg(env_.clientAddress.toString()).arg(originValue).arg(targetOrigin) - , Log::WARNING); - return isInvalid; - } - - if (!refererValue.isEmpty()) { - const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue); - if (isInvalid) - Logger::instance()->addMessage(tr("WebUI: Referer header & Target origin mismatch!") + "\n" - + tr("Source IP: '%1'. Referer header: '%2'. Target origin: '%3'") - .arg(env_.clientAddress.toString()).arg(refererValue).arg(targetOrigin) - , Log::WARNING); - return isInvalid; - } - - return true; -} - -bool AbstractWebApplication::validateHostHeader(const QStringList &domains) const -{ - const QUrl hostHeader = urlFromHostHeader(request().headers[Http::HEADER_HOST]); - const QString requestHost = hostHeader.host(); - - // (if present) try matching host header's port with local port - const int requestPort = hostHeader.port(); - if ((requestPort != -1) && (env().localPort != requestPort)) { - Logger::instance()->addMessage(tr("WebUI: Invalid Host header, port mismatch.") + "\n" - + tr("Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'") - .arg(env().clientAddress.toString()).arg(env().localPort) - .arg(request().headers[Http::HEADER_HOST]) - , Log::WARNING); - return false; - } - - // try matching host header with local address -#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) - const bool sameAddr = env().localAddress.isEqual(QHostAddress(requestHost)); -#else - const auto equal = [](const Q_IPV6ADDR &l, const Q_IPV6ADDR &r) -> bool { - for (int i = 0; i < 16; ++i) { - if (l[i] != r[i]) - return false; - } - return true; - }; - const bool sameAddr = equal(env().localAddress.toIPv6Address(), QHostAddress(requestHost).toIPv6Address()); -#endif - - if (sameAddr) - return true; - - // try matching host header with domain list - for (const auto &domain : domains) { - QRegExp domainRegex(domain, Qt::CaseInsensitive, QRegExp::Wildcard); - if (requestHost.contains(domainRegex)) - return true; - } - - Logger::instance()->addMessage(tr("WebUI: Invalid Host header.") + "\n" - + tr("Request source IP: '%1'. Received Host header: '%2'") - .arg(env().clientAddress.toString()).arg(request().headers[Http::HEADER_HOST]) - , Log::WARNING); - return false; -} - -const QStringMap AbstractWebApplication::CONTENT_TYPE_BY_EXT = { - { "htm", Http::CONTENT_TYPE_HTML }, - { "html", Http::CONTENT_TYPE_HTML }, - { "css", Http::CONTENT_TYPE_CSS }, - { "gif", Http::CONTENT_TYPE_GIF }, - { "png", Http::CONTENT_TYPE_PNG }, - { "js", Http::CONTENT_TYPE_JS }, - { "svg", Http::CONTENT_TYPE_SVG } -}; - -QStringMap AbstractWebApplication::parseCookie(const Http::Request &request) const -{ - // [rfc6265] 4.2.1. Syntax - QStringMap ret; - const QString cookieStr = request.headers.value(QLatin1String("cookie")); - const QVector cookies = cookieStr.splitRef(';', QString::SkipEmptyParts); - - for (const auto &cookie : cookies) { - const int idx = cookie.indexOf('='); - if (idx < 0) - continue; - - const QString name = cookie.left(idx).trimmed().toString(); - const QString value = Utils::String::unquote(cookie.mid(idx + 1).trimmed()) - .toString(); - ret.insert(name, value); - } - return ret; -} diff --git a/src/webui/abstractwebapplication.h b/src/webui/abstractwebapplication.h deleted file mode 100644 index 0cf62562c..000000000 --- a/src/webui/abstractwebapplication.h +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev - * - * 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. - */ - -#ifndef ABSTRACTWEBAPPLICATION_H -#define ABSTRACTWEBAPPLICATION_H - -#include -#include -#include - -#include "base/http/irequesthandler.h" -#include "base/http/responsebuilder.h" -#include "base/http/types.h" - -struct WebSession; -struct WebSessionData; - -const char C_SID[] = "SID"; // name of session id cookie -const int BAN_TIME = 3600000; // 1 hour -const int INACTIVE_TIME = 900; // Session inactive time (in secs = 15 min.) -const int MAX_AUTH_FAILED_ATTEMPTS = 5; - -class AbstractWebApplication : public Http::ResponseBuilder, public Http::IRequestHandler -{ - Q_OBJECT - Q_DISABLE_COPY(AbstractWebApplication) - -public: - explicit AbstractWebApplication(QObject *parent = 0); - virtual ~AbstractWebApplication(); - - Http::Response processRequest(const Http::Request &request, const Http::Environment &env) final; - -protected: - virtual void doProcessRequest() = 0; - - bool isBanned() const; - int failedAttempts() const; - void resetFailedAttempts(); - void increaseFailedAttempts(); - - void printFile(const QString &path); - - // Session management - bool sessionActive() const { return session_ != 0; } - bool sessionStart(); - bool sessionEnd(); - - bool isAuthNeeded(); - - bool readFile(const QString &path, QByteArray &data, QString &type); - - // save data to temporary file on disk and return its name (or empty string if fails) - static QString saveTmpFile(const QByteArray &data); - - WebSessionData *session(); - const Http::Request &request() const { return request_; } - const Http::Environment &env() const { return env_; } - -private slots: - void UnbanTimerEvent(); - void removeInactiveSessions(); - - void reloadDomainList(); - -private: - // Persistent data - QMap sessions_; - QHash clientFailedAttempts_; - QMap translatedFiles_; - - // Current data - WebSession *session_; - Http::Request request_; - Http::Environment env_; - - QStringList domainList; - - QString generateSid(); - bool sessionInitialize(); - - QStringMap parseCookie(const Http::Request &request) const; - bool isCrossSiteRequest(const Http::Request &request) const; - bool validateHostHeader(const QStringList &domains) const; - - static void translateDocument(QString &data); - - static const QStringMap CONTENT_TYPE_BY_EXT; -}; - -#endif // ABSTRACTWEBAPPLICATION_H diff --git a/src/webui/api/apicontroller.cpp b/src/webui/api/apicontroller.cpp new file mode 100644 index 000000000..c306a351b --- /dev/null +++ b/src/webui/api/apicontroller.cpp @@ -0,0 +1,91 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "apicontroller.h" + +#include +#include + +#include "apierror.h" + +APIController::APIController(ISessionManager *sessionManager, QObject *parent) + : QObject {parent} + , m_sessionManager {sessionManager} +{ +} + +QVariant APIController::run(const QString &action, const StringMap ¶ms, const DataMap &data) +{ + m_result.clear(); // clear result + m_params = params; + m_data = data; + + const QString methodName {action + QLatin1String("Action")}; + if (!QMetaObject::invokeMethod(this, methodName.toLatin1().constData())) + throw APIError(APIErrorType::NotFound); + + return m_result; +} + +ISessionManager *APIController::sessionManager() const +{ + return m_sessionManager; +} + +const StringMap &APIController::params() const +{ + return m_params; +} + +const DataMap &APIController::data() const +{ + return m_data; +} + +void APIController::checkParams(const QSet &requiredParams) const +{ + const QSet params {this->params().keys().toSet()}; + + if (!params.contains(requiredParams)) + throw APIError(APIErrorType::BadParams); +} + +void APIController::setResult(const QString &result) +{ + m_result = result; +} + +void APIController::setResult(const QJsonArray &result) +{ + m_result = QJsonDocument(result); +} + +void APIController::setResult(const QJsonObject &result) +{ + m_result = QJsonDocument(result); +} diff --git a/src/webui/api/apicontroller.h b/src/webui/api/apicontroller.h new file mode 100644 index 000000000..419d6c390 --- /dev/null +++ b/src/webui/api/apicontroller.h @@ -0,0 +1,72 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +struct ISessionManager; +using StringMap = QMap; +using DataMap = QMap; + +class APIController : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(APIController) + +#ifndef Q_MOC_RUN +#define WEBAPI_PUBLIC +#define WEBAPI_PRIVATE +#endif + +public: + explicit APIController(ISessionManager *sessionManager, QObject *parent = nullptr); + + QVariant run(const QString &action, const StringMap ¶ms, const DataMap &data = {}); + + ISessionManager *sessionManager() const; + +protected: + const StringMap ¶ms() const; + const DataMap &data() const; + void checkParams(const QSet &requiredParams) const; + + void setResult(const QString &result); + void setResult(const QJsonArray &result); + void setResult(const QJsonObject &result); + +private: + ISessionManager *m_sessionManager; + StringMap m_params; + DataMap m_data; + QVariant m_result; +}; diff --git a/src/webui/api/apierror.cpp b/src/webui/api/apierror.cpp new file mode 100644 index 000000000..37afb80f7 --- /dev/null +++ b/src/webui/api/apierror.cpp @@ -0,0 +1,40 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "apierror.h" + +APIError::APIError(APIErrorType type, const QString &message) + : RuntimeError {message} + , m_type {type} +{ +} + +APIErrorType APIError::type() const +{ + return m_type; +} diff --git a/src/webui/jsonutils.h b/src/webui/api/apierror.h similarity index 73% rename from src/webui/jsonutils.h rename to src/webui/api/apierror.h index dcd3327da..2ce43a3ae 100644 --- a/src/webui/jsonutils.h +++ b/src/webui/api/apierror.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2018 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,26 +26,26 @@ * exception statement from your version. */ -#ifndef JSONUTILS_H -#define JSONUTILS_H +#pragma once -#include -#include -#include -#include +#include "base/exceptions.h" -namespace json { +enum class APIErrorType +{ + BadParams, + BadData, + NotFound, + AccessDenied, + Conflict +}; - inline QByteArray toJson(const QVariant& var) - { - return QJsonDocument::fromVariant(var).toJson(QJsonDocument::Compact); - } +class APIError : public RuntimeError +{ +public: + explicit APIError(APIErrorType type, const QString &message = ""); - inline QVariant fromJson(const QString& json) - { - return QJsonDocument::fromJson(json.toUtf8()).toVariant(); - } + APIErrorType type() const; -} - -#endif // JSONUTILS_H +private: + const APIErrorType m_type; +}; diff --git a/src/webui/prefjson.cpp b/src/webui/api/appcontroller.cpp similarity index 90% rename from src/webui/prefjson.cpp rename to src/webui/api/appcontroller.cpp index e1be04572..2b3035c61 100644 --- a/src/webui/prefjson.cpp +++ b/src/webui/api/appcontroller.cpp @@ -1,6 +1,8 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006-2012 Ishan Arora and Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2006-2012 Christophe Dumez + * Copyright (C) 2006-2012 Ishan Arora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,37 +26,58 @@ * 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 "prefjson.h" +#include "appcontroller.h" #include +#include +#include +#include +#include +#include +#include +#include + #ifndef QT_NO_OPENSSL #include #include #endif -#include -#include -#include #include "base/bittorrent/session.h" #include "base/net/portforwarder.h" #include "base/net/proxyconfigurationmanager.h" #include "base/preferences.h" +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_session.h" #include "base/scanfoldersmodel.h" #include "base/utils/fs.h" #include "base/utils/net.h" -#include "jsonutils.h" +#include "../webapplication.h" -prefjson::prefjson() +void AppController::webapiVersionAction() { + setResult(static_cast(API_VERSION)); } -QByteArray prefjson::getPreferences() +void AppController::versionAction() { - const Preferences* const pref = Preferences::instance(); + setResult(QBT_VERSION); +} + +void AppController::shutdownAction() +{ + qDebug() << "Shutdown request from Web UI"; + + // Special case handling for shutdown, we + // need to reply to the Web UI before + // actually shutting down. + QTimer::singleShot(100, qApp, &QCoreApplication::quit); +} + +void AppController::preferencesAction() +{ + const Preferences *const pref = Preferences::instance(); auto session = BitTorrent::Session::instance(); QVariantMap data; @@ -187,14 +210,22 @@ QByteArray prefjson::getPreferences() data["dyndns_password"] = pref->getDynDNSPassword(); data["dyndns_domain"] = pref->getDynDomainName(); - return json::toJson(data); + // RSS settings + data["RSSRefreshInterval"] = RSS::Session::instance()->refreshInterval(); + data["RSSMaxArticlesPerFeed"] = RSS::Session::instance()->maxArticlesPerFeed(); + data["RSSProcessingEnabled"] = RSS::Session::instance()->isProcessingEnabled(); + data["RSSAutoDownloadingEnabled"] = RSS::AutoDownloader::instance()->isProcessingEnabled(); + + setResult(QJsonObject::fromVariantMap(data)); } -void prefjson::setPreferences(const QString& json) +void AppController::setPreferencesAction() { - Preferences* const pref = Preferences::instance(); + checkParams({"json"}); + + Preferences *const pref = Preferences::instance(); auto session = BitTorrent::Session::instance(); - const QVariantMap m = json::fromJson(json).toMap(); + const QVariantMap m = QJsonDocument::fromJson(params()["json"].toUtf8()).toVariant().toMap(); // Downloads // Hard Disk @@ -454,4 +485,14 @@ void prefjson::setPreferences(const QString& json) // Save preferences pref->apply(); + + RSS::Session::instance()->setRefreshInterval(m["RSSRefreshInterval"].toUInt()); + RSS::Session::instance()->setMaxArticlesPerFeed(m["RSSMaxArticlesPerFeed"].toInt()); + RSS::Session::instance()->setProcessingEnabled(m["RSSProcessingEnabled"].toBool()); + RSS::AutoDownloader::instance()->setProcessingEnabled(m["RSSAutoDownloadingEnabled"].toBool()); +} + +void AppController::defaultSavePathAction() +{ + setResult(BitTorrent::Session::instance()->defaultSavePath()); } diff --git a/src/webui/api/appcontroller.h b/src/webui/api/appcontroller.h new file mode 100644 index 000000000..e1b240075 --- /dev/null +++ b/src/webui/api/appcontroller.h @@ -0,0 +1,50 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * Copyright (C) 2006-2012 Christophe Dumez + * Copyright (C) 2006-2012 Ishan Arora + * + * 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. + */ + +#pragma once + +#include "apicontroller.h" + +class AppController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(AppController) + +public: + using APIController::APIController; + +private slots: + void webapiVersionAction(); + void versionAction(); + void shutdownAction(); + void preferencesAction(); + void setPreferencesAction(); + void defaultSavePathAction(); +}; diff --git a/src/webui/api/authcontroller.cpp b/src/webui/api/authcontroller.cpp new file mode 100644 index 000000000..71d190dc6 --- /dev/null +++ b/src/webui/api/authcontroller.cpp @@ -0,0 +1,108 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "authcontroller.h" + +#include + +#include "base/preferences.h" +#include "base/utils/string.h" +#include "apierror.h" +#include "isessionmanager.h" + +constexpr int BAN_TIME = 3600000; // 1 hour +constexpr int MAX_AUTH_FAILED_ATTEMPTS = 5; + +void AuthController::loginAction() +{ + if (sessionManager()->session()) { + setResult(QLatin1String("Ok.")); + return; + } + + if (isBanned()) + throw APIError(APIErrorType::AccessDenied + , tr("Your IP address has been banned after too many failed authentication attempts.")); + + QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(params()["password"].toLocal8Bit()); + QString pass = md5.result().toHex(); + + const QString username {Preferences::instance()->getWebUiUsername()}; + const QString password {Preferences::instance()->getWebUiPassword()}; + + const bool equalUser = Utils::String::slowEquals(params()["username"].toUtf8(), username.toUtf8()); + const bool equalPass = Utils::String::slowEquals(pass.toUtf8(), password.toUtf8()); + + if (equalUser && equalPass) { + sessionManager()->sessionStart(); + setResult(QLatin1String("Ok.")); + } + else { + QString addr = sessionManager()->clientId(); + increaseFailedAttempts(); + qDebug("client IP: %s (%d failed attempts)", qUtf8Printable(addr), failedAttemptsCount()); + setResult(QLatin1String("Fails.")); + } +} + +void AuthController::logoutAction() +{ + sessionManager()->sessionEnd(); +} + +bool AuthController::isBanned() const +{ + const uint now = QDateTime::currentDateTime().toTime_t(); + const FailedLogin failedLogin = m_clientFailedLogins.value(sessionManager()->clientId()); + + bool isBanned = (failedLogin.bannedAt > 0); + if (isBanned && ((now - failedLogin.bannedAt) > BAN_TIME)) { + m_clientFailedLogins.remove(sessionManager()->clientId()); + isBanned = false; + } + + return isBanned; +} + +int AuthController::failedAttemptsCount() const +{ + return m_clientFailedLogins.value(sessionManager()->clientId()).failedAttemptsCount; +} + +void AuthController::increaseFailedAttempts() +{ + FailedLogin &failedLogin = m_clientFailedLogins[sessionManager()->clientId()]; + ++failedLogin.failedAttemptsCount; + + if (failedLogin.failedAttemptsCount == MAX_AUTH_FAILED_ATTEMPTS) { + // Max number of failed attempts reached + // Start ban period + failedLogin.bannedAt = QDateTime::currentDateTime().toTime_t(); + } +} diff --git a/src/webui/api/authcontroller.h b/src/webui/api/authcontroller.h new file mode 100644 index 000000000..88d8cf861 --- /dev/null +++ b/src/webui/api/authcontroller.h @@ -0,0 +1,59 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include +#include + +#include "apicontroller.h" + +class AuthController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(AuthController) + +public: + using APIController::APIController; + +private slots: + void loginAction(); + void logoutAction(); + +private: + bool isBanned() const; + int failedAttemptsCount() const; + void increaseFailedAttempts(); + + struct FailedLogin + { + int failedAttemptsCount = 0; + uint bannedAt = 0; + }; + mutable QHash m_clientFailedLogins; +}; diff --git a/src/webui/api/isessionmanager.h b/src/webui/api/isessionmanager.h new file mode 100644 index 000000000..980b9952b --- /dev/null +++ b/src/webui/api/isessionmanager.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include +#include + +struct ISession +{ + virtual ~ISession() = default; + virtual QString id() const = 0; + virtual QVariant getData(const QString &id) const = 0; + virtual void setData(const QString &id, const QVariant &data) = 0; +}; + +struct ISessionManager +{ + virtual ~ISessionManager() = default; + virtual QString clientId() const = 0; + virtual ISession *session() = 0; + virtual void sessionStart() = 0; + virtual void sessionEnd() = 0; +}; diff --git a/src/webui/api/logcontroller.cpp b/src/webui/api/logcontroller.cpp new file mode 100644 index 000000000..0ca60c5dd --- /dev/null +++ b/src/webui/api/logcontroller.cpp @@ -0,0 +1,124 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "logcontroller.h" + +#include + +#include "base/logger.h" +#include "base/utils/string.h" + +const char KEY_LOG_ID[] = "id"; +const char KEY_LOG_TIMESTAMP[] = "timestamp"; +const char KEY_LOG_MSG_TYPE[] = "type"; +const char KEY_LOG_MSG_MESSAGE[] = "message"; +const char KEY_LOG_PEER_IP[] = "ip"; +const char KEY_LOG_PEER_BLOCKED[] = "blocked"; +const char KEY_LOG_PEER_REASON[] = "reason"; + +// Returns the log in JSON format. +// The return value is an array of dictionaries. +// The dictionary keys are: +// - "id": id of the message +// - "timestamp": milliseconds since epoch +// - "type": type of the message (int, see MsgType) +// - "message": text of the message +// GET params: +// - normal (bool): include normal messages (default true) +// - info (bool): include info messages (default true) +// - warning (bool): include warning messages (default true) +// - critical (bool): include critical messages (default true) +// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) +void LogController::mainAction() +{ + using Utils::String::parseBool; + + const bool isNormal = parseBool(params()["normal"], true); + const bool isInfo = parseBool(params()["info"], true); + const bool isWarning = parseBool(params()["warning"], true); + const bool isCritical = parseBool(params()["critical"], true); + + bool ok = false; + int lastKnownId = params()["last_known_id"].toInt(&ok); + if (!ok) + lastKnownId = -1; + + Logger *const logger = Logger::instance(); + QVariantList msgList; + + foreach (const Log::Msg &msg, logger->getMessages(lastKnownId)) { + if (!((msg.type == Log::NORMAL && isNormal) + || (msg.type == Log::INFO && isInfo) + || (msg.type == Log::WARNING && isWarning) + || (msg.type == Log::CRITICAL && isCritical))) + continue; + QVariantMap map; + map[KEY_LOG_ID] = msg.id; + map[KEY_LOG_TIMESTAMP] = msg.timestamp; + map[KEY_LOG_MSG_TYPE] = msg.type; + map[KEY_LOG_MSG_MESSAGE] = msg.message; + msgList.append(map); + } + + setResult(QJsonArray::fromVariantList(msgList)); +} + +// Returns the peer log in JSON format. +// The return value is an array of dictionaries. +// The dictionary keys are: +// - "id": id of the message +// - "timestamp": milliseconds since epoch +// - "ip": IP of the peer +// - "blocked": whether or not the peer was blocked +// - "reason": reason of the block +// GET params: +// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) +void LogController::peersAction() +{ + int lastKnownId; + bool ok; + + lastKnownId = params()["last_known_id"].toInt(&ok); + if (!ok) + lastKnownId = -1; + + Logger *const logger = Logger::instance(); + QVariantList peerList; + + foreach (const Log::Peer &peer, logger->getPeers(lastKnownId)) { + QVariantMap map; + map[KEY_LOG_ID] = peer.id; + map[KEY_LOG_TIMESTAMP] = peer.timestamp; + map[KEY_LOG_PEER_IP] = peer.ip; + map[KEY_LOG_PEER_BLOCKED] = peer.blocked; + map[KEY_LOG_PEER_REASON] = peer.reason; + peerList.append(map); + } + + setResult(QJsonArray::fromVariantList(peerList)); +} diff --git a/src/webui/websessiondata.h b/src/webui/api/logcontroller.h similarity index 79% rename from src/webui/websessiondata.h rename to src/webui/api/logcontroller.h index 01dd6ab57..281d2e220 100644 --- a/src/webui/websessiondata.h +++ b/src/webui/api/logcontroller.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015 Vladimir Golovnev + * Copyright (C) 2018 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,18 +26,19 @@ * exception statement from your version. */ -#ifndef WEBSESSIONDATA -#define WEBSESSIONDATA +#pragma once -#include +#include "apicontroller.h" -struct WebSessionData +class LogController : public APIController { - QVariantMap syncMainDataLastResponse; - QVariantMap syncMainDataLastAcceptedResponse; - QVariantMap syncTorrentPeersLastResponse; - QVariantMap syncTorrentPeersLastAcceptedResponse; + Q_OBJECT + Q_DISABLE_COPY(LogController) + +public: + using APIController::APIController; + +private slots: + void mainAction(); + void peersAction(); }; - -#endif // WEBSESSIONDATA - diff --git a/src/webui/api/rsscontroller.cpp b/src/webui/api/rsscontroller.cpp new file mode 100644 index 000000000..5c541f72c --- /dev/null +++ b/src/webui/api/rsscontroller.cpp @@ -0,0 +1,131 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "rsscontroller.h" + +#include +#include +#include + +#include "base/rss/rss_autodownloader.h" +#include "base/rss/rss_autodownloadrule.h" +#include "base/rss/rss_folder.h" +#include "base/rss/rss_session.h" +#include "base/utils/string.h" +#include "apierror.h" + +using Utils::String::parseBool; + +void RSSController::addFolderAction() +{ + checkParams({"path"}); + + const QString path = params()["path"].trimmed(); + QString error; + if (!RSS::Session::instance()->addFolder(path, &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::addFeedAction() +{ + checkParams({"url", "path"}); + + const QString url = params()["url"].trimmed(); + const QString path = params()["path"].trimmed(); + QString error; + if (!RSS::Session::instance()->addFeed(url, (path.isEmpty() ? url : path), &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::removeItemAction() +{ + checkParams({"path"}); + + const QString path = params()["path"].trimmed(); + QString error; + if (!RSS::Session::instance()->removeItem(path, &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::moveItemAction() +{ + checkParams({"itemPath", "destPath"}); + + const QString itemPath = params()["itemPath"].trimmed(); + const QString destPath = params()["destPath"].trimmed(); + QString error; + if (!RSS::Session::instance()->moveItem(itemPath, destPath, &error)) + throw APIError(APIErrorType::Conflict, error); +} + +void RSSController::itemsAction() +{ + const bool withData {parseBool(params()["withData"], false)}; + + const auto jsonVal = RSS::Session::instance()->rootFolder()->toJsonValue(withData); + setResult(jsonVal.toObject()); +} + +void RSSController::setRuleAction() +{ + checkParams({"ruleName", "ruleDef"}); + + const QString ruleName {params()["ruleName"].trimmed()}; + const QByteArray ruleDef {params()["ruleDef"].trimmed().toUtf8()}; + + const auto jsonObj = QJsonDocument::fromJson(ruleDef).object(); + RSS::AutoDownloader::instance()->insertRule(RSS::AutoDownloadRule::fromJsonObject(jsonObj, ruleName)); +} + +void RSSController::renameRuleAction() +{ + checkParams({"ruleName", "newRuleName"}); + + const QString ruleName {params()["ruleName"].trimmed()}; + const QString newRuleName {params()["newRuleName"].trimmed()}; + + RSS::AutoDownloader::instance()->renameRule(ruleName, newRuleName); +} + +void RSSController::removeRuleAction() +{ + checkParams({"ruleName"}); + + const QString ruleName {params()["ruleName"].trimmed()}; + RSS::AutoDownloader::instance()->removeRule(ruleName); +} + +void RSSController::rulesAction() +{ + const QList rules {RSS::AutoDownloader::instance()->rules()}; + QJsonObject jsonObj; + for (const auto &rule : rules) + jsonObj.insert(rule.name(), rule.toJsonObject()); + + setResult(jsonObj); +} diff --git a/src/webui/api/rsscontroller.h b/src/webui/api/rsscontroller.h new file mode 100644 index 000000000..4c72a2e09 --- /dev/null +++ b/src/webui/api/rsscontroller.h @@ -0,0 +1,51 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include "apicontroller.h" + +class RSSController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(RSSController) + +public: + using APIController::APIController; + +private slots: + void addFolderAction(); + void addFeedAction(); + void removeItemAction(); + void moveItemAction(); + void itemsAction(); + void setRuleAction(); + void renameRuleAction(); + void removeRuleAction(); + void rulesAction(); +}; diff --git a/src/webui/api/serialize/serialize_torrent.cpp b/src/webui/api/serialize/serialize_torrent.cpp new file mode 100644 index 000000000..d28b20a55 --- /dev/null +++ b/src/webui/api/serialize/serialize_torrent.cpp @@ -0,0 +1,140 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "serialize_torrent.h" + +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrenthandle.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" + +namespace +{ + QString torrentStateToString(const BitTorrent::TorrentState state) + { + switch (state) { + case BitTorrent::TorrentState::Error: + return QLatin1String("error"); + case BitTorrent::TorrentState::MissingFiles: + return QLatin1String("missingFiles"); + case BitTorrent::TorrentState::Uploading: + return QLatin1String("uploading"); + case BitTorrent::TorrentState::PausedUploading: + return QLatin1String("pausedUP"); + case BitTorrent::TorrentState::QueuedUploading: + return QLatin1String("queuedUP"); + case BitTorrent::TorrentState::StalledUploading: + return QLatin1String("stalledUP"); + case BitTorrent::TorrentState::CheckingUploading: + return QLatin1String("checkingUP"); + case BitTorrent::TorrentState::ForcedUploading: + return QLatin1String("forcedUP"); + case BitTorrent::TorrentState::Allocating: + return QLatin1String("allocating"); + case BitTorrent::TorrentState::Downloading: + return QLatin1String("downloading"); + case BitTorrent::TorrentState::DownloadingMetadata: + return QLatin1String("metaDL"); + case BitTorrent::TorrentState::PausedDownloading: + return QLatin1String("pausedDL"); + case BitTorrent::TorrentState::QueuedDownloading: + return QLatin1String("queuedDL"); + case BitTorrent::TorrentState::StalledDownloading: + return QLatin1String("stalledDL"); + case BitTorrent::TorrentState::CheckingDownloading: + return QLatin1String("checkingDL"); + case BitTorrent::TorrentState::ForcedDownloading: + return QLatin1String("forcedDL"); +#if LIBTORRENT_VERSION_NUM < 10100 + case BitTorrent::TorrentState::QueuedForChecking: + return QLatin1String("queuedForChecking"); +#endif + case BitTorrent::TorrentState::CheckingResumeData: + return QLatin1String("checkingResumeData"); + default: + return QLatin1String("unknown"); + } + } +} + +QVariantMap serialize(const BitTorrent::TorrentHandle &torrent) +{ + QVariantMap ret; + ret[KEY_TORRENT_HASH] = QString(torrent.hash()); + ret[KEY_TORRENT_NAME] = torrent.name(); + ret[KEY_TORRENT_MAGNET_URI] = torrent.toMagnetUri(); + ret[KEY_TORRENT_SIZE] = torrent.wantedSize(); + ret[KEY_TORRENT_PROGRESS] = torrent.progress(); + ret[KEY_TORRENT_DLSPEED] = torrent.downloadPayloadRate(); + ret[KEY_TORRENT_UPSPEED] = torrent.uploadPayloadRate(); + ret[KEY_TORRENT_PRIORITY] = torrent.queuePosition(); + ret[KEY_TORRENT_SEEDS] = torrent.seedsCount(); + ret[KEY_TORRENT_NUM_COMPLETE] = torrent.totalSeedsCount(); + ret[KEY_TORRENT_LEECHS] = torrent.leechsCount(); + ret[KEY_TORRENT_NUM_INCOMPLETE] = torrent.totalLeechersCount(); + const qreal ratio = torrent.realRatio(); + ret[KEY_TORRENT_RATIO] = (ratio > BitTorrent::TorrentHandle::MAX_RATIO) ? -1 : ratio; + ret[KEY_TORRENT_STATE] = torrentStateToString(torrent.state()); + ret[KEY_TORRENT_ETA] = torrent.eta(); + ret[KEY_TORRENT_SEQUENTIAL_DOWNLOAD] = torrent.isSequentialDownload(); + if (torrent.hasMetadata()) + ret[KEY_TORRENT_FIRST_LAST_PIECE_PRIO] = torrent.hasFirstLastPiecePriority(); + ret[KEY_TORRENT_CATEGORY] = torrent.category(); + ret[KEY_TORRENT_TAGS] = torrent.tags().toList().join(", "); + ret[KEY_TORRENT_SUPER_SEEDING] = torrent.superSeeding(); + ret[KEY_TORRENT_FORCE_START] = torrent.isForced(); + ret[KEY_TORRENT_SAVE_PATH] = Utils::Fs::toNativePath(torrent.savePath()); + ret[KEY_TORRENT_ADDED_ON] = torrent.addedTime().toTime_t(); + ret[KEY_TORRENT_COMPLETION_ON] = torrent.completedTime().toTime_t(); + ret[KEY_TORRENT_TRACKER] = torrent.currentTracker(); + ret[KEY_TORRENT_DL_LIMIT] = torrent.downloadLimit(); + ret[KEY_TORRENT_UP_LIMIT] = torrent.uploadLimit(); + ret[KEY_TORRENT_AMOUNT_DOWNLOADED] = torrent.totalDownload(); + ret[KEY_TORRENT_AMOUNT_UPLOADED] = torrent.totalUpload(); + ret[KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION] = torrent.totalPayloadDownload(); + ret[KEY_TORRENT_AMOUNT_UPLOADED_SESSION] = torrent.totalPayloadUpload(); + ret[KEY_TORRENT_AMOUNT_LEFT] = torrent.incompletedSize(); + ret[KEY_TORRENT_AMOUNT_COMPLETED] = torrent.completedSize(); + ret[KEY_TORRENT_RATIO_LIMIT] = torrent.maxRatio(); + ret[KEY_TORRENT_LAST_SEEN_COMPLETE_TIME] = torrent.lastSeenComplete().toTime_t(); + ret[KEY_TORRENT_AUTO_TORRENT_MANAGEMENT] = torrent.isAutoTMMEnabled(); + ret[KEY_TORRENT_TIME_ACTIVE] = torrent.activeTime(); + + if (torrent.isPaused() || torrent.isChecking()) { + ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = 0; + } + else { + QDateTime dt = QDateTime::currentDateTime(); + dt = dt.addSecs(-torrent.timeSinceActivity()); + ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = dt.toTime_t(); + } + + ret[KEY_TORRENT_TOTAL_SIZE] = torrent.totalSize(); + + return ret; +} diff --git a/src/webui/api/serialize/serialize_torrent.h b/src/webui/api/serialize/serialize_torrent.h new file mode 100644 index 000000000..8f861637a --- /dev/null +++ b/src/webui/api/serialize/serialize_torrent.h @@ -0,0 +1,79 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include + +namespace BitTorrent +{ + class TorrentHandle; +} + +// Torrent keys +const char KEY_TORRENT_HASH[] = "hash"; +const char KEY_TORRENT_NAME[] = "name"; +const char KEY_TORRENT_MAGNET_URI[] = "magnet_uri"; +const char KEY_TORRENT_SIZE[] = "size"; +const char KEY_TORRENT_PROGRESS[] = "progress"; +const char KEY_TORRENT_DLSPEED[] = "dlspeed"; +const char KEY_TORRENT_UPSPEED[] = "upspeed"; +const char KEY_TORRENT_PRIORITY[] = "priority"; +const char KEY_TORRENT_SEEDS[] = "num_seeds"; +const char KEY_TORRENT_NUM_COMPLETE[] = "num_complete"; +const char KEY_TORRENT_LEECHS[] = "num_leechs"; +const char KEY_TORRENT_NUM_INCOMPLETE[] = "num_incomplete"; +const char KEY_TORRENT_RATIO[] = "ratio"; +const char KEY_TORRENT_ETA[] = "eta"; +const char KEY_TORRENT_STATE[] = "state"; +const char KEY_TORRENT_SEQUENTIAL_DOWNLOAD[] = "seq_dl"; +const char KEY_TORRENT_FIRST_LAST_PIECE_PRIO[] = "f_l_piece_prio"; +const char KEY_TORRENT_CATEGORY[] = "category"; +const char KEY_TORRENT_TAGS[] = "tags"; +const char KEY_TORRENT_SUPER_SEEDING[] = "super_seeding"; +const char KEY_TORRENT_FORCE_START[] = "force_start"; +const char KEY_TORRENT_SAVE_PATH[] = "save_path"; +const char KEY_TORRENT_ADDED_ON[] = "added_on"; +const char KEY_TORRENT_COMPLETION_ON[] = "completion_on"; +const char KEY_TORRENT_TRACKER[] = "tracker"; +const char KEY_TORRENT_DL_LIMIT[] = "dl_limit"; +const char KEY_TORRENT_UP_LIMIT[] = "up_limit"; +const char KEY_TORRENT_AMOUNT_DOWNLOADED[] = "downloaded"; +const char KEY_TORRENT_AMOUNT_UPLOADED[] = "uploaded"; +const char KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION[] = "downloaded_session"; +const char KEY_TORRENT_AMOUNT_UPLOADED_SESSION[] = "uploaded_session"; +const char KEY_TORRENT_AMOUNT_LEFT[] = "amount_left"; +const char KEY_TORRENT_AMOUNT_COMPLETED[] = "completed"; +const char KEY_TORRENT_RATIO_LIMIT[] = "ratio_limit"; +const char KEY_TORRENT_LAST_SEEN_COMPLETE_TIME[] = "seen_complete"; +const char KEY_TORRENT_LAST_ACTIVITY_TIME[] = "last_activity"; +const char KEY_TORRENT_TOTAL_SIZE[] = "total_size"; +const char KEY_TORRENT_AUTO_TORRENT_MANAGEMENT[] = "auto_tmm"; +const char KEY_TORRENT_TIME_ACTIVE[] = "time_active"; + +QVariantMap serialize(const BitTorrent::TorrentHandle &torrent); diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp new file mode 100644 index 000000000..35704c713 --- /dev/null +++ b/src/webui/api/synccontroller.cpp @@ -0,0 +1,473 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "synccontroller.h" + +#include + +#include "base/bittorrent/peerinfo.h" +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrenthandle.h" +#include "base/net/geoipmanager.h" +#include "base/preferences.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" +#include "apierror.h" +#include "isessionmanager.h" +#include "serialize/serialize_torrent.h" + +// Sync main data keys +const char KEY_SYNC_MAINDATA_QUEUEING[] = "queueing"; +const char KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS[] = "use_alt_speed_limits"; +const char KEY_SYNC_MAINDATA_REFRESH_INTERVAL[] = "refresh_interval"; + +// Sync torrent peers keys +const char KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS[] = "show_flags"; + +// Peer keys +const char KEY_PEER_IP[] = "ip"; +const char KEY_PEER_PORT[] = "port"; +const char KEY_PEER_COUNTRY_CODE[] = "country_code"; +const char KEY_PEER_COUNTRY[] = "country"; +const char KEY_PEER_CLIENT[] = "client"; +const char KEY_PEER_PROGRESS[] = "progress"; +const char KEY_PEER_DOWN_SPEED[] = "dl_speed"; +const char KEY_PEER_UP_SPEED[] = "up_speed"; +const char KEY_PEER_TOT_DOWN[] = "downloaded"; +const char KEY_PEER_TOT_UP[] = "uploaded"; +const char KEY_PEER_CONNECTION_TYPE[] = "connection"; +const char KEY_PEER_FLAGS[] = "flags"; +const char KEY_PEER_FLAGS_DESCRIPTION[] = "flags_desc"; +const char KEY_PEER_RELEVANCE[] = "relevance"; +const char KEY_PEER_FILES[] = "files"; + +// TransferInfo keys +const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; +const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; +const char KEY_TRANSFER_DLRATELIMIT[] = "dl_rate_limit"; +const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; +const char KEY_TRANSFER_UPDATA[] = "up_info_data"; +const char KEY_TRANSFER_UPRATELIMIT[] = "up_rate_limit"; +const char KEY_TRANSFER_DHT_NODES[] = "dht_nodes"; +const char KEY_TRANSFER_CONNECTION_STATUS[] = "connection_status"; + +// Statistics keys +const char KEY_TRANSFER_ALLTIME_DL[] = "alltime_dl"; +const char KEY_TRANSFER_ALLTIME_UL[] = "alltime_ul"; +const char KEY_TRANSFER_TOTAL_WASTE_SESSION[] = "total_wasted_session"; +const char KEY_TRANSFER_GLOBAL_RATIO[] = "global_ratio"; +const char KEY_TRANSFER_TOTAL_PEER_CONNECTIONS[] = "total_peer_connections"; +const char KEY_TRANSFER_READ_CACHE_HITS[] = "read_cache_hits"; +const char KEY_TRANSFER_TOTAL_BUFFERS_SIZE[] = "total_buffers_size"; +const char KEY_TRANSFER_WRITE_CACHE_OVERLOAD[] = "write_cache_overload"; +const char KEY_TRANSFER_READ_CACHE_OVERLOAD[] = "read_cache_overload"; +const char KEY_TRANSFER_QUEUED_IO_JOBS[] = "queued_io_jobs"; +const char KEY_TRANSFER_AVERAGE_TIME_QUEUE[] = "average_time_queue"; +const char KEY_TRANSFER_TOTAL_QUEUED_SIZE[] = "total_queued_size"; + +const char KEY_FULL_UPDATE[] = "full_update"; +const char KEY_RESPONSE_ID[] = "rid"; +const char KEY_SUFFIX_REMOVED[] = "_removed"; + +namespace +{ + void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData); + void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems); + void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems); + QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData); + + QVariantMap getTranserInfo() + { + QVariantMap map; + const BitTorrent::SessionStatus &sessionStatus = BitTorrent::Session::instance()->status(); + const BitTorrent::CacheStatus &cacheStatus = BitTorrent::Session::instance()->cacheStatus(); + map[KEY_TRANSFER_DLSPEED] = sessionStatus.payloadDownloadRate; + map[KEY_TRANSFER_DLDATA] = sessionStatus.totalPayloadDownload; + map[KEY_TRANSFER_UPSPEED] = sessionStatus.payloadUploadRate; + map[KEY_TRANSFER_UPDATA] = sessionStatus.totalPayloadUpload; + map[KEY_TRANSFER_DLRATELIMIT] = BitTorrent::Session::instance()->downloadSpeedLimit(); + map[KEY_TRANSFER_UPRATELIMIT] = BitTorrent::Session::instance()->uploadSpeedLimit(); + + quint64 atd = BitTorrent::Session::instance()->getAlltimeDL(); + quint64 atu = BitTorrent::Session::instance()->getAlltimeUL(); + map[KEY_TRANSFER_ALLTIME_DL] = atd; + map[KEY_TRANSFER_ALLTIME_UL] = atu; + map[KEY_TRANSFER_TOTAL_WASTE_SESSION] = sessionStatus.totalWasted; + map[KEY_TRANSFER_GLOBAL_RATIO] = ((atd > 0) && (atu > 0)) ? Utils::String::fromDouble(static_cast(atu) / atd, 2) : "-"; + map[KEY_TRANSFER_TOTAL_PEER_CONNECTIONS] = sessionStatus.peersCount; + + qreal readRatio = cacheStatus.readRatio; + map[KEY_TRANSFER_READ_CACHE_HITS] = (readRatio >= 0) ? Utils::String::fromDouble(100 * readRatio, 2) : "-"; + map[KEY_TRANSFER_TOTAL_BUFFERS_SIZE] = cacheStatus.totalUsedBuffers * 16 * 1024; + + // num_peers is not reliable (adds up peers, which didn't even overcome tcp handshake) + quint32 peers = 0; + foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) + peers += torrent->peersCount(); + map[KEY_TRANSFER_WRITE_CACHE_OVERLOAD] = ((sessionStatus.diskWriteQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskWriteQueue) / peers, 2) : "0"; + map[KEY_TRANSFER_READ_CACHE_OVERLOAD] = ((sessionStatus.diskReadQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskReadQueue) / peers, 2) : "0"; + + map[KEY_TRANSFER_QUEUED_IO_JOBS] = cacheStatus.jobQueueLength; + map[KEY_TRANSFER_AVERAGE_TIME_QUEUE] = cacheStatus.averageJobTime; + map[KEY_TRANSFER_TOTAL_QUEUED_SIZE] = cacheStatus.queuedBytes; + + map[KEY_TRANSFER_DHT_NODES] = sessionStatus.dhtNodes; + if (!BitTorrent::Session::instance()->isListening()) + map[KEY_TRANSFER_CONNECTION_STATUS] = "disconnected"; + else + map[KEY_TRANSFER_CONNECTION_STATUS] = sessionStatus.hasIncomingConnections ? "connected" : "firewalled"; + return map; + } + + // Compare two structures (prevData, data) and calculate difference (syncData). + // Structures encoded as map. + void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData) + { + // initialize output variable + syncData.clear(); + + QVariantList removedItems; + foreach (QString key, data.keys()) { + removedItems.clear(); + + switch (static_cast(data[key].type())) { + case QMetaType::QVariantMap: { + QVariantMap map; + processMap(prevData[key].toMap(), data[key].toMap(), map); + if (!map.isEmpty()) + syncData[key] = map; + } + break; + case QMetaType::QVariantHash: { + QVariantMap map; + processHash(prevData[key].toHash(), data[key].toHash(), map, removedItems); + if (!map.isEmpty()) + syncData[key] = map; + if (!removedItems.isEmpty()) + syncData[key + KEY_SUFFIX_REMOVED] = removedItems; + } + break; + case QMetaType::QVariantList: { + QVariantList list; + processList(prevData[key].toList(), data[key].toList(), list, removedItems); + if (!list.isEmpty()) + syncData[key] = list; + if (!removedItems.isEmpty()) + syncData[key + KEY_SUFFIX_REMOVED] = removedItems; + } + break; + case QMetaType::QString: + case QMetaType::LongLong: + case QMetaType::Float: + case QMetaType::Int: + case QMetaType::Bool: + case QMetaType::Double: + case QMetaType::ULongLong: + case QMetaType::UInt: + case QMetaType::QDateTime: + if (prevData[key] != data[key]) + syncData[key] = data[key]; + break; + default: + Q_ASSERT_X(false, "processMap" + , QString("Unexpected type: %1") + .arg(QMetaType::typeName(static_cast(data[key].type()))) + .toUtf8().constData()); + } + } + } + + // Compare two lists of structures (prevData, data) and calculate difference (syncData, removedItems). + // Structures encoded as map. + // Lists are encoded as hash table (indexed by structure key value) to improve ease of searching for removed items. + void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems) + { + // initialize output variables + syncData.clear(); + removedItems.clear(); + + if (prevData.isEmpty()) { + // If list was empty before, then difference is a whole new list. + foreach (QString key, data.keys()) + syncData[key] = data[key]; + } + else { + foreach (QString key, data.keys()) { + switch (data[key].type()) { + case QVariant::Map: + if (!prevData.contains(key)) { + // new list item found - append it to syncData + syncData[key] = data[key]; + } + else { + QVariantMap map; + processMap(prevData[key].toMap(), data[key].toMap(), map); + // existing list item found - remove it from prevData + prevData.remove(key); + if (!map.isEmpty()) + // changed list item found - append its changes to syncData + syncData[key] = map; + } + break; + default: + Q_ASSERT(0); + } + } + + if (!prevData.isEmpty()) { + // prevData contains only items that are missing now - + // put them in removedItems + foreach (QString s, prevData.keys()) + removedItems << s; + } + } + } + + // Compare two lists of simple value (prevData, data) and calculate difference (syncData, removedItems). + void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems) + { + // initialize output variables + syncData.clear(); + removedItems.clear(); + + if (prevData.isEmpty()) { + // If list was empty before, then difference is a whole new list. + syncData = data; + } + else { + foreach (QVariant item, data) { + if (!prevData.contains(item)) + // new list item found - append it to syncData + syncData.append(item); + else + // unchanged list item found - remove it from prevData + prevData.removeOne(item); + } + + if (!prevData.isEmpty()) + // prevData contains only items that are missing now - + // put them in removedItems + removedItems = prevData; + } + } + + QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData) + { + QVariantMap syncData; + bool fullUpdate = true; + int lastResponseId = 0; + if (acceptedResponseId > 0) { + lastResponseId = lastData[KEY_RESPONSE_ID].toInt(); + + if (lastResponseId == acceptedResponseId) + lastAcceptedData = lastData; + + int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt(); + + if (lastAcceptedResponseId == acceptedResponseId) { + processMap(lastAcceptedData, data, syncData); + fullUpdate = false; + } + } + + if (fullUpdate) { + lastAcceptedData.clear(); + syncData = data; + syncData[KEY_FULL_UPDATE] = true; + } + + lastResponseId = lastResponseId % 1000000 + 1; // cycle between 1 and 1000000 + lastData = data; + lastData[KEY_RESPONSE_ID] = lastResponseId; + syncData[KEY_RESPONSE_ID] = lastResponseId; + + return syncData; + } +} + +// The function returns the changed data from the server to synchronize with the web client. +// Return value is map in JSON format. +// Map contain the key: +// - "Rid": ID response +// Map can contain the keys: +// - "full_update": full data update flag +// - "torrents": dictionary contains information about torrents. +// - "torrents_removed": a list of hashes of removed torrents +// - "categories": list of categories +// - "categories_removed": list of removed categories +// - "server_state": map contains information about the state of the server +// The keys of the 'torrents' dictionary are hashes of torrents. +// Each value of the 'torrents' dictionary contains map. The map can contain following keys: +// - "name": Torrent name +// - "size": Torrent size +// - "progress: Torrent progress +// - "dlspeed": Torrent download speed +// - "upspeed": Torrent upload speed +// - "priority": Torrent priority (-1 if queuing is disabled) +// - "num_seeds": Torrent seeds connected to +// - "num_complete": Torrent seeds in the swarm +// - "num_leechs": Torrent leechers connected to +// - "num_incomplete": Torrent leechers in the swarm +// - "ratio": Torrent share ratio +// - "eta": Torrent ETA +// - "state": Torrent state +// - "seq_dl": Torrent sequential download state +// - "f_l_piece_prio": Torrent first last piece priority state +// - "completion_on": Torrent copletion time +// - "tracker": Torrent tracker +// - "dl_limit": Torrent download limit +// - "up_limit": Torrent upload limit +// - "downloaded": Amount of data downloaded +// - "uploaded": Amount of data uploaded +// - "downloaded_session": Amount of data downloaded since program open +// - "uploaded_session": Amount of data uploaded since program open +// - "amount_left": Amount of data left to download +// - "save_path": Torrent save path +// - "completed": Amount of data completed +// - "ratio_limit": Upload share ratio limit +// - "seen_complete": Indicates the time when the torrent was last seen complete/whole +// - "last_activity": Last time when a chunk was downloaded/uploaded +// - "total_size": Size including unwanted data +// Server state map may contain the following keys: +// - "connection_status": connection status +// - "dht_nodes": DHT nodes count +// - "dl_info_data": bytes downloaded +// - "dl_info_speed": download speed +// - "dl_rate_limit: download rate limit +// - "up_info_data: bytes uploaded +// - "up_info_speed: upload speed +// - "up_rate_limit: upload speed limit +// - "queueing": priority system usage flag +// - "refresh_interval": torrents table refresh interval +// GET param: +// - rid (int): last response id +void SyncController::maindataAction() +{ + auto lastResponse = sessionManager()->session()->getData(QLatin1String("syncMainDataLastResponse")).toMap(); + auto lastAcceptedResponse = sessionManager()->session()->getData(QLatin1String("syncMainDataLastAcceptedResponse")).toMap(); + + QVariantMap data; + QVariantHash torrents; + + BitTorrent::Session *const session = BitTorrent::Session::instance(); + + foreach (BitTorrent::TorrentHandle *const torrent, session->torrents()) { + QVariantMap map = serialize(*torrent); + map.remove(KEY_TORRENT_HASH); + + // Calculated last activity time can differ from actual value by up to 10 seconds (this is a libtorrent issue). + // So we don't need unnecessary updates of last activity time in response. + if (lastResponse.contains("torrents") && lastResponse["torrents"].toHash().contains(torrent->hash()) && + lastResponse["torrents"].toHash()[torrent->hash()].toMap().contains(KEY_TORRENT_LAST_ACTIVITY_TIME)) { + uint lastValue = lastResponse["torrents"].toHash()[torrent->hash()].toMap()[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt(); + if (qAbs(static_cast(lastValue - map[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt())) < 15) + map[KEY_TORRENT_LAST_ACTIVITY_TIME] = lastValue; + } + + torrents[torrent->hash()] = map; + } + + data["torrents"] = torrents; + + QVariantList categories; + foreach (const QString &category, session->categories().keys()) + categories << category; + + data["categories"] = categories; + + QVariantMap serverState = getTranserInfo(); + serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); + serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); + serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); + data["server_state"] = serverState; + + const int acceptedResponseId {params()["rid"].toInt()}; + setResult(QJsonObject::fromVariantMap(generateSyncData(acceptedResponseId, data, lastAcceptedResponse, lastResponse))); + + sessionManager()->session()->setData(QLatin1String("syncMainDataLastResponse"), lastResponse); + sessionManager()->session()->setData(QLatin1String("syncMainDataLastAcceptedResponse"), lastAcceptedResponse); +} + +// GET param: +// - hash (string): torrent hash +// - rid (int): last response id +void SyncController::torrentPeersAction() +{ + auto lastResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastResponse")).toMap(); + auto lastAcceptedResponse = sessionManager()->session()->getData(QLatin1String("syncTorrentPeersLastAcceptedResponse")).toMap(); + + const QString hash {params()["hash"]}; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QVariantMap data; + QVariantHash peers; + QList peersList = torrent->peers(); +#ifndef DISABLE_COUNTRIES_RESOLUTION + bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries(); +#else + bool resolvePeerCountries = false; +#endif + + data[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS] = resolvePeerCountries; + + foreach (const BitTorrent::PeerInfo &pi, peersList) { + if (pi.address().ip.isNull()) continue; + QVariantMap peer; +#ifndef DISABLE_COUNTRIES_RESOLUTION + if (resolvePeerCountries) { + peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower(); + peer[KEY_PEER_COUNTRY] = Net::GeoIPManager::CountryName(pi.country()); + } +#endif + peer[KEY_PEER_IP] = pi.address().ip.toString(); + peer[KEY_PEER_PORT] = pi.address().port; + peer[KEY_PEER_CLIENT] = pi.client(); + peer[KEY_PEER_PROGRESS] = pi.progress(); + peer[KEY_PEER_DOWN_SPEED] = pi.payloadDownSpeed(); + peer[KEY_PEER_UP_SPEED] = pi.payloadUpSpeed(); + peer[KEY_PEER_TOT_DOWN] = pi.totalDownload(); + peer[KEY_PEER_TOT_UP] = pi.totalUpload(); + peer[KEY_PEER_CONNECTION_TYPE] = pi.connectionType(); + peer[KEY_PEER_FLAGS] = pi.flags(); + peer[KEY_PEER_FLAGS_DESCRIPTION] = pi.flagsDescription(); + peer[KEY_PEER_RELEVANCE] = pi.relevance(); + peer[KEY_PEER_FILES] = torrent->info().filesForPiece(pi.downloadingPieceIndex()).join(QLatin1String("\n")); + + peers[pi.address().ip.toString() + ":" + QString::number(pi.address().port)] = peer; + } + + data["peers"] = peers; + + const int acceptedResponseId {params()["rid"].toInt()}; + setResult(QJsonObject::fromVariantMap(generateSyncData(acceptedResponseId, data, lastAcceptedResponse, lastResponse))); + + sessionManager()->session()->setData(QLatin1String("syncTorrentPeersLastResponse"), lastResponse); + sessionManager()->session()->setData(QLatin1String("syncTorrentPeersLastAcceptedResponse"), lastAcceptedResponse); +} diff --git a/src/webui/api/synccontroller.h b/src/webui/api/synccontroller.h new file mode 100644 index 000000000..0e5e79e45 --- /dev/null +++ b/src/webui/api/synccontroller.h @@ -0,0 +1,44 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include "apicontroller.h" + +class SyncController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(SyncController) + +public: + using APIController::APIController; + +private slots: + void maindataAction(); + void torrentPeersAction(); +}; diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp new file mode 100644 index 000000000..2667f39a7 --- /dev/null +++ b/src/webui/api/torrentscontroller.cpp @@ -0,0 +1,805 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "torrentscontroller.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/bittorrent/session.h" +#include "base/bittorrent/torrenthandle.h" +#include "base/bittorrent/torrentinfo.h" +#include "base/bittorrent/trackerentry.h" +#include "base/logger.h" +#include "base/net/downloadmanager.h" +#include "base/torrentfilter.h" +#include "base/utils/fs.h" +#include "base/utils/string.h" +#include "serialize/serialize_torrent.h" +#include "apierror.h" + +// Tracker keys +const char KEY_TRACKER_URL[] = "url"; +const char KEY_TRACKER_STATUS[] = "status"; +const char KEY_TRACKER_MSG[] = "msg"; +const char KEY_TRACKER_PEERS[] = "num_peers"; + +// Web seed keys +const char KEY_WEBSEED_URL[] = "url"; + +// Torrent keys (Properties) +const char KEY_PROP_TIME_ELAPSED[] = "time_elapsed"; +const char KEY_PROP_SEEDING_TIME[] = "seeding_time"; +const char KEY_PROP_ETA[] = "eta"; +const char KEY_PROP_CONNECT_COUNT[] = "nb_connections"; +const char KEY_PROP_CONNECT_COUNT_LIMIT[] = "nb_connections_limit"; +const char KEY_PROP_DOWNLOADED[] = "total_downloaded"; +const char KEY_PROP_DOWNLOADED_SESSION[] = "total_downloaded_session"; +const char KEY_PROP_UPLOADED[] = "total_uploaded"; +const char KEY_PROP_UPLOADED_SESSION[] = "total_uploaded_session"; +const char KEY_PROP_DL_SPEED[] = "dl_speed"; +const char KEY_PROP_DL_SPEED_AVG[] = "dl_speed_avg"; +const char KEY_PROP_UP_SPEED[] = "up_speed"; +const char KEY_PROP_UP_SPEED_AVG[] = "up_speed_avg"; +const char KEY_PROP_DL_LIMIT[] = "dl_limit"; +const char KEY_PROP_UP_LIMIT[] = "up_limit"; +const char KEY_PROP_WASTED[] = "total_wasted"; +const char KEY_PROP_SEEDS[] = "seeds"; +const char KEY_PROP_SEEDS_TOTAL[] = "seeds_total"; +const char KEY_PROP_PEERS[] = "peers"; +const char KEY_PROP_PEERS_TOTAL[] = "peers_total"; +const char KEY_PROP_RATIO[] = "share_ratio"; +const char KEY_PROP_REANNOUNCE[] = "reannounce"; +const char KEY_PROP_TOTAL_SIZE[] = "total_size"; +const char KEY_PROP_PIECES_NUM[] = "pieces_num"; +const char KEY_PROP_PIECE_SIZE[] = "piece_size"; +const char KEY_PROP_PIECES_HAVE[] = "pieces_have"; +const char KEY_PROP_CREATED_BY[] = "created_by"; +const char KEY_PROP_LAST_SEEN[] = "last_seen"; +const char KEY_PROP_ADDITION_DATE[] = "addition_date"; +const char KEY_PROP_COMPLETION_DATE[] = "completion_date"; +const char KEY_PROP_CREATION_DATE[] = "creation_date"; +const char KEY_PROP_SAVE_PATH[] = "save_path"; +const char KEY_PROP_COMMENT[] = "comment"; + +// File keys +const char KEY_FILE_NAME[] = "name"; +const char KEY_FILE_SIZE[] = "size"; +const char KEY_FILE_PROGRESS[] = "progress"; +const char KEY_FILE_PRIORITY[] = "priority"; +const char KEY_FILE_IS_SEED[] = "is_seed"; +const char KEY_FILE_PIECE_RANGE[] = "piece_range"; +const char KEY_FILE_AVAILABILITY[] = "availability"; + +namespace +{ + using Utils::String::parseBool; + using Utils::String::parseTriStateBool; + + void applyToTorrents(const QStringList &hashes, const std::function &func) + { + if ((hashes.size() == 1) && (hashes[0] == QLatin1String("all"))) { + foreach (BitTorrent::TorrentHandle *torrent, BitTorrent::Session::instance()->torrents()) + func(torrent); + } + else { + for (const QString &hash : hashes) { + BitTorrent::TorrentHandle *torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) + func(torrent); + } + } + } +} + +// Returns all the torrents in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "hash": Torrent hash +// - "name": Torrent name +// - "size": Torrent size +// - "progress: Torrent progress +// - "dlspeed": Torrent download speed +// - "upspeed": Torrent upload speed +// - "priority": Torrent priority (-1 if queuing is disabled) +// - "num_seeds": Torrent seeds connected to +// - "num_complete": Torrent seeds in the swarm +// - "num_leechs": Torrent leechers connected to +// - "num_incomplete": Torrent leechers in the swarm +// - "ratio": Torrent share ratio +// - "eta": Torrent ETA +// - "state": Torrent state +// - "seq_dl": Torrent sequential download state +// - "f_l_piece_prio": Torrent first last piece priority state +// - "force_start": Torrent force start state +// - "category": Torrent category +// GET params: +// - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive +// - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") +// - sort (string): name of column for sorting by its value +// - reverse (bool): enable reverse sorting +// - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) +// - offset (int): set offset (if less than 0 - offset from end) +void TorrentsController::infoAction() +{ + const QString filter {params()["filter"]}; + const QString category {params()["category"]}; + const QString sortedColumn {params()["sort"]}; + const bool reverse {parseBool(params()["reverse"], false)}; + int limit {params()["limit"].toInt()}; + int offset {params()["offset"].toInt()}; + + QVariantList torrentList; + TorrentFilter torrentFilter(filter, TorrentFilter::AnyHash, category); + foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) { + if (torrentFilter.match(torrent)) + torrentList.append(serialize(*torrent)); + } + + std::sort(torrentList.begin(), torrentList.end() + , [sortedColumn, reverse](const QVariant &torrent1, const QVariant &torrent2) + { + return reverse + ? (torrent1.toMap().value(sortedColumn) > torrent2.toMap().value(sortedColumn)) + : (torrent1.toMap().value(sortedColumn) < torrent2.toMap().value(sortedColumn)); + }); + + const int size = torrentList.size(); + // normalize offset + if (offset < 0) + offset = size + offset; + if ((offset >= size) || (offset < 0)) + offset = 0; + // normalize limit + if (limit <= 0) + limit = -1; // unlimited + + if ((limit > 0) || (offset > 0)) + torrentList = torrentList.mid(offset, limit); + + setResult(QJsonArray::fromVariantList(torrentList)); +} + +// Returns the properties for a torrent in JSON format. +// The return value is a JSON-formatted dictionary. +// The dictionary keys are: +// - "time_elapsed": Torrent elapsed time +// - "seeding_time": Torrent elapsed time while complete +// - "eta": Torrent ETA +// - "nb_connections": Torrent connection count +// - "nb_connections_limit": Torrent connection count limit +// - "total_downloaded": Total data uploaded for torrent +// - "total_downloaded_session": Total data downloaded this session +// - "total_uploaded": Total data uploaded for torrent +// - "total_uploaded_session": Total data uploaded this session +// - "dl_speed": Torrent download speed +// - "dl_speed_avg": Torrent average download speed +// - "up_speed": Torrent upload speed +// - "up_speed_avg": Torrent average upload speed +// - "dl_limit": Torrent download limit +// - "up_limit": Torrent upload limit +// - "total_wasted": Total data wasted for torrent +// - "seeds": Torrent connected seeds +// - "seeds_total": Torrent total number of seeds +// - "peers": Torrent connected peers +// - "peers_total": Torrent total number of peers +// - "share_ratio": Torrent share ratio +// - "reannounce": Torrent next reannounce time +// - "total_size": Torrent total size +// - "pieces_num": Torrent pieces count +// - "piece_size": Torrent piece size +// - "pieces_have": Torrent pieces have +// - "created_by": Torrent creator +// - "last_seen": Torrent last seen complete +// - "addition_date": Torrent addition date +// - "completion_date": Torrent completion date +// - "creation_date": Torrent creation date +// - "save_path": Torrent save path +// - "comment": Torrent comment +void TorrentsController::propertiesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantMap dataDict; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime(); + dataDict[KEY_PROP_SEEDING_TIME] = torrent->seedingTime(); + dataDict[KEY_PROP_ETA] = torrent->eta(); + dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount(); + dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit(); + dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload(); + dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); + dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload(); + dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload(); + dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate(); + dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / (1 + torrent->activeTime() - torrent->finishedTime()); + dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate(); + dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / (1 + torrent->activeTime()); + dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit(); + dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit(); + dataDict[KEY_PROP_WASTED] = torrent->wastedSize(); + dataDict[KEY_PROP_SEEDS] = torrent->seedsCount(); + dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount(); + dataDict[KEY_PROP_PEERS] = torrent->leechsCount(); + dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount(); + const qreal ratio = torrent->realRatio(); + dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::TorrentHandle::MAX_RATIO ? -1 : ratio; + dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce(); + dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize(); + dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount(); + dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength(); + dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave(); + dataDict[KEY_PROP_CREATED_BY] = torrent->creator(); + dataDict[KEY_PROP_ADDITION_DATE] = torrent->addedTime().toTime_t(); + if (torrent->hasMetadata()) { + dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? static_cast(torrent->lastSeenComplete().toTime_t()) : -1; + dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? static_cast(torrent->completedTime().toTime_t()) : -1; + dataDict[KEY_PROP_CREATION_DATE] = torrent->creationDate().toTime_t(); + } + else { + dataDict[KEY_PROP_LAST_SEEN] = -1; + dataDict[KEY_PROP_COMPLETION_DATE] = -1; + dataDict[KEY_PROP_CREATION_DATE] = -1; + } + dataDict[KEY_PROP_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); + dataDict[KEY_PROP_COMMENT] = torrent->comment(); + + setResult(QJsonObject::fromVariantMap(dataDict)); +} + +// Returns the trackers for a torrent in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "url": Tracker URL +// - "status": Tracker status +// - "num_peers": Tracker peer count +// - "msg": Tracker message (last) +void TorrentsController::trackersAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList trackerList; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + QHash trackersData = torrent->trackerInfos(); + foreach (const BitTorrent::TrackerEntry &tracker, torrent->trackers()) { + QVariantMap trackerDict; + trackerDict[KEY_TRACKER_URL] = tracker.url(); + const BitTorrent::TrackerInfo data = trackersData.value(tracker.url()); + QString status; + switch (tracker.status()) { + case BitTorrent::TrackerEntry::NotContacted: + status = tr("Not contacted yet"); break; + case BitTorrent::TrackerEntry::Updating: + status = tr("Updating..."); break; + case BitTorrent::TrackerEntry::Working: + status = tr("Working"); break; + case BitTorrent::TrackerEntry::NotWorking: + status = tr("Not working"); break; + } + trackerDict[KEY_TRACKER_STATUS] = status; + trackerDict[KEY_TRACKER_PEERS] = data.numPeers; + trackerDict[KEY_TRACKER_MSG] = data.lastMessage.trimmed(); + + trackerList.append(trackerDict); + } + + setResult(QJsonArray::fromVariantList(trackerList)); +} + +// Returns the web seeds for a torrent in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "url": Web seed URL +void TorrentsController::webseedsAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList webSeedList; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + foreach (const QUrl &webseed, torrent->urlSeeds()) { + QVariantMap webSeedDict; + webSeedDict[KEY_WEBSEED_URL] = webseed.toString(); + webSeedList.append(webSeedDict); + } + + setResult(QJsonArray::fromVariantList(webSeedList)); +} + +// Returns the files in a torrent in JSON format. +// The return value is a JSON-formatted list of dictionaries. +// The dictionary keys are: +// - "name": File name +// - "size": File size +// - "progress": File progress +// - "priority": File priority +// - "is_seed": Flag indicating if torrent is seeding/complete +// - "piece_range": Piece index range, the first number is the starting piece index +// and the second number is the ending piece index (inclusive) +void TorrentsController::filesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList fileList; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + if (torrent->hasMetadata()) { + const QVector priorities = torrent->filePriorities(); + const QVector fp = torrent->filesProgress(); + const QVector fileAvailability = torrent->availableFileFractions(); + const BitTorrent::TorrentInfo info = torrent->info(); + for (int i = 0; i < torrent->filesCount(); ++i) { + QVariantMap fileDict; + fileDict[KEY_FILE_PROGRESS] = fp[i]; + fileDict[KEY_FILE_PRIORITY] = priorities[i]; + fileDict[KEY_FILE_SIZE] = torrent->fileSize(i); + fileDict[KEY_FILE_AVAILABILITY] = fileAvailability[i]; + + QString fileName = torrent->filePath(i); + if (fileName.endsWith(QB_EXT, Qt::CaseInsensitive)) + fileName.chop(QB_EXT.size()); + fileDict[KEY_FILE_NAME] = Utils::Fs::toNativePath(fileName); + + const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(i); + fileDict[KEY_FILE_PIECE_RANGE] = QVariantList {idx.first(), idx.last()}; + + if (i == 0) + fileDict[KEY_FILE_IS_SEED] = torrent->isSeed(); + + fileList.append(fileDict); + } + } + + setResult(QJsonArray::fromVariantList(fileList)); +} + +// Returns an array of hashes (of each pieces respectively) for a torrent in JSON format. +// The return value is a JSON-formatted array of strings (hex strings). +void TorrentsController::pieceHashesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList pieceHashes; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + const QVector hashes = torrent->info().pieceHashes(); + pieceHashes.reserve(hashes.size()); + foreach (const QByteArray &hash, hashes) + pieceHashes.append(hash.toHex()); + + setResult(QJsonArray::fromVariantList(pieceHashes)); +} + +// Returns an array of states (of each pieces respectively) for a torrent in JSON format. +// The return value is a JSON-formatted array of ints. +// 0: piece not downloaded +// 1: piece requested or downloading +// 2: piece already downloaded +void TorrentsController::pieceStatesAction() +{ + checkParams({"hash"}); + + const QString hash {params()["hash"]}; + QVariantList pieceStates; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + const QBitArray states = torrent->pieces(); + pieceStates.reserve(states.size()); + for (int i = 0; i < states.size(); ++i) + pieceStates.append(static_cast(states[i]) * 2); + + const QBitArray dlstates = torrent->downloadingPieces(); + for (int i = 0; i < states.size(); ++i) { + if (dlstates[i]) + pieceStates[i] = 1; + } + + setResult(QJsonArray::fromVariantList(pieceStates)); +} + +void TorrentsController::addAction() +{ + const QString urls = params()["urls"]; + + const bool skipChecking = parseBool(params()["skip_checking"], false); + const bool seqDownload = parseBool(params()["sequentialDownload"], false); + const bool firstLastPiece = parseBool(params()["firstLastPiecePrio"], false); + const TriStateBool addPaused = parseTriStateBool(params()["paused"]); + const TriStateBool rootFolder = parseTriStateBool(params()["root_folder"]); + const QString savepath = params()["savepath"].trimmed(); + const QString category = params()["category"].trimmed(); + const QString cookie = params()["cookie"]; + const QString torrentName = params()["rename"].trimmed(); + const int upLimit = params()["upLimit"].toInt(); + const int dlLimit = params()["dlLimit"].toInt(); + + QList cookies; + if (!cookie.isEmpty()) { + const QStringList cookiesStr = cookie.split("; "); + for (QString cookieStr : cookiesStr) { + cookieStr = cookieStr.trimmed(); + int index = cookieStr.indexOf('='); + if (index > 1) { + QByteArray name = cookieStr.left(index).toLatin1(); + QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1(); + cookies += QNetworkCookie(name, value); + } + } + } + + BitTorrent::AddTorrentParams params; + // TODO: Check if destination actually exists + params.skipChecking = skipChecking; + params.sequential = seqDownload; + params.firstLastPiecePriority = firstLastPiece; + params.addPaused = addPaused; + params.createSubfolder = rootFolder; + params.savePath = savepath; + params.category = category; + params.name = torrentName; + params.uploadLimit = (upLimit > 0) ? upLimit : -1; + params.downloadLimit = (dlLimit > 0) ? dlLimit : -1; + + bool partialSuccess = false; + for (QString url : urls.split('\n')) { + url = url.trimmed(); + if (!url.isEmpty()) { + Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8())); + partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, params); + } + } + + for (auto it = data().constBegin(); it != data().constEnd(); ++it) { + const BitTorrent::TorrentInfo torrentInfo = BitTorrent::TorrentInfo::load(it.value()); + if (!torrentInfo.isValid()) { + throw APIError(APIErrorType::BadData + , tr("Error: '%1' is not a valid torrent file.").arg(it.key())); + } + + partialSuccess |= BitTorrent::Session::instance()->addTorrent(torrentInfo, params); + } + + if (partialSuccess) + setResult("Ok."); + else + setResult("Fails."); +} + +void TorrentsController::addTrackersAction() +{ + checkParams({"hash", "urls"}); + + const QString hash = params()["hash"]; + + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) { + QList trackers; + foreach (QString url, params()["urls"].split('\n')) { + url = url.trimmed(); + if (!url.isEmpty()) + trackers << url; + } + torrent->addTrackers(trackers); + } +} + +void TorrentsController::pauseAction() +{ + checkParams({"hashes"}); + + const QStringList hashes = params()["hashes"].split('|'); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->pause(); }); +} + +void TorrentsController::resumeAction() +{ + checkParams({"hashes"}); + + const QStringList hashes = params()["hashes"].split('|'); + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->resume(); }); +} + +void TorrentsController::filePrioAction() +{ + checkParams({"hash", "id", "priority"}); + + const QString hash = params()["hash"]; + int fileID = params()["id"].toInt(); + int priority = params()["priority"].toInt(); + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + + if (torrent && torrent->hasMetadata()) + torrent->setFilePriority(fileID, priority); +} + +void TorrentsController::uploadLimitAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + QVariantMap map; + foreach (const QString &hash, hashes) { + int limit = -1; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) + limit = torrent->uploadLimit(); + map[hash] = limit; + } + + setResult(QJsonObject::fromVariantMap(map)); +} + +void TorrentsController::downloadLimitAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + QVariantMap map; + foreach (const QString &hash, hashes) { + int limit = -1; + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (torrent) + limit = torrent->downloadLimit(); + map[hash] = limit; + } + + setResult(QJsonObject::fromVariantMap(map)); +} + +void TorrentsController::setUploadLimitAction() +{ + checkParams({"hashes", "limit"}); + + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) + limit = -1; + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *torrent) { torrent->setUploadLimit(limit); }); +} + +void TorrentsController::setDownloadLimitAction() +{ + checkParams({"hashes", "limit"}); + + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) + limit = -1; + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [limit](BitTorrent::TorrentHandle *torrent) { torrent->setDownloadLimit(limit); }); +} + +void TorrentsController::toggleSequentialDownloadAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->toggleSequentialDownload(); }); +} + +void TorrentsController::toggleFirstLastPiecePrioAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->toggleFirstLastPiecePriority(); }); +} + +void TorrentsController::setSuperSeedingAction() +{ + checkParams({"hashes", "value"}); + + const bool value {parseBool(params()["value"], false)}; + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *torrent) { torrent->setSuperSeeding(value); }); +} + +void TorrentsController::setForceStartAction() +{ + checkParams({"hashes", "value"}); + + const bool value {parseBool(params()["value"], false)}; + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [value](BitTorrent::TorrentHandle *torrent) { torrent->resume(value); }); +} + +void TorrentsController::deleteAction() +{ + checkParams({"hashes", "delete_files"}); + + const QStringList hashes {params()["hashes"].split('|')}; + const bool deleteFiles {parseBool(params()["delete_files"], false)}; + applyToTorrents(hashes, [deleteFiles](BitTorrent::TorrentHandle *torrent) + { + BitTorrent::Session::instance()->deleteTorrent(torrent->hash(), deleteFiles); + }); +} + +void TorrentsController::increasePrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->increaseTorrentsPriority(hashes); +} + +void TorrentsController::decreasePrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->decreaseTorrentsPriority(hashes); +} + +void TorrentsController::topPrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->topTorrentsPriority(hashes); +} + +void TorrentsController::bottomPrioAction() +{ + checkParams({"hashes"}); + + if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) + throw APIError(APIErrorType::Conflict, tr("Torrent queueing must be enabled")); + + const QStringList hashes {params()["hashes"].split('|')}; + BitTorrent::Session::instance()->bottomTorrentsPriority(hashes); +} + +void TorrentsController::setLocationAction() +{ + checkParams({"hashes", "location"}); + + const QStringList hashes {params()["hashes"].split("|")}; + const QString newLocation {params()["location"].trimmed()}; + + // check if the location exists + if (newLocation.isEmpty() || !QDir(newLocation).exists()) + return; + + applyToTorrents(hashes, [newLocation](BitTorrent::TorrentHandle *torrent) + { + LogMsg(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"") + .arg(torrent->name()).arg(torrent->savePath()).arg(newLocation)); + torrent->move(Utils::Fs::expandPathAbs(newLocation)); + }); +} + +void TorrentsController::renameAction() +{ + checkParams({"hash", "name"}); + + const QString hash = params()["hash"]; + QString name = params()["name"].trimmed(); + + if (name.isEmpty()) + throw APIError(APIErrorType::Conflict, tr("Incorrect torrent name")); + + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) + throw APIError(APIErrorType::NotFound); + + name.replace(QRegularExpression("\r?\n|\r"), " "); + qDebug() << "Renaming" << torrent->name() << "to" << name; + torrent->setName(name); +} + +void TorrentsController::setAutoManagementAction() +{ + checkParams({"hashes", "enable"}); + + const QStringList hashes {params()["hashes"].split('|')}; + const bool isEnabled {parseBool(params()["enable"], false)}; + + applyToTorrents(hashes, [isEnabled](BitTorrent::TorrentHandle *torrent) + { + torrent->setAutoTMMEnabled(isEnabled); + }); +} + +void TorrentsController::recheckAction() +{ + checkParams({"hashes"}); + + const QStringList hashes {params()["hashes"].split('|')}; + applyToTorrents(hashes, [](BitTorrent::TorrentHandle *torrent) { torrent->forceRecheck(); }); +} + +void TorrentsController::setCategoryAction() +{ + checkParams({"hashes", "category"}); + + const QStringList hashes {params()["hashes"].split('|')}; + const QString category {params()["category"].trimmed()}; + applyToTorrents(hashes, [category](BitTorrent::TorrentHandle *torrent) + { + if (!torrent->setCategory(category)) + throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); + }); +} + +void TorrentsController::createCategoryAction() +{ + checkParams({"category"}); + + const QString category {params()["category"].trimmed()}; + if (!BitTorrent::Session::isValidCategoryName(category) && !category.isEmpty()) + throw APIError(APIErrorType::Conflict, tr("Incorrect category name")); + + BitTorrent::Session::instance()->addCategory(category); +} + +void TorrentsController::removeCategoriesAction() +{ + checkParams({"categories"}); + + const QStringList categories {params()["categories"].split('\n')}; + for (const QString &category : categories) + BitTorrent::Session::instance()->removeCategory(category); +} diff --git a/src/webui/api/torrentscontroller.h b/src/webui/api/torrentscontroller.h new file mode 100644 index 000000000..0b1814ef2 --- /dev/null +++ b/src/webui/api/torrentscontroller.h @@ -0,0 +1,74 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include "apicontroller.h" + +class TorrentsController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(TorrentsController) + +public: + using APIController::APIController; + +private slots: + void infoAction(); + void propertiesAction(); + void trackersAction(); + void webseedsAction(); + void filesAction(); + void pieceHashesAction(); + void pieceStatesAction(); + void resumeAction(); + void pauseAction(); + void recheckAction(); + void renameAction(); + void setCategoryAction(); + void createCategoryAction(); + void removeCategoriesAction(); + void addAction(); + void deleteAction(); + void addTrackersAction(); + void filePrioAction(); + void uploadLimitAction(); + void downloadLimitAction(); + void setUploadLimitAction(); + void setDownloadLimitAction(); + void increasePrioAction(); + void decreasePrioAction(); + void topPrioAction(); + void bottomPrioAction(); + void setLocationAction(); + void setAutoManagementAction(); + void setSuperSeedingAction(); + void setForceStartAction(); + void toggleSequentialDownloadAction(); + void toggleFirstLastPiecePrioAction(); +}; diff --git a/src/webui/api/transfercontroller.cpp b/src/webui/api/transfercontroller.cpp new file mode 100644 index 000000000..d623ad79b --- /dev/null +++ b/src/webui/api/transfercontroller.cpp @@ -0,0 +1,113 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#include "transfercontroller.h" + +#include + +#include "base/bittorrent/session.h" + +const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; +const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; +const char KEY_TRANSFER_DLRATELIMIT[] = "dl_rate_limit"; +const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; +const char KEY_TRANSFER_UPDATA[] = "up_info_data"; +const char KEY_TRANSFER_UPRATELIMIT[] = "up_rate_limit"; +const char KEY_TRANSFER_DHT_NODES[] = "dht_nodes"; +const char KEY_TRANSFER_CONNECTION_STATUS[] = "connection_status"; + +// Returns the global transfer information in JSON format. +// The return value is a JSON-formatted dictionary. +// The dictionary keys are: +// - "dl_info_speed": Global download rate +// - "dl_info_data": Data downloaded this session +// - "up_info_speed": Global upload rate +// - "up_info_data": Data uploaded this session +// - "dl_rate_limit": Download rate limit +// - "up_rate_limit": Upload rate limit +// - "dht_nodes": DHT nodes connected to +// - "connection_status": Connection status +void TransferController::infoAction() +{ + const BitTorrent::SessionStatus &sessionStatus = BitTorrent::Session::instance()->status(); + + QJsonObject dict; + + dict[KEY_TRANSFER_DLSPEED] = static_cast(sessionStatus.payloadDownloadRate); + dict[KEY_TRANSFER_DLDATA] = static_cast(sessionStatus.totalPayloadDownload); + dict[KEY_TRANSFER_UPSPEED] = static_cast(sessionStatus.payloadUploadRate); + dict[KEY_TRANSFER_UPDATA] = static_cast(sessionStatus.totalPayloadUpload); + dict[KEY_TRANSFER_DLRATELIMIT] = BitTorrent::Session::instance()->downloadSpeedLimit(); + dict[KEY_TRANSFER_UPRATELIMIT] = BitTorrent::Session::instance()->uploadSpeedLimit(); + dict[KEY_TRANSFER_DHT_NODES] = static_cast(sessionStatus.dhtNodes); + if (!BitTorrent::Session::instance()->isListening()) + dict[KEY_TRANSFER_CONNECTION_STATUS] = QLatin1String("disconnected"); + else + dict[KEY_TRANSFER_CONNECTION_STATUS] = QLatin1String(sessionStatus.hasIncomingConnections ? "connected" : "firewalled"); + + setResult(dict); +} + +void TransferController::uploadLimitAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->uploadSpeedLimit())); +} + +void TransferController::downloadLimitAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->downloadSpeedLimit())); +} + +void TransferController::setUploadLimitAction() +{ + checkParams({"limit"}); + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) limit = -1; + + BitTorrent::Session::instance()->setUploadSpeedLimit(limit); +} + +void TransferController::setDownloadLimitAction() +{ + checkParams({"limit"}); + qlonglong limit = params()["limit"].toLongLong(); + if (limit == 0) limit = -1; + + BitTorrent::Session::instance()->setDownloadSpeedLimit(limit); +} + +void TransferController::toggleSpeedLimitsModeAction() +{ + BitTorrent::Session *const session = BitTorrent::Session::instance(); + session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled()); +} + +void TransferController::speedLimitsModeAction() +{ + setResult(QString::number(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled())); +} diff --git a/src/webui/api/transfercontroller.h b/src/webui/api/transfercontroller.h new file mode 100644 index 000000000..c6ffe713e --- /dev/null +++ b/src/webui/api/transfercontroller.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Vladimir Golovnev + * + * 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. + */ + +#pragma once + +#include "apicontroller.h" + +class TransferController : public APIController +{ + Q_OBJECT + Q_DISABLE_COPY(TransferController) + +public: + using APIController::APIController; + +private slots: + void infoAction(); + void speedLimitsModeAction(); + void toggleSpeedLimitsModeAction(); + void uploadLimitAction(); + void downloadLimitAction(); + void setUploadLimitAction(); + void setDownloadLimitAction(); +}; diff --git a/src/webui/btjson.cpp b/src/webui/btjson.cpp deleted file mode 100644 index c6beb383e..000000000 --- a/src/webui/btjson.cpp +++ /dev/null @@ -1,1134 +0,0 @@ -/* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2012, 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 "btjson.h" - -#include -#include -#include - -#include - -#include "base/bittorrent/cachestatus.h" -#include "base/bittorrent/session.h" -#include "base/bittorrent/sessionstatus.h" -#include "base/bittorrent/peerinfo.h" -#include "base/bittorrent/torrenthandle.h" -#include "base/bittorrent/trackerentry.h" -#include "base/logger.h" -#include "base/net/geoipmanager.h" -#include "base/preferences.h" -#include "base/torrentfilter.h" -#include "base/utils/fs.h" -#include "base/utils/misc.h" -#include "base/utils/string.h" -#include "jsonutils.h" - -#define CACHED_VARIABLE(VARTYPE, VAR, DUR) \ - static VARTYPE VAR; \ - static QElapsedTimer cacheTimer; \ - static bool initialized = false; \ - if (initialized && !cacheTimer.hasExpired(DUR)) \ - return json::toJson(VAR); \ - initialized = true; \ - cacheTimer.start(); \ - VAR = VARTYPE() - -#define CACHED_VARIABLE_FOR_HASH(VARTYPE, VAR, DUR, HASH) \ - static VARTYPE VAR; \ - static QString prev_hash; \ - static QElapsedTimer cacheTimer; \ - if (prev_hash == HASH && !cacheTimer.hasExpired(DUR)) \ - return json::toJson(VAR); \ - prev_hash = HASH; \ - cacheTimer.start(); \ - VAR = VARTYPE() - - -// Numerical constants -static const int CACHE_DURATION_MS = 1500; // 1500ms - -// Torrent keys -static const char KEY_TORRENT_HASH[] = "hash"; -static const char KEY_TORRENT_NAME[] = "name"; -static const char KEY_TORRENT_MAGNET_URI[] = "magnet_uri"; -static const char KEY_TORRENT_SIZE[] = "size"; -static const char KEY_TORRENT_PROGRESS[] = "progress"; -static const char KEY_TORRENT_DLSPEED[] = "dlspeed"; -static const char KEY_TORRENT_UPSPEED[] = "upspeed"; -static const char KEY_TORRENT_PRIORITY[] = "priority"; -static const char KEY_TORRENT_SEEDS[] = "num_seeds"; -static const char KEY_TORRENT_NUM_COMPLETE[] = "num_complete"; -static const char KEY_TORRENT_LEECHS[] = "num_leechs"; -static const char KEY_TORRENT_NUM_INCOMPLETE[] = "num_incomplete"; -static const char KEY_TORRENT_RATIO[] = "ratio"; -static const char KEY_TORRENT_ETA[] = "eta"; -static const char KEY_TORRENT_STATE[] = "state"; -static const char KEY_TORRENT_SEQUENTIAL_DOWNLOAD[] = "seq_dl"; -static const char KEY_TORRENT_FIRST_LAST_PIECE_PRIO[] = "f_l_piece_prio"; -static const char KEY_TORRENT_CATEGORY[] = "category"; -static const char KEY_TORRENT_TAGS[] = "tags"; -static const char KEY_TORRENT_SUPER_SEEDING[] = "super_seeding"; -static const char KEY_TORRENT_FORCE_START[] = "force_start"; -static const char KEY_TORRENT_SAVE_PATH[] = "save_path"; -static const char KEY_TORRENT_ADDED_ON[] = "added_on"; -static const char KEY_TORRENT_COMPLETION_ON[] = "completion_on"; -static const char KEY_TORRENT_TRACKER[] = "tracker"; -static const char KEY_TORRENT_DL_LIMIT[] = "dl_limit"; -static const char KEY_TORRENT_UP_LIMIT[] = "up_limit"; -static const char KEY_TORRENT_AMOUNT_DOWNLOADED[] = "downloaded"; -static const char KEY_TORRENT_AMOUNT_UPLOADED[] = "uploaded"; -static const char KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION[] = "downloaded_session"; -static const char KEY_TORRENT_AMOUNT_UPLOADED_SESSION[] = "uploaded_session"; -static const char KEY_TORRENT_AMOUNT_LEFT[] = "amount_left"; -static const char KEY_TORRENT_AMOUNT_COMPLETED[] = "completed"; -static const char KEY_TORRENT_RATIO_LIMIT[] = "ratio_limit"; -static const char KEY_TORRENT_LAST_SEEN_COMPLETE_TIME[] = "seen_complete"; -static const char KEY_TORRENT_LAST_ACTIVITY_TIME[] = "last_activity"; -static const char KEY_TORRENT_TOTAL_SIZE[] = "total_size"; -static const char KEY_TORRENT_AUTO_TORRENT_MANAGEMENT[] = "auto_tmm"; -static const char KEY_TORRENT_TIME_ACTIVE[] = "time_active"; - -// Peer keys -static const char KEY_PEER_IP[] = "ip"; -static const char KEY_PEER_PORT[] = "port"; -static const char KEY_PEER_COUNTRY_CODE[] = "country_code"; -static const char KEY_PEER_COUNTRY[] = "country"; -static const char KEY_PEER_CLIENT[] = "client"; -static const char KEY_PEER_PROGRESS[] = "progress"; -static const char KEY_PEER_DOWN_SPEED[] = "dl_speed"; -static const char KEY_PEER_UP_SPEED[] = "up_speed"; -static const char KEY_PEER_TOT_DOWN[] = "downloaded"; -static const char KEY_PEER_TOT_UP[] = "uploaded"; -static const char KEY_PEER_CONNECTION_TYPE[] = "connection"; -static const char KEY_PEER_FLAGS[] = "flags"; -static const char KEY_PEER_FLAGS_DESCRIPTION[] = "flags_desc"; -static const char KEY_PEER_RELEVANCE[] = "relevance"; -static const char KEY_PEER_FILES[] = "files"; - -// Tracker keys -static const char KEY_TRACKER_URL[] = "url"; -static const char KEY_TRACKER_STATUS[] = "status"; -static const char KEY_TRACKER_MSG[] = "msg"; -static const char KEY_TRACKER_PEERS[] = "num_peers"; - -// Web seed keys -static const char KEY_WEBSEED_URL[] = "url"; - -// Torrent keys (Properties) -static const char KEY_PROP_TIME_ELAPSED[] = "time_elapsed"; -static const char KEY_PROP_SEEDING_TIME[] = "seeding_time"; -static const char KEY_PROP_ETA[] = "eta"; -static const char KEY_PROP_CONNECT_COUNT[] = "nb_connections"; -static const char KEY_PROP_CONNECT_COUNT_LIMIT[] = "nb_connections_limit"; -static const char KEY_PROP_DOWNLOADED[] = "total_downloaded"; -static const char KEY_PROP_DOWNLOADED_SESSION[] = "total_downloaded_session"; -static const char KEY_PROP_UPLOADED[] = "total_uploaded"; -static const char KEY_PROP_UPLOADED_SESSION[] = "total_uploaded_session"; -static const char KEY_PROP_DL_SPEED[] = "dl_speed"; -static const char KEY_PROP_DL_SPEED_AVG[] = "dl_speed_avg"; -static const char KEY_PROP_UP_SPEED[] = "up_speed"; -static const char KEY_PROP_UP_SPEED_AVG[] = "up_speed_avg"; -static const char KEY_PROP_DL_LIMIT[] = "dl_limit"; -static const char KEY_PROP_UP_LIMIT[] = "up_limit"; -static const char KEY_PROP_WASTED[] = "total_wasted"; -static const char KEY_PROP_SEEDS[] = "seeds"; -static const char KEY_PROP_SEEDS_TOTAL[] = "seeds_total"; -static const char KEY_PROP_PEERS[] = "peers"; -static const char KEY_PROP_PEERS_TOTAL[] = "peers_total"; -static const char KEY_PROP_RATIO[] = "share_ratio"; -static const char KEY_PROP_REANNOUNCE[] = "reannounce"; -static const char KEY_PROP_TOTAL_SIZE[] = "total_size"; -static const char KEY_PROP_PIECES_NUM[] = "pieces_num"; -static const char KEY_PROP_PIECE_SIZE[] = "piece_size"; -static const char KEY_PROP_PIECES_HAVE[] = "pieces_have"; -static const char KEY_PROP_CREATED_BY[] = "created_by"; -static const char KEY_PROP_LAST_SEEN[] = "last_seen"; -static const char KEY_PROP_ADDITION_DATE[] = "addition_date"; -static const char KEY_PROP_COMPLETION_DATE[] = "completion_date"; -static const char KEY_PROP_CREATION_DATE[] = "creation_date"; -static const char KEY_PROP_SAVE_PATH[] = "save_path"; -static const char KEY_PROP_COMMENT[] = "comment"; - -// File keys -static const char KEY_FILE_NAME[] = "name"; -static const char KEY_FILE_SIZE[] = "size"; -static const char KEY_FILE_PROGRESS[] = "progress"; -static const char KEY_FILE_PRIORITY[] = "priority"; -static const char KEY_FILE_IS_SEED[] = "is_seed"; -static const char KEY_FILE_PIECE_RANGE[] = "piece_range"; -static const char KEY_FILE_AVAILABILITY[] = "availability"; - -// TransferInfo keys -static const char KEY_TRANSFER_DLSPEED[] = "dl_info_speed"; -static const char KEY_TRANSFER_DLDATA[] = "dl_info_data"; -static const char KEY_TRANSFER_DLRATELIMIT[] = "dl_rate_limit"; -static const char KEY_TRANSFER_UPSPEED[] = "up_info_speed"; -static const char KEY_TRANSFER_UPDATA[] = "up_info_data"; -static const char KEY_TRANSFER_UPRATELIMIT[] = "up_rate_limit"; -static const char KEY_TRANSFER_DHT_NODES[] = "dht_nodes"; -static const char KEY_TRANSFER_CONNECTION_STATUS[] = "connection_status"; - -// Statistics keys -static const char KEY_TRANSFER_ALLTIME_DL[] = "alltime_dl"; -static const char KEY_TRANSFER_ALLTIME_UL[] = "alltime_ul"; -static const char KEY_TRANSFER_TOTAL_WASTE_SESSION[] = "total_wasted_session"; -static const char KEY_TRANSFER_GLOBAL_RATIO[] = "global_ratio"; -static const char KEY_TRANSFER_TOTAL_PEER_CONNECTIONS[] = "total_peer_connections"; -static const char KEY_TRANSFER_READ_CACHE_HITS[] = "read_cache_hits"; -static const char KEY_TRANSFER_TOTAL_BUFFERS_SIZE[] = "total_buffers_size"; -static const char KEY_TRANSFER_WRITE_CACHE_OVERLOAD[] = "write_cache_overload"; -static const char KEY_TRANSFER_READ_CACHE_OVERLOAD[] = "read_cache_overload"; -static const char KEY_TRANSFER_QUEUED_IO_JOBS[] = "queued_io_jobs"; -static const char KEY_TRANSFER_AVERAGE_TIME_QUEUE[] = "average_time_queue"; -static const char KEY_TRANSFER_TOTAL_QUEUED_SIZE[] = "total_queued_size"; - -// Sync main data keys -static const char KEY_SYNC_MAINDATA_QUEUEING[] = "queueing"; -static const char KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS[] = "use_alt_speed_limits"; -static const char KEY_SYNC_MAINDATA_REFRESH_INTERVAL[] = "refresh_interval"; - -// Sync torrent peers keys -static const char KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS[] = "show_flags"; - -static const char KEY_FULL_UPDATE[] = "full_update"; -static const char KEY_RESPONSE_ID[] = "rid"; -static const char KEY_SUFFIX_REMOVED[] = "_removed"; - -// Log keys -static const char KEY_LOG_ID[] = "id"; -static const char KEY_LOG_TIMESTAMP[] = "timestamp"; -static const char KEY_LOG_MSG_TYPE[] = "type"; -static const char KEY_LOG_MSG_MESSAGE[] = "message"; -static const char KEY_LOG_PEER_IP[] = "ip"; -static const char KEY_LOG_PEER_BLOCKED[] = "blocked"; -static const char KEY_LOG_PEER_REASON[] = "reason"; - -namespace -{ - QString torrentStateToString(const BitTorrent::TorrentState state) - { - switch (state) { - case BitTorrent::TorrentState::Error: - return QLatin1String("error"); - case BitTorrent::TorrentState::MissingFiles: - return QLatin1String("missingFiles"); - case BitTorrent::TorrentState::Uploading: - return QLatin1String("uploading"); - case BitTorrent::TorrentState::PausedUploading: - return QLatin1String("pausedUP"); - case BitTorrent::TorrentState::QueuedUploading: - return QLatin1String("queuedUP"); - case BitTorrent::TorrentState::StalledUploading: - return QLatin1String("stalledUP"); - case BitTorrent::TorrentState::CheckingUploading: - return QLatin1String("checkingUP"); - case BitTorrent::TorrentState::ForcedUploading: - return QLatin1String("forcedUP"); - case BitTorrent::TorrentState::Allocating: - return QLatin1String("allocating"); - case BitTorrent::TorrentState::Downloading: - return QLatin1String("downloading"); - case BitTorrent::TorrentState::DownloadingMetadata: - return QLatin1String("metaDL"); - case BitTorrent::TorrentState::PausedDownloading: - return QLatin1String("pausedDL"); - case BitTorrent::TorrentState::QueuedDownloading: - return QLatin1String("queuedDL"); - case BitTorrent::TorrentState::StalledDownloading: - return QLatin1String("stalledDL"); - case BitTorrent::TorrentState::CheckingDownloading: - return QLatin1String("checkingDL"); - case BitTorrent::TorrentState::ForcedDownloading: - return QLatin1String("forcedDL"); -#if LIBTORRENT_VERSION_NUM < 10100 - case BitTorrent::TorrentState::QueuedForChecking: - return QLatin1String("queuedForChecking"); -#endif - case BitTorrent::TorrentState::CheckingResumeData: - return QLatin1String("checkingResumeData"); - default: - return QLatin1String("unknown"); - } - } - - class QTorrentCompare - { - public: - QTorrentCompare(const QString &key, bool greaterThan = false) - : m_key(key) - , m_greaterThan(greaterThan) - { - } - - bool operator()(const QVariant &torrent1, const QVariant &torrent2) - { - return m_greaterThan - ? (torrent1.toMap().value(m_key) > torrent2.toMap().value(m_key)) - : (torrent1.toMap().value(m_key) < torrent2.toMap().value(m_key)); - } - - private: - const QString m_key; - const bool m_greaterThan; - }; - - QVariantMap getTranserInfoMap(); - QVariantMap toMap(BitTorrent::TorrentHandle *const torrent); - void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData); - void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems); - void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems); - QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData); - - QVariantMap getTranserInfoMap() - { - QVariantMap map; - const BitTorrent::SessionStatus &sessionStatus = BitTorrent::Session::instance()->status(); - const BitTorrent::CacheStatus &cacheStatus = BitTorrent::Session::instance()->cacheStatus(); - map[KEY_TRANSFER_DLSPEED] = sessionStatus.payloadDownloadRate; - map[KEY_TRANSFER_DLDATA] = sessionStatus.totalPayloadDownload; - map[KEY_TRANSFER_UPSPEED] = sessionStatus.payloadUploadRate; - map[KEY_TRANSFER_UPDATA] = sessionStatus.totalPayloadUpload; - map[KEY_TRANSFER_DLRATELIMIT] = BitTorrent::Session::instance()->downloadSpeedLimit(); - map[KEY_TRANSFER_UPRATELIMIT] = BitTorrent::Session::instance()->uploadSpeedLimit(); - - quint64 atd = BitTorrent::Session::instance()->getAlltimeDL(); - quint64 atu = BitTorrent::Session::instance()->getAlltimeUL(); - map[KEY_TRANSFER_ALLTIME_DL] = atd; - map[KEY_TRANSFER_ALLTIME_UL] = atu; - map[KEY_TRANSFER_TOTAL_WASTE_SESSION] = sessionStatus.totalWasted; - map[KEY_TRANSFER_GLOBAL_RATIO] = ( atd > 0 && atu > 0 ) ? Utils::String::fromDouble(static_cast(atu) / atd, 2) : "-"; - map[KEY_TRANSFER_TOTAL_PEER_CONNECTIONS] = sessionStatus.peersCount; - - qreal readRatio = cacheStatus.readRatio; - map[KEY_TRANSFER_READ_CACHE_HITS] = (readRatio >= 0) ? Utils::String::fromDouble(100 * readRatio, 2) : "-"; - map[KEY_TRANSFER_TOTAL_BUFFERS_SIZE] = cacheStatus.totalUsedBuffers * 16 * 1024; - - // num_peers is not reliable (adds up peers, which didn't even overcome tcp handshake) - quint32 peers = 0; - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - peers += torrent->peersCount(); - map[KEY_TRANSFER_WRITE_CACHE_OVERLOAD] = ((sessionStatus.diskWriteQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskWriteQueue) / peers, 2) : "0"; - map[KEY_TRANSFER_READ_CACHE_OVERLOAD] = ((sessionStatus.diskReadQueue > 0) && (peers > 0)) ? Utils::String::fromDouble((100. * sessionStatus.diskReadQueue) / peers, 2) : "0"; - - map[KEY_TRANSFER_QUEUED_IO_JOBS] = cacheStatus.jobQueueLength; - map[KEY_TRANSFER_AVERAGE_TIME_QUEUE] = cacheStatus.averageJobTime; - map[KEY_TRANSFER_TOTAL_QUEUED_SIZE] = cacheStatus.queuedBytes; - - map[KEY_TRANSFER_DHT_NODES] = sessionStatus.dhtNodes; - if (!BitTorrent::Session::instance()->isListening()) - map[KEY_TRANSFER_CONNECTION_STATUS] = "disconnected"; - else - map[KEY_TRANSFER_CONNECTION_STATUS] = sessionStatus.hasIncomingConnections ? "connected" : "firewalled"; - return map; - } - - QVariantMap toMap(BitTorrent::TorrentHandle *const torrent) - { - QVariantMap ret; - ret[KEY_TORRENT_HASH] = QString(torrent->hash()); - ret[KEY_TORRENT_NAME] = torrent->name(); - ret[KEY_TORRENT_MAGNET_URI] = torrent->toMagnetUri(); - ret[KEY_TORRENT_SIZE] = torrent->wantedSize(); - ret[KEY_TORRENT_PROGRESS] = torrent->progress(); - ret[KEY_TORRENT_DLSPEED] = torrent->downloadPayloadRate(); - ret[KEY_TORRENT_UPSPEED] = torrent->uploadPayloadRate(); - ret[KEY_TORRENT_PRIORITY] = torrent->queuePosition(); - ret[KEY_TORRENT_SEEDS] = torrent->seedsCount(); - ret[KEY_TORRENT_NUM_COMPLETE] = torrent->totalSeedsCount(); - ret[KEY_TORRENT_LEECHS] = torrent->leechsCount(); - ret[KEY_TORRENT_NUM_INCOMPLETE] = torrent->totalLeechersCount(); - const qreal ratio = torrent->realRatio(); - ret[KEY_TORRENT_RATIO] = (ratio > BitTorrent::TorrentHandle::MAX_RATIO) ? -1 : ratio; - ret[KEY_TORRENT_STATE] = torrentStateToString(torrent->state()); - ret[KEY_TORRENT_ETA] = torrent->eta(); - ret[KEY_TORRENT_SEQUENTIAL_DOWNLOAD] = torrent->isSequentialDownload(); - if (torrent->hasMetadata()) - ret[KEY_TORRENT_FIRST_LAST_PIECE_PRIO] = torrent->hasFirstLastPiecePriority(); - ret[KEY_TORRENT_CATEGORY] = torrent->category(); - ret[KEY_TORRENT_TAGS] = torrent->tags().toList().join(", "); - ret[KEY_TORRENT_SUPER_SEEDING] = torrent->superSeeding(); - ret[KEY_TORRENT_FORCE_START] = torrent->isForced(); - ret[KEY_TORRENT_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); - ret[KEY_TORRENT_ADDED_ON] = torrent->addedTime().toTime_t(); - ret[KEY_TORRENT_COMPLETION_ON] = torrent->completedTime().toTime_t(); - ret[KEY_TORRENT_TRACKER] = torrent->currentTracker(); - ret[KEY_TORRENT_DL_LIMIT] = torrent->downloadLimit(); - ret[KEY_TORRENT_UP_LIMIT] = torrent->uploadLimit(); - ret[KEY_TORRENT_AMOUNT_DOWNLOADED] = torrent->totalDownload(); - ret[KEY_TORRENT_AMOUNT_UPLOADED] = torrent->totalUpload(); - ret[KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); - ret[KEY_TORRENT_AMOUNT_UPLOADED_SESSION] = torrent->totalPayloadUpload(); - ret[KEY_TORRENT_AMOUNT_LEFT] = torrent->incompletedSize(); - ret[KEY_TORRENT_AMOUNT_COMPLETED] = torrent->completedSize(); - ret[KEY_TORRENT_RATIO_LIMIT] = torrent->maxRatio(); - ret[KEY_TORRENT_LAST_SEEN_COMPLETE_TIME] = torrent->lastSeenComplete().toTime_t(); - ret[KEY_TORRENT_AUTO_TORRENT_MANAGEMENT] = torrent->isAutoTMMEnabled(); - ret[KEY_TORRENT_TIME_ACTIVE] = torrent->activeTime(); - - if (torrent->isPaused() || torrent->isChecking()) - ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = 0; - else { - QDateTime dt = QDateTime::currentDateTime(); - dt = dt.addSecs(-torrent->timeSinceActivity()); - ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = dt.toTime_t(); - } - - ret[KEY_TORRENT_TOTAL_SIZE] = torrent->totalSize(); - - return ret; - } - - // Compare two structures (prevData, data) and calculate difference (syncData). - // Structures encoded as map. - void processMap(const QVariantMap &prevData, const QVariantMap &data, QVariantMap &syncData) - { - // initialize output variable - syncData.clear(); - - QVariantList removedItems; - foreach (QString key, data.keys()) { - removedItems.clear(); - - switch (static_cast(data[key].type())) { - case QMetaType::QVariantMap: { - QVariantMap map; - processMap(prevData[key].toMap(), data[key].toMap(), map); - if (!map.isEmpty()) - syncData[key] = map; - } - break; - case QMetaType::QVariantHash: { - QVariantMap map; - processHash(prevData[key].toHash(), data[key].toHash(), map, removedItems); - if (!map.isEmpty()) - syncData[key] = map; - if (!removedItems.isEmpty()) - syncData[key + KEY_SUFFIX_REMOVED] = removedItems; - } - break; - case QMetaType::QVariantList: { - QVariantList list; - processList(prevData[key].toList(), data[key].toList(), list, removedItems); - if (!list.isEmpty()) - syncData[key] = list; - if (!removedItems.isEmpty()) - syncData[key + KEY_SUFFIX_REMOVED] = removedItems; - } - break; - case QMetaType::QString: - case QMetaType::LongLong: - case QMetaType::Float: - case QMetaType::Int: - case QMetaType::Bool: - case QMetaType::Double: - case QMetaType::ULongLong: - case QMetaType::UInt: - case QMetaType::QDateTime: - if (prevData[key] != data[key]) - syncData[key] = data[key]; - break; - default: - Q_ASSERT_X(false, "processMap" - , QString("Unexpected type: %1") - .arg(QMetaType::typeName(static_cast(data[key].type()))) - .toUtf8().constData()); - } - } - } - - // Compare two lists of structures (prevData, data) and calculate difference (syncData, removedItems). - // Structures encoded as map. - // Lists are encoded as hash table (indexed by structure key value) to improve ease of searching for removed items. - void processHash(QVariantHash prevData, const QVariantHash &data, QVariantMap &syncData, QVariantList &removedItems) - { - // initialize output variables - syncData.clear(); - removedItems.clear(); - - if (prevData.isEmpty()) { - // If list was empty before, then difference is a whole new list. - foreach (QString key, data.keys()) - syncData[key] = data[key]; - } - else { - foreach (QString key, data.keys()) { - switch (data[key].type()) { - case QVariant::Map: - if (!prevData.contains(key)) { - // new list item found - append it to syncData - syncData[key] = data[key]; - } - else { - QVariantMap map; - processMap(prevData[key].toMap(), data[key].toMap(), map); - // existing list item found - remove it from prevData - prevData.remove(key); - if (!map.isEmpty()) - // changed list item found - append its changes to syncData - syncData[key] = map; - } - break; - default: - Q_ASSERT(0); - } - } - - if (!prevData.isEmpty()) { - // prevData contains only items that are missing now - - // put them in removedItems - foreach (QString s, prevData.keys()) - removedItems << s; - } - } - } - - // Compare two lists of simple value (prevData, data) and calculate difference (syncData, removedItems). - void processList(QVariantList prevData, const QVariantList &data, QVariantList &syncData, QVariantList &removedItems) - { - // initialize output variables - syncData.clear(); - removedItems.clear(); - - if (prevData.isEmpty()) { - // If list was empty before, then difference is a whole new list. - syncData = data; - } - else { - foreach (QVariant item, data) { - if (!prevData.contains(item)) - // new list item found - append it to syncData - syncData.append(item); - else - // unchanged list item found - remove it from prevData - prevData.removeOne(item); - } - - if (!prevData.isEmpty()) - // prevData contains only items that are missing now - - // put them in removedItems - removedItems = prevData; - } - } - - QVariantMap generateSyncData(int acceptedResponseId, const QVariantMap &data, QVariantMap &lastAcceptedData, QVariantMap &lastData) - { - QVariantMap syncData; - bool fullUpdate = true; - int lastResponseId = 0; - if (acceptedResponseId > 0) { - lastResponseId = lastData[KEY_RESPONSE_ID].toInt(); - - if (lastResponseId == acceptedResponseId) - lastAcceptedData = lastData; - - int lastAcceptedResponseId = lastAcceptedData[KEY_RESPONSE_ID].toInt(); - - if (lastAcceptedResponseId == acceptedResponseId) { - processMap(lastAcceptedData, data, syncData); - fullUpdate = false; - } - } - - if (fullUpdate) { - lastAcceptedData.clear(); - syncData = data; - syncData[KEY_FULL_UPDATE] = true; - } - - lastResponseId = lastResponseId % 1000000 + 1; // cycle between 1 and 1000000 - lastData = data; - lastData[KEY_RESPONSE_ID] = lastResponseId; - syncData[KEY_RESPONSE_ID] = lastResponseId; - - return syncData; - } -} - -/** - * Returns all the torrents in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "hash": Torrent hash - * - "name": Torrent name - * - "size": Torrent size - * - "progress: Torrent progress - * - "dlspeed": Torrent download speed - * - "upspeed": Torrent upload speed - * - "priority": Torrent priority (-1 if queuing is disabled) - * - "num_seeds": Torrent seeds connected to - * - "num_complete": Torrent seeds in the swarm - * - "num_leechs": Torrent leechers connected to - * - "num_incomplete": Torrent leechers in the swarm - * - "ratio": Torrent share ratio - * - "eta": Torrent ETA - * - "state": Torrent state - * - "seq_dl": Torrent sequential download state - * - "f_l_piece_prio": Torrent first last piece priority state - * - "force_start": Torrent force start state - * - "category": Torrent category - */ -QByteArray btjson::getTorrents(QString filter, QString category, - QString sortedColumn, bool reverse, int limit, int offset) -{ - QVariantList torrentList; - TorrentFilter torrentFilter(filter, TorrentFilter::AnyHash, category); - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) { - if (torrentFilter.match(torrent)) - torrentList.append(toMap(torrent)); - } - - std::sort(torrentList.begin(), torrentList.end(), QTorrentCompare(sortedColumn, reverse)); - int size = torrentList.size(); - // normalize offset - if (offset < 0) - offset = size + offset; - if ((offset >= size) || (offset < 0)) - offset = 0; - // normalize limit - if (limit <= 0) - limit = -1; // unlimited - - if ((limit > 0) || (offset > 0)) - return json::toJson(torrentList.mid(offset, limit)); - else - return json::toJson(torrentList); -} - -/** - * The function returns the changed data from the server to synchronize with the web client. - * Return value is map in JSON format. - * Map contain the key: - * - "Rid": ID response - * Map can contain the keys: - * - "full_update": full data update flag - * - "torrents": dictionary contains information about torrents. - * - "torrents_removed": a list of hashes of removed torrents - * - "categories": list of categories - * - "categories_removed": list of removed categories - * - "server_state": map contains information about the state of the server - * The keys of the 'torrents' dictionary are hashes of torrents. - * Each value of the 'torrents' dictionary contains map. The map can contain following keys: - * - "name": Torrent name - * - "size": Torrent size - * - "progress: Torrent progress - * - "dlspeed": Torrent download speed - * - "upspeed": Torrent upload speed - * - "priority": Torrent priority (-1 if queuing is disabled) - * - "num_seeds": Torrent seeds connected to - * - "num_complete": Torrent seeds in the swarm - * - "num_leechs": Torrent leechers connected to - * - "num_incomplete": Torrent leechers in the swarm - * - "ratio": Torrent share ratio - * - "eta": Torrent ETA - * - "state": Torrent state - * - "seq_dl": Torrent sequential download state - * - "f_l_piece_prio": Torrent first last piece priority state - * - "completion_on": Torrent copletion time - * - "tracker": Torrent tracker - * - "dl_limit": Torrent download limit - * - "up_limit": Torrent upload limit - * - "downloaded": Amount of data downloaded - * - "uploaded": Amount of data uploaded - * - "downloaded_session": Amount of data downloaded since program open - * - "uploaded_session": Amount of data uploaded since program open - * - "amount_left": Amount of data left to download - * - "save_path": Torrent save path - * - "completed": Amount of data completed - * - "ratio_limit": Upload share ratio limit - * - "seen_complete": Indicates the time when the torrent was last seen complete/whole - * - "last_activity": Last time when a chunk was downloaded/uploaded - * - "total_size": Size including unwanted data - * Server state map may contain the following keys: - * - "connection_status": connection status - * - "dht_nodes": DHT nodes count - * - "dl_info_data": bytes downloaded - * - "dl_info_speed": download speed - * - "dl_rate_limit: download rate limit - * - "up_info_data: bytes uploaded - * - "up_info_speed: upload speed - * - "up_rate_limit: upload speed limit - * - "queueing": priority system usage flag - * - "refresh_interval": torrents table refresh interval - */ -QByteArray btjson::getSyncMainData(int acceptedResponseId, QVariantMap &lastData, QVariantMap &lastAcceptedData) -{ - QVariantMap data; - QVariantHash torrents; - - BitTorrent::Session *const session = BitTorrent::Session::instance(); - - foreach (BitTorrent::TorrentHandle *const torrent, session->torrents()) { - QVariantMap map = toMap(torrent); - map.remove(KEY_TORRENT_HASH); - - // Calculated last activity time can differ from actual value by up to 10 seconds (this is a libtorrent issue). - // So we don't need unnecessary updates of last activity time in response. - if (lastData.contains("torrents") && lastData["torrents"].toHash().contains(torrent->hash()) && - lastData["torrents"].toHash()[torrent->hash()].toMap().contains(KEY_TORRENT_LAST_ACTIVITY_TIME)) { - uint lastValue = lastData["torrents"].toHash()[torrent->hash()].toMap()[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt(); - if (qAbs(static_cast(lastValue - map[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt())) < 15) - map[KEY_TORRENT_LAST_ACTIVITY_TIME] = lastValue; - } - - torrents[torrent->hash()] = map; - } - - data["torrents"] = torrents; - - QVariantList categories; - foreach (const QString &category, session->categories().keys()) - categories << category; - - data["categories"] = categories; - - QVariantMap serverState = getTranserInfoMap(); - serverState[KEY_SYNC_MAINDATA_QUEUEING] = session->isQueueingSystemEnabled(); - serverState[KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS] = session->isAltGlobalSpeedLimitEnabled(); - serverState[KEY_SYNC_MAINDATA_REFRESH_INTERVAL] = session->refreshInterval(); - data["server_state"] = serverState; - - return json::toJson(generateSyncData(acceptedResponseId, data, lastAcceptedData, lastData)); -} - -QByteArray btjson::getSyncTorrentPeersData(int acceptedResponseId, QString hash, QVariantMap &lastData, QVariantMap &lastAcceptedData) -{ - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - QVariantMap data; - QVariantHash peers; - QList peersList = torrent->peers(); -#ifndef DISABLE_COUNTRIES_RESOLUTION - bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries(); -#else - bool resolvePeerCountries = false; -#endif - - data[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS] = resolvePeerCountries; - - foreach (const BitTorrent::PeerInfo &pi, peersList) { - if (pi.address().ip.isNull()) continue; - QVariantMap peer; -#ifndef DISABLE_COUNTRIES_RESOLUTION - if (resolvePeerCountries) { - peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower(); - peer[KEY_PEER_COUNTRY] = Net::GeoIPManager::CountryName(pi.country()); - } -#endif - peer[KEY_PEER_IP] = pi.address().ip.toString(); - peer[KEY_PEER_PORT] = pi.address().port; - peer[KEY_PEER_CLIENT] = pi.client(); - peer[KEY_PEER_PROGRESS] = pi.progress(); - peer[KEY_PEER_DOWN_SPEED] = pi.payloadDownSpeed(); - peer[KEY_PEER_UP_SPEED] = pi.payloadUpSpeed(); - peer[KEY_PEER_TOT_DOWN] = pi.totalDownload(); - peer[KEY_PEER_TOT_UP] = pi.totalUpload(); - peer[KEY_PEER_CONNECTION_TYPE] = pi.connectionType(); - peer[KEY_PEER_FLAGS] = pi.flags(); - peer[KEY_PEER_FLAGS_DESCRIPTION] = pi.flagsDescription(); - peer[KEY_PEER_RELEVANCE] = pi.relevance(); - peer[KEY_PEER_FILES] = torrent->info().filesForPiece(pi.downloadingPieceIndex()).join(QLatin1String("\n")); - - peers[pi.address().ip.toString() + ":" + QString::number(pi.address().port)] = peer; - } - - data["peers"] = peers; - - return json::toJson(generateSyncData(acceptedResponseId, data, lastAcceptedData, lastData)); -} - -/** - * Returns the trackers for a torrent in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "url": Tracker URL - * - "status": Tracker status - * - "num_peers": Tracker peer count - * - "msg": Tracker message (last) - */ -QByteArray btjson::getTrackersForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, trackerList, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - QHash trackers_data = torrent->trackerInfos(); - foreach (const BitTorrent::TrackerEntry &tracker, torrent->trackers()) { - QVariantMap trackerDict; - trackerDict[KEY_TRACKER_URL] = tracker.url(); - const BitTorrent::TrackerInfo data = trackers_data.value(tracker.url()); - QString status; - switch (tracker.status()) { - case BitTorrent::TrackerEntry::NotContacted: - status = tr("Not contacted yet"); break; - case BitTorrent::TrackerEntry::Updating: - status = tr("Updating..."); break; - case BitTorrent::TrackerEntry::Working: - status = tr("Working"); break; - case BitTorrent::TrackerEntry::NotWorking: - status = tr("Not working"); break; - } - trackerDict[KEY_TRACKER_STATUS] = status; - trackerDict[KEY_TRACKER_PEERS] = data.numPeers; - trackerDict[KEY_TRACKER_MSG] = data.lastMessage.trimmed(); - - trackerList.append(trackerDict); - } - - return json::toJson(trackerList); -} - -/** - * Returns the web seeds for a torrent in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "url": Web seed URL - */ -QByteArray btjson::getWebSeedsForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, webSeedList, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - foreach (const QUrl &webseed, torrent->urlSeeds()) { - QVariantMap webSeedDict; - webSeedDict[KEY_WEBSEED_URL] = webseed.toString(); - webSeedList.append(webSeedDict); - } - - return json::toJson(webSeedList); -} - -/** - * Returns the properties for a torrent in JSON format. - * - * The return value is a JSON-formatted dictionary. - * The dictionary keys are: - * - "time_elapsed": Torrent elapsed time - * - "seeding_time": Torrent elapsed time while complete - * - "eta": Torrent ETA - * - "nb_connections": Torrent connection count - * - "nb_connections_limit": Torrent connection count limit - * - "total_downloaded": Total data uploaded for torrent - * - "total_downloaded_session": Total data downloaded this session - * - "total_uploaded": Total data uploaded for torrent - * - "total_uploaded_session": Total data uploaded this session - * - "dl_speed": Torrent download speed - * - "dl_speed_avg": Torrent average download speed - * - "up_speed": Torrent upload speed - * - "up_speed_avg": Torrent average upload speed - * - "dl_limit": Torrent download limit - * - "up_limit": Torrent upload limit - * - "total_wasted": Total data wasted for torrent - * - "seeds": Torrent connected seeds - * - "seeds_total": Torrent total number of seeds - * - "peers": Torrent connected peers - * - "peers_total": Torrent total number of peers - * - "share_ratio": Torrent share ratio - * - "reannounce": Torrent next reannounce time - * - "total_size": Torrent total size - * - "pieces_num": Torrent pieces count - * - "piece_size": Torrent piece size - * - "pieces_have": Torrent pieces have - * - "created_by": Torrent creator - * - "last_seen": Torrent last seen complete - * - "addition_date": Torrent addition date - * - "completion_date": Torrent completion date - * - "creation_date": Torrent creation date - * - "save_path": Torrent save path - * - "comment": Torrent comment - */ -QByteArray btjson::getPropertiesForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantMap, dataDict, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - dataDict[KEY_PROP_TIME_ELAPSED] = torrent->activeTime(); - dataDict[KEY_PROP_SEEDING_TIME] = torrent->seedingTime(); - dataDict[KEY_PROP_ETA] = torrent->eta(); - dataDict[KEY_PROP_CONNECT_COUNT] = torrent->connectionsCount(); - dataDict[KEY_PROP_CONNECT_COUNT_LIMIT] = torrent->connectionsLimit(); - dataDict[KEY_PROP_DOWNLOADED] = torrent->totalDownload(); - dataDict[KEY_PROP_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); - dataDict[KEY_PROP_UPLOADED] = torrent->totalUpload(); - dataDict[KEY_PROP_UPLOADED_SESSION] = torrent->totalPayloadUpload(); - dataDict[KEY_PROP_DL_SPEED] = torrent->downloadPayloadRate(); - dataDict[KEY_PROP_DL_SPEED_AVG] = torrent->totalDownload() / (1 + torrent->activeTime() - torrent->finishedTime()); - dataDict[KEY_PROP_UP_SPEED] = torrent->uploadPayloadRate(); - dataDict[KEY_PROP_UP_SPEED_AVG] = torrent->totalUpload() / (1 + torrent->activeTime()); - dataDict[KEY_PROP_DL_LIMIT] = torrent->downloadLimit() <= 0 ? -1 : torrent->downloadLimit(); - dataDict[KEY_PROP_UP_LIMIT] = torrent->uploadLimit() <= 0 ? -1 : torrent->uploadLimit(); - dataDict[KEY_PROP_WASTED] = torrent->wastedSize(); - dataDict[KEY_PROP_SEEDS] = torrent->seedsCount(); - dataDict[KEY_PROP_SEEDS_TOTAL] = torrent->totalSeedsCount(); - dataDict[KEY_PROP_PEERS] = torrent->leechsCount(); - dataDict[KEY_PROP_PEERS_TOTAL] = torrent->totalLeechersCount(); - const qreal ratio = torrent->realRatio(); - dataDict[KEY_PROP_RATIO] = ratio > BitTorrent::TorrentHandle::MAX_RATIO ? -1 : ratio; - dataDict[KEY_PROP_REANNOUNCE] = torrent->nextAnnounce(); - dataDict[KEY_PROP_TOTAL_SIZE] = torrent->totalSize(); - dataDict[KEY_PROP_PIECES_NUM] = torrent->piecesCount(); - dataDict[KEY_PROP_PIECE_SIZE] = torrent->pieceLength(); - dataDict[KEY_PROP_PIECES_HAVE] = torrent->piecesHave(); - dataDict[KEY_PROP_CREATED_BY] = torrent->creator(); - dataDict[KEY_PROP_ADDITION_DATE] = torrent->addedTime().toTime_t(); - if (torrent->hasMetadata()) { - dataDict[KEY_PROP_LAST_SEEN] = torrent->lastSeenComplete().isValid() ? static_cast(torrent->lastSeenComplete().toTime_t()) : -1; - dataDict[KEY_PROP_COMPLETION_DATE] = torrent->completedTime().isValid() ? static_cast(torrent->completedTime().toTime_t()) : -1; - dataDict[KEY_PROP_CREATION_DATE] = torrent->creationDate().toTime_t(); - } - else { - dataDict[KEY_PROP_LAST_SEEN] = -1; - dataDict[KEY_PROP_COMPLETION_DATE] = -1; - dataDict[KEY_PROP_CREATION_DATE] = -1; - } - dataDict[KEY_PROP_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); - dataDict[KEY_PROP_COMMENT] = torrent->comment(); - - return json::toJson(dataDict); -} - -/** - * Returns the files in a torrent in JSON format. - * - * The return value is a JSON-formatted list of dictionaries. - * The dictionary keys are: - * - "name": File name - * - "size": File size - * - "progress": File progress - * - "priority": File priority - * - "is_seed": Flag indicating if torrent is seeding/complete - * - "piece_range": Piece index range, the first number is the starting piece index - * and the second number is the ending piece index (inclusive) - */ -QByteArray btjson::getFilesForTorrent(const QString& hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, fileList, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - if (!torrent->hasMetadata()) - return json::toJson(fileList); - - const QVector priorities = torrent->filePriorities(); - const QVector fp = torrent->filesProgress(); - const QVector fileAvailability = torrent->availableFileFractions(); - const BitTorrent::TorrentInfo info = torrent->info(); - for (int i = 0; i < torrent->filesCount(); ++i) { - QVariantMap fileDict; - fileDict[KEY_FILE_PROGRESS] = fp[i]; - fileDict[KEY_FILE_PRIORITY] = priorities[i]; - fileDict[KEY_FILE_SIZE] = torrent->fileSize(i); - fileDict[KEY_FILE_AVAILABILITY] = fileAvailability[i]; - - QString fileName = torrent->filePath(i); - if (fileName.endsWith(QB_EXT, Qt::CaseInsensitive)) - fileName.chop(QB_EXT.size()); - fileDict[KEY_FILE_NAME] = Utils::Fs::toNativePath(fileName); - - const BitTorrent::TorrentInfo::PieceRange idx = info.filePieces(i); - fileDict[KEY_FILE_PIECE_RANGE] = QVariantList {idx.first(), idx.last()}; - - if (i == 0) - fileDict[KEY_FILE_IS_SEED] = torrent->isSeed(); - - fileList.append(fileDict); - } - - return json::toJson(fileList); -} - -/** - * Returns an array of hashes (of each pieces respectively) for a torrent in JSON format. - * - * The return value is a JSON-formatted array of strings (hex strings). - */ -QByteArray btjson::getPieceHashesForTorrent(const QString &hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, pieceHashes, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - const QVector hashes = torrent->info().pieceHashes(); - pieceHashes.reserve(hashes.size()); - foreach (const QByteArray &hash, hashes) - pieceHashes.append(hash.toHex()); - - return json::toJson(pieceHashes); -} - -/** - * Returns an array of states (of each pieces respectively) for a torrent in JSON format. - * - * The return value is a JSON-formatted array of ints. - * 0: piece not downloaded - * 1: piece requested or downloading - * 2: piece already downloaded - */ -QByteArray btjson::getPieceStatesForTorrent(const QString &hash) -{ - CACHED_VARIABLE_FOR_HASH(QVariantList, pieceStates, CACHE_DURATION_MS, hash); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (!torrent) { - qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); - return QByteArray(); - } - - const QBitArray states = torrent->pieces(); - pieceStates.reserve(states.size()); - for (int i = 0; i < states.size(); ++i) - pieceStates.append(static_cast(states[i]) * 2); - - const QBitArray dlstates = torrent->downloadingPieces(); - for (int i = 0; i < states.size(); ++i) { - if (dlstates[i]) - pieceStates[i] = 1; - } - - return json::toJson(pieceStates); -} - -/** - * Returns the global transfer information in JSON format. - * - * The return value is a JSON-formatted dictionary. - * The dictionary keys are: - * - "dl_info_speed": Global download rate - * - "dl_info_data": Data downloaded this session - * - "up_info_speed": Global upload rate - * - "up_info_data": Data uploaded this session - * - "dl_rate_limit": Download rate limit - * - "up_rate_limit": Upload rate limit - * - "dht_nodes": DHT nodes connected to - * - "connection_status": Connection status - */ -QByteArray btjson::getTransferInfo() -{ - return json::toJson(getTranserInfoMap()); -} - -QByteArray btjson::getTorrentsRatesLimits(QStringList &hashes, bool downloadLimits) -{ - QVariantMap map; - - foreach (const QString &hash, hashes) { - int limit = -1; - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - limit = downloadLimits ? torrent->downloadLimit() : torrent->uploadLimit(); - map[hash] = limit; - } - - return json::toJson(map); -} - -/** - * Returns the log in JSON format. - * - * The return value is an array of dictionaries. - * The dictionary keys are: - * - "id": id of the message - * - "timestamp": milliseconds since epoch - * - "type": type of the message (int, see MsgType) - * - "message": text of the message - */ -QByteArray btjson::getLog(bool normal, bool info, bool warning, bool critical, int lastKnownId) -{ - Logger* const logger = Logger::instance(); - QVariantList msgList; - - foreach (const Log::Msg& msg, logger->getMessages(lastKnownId)) { - if (!((msg.type == Log::NORMAL && normal) - || (msg.type == Log::INFO && info) - || (msg.type == Log::WARNING && warning) - || (msg.type == Log::CRITICAL && critical))) - continue; - QVariantMap map; - map[KEY_LOG_ID] = msg.id; - map[KEY_LOG_TIMESTAMP] = msg.timestamp; - map[KEY_LOG_MSG_TYPE] = msg.type; - map[KEY_LOG_MSG_MESSAGE] = msg.message; - msgList.append(map); - } - - return json::toJson(msgList); -} - -/** - * Returns the peer log in JSON format. - * - * The return value is an array of dictionaries. - * The dictionary keys are: - * - "id": id of the message - * - "timestamp": milliseconds since epoch - * - "ip": IP of the peer - * - "blocked": whether or not the peer was blocked - * - "reason": reason of the block - */ -QByteArray btjson::getPeerLog(int lastKnownId) -{ - Logger* const logger = Logger::instance(); - QVariantList peerList; - - foreach (const Log::Peer& peer, logger->getPeers(lastKnownId)) { - QVariantMap map; - map[KEY_LOG_ID] = peer.id; - map[KEY_LOG_TIMESTAMP] = peer.timestamp; - map[KEY_LOG_PEER_IP] = peer.ip; - map[KEY_LOG_PEER_BLOCKED] = peer.blocked; - map[KEY_LOG_PEER_REASON] = peer.reason; - peerList.append(map); - } - - return json::toJson(peerList); -} diff --git a/src/webui/btjson.h b/src/webui/btjson.h deleted file mode 100644 index 914fb1aca..000000000 --- a/src/webui/btjson.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2012, 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 BTJSON_H -#define BTJSON_H - -#include -#include -#include - -class btjson -{ - Q_DECLARE_TR_FUNCTIONS(misc) - -private: - btjson() {} - -public: - static QByteArray getTorrents(QString filter = "all", QString category = QString(), - QString sortedColumn = "name", bool reverse = false, int limit = 0, int offset = 0); - static QByteArray getSyncMainData(int acceptedResponseId, QVariantMap &lastData, QVariantMap &lastAcceptedData); - static QByteArray getSyncTorrentPeersData(int acceptedResponseId, QString hash, QVariantMap &lastData, QVariantMap &lastAcceptedData); - static QByteArray getTrackersForTorrent(const QString& hash); - static QByteArray getWebSeedsForTorrent(const QString& hash); - static QByteArray getPropertiesForTorrent(const QString& hash); - static QByteArray getFilesForTorrent(const QString& hash); - static QByteArray getPieceHashesForTorrent(const QString &hash); - static QByteArray getPieceStatesForTorrent(const QString &hash); - static QByteArray getTransferInfo(); - static QByteArray getTorrentsRatesLimits(QStringList& hashes, bool downloadLimits); - static QByteArray getLog(bool normal, bool info, bool warning, bool critical, int lastKnownId); - static QByteArray getPeerLog(int lastKnownId); -}; // class btjson - -#endif // BTJSON_H diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index c888bc910..d55464858 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -28,963 +28,708 @@ #include "webapplication.h" +#include +#include #include +#include #include #include -#include +#include #include -#include +#include +#include +#include +#include +#include +#include #include +#include -#include "base/bittorrent/session.h" -#include "base/bittorrent/torrenthandle.h" -#include "base/bittorrent/torrentinfo.h" -#include "base/bittorrent/trackerentry.h" +#include "base/http/httperror.h" #include "base/iconprovider.h" #include "base/logger.h" -#include "base/net/downloadmanager.h" #include "base/preferences.h" -#include "base/tristatebool.h" #include "base/utils/fs.h" #include "base/utils/misc.h" +#include "base/utils/net.h" +#include "base/utils/random.h" #include "base/utils/string.h" -#include "btjson.h" -#include "jsonutils.h" -#include "prefjson.h" -#include "websessiondata.h" +#include "api/apierror.h" +#include "api/appcontroller.h" +#include "api/authcontroller.h" +#include "api/logcontroller.h" +#include "api/rsscontroller.h" +#include "api/synccontroller.h" +#include "api/torrentscontroller.h" +#include "api/transfercontroller.h" -static const int API_VERSION = 17; -static const int API_VERSION_MIN = 15; +constexpr int MAX_ALLOWED_FILESIZE = 10 * 1024 * 1024; -const QString WWW_FOLDER = ":/www/public/"; -const QString PRIVATE_FOLDER = ":/www/private/"; -const QString DEFAULT_SCOPE = "public"; -const QString SCOPE_IMAGES = "images"; -const QString SCOPE_THEME = "theme"; -const QString DEFAULT_ACTION = "index"; -const QString WEBUI_ACTION = "webui"; -const QString VERSION_INFO = "version"; -const QString MAX_AGE_MONTH = "public, max-age=2592000"; - -#define ADD_ACTION(scope, action) actions[#scope][#action] = &WebApplication::action_##scope##_##action - -QMap> WebApplication::initializeActions() -{ - QMap> actions; - - ADD_ACTION(public, webui); - ADD_ACTION(public, index); - ADD_ACTION(public, login); - ADD_ACTION(public, logout); - ADD_ACTION(public, theme); - ADD_ACTION(public, images); - ADD_ACTION(query, torrents); - ADD_ACTION(query, preferences); - ADD_ACTION(query, transferInfo); - ADD_ACTION(query, propertiesGeneral); - ADD_ACTION(query, propertiesTrackers); - ADD_ACTION(query, propertiesWebSeeds); - ADD_ACTION(query, propertiesFiles); - ADD_ACTION(query, getLog); - ADD_ACTION(query, getPeerLog); - ADD_ACTION(query, getPieceHashes); - ADD_ACTION(query, getPieceStates); - ADD_ACTION(sync, maindata); - ADD_ACTION(sync, torrent_peers); - ADD_ACTION(command, shutdown); - ADD_ACTION(command, download); - ADD_ACTION(command, upload); - ADD_ACTION(command, addTrackers); - ADD_ACTION(command, resumeAll); - ADD_ACTION(command, pauseAll); - ADD_ACTION(command, resume); - ADD_ACTION(command, pause); - ADD_ACTION(command, setPreferences); - ADD_ACTION(command, setFilePrio); - ADD_ACTION(command, getGlobalUpLimit); - ADD_ACTION(command, getGlobalDlLimit); - ADD_ACTION(command, setGlobalUpLimit); - ADD_ACTION(command, setGlobalDlLimit); - ADD_ACTION(command, getTorrentsUpLimit); - ADD_ACTION(command, getTorrentsDlLimit); - ADD_ACTION(command, setTorrentsUpLimit); - ADD_ACTION(command, setTorrentsDlLimit); - ADD_ACTION(command, alternativeSpeedLimitsEnabled); - ADD_ACTION(command, toggleAlternativeSpeedLimits); - ADD_ACTION(command, toggleSequentialDownload); - ADD_ACTION(command, toggleFirstLastPiecePrio); - ADD_ACTION(command, setSuperSeeding); - ADD_ACTION(command, setForceStart); - ADD_ACTION(command, delete); - ADD_ACTION(command, deletePerm); - ADD_ACTION(command, increasePrio); - ADD_ACTION(command, decreasePrio); - ADD_ACTION(command, topPrio); - ADD_ACTION(command, bottomPrio); - ADD_ACTION(command, setLocation); - ADD_ACTION(command, rename); - ADD_ACTION(command, setAutoTMM); - ADD_ACTION(command, recheck); - ADD_ACTION(command, setCategory); - ADD_ACTION(command, addCategory); - ADD_ACTION(command, removeCategories); - ADD_ACTION(command, getSavePath); - ADD_ACTION(version, api); - ADD_ACTION(version, api_min); - ADD_ACTION(version, qbittorrent); - - return actions; -} +const QString PATH_PREFIX_IMAGES {"/images/"}; +const QString PATH_PREFIX_THEME {"/theme/"}; +const QString WWW_FOLDER {":/www"}; +const QString PUBLIC_FOLDER {"/public"}; +const QString PRIVATE_FOLDER {"/private"}; +const QString MAX_AGE_MONTH {"public, max-age=2592000"}; namespace { -#define CHECK_URI(ARGS_NUM) \ - if (args_.size() != ARGS_NUM) { \ - status(404, "Not Found"); \ - return; \ - } -#define CHECK_PARAMETERS(PARAMETERS) \ - QStringList parameters; \ - parameters << PARAMETERS; \ - if (parameters.size() != request().posts.size()) { \ - status(400, "Bad Request"); \ - return; \ - } \ - foreach (QString key, request().posts.keys()) { \ - if (!parameters.contains(key, Qt::CaseInsensitive)) { \ - status(400, "Bad Request"); \ - return; \ - } \ - } - - bool parseBool(const QString &string, const bool defaultValue) + QStringMap parseCookie(const QString &cookieStr) { - if (defaultValue) - return (string.compare("false", Qt::CaseInsensitive) == 0) ? false : true; - return (string.compare("true", Qt::CaseInsensitive) == 0) ? true : false; + // [rfc6265] 4.2.1. Syntax + QStringMap ret; + const QVector cookies = cookieStr.splitRef(';', QString::SkipEmptyParts); + + for (const auto &cookie : cookies) { + const int idx = cookie.indexOf('='); + if (idx < 0) + continue; + + const QString name = cookie.left(idx).trimmed().toString(); + const QString value = Utils::String::unquote(cookie.mid(idx + 1).trimmed()).toString(); + ret.insert(name, value); + } + return ret; } - TriStateBool parseTristatebool(const QString &string) + void translateDocument(QString &data) { - if (string.compare("true", Qt::CaseInsensitive) == 0) - return TriStateBool::True; - if (string.compare("false", Qt::CaseInsensitive) == 0) - return TriStateBool::False; - return TriStateBool::Undefined; - } -} + const QRegExp regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR(\\[CONTEXT=([a-zA-Z_][a-zA-Z0-9_]*)\\])"); + const QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?"); + int i = 0; + bool found = true; -void WebApplication::action_public_index() -{ - QString path; + const QString locale = Preferences::instance()->getLocale(); + bool isTranslationNeeded = !locale.startsWith("en") || locale.startsWith("en_AU") || locale.startsWith("en_GB"); - if (!args_.isEmpty()) { - if (args_.back() == "favicon.ico") - path = ":/icons/skin/qbittorrent16.png"; - else - path = WWW_FOLDER + args_.join("/"); - } + while (i < data.size() && found) { + i = regex.indexIn(data, i); + if (i >= 0) { + //qDebug("Found translatable string: %s", regex.cap(1).toUtf8().data()); + QByteArray word = regex.cap(1).toUtf8(); - printFile(path); -} + QString translation = word; + if (isTranslationNeeded) { + QString context = regex.cap(4); + translation = qApp->translate(context.toUtf8().constData(), word.constData(), 0, 1); + } + // Remove keyboard shortcuts + translation.replace(mnemonic, ""); -void WebApplication::action_public_webui() -{ - if (!sessionActive()) - printFile(PRIVATE_FOLDER + "login.html"); - else - printFile(PRIVATE_FOLDER + "index.html"); -} + // Use HTML code for quotes to prevent issues with JS + translation.replace("'", "'"); + translation.replace("\"", """); -void WebApplication::action_public_login() -{ - if (sessionActive()) { - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - return; - } - - const Preferences *const pref = Preferences::instance(); - QCryptographicHash md5(QCryptographicHash::Md5); - - md5.addData(request().posts["password"].toLocal8Bit()); - QString pass = md5.result().toHex(); - - bool equalUser = Utils::String::slowEquals(request().posts["username"].toUtf8(), pref->getWebUiUsername().toUtf8()); - bool equalPass = Utils::String::slowEquals(pass.toUtf8(), pref->getWebUiPassword().toUtf8()); - - if (equalUser && equalPass) { - sessionStart(); - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - } - else { - QString addr = env().clientAddress.toString(); - increaseFailedAttempts(); - qDebug("client IP: %s (%d failed attempts)", qUtf8Printable(addr), failedAttempts()); - print(QByteArray("Fails."), Http::CONTENT_TYPE_TXT); - } -} - -void WebApplication::action_public_logout() -{ - CHECK_URI(0); - sessionEnd(); -} - -void WebApplication::action_public_theme() -{ - if (args_.size() != 1) { - status(404, "Not Found"); - return; - } - - QString url = IconProvider::instance()->getIconPath(args_.front()); - qDebug() << Q_FUNC_INFO << "There icon:" << url; - - printFile(url); - header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); -} - -void WebApplication::action_public_images() -{ - const QString path = ":/icons/" + args_.join("/"); - printFile(path); - header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); -} - -// GET params: -// - filter (string): all, downloading, seeding, completed, paused, resumed, active, inactive -// - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") -// - sort (string): name of column for sorting by its value -// - reverse (bool): enable reverse sorting -// - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) -// - offset (int): set offset (if less than 0 - offset from end) -void WebApplication::action_query_torrents() -{ - CHECK_URI(0); - - const QStringMap &gets = request().gets; - print(btjson::getTorrents( - gets["filter"], gets["category"], gets["sort"], parseBool(gets["reverse"], false), - gets["limit"].toInt(), gets["offset"].toInt()) - , Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_preferences() -{ - CHECK_URI(0); - print(prefjson::getPreferences(), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_transferInfo() -{ - CHECK_URI(0); - print(btjson::getTransferInfo(), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesGeneral() -{ - CHECK_URI(1); - print(btjson::getPropertiesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesTrackers() -{ - CHECK_URI(1); - print(btjson::getTrackersForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesWebSeeds() -{ - CHECK_URI(1); - print(btjson::getWebSeedsForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_propertiesFiles() -{ - CHECK_URI(1); - print(btjson::getFilesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -// GET params: -// - normal (bool): include normal messages (default true) -// - info (bool): include info messages (default true) -// - warning (bool): include warning messages (default true) -// - critical (bool): include critical messages (default true) -// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) -void WebApplication::action_query_getLog() -{ - CHECK_URI(0); - - const bool isNormal = parseBool(request().gets["normal"], true); - const bool isInfo = parseBool(request().gets["info"], true); - const bool isWarning = parseBool(request().gets["warning"], true); - const bool isCritical = parseBool(request().gets["critical"], true); - - bool ok = false; - int lastKnownId = request().gets["last_known_id"].toInt(&ok); - if (!ok) - lastKnownId = -1; - - print(btjson::getLog(isNormal, isInfo, isWarning, isCritical, lastKnownId), Http::CONTENT_TYPE_JSON); -} - -// GET params: -// - last_known_id (int): exclude messages with id <= 'last_known_id' (default -1) -void WebApplication::action_query_getPeerLog() -{ - CHECK_URI(0); - int lastKnownId; - bool ok; - - lastKnownId = request().gets["last_known_id"].toInt(&ok); - if (!ok) - lastKnownId = -1; - - print(btjson::getPeerLog(lastKnownId), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_getPieceHashes() -{ - CHECK_URI(1); - print(btjson::getPieceHashesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_query_getPieceStates() -{ - CHECK_URI(1); - print(btjson::getPieceStatesForTorrent(args_.front()), Http::CONTENT_TYPE_JSON); -} - -// GET param: -// - rid (int): last response id -void WebApplication::action_sync_maindata() -{ - CHECK_URI(0); - print(btjson::getSyncMainData(request().gets["rid"].toInt(), - session()->syncMainDataLastResponse, - session()->syncMainDataLastAcceptedResponse), Http::CONTENT_TYPE_JSON); -} - -// GET param: -// - hash (string): torrent hash -// - rid (int): last response id -void WebApplication::action_sync_torrent_peers() -{ - CHECK_URI(0); - print(btjson::getSyncTorrentPeersData(request().gets["rid"].toInt(), - request().gets["hash"], - session()->syncTorrentPeersLastResponse, - session()->syncTorrentPeersLastAcceptedResponse), Http::CONTENT_TYPE_JSON); -} - - -void WebApplication::action_version_api() -{ - CHECK_URI(0); - print(QString::number(API_VERSION), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_version_api_min() -{ - CHECK_URI(0); - print(QString::number(API_VERSION_MIN), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_version_qbittorrent() -{ - CHECK_URI(0); - print(QString(QBT_VERSION), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_shutdown() -{ - qDebug() << "Shutdown request from Web UI"; - CHECK_URI(0); - - // Special case handling for shutdown, we - // need to reply to the Web UI before - // actually shutting down. - QTimer::singleShot(100, qApp, SLOT(quit())); -} - -void WebApplication::action_command_download() -{ - CHECK_URI(0); - - const QString urls = request().posts.value("urls"); - const bool skipChecking = parseBool(request().posts.value("skip_checking"), false); - const bool seqDownload = parseBool(request().posts.value("sequentialDownload"), false); - const bool firstLastPiece = parseBool(request().posts.value("firstLastPiecePrio"), false); - const TriStateBool addPaused = parseTristatebool(request().posts.value("paused")); - const TriStateBool rootFolder = parseTristatebool(request().posts.value("root_folder")); - const QString savepath = request().posts.value("savepath").trimmed(); - const QString category = request().posts.value("category").trimmed(); - const QString cookie = request().posts.value("cookie"); - const QString torrentName = request().posts.value("rename").trimmed(); - const int upLimit = request().posts.value("upLimit").toInt(); - const int dlLimit = request().posts.value("dlLimit").toInt(); - - QList cookies; - if (!cookie.isEmpty()) { - const QStringList cookiesStr = cookie.split("; "); - for (QString cookieStr : cookiesStr) { - cookieStr = cookieStr.trimmed(); - int index = cookieStr.indexOf('='); - if (index > 1) { - QByteArray name = cookieStr.left(index).toLatin1(); - QByteArray value = cookieStr.right(cookieStr.length() - index - 1).toLatin1(); - cookies += QNetworkCookie(name, value); + data.replace(i, regex.matchedLength(), translation); + i += translation.length(); } + else { + found = false; // no more translatable strings + } + + data.replace(QLatin1String("${LANG}"), locale.left(2)); + data.replace(QLatin1String("${VERSION}"), QBT_VERSION); } } - BitTorrent::AddTorrentParams params; - // TODO: Check if destination actually exists - params.skipChecking = skipChecking; - params.sequential = seqDownload; - params.firstLastPiecePriority = firstLastPiece; - params.addPaused = addPaused; - params.createSubfolder = rootFolder; - params.savePath = savepath; - params.category = category; - params.name = torrentName; - params.uploadLimit = (upLimit > 0) ? upLimit : -1; - params.downloadLimit = (dlLimit > 0) ? dlLimit : -1; - - bool partialSuccess = false; - for (QString url : urls.split('\n')) { - url = url.trimmed(); - if (!url.isEmpty()) { - Net::DownloadManager::instance()->setCookiesFromUrl(cookies, QUrl::fromEncoded(url.toUtf8())); - partialSuccess |= BitTorrent::Session::instance()->addTorrent(url, params); - } + inline QUrl urlFromHostHeader(const QString &hostHeader) + { + if (!hostHeader.contains(QLatin1String("://"))) + return QUrl(QLatin1String("http://") + hostHeader); + return hostHeader; } - - if (partialSuccess) - print(QByteArray("Ok."), Http::CONTENT_TYPE_TXT); - else - print(QByteArray("Fails."), Http::CONTENT_TYPE_TXT); } -void WebApplication::action_command_upload() +WebApplication::WebApplication(QObject *parent) + : QObject(parent) { - CHECK_URI(0); + registerAPIController(QLatin1String("app"), new AppController(this, this)); + registerAPIController(QLatin1String("auth"), new AuthController(this, this)); + registerAPIController(QLatin1String("log"), new LogController(this, this)); + registerAPIController(QLatin1String("rss"), new RSSController(this, this)); + registerAPIController(QLatin1String("sync"), new SyncController(this, this)); + registerAPIController(QLatin1String("torrents"), new TorrentsController(this, this)); + registerAPIController(QLatin1String("transfer"), new TransferController(this, this)); - const bool skipChecking = parseBool(request().posts.value("skip_checking"), false); - const bool seqDownload = parseBool(request().posts.value("sequentialDownload"), false); - const bool firstLastPiece = parseBool(request().posts.value("firstLastPiecePrio"), false); - const TriStateBool addPaused = parseTristatebool(request().posts.value("paused")); - const TriStateBool rootFolder = parseTristatebool(request().posts.value("root_folder")); - const QString savepath = request().posts.value("savepath").trimmed(); - const QString category = request().posts.value("category").trimmed(); - const QString torrentName = request().posts.value("rename").trimmed(); - const int upLimit = request().posts.value("upLimit").toInt(); - const int dlLimit = request().posts.value("dlLimit").toInt(); + declarePublicAPI(QLatin1String("auth/login")); - for (const Http::UploadedFile &torrent : request().files) { - const QString filePath = saveTmpFile(torrent.data); - if (filePath.isEmpty()) { - qWarning() << "I/O Error: Could not create temporary file"; + configure(); + connect(Preferences::instance(), &Preferences::changed, this, &WebApplication::configure); +} + +WebApplication::~WebApplication() +{ + // cleanup sessions data + qDeleteAll(m_sessions); +} + +void WebApplication::sendWebUIFile() +{ + const QStringList pathItems {request().path.split('/', QString::SkipEmptyParts)}; + if (pathItems.contains(".") || pathItems.contains("..")) + throw InternalServerErrorHTTPError(); + + if (!m_isAltUIUsed) { + if (request().path.startsWith(PATH_PREFIX_IMAGES)) { + const QString imageFilename {request().path.mid(PATH_PREFIX_IMAGES.size())}; + sendFile(QLatin1String(":/icons/") + imageFilename); + header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); + return; + } + + if (request().path.startsWith(PATH_PREFIX_THEME)) { + const QString iconId {request().path.mid(PATH_PREFIX_THEME.size())}; + sendFile(IconProvider::instance()->getIconPath(iconId)); + header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); + return; + } + } + + const QString path { + (request().path != QLatin1String("/") + ? request().path + : (session() + ? QLatin1String("/index.html") + : QLatin1String("/login.html"))) + }; + + QString localPath { + m_rootFolder + + (session() ? PRIVATE_FOLDER : PUBLIC_FOLDER) + + path + }; + + QFileInfo fileInfo {localPath}; + + if (!fileInfo.exists() && session()) { + // try to send public file if there is no private one + localPath = m_rootFolder + PUBLIC_FOLDER + path; + fileInfo.setFile(localPath); + } + + if (m_isAltUIUsed) { +#ifdef Q_OS_UNIX + if (!Utils::Fs::isRegularFile(localPath)) { status(500, "Internal Server Error"); - print(QObject::tr("I/O Error: Could not create temporary file."), Http::CONTENT_TYPE_TXT); - continue; + print(tr("Unacceptable file type, only regular file is allowed."), Http::CONTENT_TYPE_TXT); + return; } +#endif - const BitTorrent::TorrentInfo torrentInfo = BitTorrent::TorrentInfo::loadFromFile(filePath); - if (!torrentInfo.isValid()) { - status(415, "Unsupported Media Type"); - print(QObject::tr("Error: '%1' is not a valid torrent file.\n").arg(torrent.filename), Http::CONTENT_TYPE_TXT); - } - else { - BitTorrent::AddTorrentParams params; - // TODO: Check if destination actually exists - params.skipChecking = skipChecking; - params.sequential = seqDownload; - params.firstLastPiecePriority = firstLastPiece; - params.addPaused = addPaused; - params.createSubfolder = rootFolder; - params.savePath = savepath; - params.category = category; - params.name = torrentName; - params.uploadLimit = (upLimit > 0) ? upLimit : -1; - params.downloadLimit = (dlLimit > 0) ? dlLimit : -1; + while (fileInfo.filePath() != m_rootFolder) { + if (fileInfo.isSymLink()) + throw InternalServerErrorHTTPError(tr("Symlinks inside alternative UI folder are forbidden.")); - if (!BitTorrent::Session::instance()->addTorrent(torrentInfo, params)) { - status(500, "Internal Server Error"); - print(QObject::tr("Error: Could not add torrent to session."), Http::CONTENT_TYPE_TXT); - } - } - // Clean up - Utils::Fs::forceRemove(filePath); - } -} - -void WebApplication::action_command_addTrackers() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "urls"); - QString hash = request().posts["hash"]; - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - QList trackers; - foreach (QString url, request().posts["urls"].split('\n')) { - url = url.trimmed(); - if (!url.isEmpty()) - trackers << url; - } - torrent->addTrackers(trackers); - } -} - -void WebApplication::action_command_resumeAll() -{ - CHECK_URI(0); - - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - torrent->resume(); -} - -void WebApplication::action_command_pauseAll() -{ - CHECK_URI(0); - - foreach (BitTorrent::TorrentHandle *const torrent, BitTorrent::Session::instance()->torrents()) - torrent->pause(); -} - -void WebApplication::action_command_resume() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->resume(); -} - -void WebApplication::action_command_pause() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->pause(); -} - -void WebApplication::action_command_setPreferences() -{ - CHECK_URI(0); - CHECK_PARAMETERS("json"); - prefjson::setPreferences(request().posts["json"]); -} - -void WebApplication::action_command_setFilePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "id" << "priority"); - QString hash = request().posts["hash"]; - int fileID = request().posts["id"].toInt(); - int priority = request().posts["priority"].toInt(); - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - - if (torrent && torrent->hasMetadata()) - torrent->setFilePriority(fileID, priority); -} - -void WebApplication::action_command_getGlobalUpLimit() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->uploadSpeedLimit()), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_getGlobalDlLimit() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->downloadSpeedLimit()), Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_setGlobalUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - BitTorrent::Session::instance()->setUploadSpeedLimit(limit); -} - -void WebApplication::action_command_setGlobalDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - BitTorrent::Session::instance()->setDownloadSpeedLimit(limit); -} - -void WebApplication::action_command_getTorrentsUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - print(btjson::getTorrentsRatesLimits(hashes, false), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_command_getTorrentsDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - print(btjson::getTorrentsRatesLimits(hashes, true), Http::CONTENT_TYPE_JSON); -} - -void WebApplication::action_command_setTorrentsUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "limit"); - - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) - limit = -1; - - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setUploadLimit(limit); - } -} - -void WebApplication::action_command_setTorrentsDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "limit"); - - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) - limit = -1; - - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setDownloadLimit(limit); - } -} - -void WebApplication::action_command_toggleAlternativeSpeedLimits() -{ - CHECK_URI(0); - BitTorrent::Session *const session = BitTorrent::Session::instance(); - session->setAltGlobalSpeedLimitEnabled(!session->isAltGlobalSpeedLimitEnabled()); -} - -void WebApplication::action_command_alternativeSpeedLimitsEnabled() -{ - CHECK_URI(0); - print(QByteArray::number(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled()) - , Http::CONTENT_TYPE_TXT); -} - -void WebApplication::action_command_toggleSequentialDownload() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->toggleSequentialDownload(); - } -} - -void WebApplication::action_command_toggleFirstLastPiecePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->toggleFirstLastPiecePriority(); - } -} - -void WebApplication::action_command_setSuperSeeding() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "value"); - - const bool value = parseBool(request().posts["value"], false); - const QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setSuperSeeding(value); - } -} - -void WebApplication::action_command_setForceStart() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "value"); - - const bool value = parseBool(request().posts["value"], false); - const QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->resume(value); - } -} - -void WebApplication::action_command_delete() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) - BitTorrent::Session::instance()->deleteTorrent(hash, false); -} - -void WebApplication::action_command_deletePerm() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split('|'); - foreach (const QString &hash, hashes) - BitTorrent::Session::instance()->deleteTorrent(hash, true); -} - -void WebApplication::action_command_increasePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; - } - - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->increaseTorrentsPriority(hashes); -} - -void WebApplication::action_command_decreasePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; - } - - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->decreaseTorrentsPriority(hashes); -} - -void WebApplication::action_command_topPrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; - } - - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->topTorrentsPriority(hashes); -} - -void WebApplication::action_command_bottomPrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - - if (!BitTorrent::Session::instance()->isQueueingSystemEnabled()) { - status(403, "Torrent queueing must be enabled"); - return; - } - - QStringList hashes = request().posts["hashes"].split('|'); - BitTorrent::Session::instance()->bottomTorrentsPriority(hashes); -} - -void WebApplication::action_command_setLocation() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "location"); - - QStringList hashes = request().posts["hashes"].split("|"); - QString newLocation = request().posts["location"].trimmed(); - - // check if the location exists - if (newLocation.isEmpty() || !QDir(newLocation).exists()) - return; - - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - Logger::instance()->addMessage(tr("WebUI Set location: moving \"%1\", from \"%2\" to \"%3\"").arg(torrent->name()).arg(torrent->savePath()).arg(newLocation)); - - torrent->move(Utils::Fs::expandPathAbs(newLocation)); + fileInfo.setFile(fileInfo.path()); } } + + sendFile(localPath); } -void WebApplication::action_command_rename() +WebSession *WebApplication::session() { - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "name"); - - QString hash = request().posts["hash"]; - QString name = request().posts["name"].trimmed(); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent && !name.isEmpty()) { - name.replace(QRegularExpression("\r?\n|\r"), " "); - qDebug() << "Renaming" << torrent->name() << "to" << name; - torrent->setName(name); - } - else { - status(400, "Incorrect torrent hash or name"); - } + return m_currentSession; } -void WebApplication::action_command_setAutoTMM() +const Http::Request &WebApplication::request() const { - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "enable"); - - const QStringList hashes = request().posts["hashes"].split('|'); - const bool isEnabled = parseBool(request().posts["enable"], false); - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) - torrent->setAutoTMMEnabled(isEnabled); - } + return m_request; } -void WebApplication::action_command_recheck() +const Http::Environment &WebApplication::env() const { - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(request().posts["hash"]); - if (torrent) - torrent->forceRecheck(); -} - -void WebApplication::action_command_setCategory() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes" << "category"); - - QStringList hashes = request().posts["hashes"].split('|'); - QString category = request().posts["category"].trimmed(); - - foreach (const QString &hash, hashes) { - BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); - if (torrent) { - if (!torrent->setCategory(category)) { - status(400, "Incorrect category name"); - return; - } - } - } -} - -void WebApplication::action_command_addCategory() -{ - CHECK_URI(0); - CHECK_PARAMETERS("category"); - - QString category = request().posts["category"].trimmed(); - - if (!BitTorrent::Session::isValidCategoryName(category) && !category.isEmpty()) { - status(400, tr("Incorrect category name")); - return; - } - - BitTorrent::Session::instance()->addCategory(category); -} - -void WebApplication::action_command_removeCategories() -{ - CHECK_URI(0); - CHECK_PARAMETERS("categories"); - - QStringList categories = request().posts["categories"].split('\n'); - foreach (const QString &category, categories) - BitTorrent::Session::instance()->removeCategory(category); -} - -void WebApplication::action_command_getSavePath() -{ - CHECK_URI(0); - print(BitTorrent::Session::instance()->defaultSavePath()); -} - -bool WebApplication::isPublicScope() -{ - return (scope_ == DEFAULT_SCOPE || scope_ == VERSION_INFO); + return m_env; } void WebApplication::doProcessRequest() { - scope_ = DEFAULT_SCOPE; - action_ = DEFAULT_ACTION; + QStringMap *params = &((request().method == QLatin1String("GET")) + ? m_request.gets : m_request.posts); + QString scope, action; - parsePath(); + const auto findAPICall = [&]() -> bool + { + QRegularExpressionMatch match = m_apiPathPattern.match(request().path); + if (!match.hasMatch()) return false; - if (args_.contains(".") || args_.contains("..")) { - qDebug() << Q_FUNC_INFO << "Invalid path:" << request().path; - status(404, "Not Found"); - return; - } + action = match.captured(QLatin1String("action")); + scope = match.captured(QLatin1String("scope")); + return true; + }; - if (!isPublicScope() && !sessionActive()) { - status(403, "Forbidden"); - return; - } + const auto findLegacyAPICall = [&]() -> bool + { + QRegularExpressionMatch match = m_apiLegacyPathPattern.match(request().path); + if (!match.hasMatch()) return false; - if (actions_.value(scope_).value(action_) != 0) { - (this->*(actions_[scope_][action_]))(); + struct APICompatInfo + { + QString scope; + QString action; + }; + const QMap APICompatMapping { + {"sync/maindata", {"sync", "maindata"}}, + {"sync/torrent_peers", {"sync", "torrentPeers"}}, + + {"login", {"auth", "login"}}, + {"logout", {"auth", "logout"}}, + + {"command/shutdown", {"app", "shutdown"}}, + {"query/preferences", {"app", "preferences"}}, + {"command/setPreferences", {"app", "setPreferences"}}, + {"command/getSavePath", {"app", "defaultSavePath"}}, + {"version/qbittorrent", {"app", "version"}}, + + {"query/getLog", {"log", "main"}}, + {"query/getPeerLog", {"log", "peers"}}, + + {"query/torrents", {"torrents", "info"}}, + {"query/propertiesGeneral", {"torrents", "properties"}}, + {"query/propertiesTrackers", {"torrents", "trackers"}}, + {"query/propertiesWebSeeds", {"torrents", "webseeds"}}, + {"query/propertiesFiles", {"torrents", "files"}}, + {"query/getPieceHashes", {"torrents", "pieceHashes"}}, + {"query/getPieceStates", {"torrents", "pieceStates"}}, + {"command/resume", {"torrents", "resume"}}, + {"command/pause", {"torrents", "pause"}}, + {"command/recheck", {"torrents", "recheck"}}, + {"command/resumeAll", {"torrents", "resume"}}, + {"command/pauseAll", {"torrents", "pause"}}, + {"command/rename", {"torrents", "rename"}}, + {"command/download", {"torrents", "add"}}, + {"command/upload", {"torrents", "add"}}, + {"command/delete", {"torrents", "delete"}}, + {"command/deletePerm", {"torrents", "delete"}}, + {"command/addTrackers", {"torrents", "addTrackers"}}, + {"command/setFilePrio", {"torrents", "filePrio"}}, + {"command/setCategory", {"torrents", "setCategory"}}, + {"command/addCategory", {"torrents", "createCategory"}}, + {"command/removeCategories", {"torrents", "removeCategories"}}, + {"command/getTorrentsUpLimit", {"torrents", "uploadLimit"}}, + {"command/getTorrentsDlLimit", {"torrents", "downloadLimit"}}, + {"command/setTorrentsUpLimit", {"torrents", "setUploadLimit"}}, + {"command/setTorrentsDlLimit", {"torrents", "setDownloadLimit"}}, + {"command/increasePrio", {"torrents", "increasePrio"}}, + {"command/decreasePrio", {"torrents", "decreasePrio"}}, + {"command/topPrio", {"torrents", "topPrio"}}, + {"command/bottomPrio", {"torrents", "bottomPrio"}}, + {"command/setLocation", {"torrents", "setLocation"}}, + {"command/setAutoTMM", {"torrents", "setAutoManagement"}}, + {"command/setSuperSeeding", {"torrents", "setSuperSeeding"}}, + {"command/setForceStart", {"torrents", "setForceStart"}}, + {"command/toggleSequentialDownload", {"torrents", "toggleSequentialDownload"}}, + {"command/toggleFirstLastPiecePrio", {"torrents", "toggleFirstLastPiecePrio"}}, + + {"query/transferInfo", {"transfer", "info"}}, + {"command/alternativeSpeedLimitsEnabled", {"transfer", "speedLimitsMode"}}, + {"command/toggleAlternativeSpeedLimits", {"transfer", "toggleSpeedLimitsMode"}}, + {"command/getGlobalUpLimit", {"transfer", "uploadLimit"}}, + {"command/getGlobalDlLimit", {"transfer", "downloadLimit"}}, + {"command/setGlobalUpLimit", {"transfer", "setUploadLimit"}}, + {"command/setGlobalDlLimit", {"transfer", "setDownloadLimit"}} + }; + + const QString legacyAction {match.captured(QLatin1String("action"))}; + const APICompatInfo compatInfo = APICompatMapping.value(legacyAction); + + scope = compatInfo.scope; + action = compatInfo.action; + + if (legacyAction == QLatin1String("command/delete")) + (*params)["deleteFiles"] = "false"; + else if (legacyAction == QLatin1String("command/deletePerm")) + (*params)["deleteFiles"] = "true"; + + const QString hash {match.captured(QLatin1String("hash"))}; + (*params)[QLatin1String("hash")] = hash; + + return true; + }; + + if (!findAPICall()) + findLegacyAPICall(); + + APIController *controller = m_apiControllers.value(scope); + if (!controller) { + if (request().path == QLatin1String("/version/api")) { + print(QString(COMPAT_API_VERSION), Http::CONTENT_TYPE_TXT); + return; + } + + if (request().path == QLatin1String("/version/api_min")) { + print(QString(COMPAT_API_VERSION_MIN), Http::CONTENT_TYPE_TXT); + return; + } + + sendWebUIFile(); } else { - status(404, "Not Found"); - qDebug() << Q_FUNC_INFO << "Resource not found:" << request().path; + if (!session() && !isPublicAPI(scope, action)) + throw ForbiddenHTTPError(); + + DataMap data; + for (const Http::UploadedFile &torrent : request().files) + data[torrent.filename] = torrent.data; + + try { + const QVariant result = controller->run(action, *params, data); + switch (result.userType()) { + case QMetaType::QString: + print(result.toString(), Http::CONTENT_TYPE_TXT); + break; + case QMetaType::QJsonDocument: + print(result.toJsonDocument().toJson(QJsonDocument::Compact), Http::CONTENT_TYPE_JSON); + break; + default: + print(result.toString(), Http::CONTENT_TYPE_TXT); + break; + } + } + catch (const APIError &error) { + // re-throw as HTTPError + switch (error.type()) { + case APIErrorType::AccessDenied: + throw ForbiddenHTTPError(error.message()); + case APIErrorType::BadData: + throw UnsupportedMediaTypeHTTPError(error.message()); + case APIErrorType::BadParams: + throw BadRequestHTTPError(error.message()); + case APIErrorType::Conflict: + throw ConflictHTTPError(error.message()); + case APIErrorType::NotFound: + throw NotFoundHTTPError(error.message()); + default: + Q_ASSERT(false); + } + } } } -void WebApplication::parsePath() +void WebApplication::configure() { - if (request().path == "/") action_ = WEBUI_ACTION; + const auto pref = Preferences::instance(); - // check action for requested path - QStringList pathItems = request().path.split('/', QString::SkipEmptyParts); - if (!pathItems.empty() && actions_.contains(pathItems.front())) { - scope_ = pathItems.front(); - pathItems.pop_front(); + m_domainList = Preferences::instance()->getServerDomains().split(';', QString::SkipEmptyParts); + std::for_each(m_domainList.begin(), m_domainList.end(), [](QString &entry) { entry = entry.trimmed(); }); + + const QString rootFolder = Utils::Fs::expandPathAbs( + !pref->isAltWebUiEnabled() ? WWW_FOLDER : pref->getWebUiRootFolder()); + if (rootFolder != m_rootFolder) { + m_translatedFiles.clear(); + m_rootFolder = rootFolder; } - - if (!pathItems.empty() && actions_[scope_].contains(pathItems.front())) { - action_ = pathItems.front(); - pathItems.pop_front(); - } - - args_ = pathItems; } -WebApplication::WebApplication(QObject *parent) - : AbstractWebApplication(parent) +void WebApplication::registerAPIController(const QString &scope, APIController *controller) { + Q_ASSERT(controller); + Q_ASSERT(!m_apiControllers.value(scope)); + + m_apiControllers[scope] = controller; } -QMap > WebApplication::actions_ = WebApplication::initializeActions(); +void WebApplication::declarePublicAPI(const QString &apiPath) +{ + m_publicAPIs << apiPath; +} + +void WebApplication::sendFile(const QString &path) +{ + const QDateTime lastModified {QFileInfo(path).lastModified()}; + + // find translated file in cache + auto it = m_translatedFiles.constFind(path); + if ((it != m_translatedFiles.constEnd()) && (lastModified <= (*it).lastModified)) { + print((*it).data, QMimeDatabase().mimeTypeForFileNameAndData(path, (*it).data).name()); + return; + } + + QFile file {path}; + if (!file.open(QIODevice::ReadOnly)) { + qDebug("File %s was not found!", qUtf8Printable(path)); + throw NotFoundHTTPError(); + } + + if (file.size() > MAX_ALLOWED_FILESIZE) { + qWarning("%s: exceeded the maximum allowed file size!", qUtf8Printable(path)); + throw InternalServerErrorHTTPError(tr("Exceeded the maximum allowed file size (%1)!") + .arg(Utils::Misc::friendlyUnit(MAX_ALLOWED_FILESIZE))); + } + + QByteArray data {file.readAll()}; + file.close(); + + const QMimeType type {QMimeDatabase().mimeTypeForFileNameAndData(path, data)}; + const bool isTranslatable {type.inherits(QLatin1String("text/plain"))}; + + // Translate the file + if (isTranslatable) { + QString dataStr {data}; + translateDocument(dataStr); + data = dataStr.toUtf8(); + + m_translatedFiles[path] = {data, lastModified}; // caching translated file + } + + print(data, type.name()); +} + +Http::Response WebApplication::processRequest(const Http::Request &request, const Http::Environment &env) +{ + m_currentSession = nullptr; + m_request = request; + m_env = env; + + // clear response + clear(); + + try { + // block cross-site requests + if (isCrossSiteRequest(m_request) || !validateHostHeader(m_domainList)) + throw UnauthorizedHTTPError(); + + sessionInitialize(); + doProcessRequest(); + } + catch (const HTTPError &error) { + status(error.statusCode(), error.statusText()); + if (!error.message().isEmpty()) + print(error.message(), Http::CONTENT_TYPE_TXT); + } + + // avoid clickjacking attacks + header(Http::HEADER_X_FRAME_OPTIONS, "SAMEORIGIN"); + header(Http::HEADER_X_XSS_PROTECTION, "1; mode=block"); + header(Http::HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff"); + header(Http::HEADER_CONTENT_SECURITY_POLICY, "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none';"); + + return response(); +} + +QString WebApplication::clientId() const +{ + return env().clientAddress.toString(); +} + +void WebApplication::sessionInitialize() +{ + Q_ASSERT(!m_currentSession); + + const QString sessionId {parseCookie(m_request.headers.value(QLatin1String("cookie"))).value(C_SID)}; + + // TODO: Additional session check + + if (!sessionId.isEmpty()) { + m_currentSession = m_sessions.value(sessionId); + if (m_currentSession) { + const uint now = QDateTime::currentDateTime().toTime_t(); + if ((now - m_currentSession->m_timestamp) > INACTIVE_TIME) { + // session is outdated - removing it + delete m_sessions.take(sessionId); + m_currentSession = nullptr; + } + else { + m_currentSession->updateTimestamp(); + } + } + else { + qDebug() << Q_FUNC_INFO << "session does not exist!"; + } + } + + if (!m_currentSession && !isAuthNeeded()) + sessionStart(); +} + +QString WebApplication::generateSid() const +{ + QString sid; + + do { + const size_t size = 6; + quint32 tmp[size]; + + for (size_t i = 0; i < size; ++i) + tmp[i] = Utils::Random::rand(); + + sid = QByteArray::fromRawData(reinterpret_cast(tmp), sizeof(quint32) * size).toBase64(); + } + while (m_sessions.contains(sid)); + + return sid; +} + +bool WebApplication::isAuthNeeded() +{ + qDebug("Checking auth rules against client address %s", qPrintable(m_env.clientAddress.toString())); + const Preferences *pref = Preferences::instance(); + if (!pref->isWebUiLocalAuthEnabled() && Utils::Net::isLoopbackAddress(m_env.clientAddress)) + return false; + if (pref->isWebUiAuthSubnetWhitelistEnabled() && Utils::Net::isIPInRange(m_env.clientAddress, pref->getWebUiAuthSubnetWhitelist())) + return false; + return true; +} + +bool WebApplication::isPublicAPI(const QString &scope, const QString &action) const +{ + return m_publicAPIs.contains(QString::fromLatin1("%1/%2").arg(scope).arg(action)); +} + +void WebApplication::sessionStart() +{ + Q_ASSERT(!m_currentSession); + + // remove outdated sessions + const uint now = QDateTime::currentDateTime().toTime_t(); + foreach (const auto session, m_sessions) { + if ((now - session->timestamp()) > INACTIVE_TIME) + delete m_sessions.take(session->id()); + } + + m_currentSession = new WebSession(generateSid()); + m_sessions[m_currentSession->id()] = m_currentSession; + + QNetworkCookie cookie(C_SID, m_currentSession->id().toUtf8()); + cookie.setHttpOnly(true); + cookie.setPath(QLatin1String("/")); + header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); +} + +void WebApplication::sessionEnd() +{ + Q_ASSERT(m_currentSession); + + QNetworkCookie cookie(C_SID); + cookie.setPath(QLatin1String("/")); + cookie.setExpirationDate(QDateTime::currentDateTime().addDays(-1)); + + delete m_sessions.take(m_currentSession->id()); + m_currentSession = nullptr; + + header(Http::HEADER_SET_COOKIE, cookie.toRawForm()); +} + +bool WebApplication::isCrossSiteRequest(const Http::Request &request) const +{ + // https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers + + const auto isSameOrigin = [](const QUrl &left, const QUrl &right) -> bool + { + // [rfc6454] 5. Comparing Origins + return ((left.port() == right.port()) + // && (left.scheme() == right.scheme()) // not present in this context + && (left.host() == right.host())); + }; + + const QString targetOrigin = request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST)); + const QString originValue = request.headers.value(Http::HEADER_ORIGIN); + const QString refererValue = request.headers.value(Http::HEADER_REFERER); + + if (originValue.isEmpty() && refererValue.isEmpty()) { + // owasp.org recommends to block this request, but doing so will inevitably lead Web API users to spoof headers + // so lets be permissive here + return false; + } + + // sent with CORS requests, as well as with POST requests + if (!originValue.isEmpty()) { + const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), originValue); + if (isInvalid) + LogMsg(tr("WebUI: Origin header & Target origin mismatch! Source IP: '%1'. Origin header: '%2'. Target origin: '%3'") + .arg(m_env.clientAddress.toString()).arg(originValue).arg(targetOrigin) + , Log::WARNING); + return isInvalid; + } + + if (!refererValue.isEmpty()) { + const bool isInvalid = !isSameOrigin(urlFromHostHeader(targetOrigin), refererValue); + if (isInvalid) + LogMsg(tr("WebUI: Referer header & Target origin mismatch! Source IP: '%1'. Referer header: '%2'. Target origin: '%3'") + .arg(m_env.clientAddress.toString()).arg(refererValue).arg(targetOrigin) + , Log::WARNING); + return isInvalid; + } + + return true; +} + +bool WebApplication::validateHostHeader(const QStringList &domains) const +{ + const QUrl hostHeader = urlFromHostHeader(m_request.headers[Http::HEADER_HOST]); + const QString requestHost = hostHeader.host(); + + // (if present) try matching host header's port with local port + const int requestPort = hostHeader.port(); + if ((requestPort != -1) && (m_env.localPort != requestPort)) { + LogMsg(tr("WebUI: Invalid Host header, port mismatch. Request source IP: '%1'. Server port: '%2'. Received Host header: '%3'") + .arg(m_env.clientAddress.toString()).arg(m_env.localPort) + .arg(m_request.headers[Http::HEADER_HOST]) + , Log::WARNING); + return false; + } + + // try matching host header with local address +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + const bool sameAddr = m_env.localAddress.isEqual(QHostAddress(requestHost)); +#else + const auto equal = [](const Q_IPV6ADDR &l, const Q_IPV6ADDR &r) -> bool + { + for (int i = 0; i < 16; ++i) { + if (l[i] != r[i]) + return false; + } + return true; + }; + const bool sameAddr = equal(m_env.localAddress.toIPv6Address(), QHostAddress(requestHost).toIPv6Address()); +#endif + + if (sameAddr) + return true; + + // try matching host header with domain list + for (const auto &domain : domains) { + QRegExp domainRegex(domain, Qt::CaseInsensitive, QRegExp::Wildcard); + if (requestHost.contains(domainRegex)) + return true; + } + + LogMsg(tr("WebUI: Invalid Host header. Request source IP: '%1'. Received Host header: '%2'") + .arg(m_env.clientAddress.toString()).arg(m_request.headers[Http::HEADER_HOST]) + , Log::WARNING); + return false; +} + +// WebSession + +WebSession::WebSession(const QString &sid) + : m_sid {sid} +{ + updateTimestamp(); +} + +QString WebSession::id() const +{ + return m_sid; +} + +uint WebSession::timestamp() const +{ + return m_timestamp; +} + +QVariant WebSession::getData(const QString &id) const +{ + return m_data.value(id); +} + +void WebSession::setData(const QString &id, const QVariant &data) +{ + m_data[id] = data; +} + +void WebSession::updateTimestamp() +{ + m_timestamp = QDateTime::currentDateTime().toTime_t(); +} diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 318e1ae7d..2d098f6cb 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2014, 2017 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -26,95 +26,118 @@ * exception statement from your version. */ -#ifndef WEBAPPLICATION_H -#define WEBAPPLICATION_H +#pragma once -#include -#include "abstractwebapplication.h" +#include +#include +#include +#include +#include +#include -class WebApplication : public AbstractWebApplication +#include "api/isessionmanager.h" +#include "base/http/irequesthandler.h" +#include "base/http/responsebuilder.h" +#include "base/http/types.h" +#include "base/utils/version.h" + +constexpr Utils::Version API_VERSION {2, 0, 0}; +constexpr int COMPAT_API_VERSION = 18; +constexpr int COMPAT_API_VERSION_MIN = 18; + +class APIController; +class WebApplication; + +constexpr char C_SID[] = "SID"; // name of session id cookie +constexpr int INACTIVE_TIME = 900; // Session inactive time (in secs = 15 min.) + +class WebSession : public ISession { + friend class WebApplication; + +public: + explicit WebSession(const QString &sid); + + QString id() const override; + uint timestamp() const; + + QVariant getData(const QString &id) const override; + void setData(const QString &id, const QVariant &data) override; + +private: + void updateTimestamp(); + + const QString m_sid; + uint m_timestamp; + QVariantHash m_data; +}; + +class WebApplication + : public QObject, public Http::IRequestHandler, public ISessionManager + , private Http::ResponseBuilder +{ + Q_OBJECT Q_DISABLE_COPY(WebApplication) +#ifndef Q_MOC_RUN +#define WEBAPI_PUBLIC +#define WEBAPI_PRIVATE +#endif + public: explicit WebApplication(QObject *parent = nullptr); + ~WebApplication() override; + + Http::Response processRequest(const Http::Request &request, const Http::Environment &env); + + QString clientId() const override; + WebSession *session() override; + void sessionStart() override; + void sessionEnd() override; + + const Http::Request &request() const; + const Http::Environment &env() const; private: - // Actions - void action_public_webui(); - void action_public_index(); - void action_public_login(); - void action_public_logout(); - void action_public_theme(); - void action_public_images(); - void action_query_torrents(); - void action_query_preferences(); - void action_query_transferInfo(); - void action_query_propertiesGeneral(); - void action_query_propertiesTrackers(); - void action_query_propertiesWebSeeds(); - void action_query_propertiesFiles(); - void action_query_getLog(); - void action_query_getPeerLog(); - void action_query_getPieceHashes(); - void action_query_getPieceStates(); - void action_sync_maindata(); - void action_sync_torrent_peers(); - void action_command_shutdown(); - void action_command_download(); - void action_command_upload(); - void action_command_addTrackers(); - void action_command_resumeAll(); - void action_command_pauseAll(); - void action_command_resume(); - void action_command_pause(); - void action_command_setPreferences(); - void action_command_setFilePrio(); - void action_command_getGlobalUpLimit(); - void action_command_getGlobalDlLimit(); - void action_command_setGlobalUpLimit(); - void action_command_setGlobalDlLimit(); - void action_command_getTorrentsUpLimit(); - void action_command_getTorrentsDlLimit(); - void action_command_setTorrentsUpLimit(); - void action_command_setTorrentsDlLimit(); - void action_command_alternativeSpeedLimitsEnabled(); - void action_command_toggleAlternativeSpeedLimits(); - void action_command_toggleSequentialDownload(); - void action_command_toggleFirstLastPiecePrio(); - void action_command_setSuperSeeding(); - void action_command_setForceStart(); - void action_command_delete(); - void action_command_deletePerm(); - void action_command_increasePrio(); - void action_command_decreasePrio(); - void action_command_topPrio(); - void action_command_bottomPrio(); - void action_command_setLocation(); - void action_command_rename(); - void action_command_setAutoTMM(); - void action_command_recheck(); - void action_command_setCategory(); - void action_command_addCategory(); - void action_command_removeCategories(); - void action_command_getSavePath(); - void action_version_api(); - void action_version_api_min(); - void action_version_qbittorrent(); + void doProcessRequest(); + void configure(); - typedef void (WebApplication::*Action)(); + void registerAPIController(const QString &scope, APIController *controller); + void declarePublicAPI(const QString &apiPath); - QString scope_; - QString action_; - QStringList args_; + void sendFile(const QString &path); + void sendWebUIFile(); - void doProcessRequest() override; + // Session management + QString generateSid() const; + void sessionInitialize(); + bool isAuthNeeded(); + bool isPublicAPI(const QString &scope, const QString &action) const; - bool isPublicScope(); - void parsePath(); + bool isCrossSiteRequest(const Http::Request &request) const; + bool validateHostHeader(const QStringList &domains) const; - static QMap > initializeActions(); - static QMap > actions_; + // Persistent data + QMap m_sessions; + + // Current data + WebSession *m_currentSession = nullptr; + Http::Request m_request; + Http::Environment m_env; + + const QRegularExpression m_apiPathPattern {(QLatin1String("^/api/v2/(?[A-Za-z_][A-Za-z_0-9]*)/(?[A-Za-z_][A-Za-z_0-9]*)$"))}; + const QRegularExpression m_apiLegacyPathPattern {QLatin1String("^/(?((sync|control|query)/[A-Za-z_][A-Za-z_0-9]*|login|logout))(/(?[^/]+))?$")}; + + QHash m_apiControllers; + QSet m_publicAPIs; + bool m_isAltUIUsed = false; + QString m_rootFolder; + QStringList m_domainList; + + struct TranslatedFile + { + QByteArray data; + QDateTime lastModified; + }; + QMap m_translatedFiles; }; - -#endif // WEBAPPLICATION_H diff --git a/src/webui/webui.h b/src/webui/webui.h index c552405e5..55ab815e2 100644 --- a/src/webui/webui.h +++ b/src/webui/webui.h @@ -42,7 +42,7 @@ namespace Net class DNSUpdater; } -class AbstractWebApplication; +class WebApplication; class WebUI : public QObject { @@ -64,7 +64,7 @@ private: bool m_isErrored; QPointer m_httpServer; QPointer m_dnsUpdater; - QPointer m_webapp; + QPointer m_webapp; quint16 m_port; }; diff --git a/src/webui/webui.pri b/src/webui/webui.pri index e07add1fd..8d27a8d48 100644 --- a/src/webui/webui.pri +++ b/src/webui/webui.pri @@ -1,17 +1,30 @@ HEADERS += \ - $$PWD/abstractwebapplication.h \ - $$PWD/btjson.h \ + $$PWD/api/apicontroller.h \ + $$PWD/api/apierror.h \ + $$PWD/api/appcontroller.h \ + $$PWD/api/authcontroller.h \ + $$PWD/api/isessionmanager.h \ + $$PWD/api/logcontroller.h \ + $$PWD/api/rsscontroller.h \ + $$PWD/api/synccontroller.h \ + $$PWD/api/torrentscontroller.h \ + $$PWD/api/transfercontroller.h \ + $$PWD/api/serialize/serialize_torrent.h \ $$PWD/extra_translations.h \ - $$PWD/jsonutils.h \ - $$PWD/prefjson.h \ $$PWD/webapplication.h \ - $$PWD/websessiondata.h \ $$PWD/webui.h SOURCES += \ - $$PWD/abstractwebapplication.cpp \ - $$PWD/btjson.cpp \ - $$PWD/prefjson.cpp \ + $$PWD/api/apicontroller.cpp \ + $$PWD/api/apierror.cpp \ + $$PWD/api/appcontroller.cpp \ + $$PWD/api/authcontroller.cpp \ + $$PWD/api/logcontroller.cpp \ + $$PWD/api/rsscontroller.cpp \ + $$PWD/api/synccontroller.cpp \ + $$PWD/api/torrentscontroller.cpp \ + $$PWD/api/transfercontroller.cpp \ + $$PWD/api/serialize/serialize_torrent.cpp \ $$PWD/webapplication.cpp \ $$PWD/webui.cpp diff --git a/src/webui/webui.qrc b/src/webui/webui.qrc index c1e481195..048ff7909 100644 --- a/src/webui/webui.qrc +++ b/src/webui/webui.qrc @@ -1,47 +1,49 @@ + www/private/css/Core.css + www/private/css/dynamicTable.css + www/private/css/Layout.css + www/private/css/style.css + www/private/css/Tabs.css + www/private/css/Window.css + www/private/scripts/client.js + www/private/scripts/clipboard.min.js + www/private/scripts/contextmenu.js + www/private/scripts/download.js + www/private/scripts/dynamicTable.js + www/private/scripts/excanvas-compressed.js + www/private/scripts/misc.js + www/private/scripts/mocha.js + www/private/scripts/mocha-init.js + www/private/scripts/mocha-yc.js + www/private/scripts/mootools-1.2-core-yc.js + www/private/scripts/mootools-1.2-more.js + www/private/scripts/parametrics.js + www/private/scripts/progressbar.js + www/private/scripts/prop-files.js + www/private/scripts/prop-general.js + www/private/scripts/prop-trackers.js + www/private/scripts/prop-webseeds.js + www/private/about.html + www/private/addtrackers.html + www/private/confirmdeletion.html + www/private/download.html + www/private/downloadlimit.html + www/private/filters.html www/private/index.html - www/private/login.html - www/public/about.html - www/public/addtrackers.html - www/public/confirmdeletion.html - www/public/css/Core.css - www/public/css/dynamicTable.css - www/public/css/Layout.css + www/private/newcategory.html + www/private/preferences.html + www/private/preferences_content.html + www/private/properties.html + www/private/properties_content.html + www/private/rename.html + www/private/setlocation.html + www/private/statistics.html + www/private/transferlist.html + www/private/upload.html + www/private/uploadlimit.html www/public/css/style.css - www/public/css/Tabs.css - www/public/css/Window.css - www/public/download.html - www/public/downloadlimit.html - www/public/filters.html - www/public/newcategory.html - www/public/preferences.html - www/public/preferences_content.html - www/public/properties.html - www/public/properties_content.html - www/public/rename.html - www/public/scripts/client.js - www/public/scripts/clipboard.min.js - www/public/scripts/contextmenu.js - www/public/scripts/download.js - www/public/scripts/dynamicTable.js - www/public/scripts/excanvas-compressed.js - www/public/scripts/misc.js - www/public/scripts/mocha-init.js - www/public/scripts/mocha-yc.js - www/public/scripts/mocha.js www/public/scripts/mootools-1.2-core-yc.js - www/public/scripts/mootools-1.2-more.js - www/public/scripts/parametrics.js - www/public/scripts/progressbar.js - www/public/scripts/prop-files.js - www/public/scripts/prop-general.js - www/public/scripts/prop-trackers.js - www/public/scripts/prop-webseeds.js - www/public/setlocation.html - www/public/statistics.html - www/public/transferlist.html - www/public/upload.html - www/public/uploadlimit.html + www/public/login.html diff --git a/src/webui/www/public/about.html b/src/webui/www/private/about.html similarity index 92% rename from src/webui/www/public/about.html rename to src/webui/www/private/about.html index 84ee00ac9..4d976d6cf 100644 --- a/src/webui/www/public/about.html +++ b/src/webui/www/private/about.html @@ -1,4 +1,4 @@ - +qBittorrent Mascot

qBittorrent ${VERSION} QBT_TR(Web UI)QBT_TR[CONTEXT=OptionsDialog]

QBT_TR(An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.)QBT_TR[CONTEXT=about]

Copyright (c) 2011-2018 The qBittorrent project

diff --git a/src/webui/www/public/addtrackers.html b/src/webui/www/private/addtrackers.html similarity index 79% rename from src/webui/www/public/addtrackers.html rename to src/webui/www/private/addtrackers.html index c2f988571..a12141ff2 100644 --- a/src/webui/www/public/addtrackers.html +++ b/src/webui/www/private/addtrackers.html @@ -1,5 +1,5 @@ - - + + QBT_TR(Trackers addition dialog)QBT_TR[CONTEXT=TrackersAdditionDlg] @@ -13,9 +13,12 @@ new Event(e).stop(); var hash = new URI().getData('hash'); new Request({ - url: 'command/addTrackers', + url: 'api/v2/torrents/addTrackers', method: 'post', - data: {hash: hash, urls: $('trackersUrls').value}, + data: { + hash: hash, + urls: $('trackersUrls').value + }, onComplete: function() { window.parent.closeWindows(); } @@ -25,12 +28,12 @@ -
+

QBT_TR(List of trackers to add (one per line):)QBT_TR[CONTEXT=TrackersAdditionDlg]


-
+ diff --git a/src/webui/www/public/confirmdeletion.html b/src/webui/www/private/confirmdeletion.html similarity index 82% rename from src/webui/www/public/confirmdeletion.html rename to src/webui/www/private/confirmdeletion.html index 4640b03aa..2f5e9faea 100644 --- a/src/webui/www/public/confirmdeletion.html +++ b/src/webui/www/private/confirmdeletion.html @@ -1,5 +1,5 @@ - - + + QBT_TR(Deletion confirmation - qBittorrent)QBT_TR[CONTEXT=confirmDeletionDlg] @@ -17,14 +17,14 @@ $('confirmBtn').addEvent('click', function(e){ parent.torrentsTable.deselectAll(); new Event(e).stop(); - var cmd = 'command/delete'; - if($('deleteFromDiskCB').get('checked')) - cmd = 'command/deletePerm'; + var cmd = 'api/v2/torrents/delete'; + var deleteFiles = $('deleteFromDiskCB').get('checked'); new Request({ url: cmd, method: 'post', data: { - 'hashes': hashes.join('|') + 'hashes': hashes.join('|'), + 'deleteFiles': deleteFiles }, onComplete: function() { window.parent.closeWindows(); diff --git a/src/webui/www/public/css/Core.css b/src/webui/www/private/css/Core.css similarity index 100% rename from src/webui/www/public/css/Core.css rename to src/webui/www/private/css/Core.css diff --git a/src/webui/www/public/css/Layout.css b/src/webui/www/private/css/Layout.css similarity index 100% rename from src/webui/www/public/css/Layout.css rename to src/webui/www/private/css/Layout.css diff --git a/src/webui/www/public/css/Tabs.css b/src/webui/www/private/css/Tabs.css similarity index 100% rename from src/webui/www/public/css/Tabs.css rename to src/webui/www/private/css/Tabs.css diff --git a/src/webui/www/public/css/Window.css b/src/webui/www/private/css/Window.css similarity index 100% rename from src/webui/www/public/css/Window.css rename to src/webui/www/private/css/Window.css diff --git a/src/webui/www/public/css/dynamicTable.css b/src/webui/www/private/css/dynamicTable.css similarity index 100% rename from src/webui/www/public/css/dynamicTable.css rename to src/webui/www/private/css/dynamicTable.css diff --git a/src/webui/www/private/css/style.css b/src/webui/www/private/css/style.css new file mode 100644 index 000000000..f8c19ac85 --- /dev/null +++ b/src/webui/www/private/css/style.css @@ -0,0 +1,481 @@ +/* Reset */ + +/*ul,ol,dl,li,dt,dd,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,fieldset,input,object,iframe { margin: 0; padding: 0; }*/ +a img,:link img,:visited img { border: none; } +/*table { border-collapse: collapse; border-spacing: 0; }*/ +:focus { outline: none; } + +/* Structure */ + +body { + margin: 0; + text-align: left; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 18px; + color: #555; +} + +.aside { + width: 300px; +} + +.invisible { + display: none; +} + +/* Typography */ + +h2, h3, h4 { + margin: 0; + padding: 0 0 5px 0; + font-size: 12px; + font-weight: bold; + color: #333; +} + +h2 { + font-size: 14px; + color: #555; + font-weight: bold; +} + +#mochaPage h3 { + display: block; + font-size: 12px; + padding: 6px 0 6px 0; + margin: 0 0 8px 0; + border-bottom: 1px solid #bbb; +} + +#error_div { + color: #f00; + font-weight: bold; +} + +h4 { + font-size: 11px; +} + +a { + color: #e60; + text-decoration: none; + cursor: pointer; +} + +a:hover { + text-decoration: none; +} + +p { + margin: 0; + padding: 0 0 9px 0; +} + +/* List Elements */ + +ul { + list-style: outside; + margin: 0 0 9px 16px; +} + +dt { + font-weight: bold; +} + +dd { + padding: 0 0 9px 0; +} + +/* Code */ + +pre { + background-color: #f6f6f6; + color: #006600; + display: block; + font-family: 'Courier New', Courier, monospace; + font-size: 11px; + max-height: 250px; + overflow: auto; + margin: 0 0 10px 0; + padding: 10px; + border: 1px solid #d1d7dc; + } + +/* Dividers */ + +hr { + background-color: #ddd; + color: #ccc; + height: 1px; + border: 0px; +} + +.vcenter { + vertical-align: middle; +} + +#urls { + width:90%; + height:100%; +} + +#trackersUrls { + width:90%; + height:100%; +} + +#Filters ul { + list-style-type: none; +} + +#Filters ul li { + margin-left: -16px; +} + +#Filters ul img { + padding: 2px 4px; + vertical-align: middle; + width: 16px; + height: 16px; +} + +.selectedFilter { + background-color: #415A8D; + color: #FFFFFF; +} + +.selectedFilter a { + color: #FFFFFF; +} + +#properties { + background-color: #e5e5e5; +} + +a.propButton { + border: 1px solid rgb(85, 81, 91); + /*border-radius: 3px;*/ + padding: 2px; + margin-left: 3px; + margin-right: 3px; +} + +a.propButton img { + margin-bottom: -4px; +} + +.scrollableMenu { + overflow-y: auto; + overflow-x: hidden; +} + +/* context menu specific */ + +.contextMenu { border:1px solid #999; padding:0; background:#eee; list-style-type:none; display:none;} +.contextMenu .separator { border-top:1px solid #999; } +.contextMenu li { margin:0; padding:0;} +.contextMenu li a { + display: block; + padding: 5px 20px 5px 5px; + font-size: 12px; + text-decoration: none; + font-family: tahoma,arial,sans-serif; + color: #000; + white-space: nowrap; +} +.contextMenu li a:hover { background-color:#ddd; } +.contextMenu li a.disabled { color:#ccc; font-style:italic; } +.contextMenu li a.disabled:hover { background-color:#eee; } +.contextMenu li ul { + padding: 0; + border:1px solid #999; padding:0; background:#eee; + list-style-type:none; + position: absolute; + left: -999em; + z-index: 8000; + margin: -29px 0 0 100%; + width: 164px; +} +.contextMenu li ul li a { + position: relative; +} +.contextMenu li a.arrow-right, .contextMenu li a:hover.arrow-right { + background-image: url(../images/skin/arrow-right.gif); + background-repeat: no-repeat; + background-position: right center; +} +.contextMenu li:hover ul, +.contextMenu li.ieHover ul, +.contextMenu li li.ieHover ul, +.contextMenu li li li.ieHover ul, +.contextMenu li li:hover ul, +.contextMenu li li li:hover ul { /* lists nested under hovered list items */ + left: auto; +} + +.contextMenu li img { + width: 16px; + height: 16px; + margin-bottom: -4px; + -ms-interpolation-mode : bicubic; +} + +/* Sliders */ + +.slider { + clear: both; + position: relative; + font-size: 12px; + font-weight: bold; + width: 400px; + margin-bottom: 15px; +} + +.sliderWrapper { + position: relative; + font-size: 1px; + line-height: 1px; + height: 9px; + width: 422px; +} + +.sliderarea { + position: absolute; + top: 0; + left: 0; + height: 7px; + width: 420px; + font-size: 1px; + line-height: 1px; + background: #f2f2f2 url(../images/skin/slider-area.gif) repeat-x; + border: 1px solid #a3a3a3; + border-bottom: 1px solid #ccc; + border-left: 1px solid #ccc; + margin: 0; + padding: 0; + overflow: hidden; +} + +.sliderknob { + position: absolute; + top: 0; + left: 0; + height: 9px; + width: 19px; + font-size: 1px; + line-height: 1px; + background: url(../images/skin/knob.gif) no-repeat; + cursor: pointer; + overflow: hidden; + z-index: 2; +} + +.update { + padding-bottom: 5px; +} + +.mochaToolButton { + margin-right: 10px; +} + +/* Mocha Customization */ +#mochaToolbar { + margin-top: 5px; +} + +#mochaToolbar .divider { + background-image: url(../images/skin/toolbox-divider.gif); + background-repeat: no-repeat; + background-position: left center; + padding-left: 14px; + padding-top: 15px; +} + +.MyMenuIcon { + margin-left: -18px; + margin-bottom: -3px; + padding-right: 5px; +} + +/* Tri-state checkbox */ + +label.tristate { + background: url(../images/3-state-checkbox.gif) 0 0 no-repeat; + display: block; + float: left; + height: 13px; + margin: .15em 8px 5px 0px; + overflow: hidden; + text-indent: -999em; + width: 13px; +} + +label.checked { + background-position: 0 -13px; +} + +label.partial { + background-position: 0 -26px; +} + +fieldset.settings { + border: solid 1px black; + border-radius: 8px; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + padding: 4px 4px 4px 10px; +} + +fieldset.settings legend { + margin-left: 8px; + padding: 4px; + font-weight: bold; +} + +fieldset.settings label { + padding: 2px; +} + +fieldset.settings .leftLabelSmall { + width: 5em; + float: left; + text-align: right; + margin-right: 0.5em; + display: block; +} + +fieldset.settings .leftLabelLarge { + width: 14em; + float: left; + text-align: right; + margin-right: 0.5em; + display: block; +} + +div.formRow { + clear: left; + display: block; +} + +.filterTitle { + font-weight: bold; + text-transform: uppercase; + padding-left: 5px; +} + +ul.filterList { + margin: 0 0 0 16px; + padding-left: 0; +} + +ul.filterList a { + display: block; +} + +ul.filterList li:hover { + background-color: #e60; +} + +ul.filterList li:hover a { + color: white; +} + +td.generalLabel { + white-space: nowrap; + text-align: right; + width: 1px; + vertical-align: top; +} + +#filesTable { + line-height: 20px; +} + +#trackersTable, #webseedsTable { + line-height: 25px; +} + +#addTrackersPlus { + width: 16px; + cursor: pointer; + margin-bottom: -3px; +} + +.unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +#prop_general { + padding: 2px; +} + +#watched_folders_tab { + border-collapse: collapse; +} + +#watched_folders_tab td, #watched_folders_tab th { + padding: 2px 4px; + border: 1px solid black; +} + +.select-watched-folder-editable { + position:relative; + background-color: white; + border: solid grey 1px; + width: 160px; + height: 20px; +} + +.select-watched-folder-editable select { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + border: none; + width: 160px; + margin: 0; +} + +.select-watched-folder-editable input { + position: absolute; + top: 0px; + left: 0px; + width: 140px; + padding: 1px; + border: none; +} + +.select-watched-folder-editable select:focus, .select-editable input:focus { + outline: none; +} + +/* + * Workaround to prevent the transfer list from + * disappearing when zooming in the browser. + */ +#filtersColumn_handle { + margin-left: -1px; +} + +#error_div { + float: left; + font-size: 14px; +} + +.combo_priority { + font-size: 1em; +} + +td.statusBarSeparator { + width: 22px; + background-image: url('../images/skin/toolbox-divider.gif'); + background-repeat: no-repeat; + background-position: center 1px; + background-size: 2px 18px; +} diff --git a/src/webui/www/public/download.html b/src/webui/www/private/download.html similarity index 80% rename from src/webui/www/public/download.html rename to src/webui/www/private/download.html index 267c89253..c42cb1f46 100644 --- a/src/webui/www/public/download.html +++ b/src/webui/www/private/download.html @@ -1,5 +1,5 @@ - - + + QBT_TR(Add Torrent Links)QBT_TR[CONTEXT=downloadFromURL] @@ -10,8 +10,8 @@ -
-
+ +

QBT_TR(Download Torrents from their URLs or Magnet links)QBT_TR[CONTEXT=HttpServer]

@@ -27,7 +27,7 @@
- +
@@ -40,33 +40,33 @@
- +
- -
+
- +
- +
- +
- +
-
+ +