From 8a65dbaa4fc6ee2fcea0a7e389fe6b012d3f7b63 Mon Sep 17 00:00:00 2001 From: "Vladimir Golovnev (Glassez)" Date: Fri, 22 Aug 2014 23:08:44 +0400 Subject: [PATCH] WebUI core redesign. --- src/preferences/preferences.cpp | 6 - src/preferences/preferences.h | 1 - src/qtlibtorrent/qbtsession.cpp | 2 - src/tracker/qtracker.cpp | 53 +-- src/tracker/qtracker.h | 2 +- src/webui/abstractrequesthandler.cpp | 174 +++++++ src/webui/abstractrequesthandler.h | 89 ++++ src/webui/extra_translations.h | 75 +++ src/webui/httpconnection.cpp | 682 +++------------------------ src/webui/httpconnection.h | 46 +- src/webui/httpserver.cpp | 262 +--------- src/webui/httpserver.h | 43 +- src/webui/jsonutils.h | 4 +- src/webui/requesthandler.cpp | 588 +++++++++++++++++++++++ src/webui/requesthandler.h | 100 ++++ src/webui/webapplication.cpp | 309 ++++++++++++ src/webui/webapplication.h | 87 ++++ src/webui/webui.pri | 2 + 18 files changed, 1533 insertions(+), 992 deletions(-) create mode 100644 src/webui/abstractrequesthandler.cpp create mode 100644 src/webui/abstractrequesthandler.h create mode 100644 src/webui/extra_translations.h create mode 100644 src/webui/requesthandler.cpp create mode 100644 src/webui/requesthandler.h create mode 100644 src/webui/webapplication.cpp create mode 100644 src/webui/webapplication.h diff --git a/src/preferences/preferences.cpp b/src/preferences/preferences.cpp index 2fcbe4cf2..542c9d312 100644 --- a/src/preferences/preferences.cpp +++ b/src/preferences/preferences.cpp @@ -951,7 +951,6 @@ QString Preferences::getWebUiPassword() const { QString pass_ha1 = value("Preferences/WebUI/Password_ha1").toString(); if (pass_ha1.isEmpty()) { QCryptographicHash md5(QCryptographicHash::Md5); - md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":"); md5.addData("adminadmin"); pass_ha1 = md5.result().toHex(); } @@ -959,13 +958,8 @@ QString Preferences::getWebUiPassword() const { } void Preferences::setWebUiPassword(const QString &new_password) { - // Get current password md5 - QString current_pass_md5 = getWebUiPassword(); - // Check if password did not change - if (current_pass_md5 == new_password) return; // Encode to md5 and save QCryptographicHash md5(QCryptographicHash::Md5); - md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":"); md5.addData(new_password.toLocal8Bit()); setValue("Preferences/WebUI/Password_ha1", md5.result().toHex()); diff --git a/src/preferences/preferences.h b/src/preferences/preferences.h index 272288890..2d8718201 100755 --- a/src/preferences/preferences.h +++ b/src/preferences/preferences.h @@ -44,7 +44,6 @@ #include -#define QBT_REALM "Web UI Access" enum scheduler_days { EVERY_DAY, WEEK_DAYS, WEEK_ENDS, MON, TUE, WED, THU, FRI, SAT, SUN }; enum maxRatioAction {PAUSE_ACTION, REMOVE_ACTION}; namespace Proxy { diff --git a/src/qtlibtorrent/qbtsession.cpp b/src/qtlibtorrent/qbtsession.cpp index 502c310bb..00021832c 100755 --- a/src/qtlibtorrent/qbtsession.cpp +++ b/src/qtlibtorrent/qbtsession.cpp @@ -671,8 +671,6 @@ void QBtSession::initWebUi() { } #endif - httpServer->setAuthorization(username, password); - httpServer->setlocalAuthEnabled(pref->isWebUiLocalAuthEnabled()); if (!httpServer->isListening()) { bool success = httpServer->listen(QHostAddress::Any, port); if (success) diff --git a/src/tracker/qtracker.cpp b/src/tracker/qtracker.cpp index cfab904f3..0e334ccda 100644 --- a/src/tracker/qtracker.cpp +++ b/src/tracker/qtracker.cpp @@ -29,18 +29,14 @@ */ #include -#include -#if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) -#include -#endif #include #include -#include "httprequestheader.h" -#include "httpresponseheader.h" #include "qtracker.h" #include "preferences.h" +#include "httpresponsegenerator.h" +#include "httprequestparser.h" #include @@ -93,53 +89,38 @@ void QTracker::readRequest() QTcpSocket *socket = static_cast(sender()); QByteArray input = socket->readAll(); //qDebug("QTracker: Raw request:\n%s", input.data()); - HttpRequestHeader http_request(input); - if (!http_request.isValid()) { - qDebug("QTracker: Invalid HTTP Request:\n %s", qPrintable(http_request.toString())); + HttpRequest request; + if (HttpRequestParser::parse(input, request) != HttpRequestParser::NoError) { + qDebug("QTracker: Invalid HTTP Request:\n %s", qPrintable(input)); respondInvalidRequest(socket, 100, "Invalid request type"); return; } //qDebug("QTracker received the following request:\n%s", qPrintable(parser.toString())); // Request is correct, is it a GET request? - if (http_request.method() != "GET") { - qDebug("QTracker: Unsupported HTTP request: %s", qPrintable(http_request.method())); + if (request.method != "GET") { + qDebug("QTracker: Unsupported HTTP request: %s", qPrintable(request.method)); respondInvalidRequest(socket, 100, "Invalid request type"); return; } - if (!http_request.path().startsWith("/announce", Qt::CaseInsensitive)) { - qDebug("QTracker: Unrecognized path: %s", qPrintable(http_request.path())); + if (!request.path.startsWith("/announce", Qt::CaseInsensitive)) { + qDebug("QTracker: Unrecognized path: %s", qPrintable(request.path)); respondInvalidRequest(socket, 100, "Invalid request type"); return; } // OK, this is a GET request - // Parse GET parameters - QHash get_parameters; - QUrl url = QUrl::fromEncoded(http_request.path().toLatin1()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) - QUrlQuery query(url); - QListIterator > i(query.queryItems()); -#else - QListIterator > i(url.queryItems()); -#endif - while (i.hasNext()) { - QPair pair = i.next(); - get_parameters[pair.first] = pair.second; - } - - respondToAnnounceRequest(socket, get_parameters); + respondToAnnounceRequest(socket, request.gets); } void QTracker::respondInvalidRequest(QTcpSocket *socket, int code, QString msg) { - HttpResponseHeader response; - response.setStatusLine(code, msg); - socket->write(response.toString().toLocal8Bit()); + HttpResponse response(code, msg); + socket->write(HttpResponseGenerator::generate(response)); socket->disconnectFromHost(); } void QTracker::respondToAnnounceRequest(QTcpSocket *socket, - const QHash& get_parameters) + const QMap& get_parameters) { TrackerAnnounceRequest annonce_req; // IP @@ -246,12 +227,12 @@ void QTracker::ReplyWithPeerList(QTcpSocket *socket, const TrackerAnnounceReques // bencode std::vector buf; bencode(std::back_inserter(buf), reply_entry); - QByteArray reply(&buf[0], buf.size()); + QByteArray reply(&buf[0], static_cast(buf.size())); qDebug("QTracker: reply with the following bencoded data:\n %s", reply.constData()); // HTTP reply - HttpResponseHeader response; - response.setStatusLine(200, "OK"); - socket->write(response.toString().toLocal8Bit() + reply); + HttpResponse response(200, "OK"); + response.content = reply; + socket->write(HttpResponseGenerator::generate(response)); socket->disconnectFromHost(); } diff --git a/src/tracker/qtracker.h b/src/tracker/qtracker.h index 4d76bc91e..bf313ba18 100644 --- a/src/tracker/qtracker.h +++ b/src/tracker/qtracker.h @@ -61,7 +61,7 @@ protected slots: void readRequest(); void handlePeerConnection(); void respondInvalidRequest(QTcpSocket *socket, int code, QString msg); - void respondToAnnounceRequest(QTcpSocket *socket, const QHash& get_parameters); + void respondToAnnounceRequest(QTcpSocket *socket, const QMap& get_parameters); void ReplyWithPeerList(QTcpSocket *socket, const TrackerAnnounceRequest &annonce_req); private: diff --git a/src/webui/abstractrequesthandler.cpp b/src/webui/abstractrequesthandler.cpp new file mode 100644 index 000000000..8e6d97d19 --- /dev/null +++ b/src/webui/abstractrequesthandler.cpp @@ -0,0 +1,174 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2014 Vladimir Golovnev + * 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. + */ + +#include +#include +#include +#include +#include "preferences.h" +#include "webapplication.h" +#include "abstractrequesthandler.h" + +AbstractRequestHandler::AbstractRequestHandler(const HttpRequest &request, const HttpEnvironment &env, WebApplication *app) + : app_(app), session_(0), request_(request), env_(env) +{ + if (isBanned()) + { + status(403, "Forbidden"); + print(QObject::tr("Your IP address has been banned after too many failed authentication attempts.")); + return; + } + + sessionInitialize(); + + if (!sessionActive() && !isAuthNeeded()) sessionStart(); +} + +HttpResponse AbstractRequestHandler::run() +{ + response_ = HttpResponse(); + processRequest(); + return response_; +} + +bool AbstractRequestHandler::isAuthNeeded() +{ + return + ( + env_.clientAddress != QHostAddress::LocalHost && + env_.clientAddress != QHostAddress::LocalHostIPv6 + ) || Preferences::instance()->isWebUiLocalAuthEnabled(); +} + +void AbstractRequestHandler::status(uint code, const QString& text) +{ + response_.status = HttpResponseStatus(code, text); +} + +void AbstractRequestHandler::header(const QString& name, const QString& value) +{ + response_.headers[name] = value; +} + +void AbstractRequestHandler::print(const QString& text, const QString& type) +{ + print_impl(text.toUtf8(), type); +} + +void AbstractRequestHandler::print(const QByteArray& data, const QString& type) +{ + print_impl(data, type); +} + +void AbstractRequestHandler::print_impl(const QByteArray& data, const QString& type) +{ + if (!response_.headers.contains(HEADER_CONTENT_TYPE)) + response_.headers[HEADER_CONTENT_TYPE] = type; + + response_.content += data; +} + +void AbstractRequestHandler::printFile(const QString& path) +{ + QByteArray data; + QString type; + + if (!app_->readFile(path, data, type)) + { + status(404, "Not Found"); + return; + } + + print(data, type); +} + +void AbstractRequestHandler::sessionInitialize() +{ + app_->sessionInitialize(this); +} + +void AbstractRequestHandler::sessionStart() +{ + if (app_->sessionStart(this)) + { + QNetworkCookie cookie(C_SID.toUtf8(), session_->id.toUtf8()); + cookie.setPath("/"); + header(HEADER_SET_COOKIE, cookie.toRawForm()); + } +} + +void AbstractRequestHandler::sessionEnd() +{ + if (sessionActive()) + { + QNetworkCookie cookie(C_SID.toUtf8(), session_->id.toUtf8()); + cookie.setPath("/"); + cookie.setExpirationDate(QDateTime::currentDateTime()); + + if (app_->sessionEnd(this)) + { + header(HEADER_SET_COOKIE, cookie.toRawForm()); + } + } +} + +bool AbstractRequestHandler::isBanned() const +{ + return app_->isBanned(this); +} + +int AbstractRequestHandler::failedAttempts() const +{ + return app_->failedAttempts(this); +} + +void AbstractRequestHandler::resetFailedAttempts() +{ + app_->resetFailedAttempts(this); +} + +void AbstractRequestHandler::increaseFailedAttempts() +{ + app_->increaseFailedAttempts(this); +} + +QString AbstractRequestHandler::saveTmpFile(const QByteArray &data) +{ + QTemporaryFile tmpfile(QDir::temp().absoluteFilePath("qBT-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(); +} diff --git a/src/webui/abstractrequesthandler.h b/src/webui/abstractrequesthandler.h new file mode 100644 index 000000000..9f2c3b605 --- /dev/null +++ b/src/webui/abstractrequesthandler.h @@ -0,0 +1,89 @@ +/* + * 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 ABSTRACTREQUESTHANDLER_H +#define ABSTRACTREQUESTHANDLER_H + +#include +#include "httptypes.h" + +class WebApplication; +struct WebSession; + +class AbstractRequestHandler +{ + friend class WebApplication; + +public: + AbstractRequestHandler( + const HttpRequest& request, const HttpEnvironment& env, + WebApplication* app); + + HttpResponse run(); + +protected: + virtual void processRequest() = 0; + + void status(uint code, const QString& text); + void header(const QString& name, const QString& value); + void print(const QString& text, const QString& type = CONTENT_TYPE_HTML); + void print(const QByteArray& data, const QString& type = CONTENT_TYPE_HTML); + void printFile(const QString& path); + + // Session management + bool sessionActive() const { return session_ != 0; } + void sessionInitialize(); + void sessionStart(); + void sessionEnd(); + + // Ban management + bool isBanned() const; + int failedAttempts() const; + void resetFailedAttempts(); + void increaseFailedAttempts(); + + bool isAuthNeeded(); + + // save data to temporary file on disk and return its name (or empty string if fails) + static QString saveTmpFile(const QByteArray& data); + + inline WebSession* session() { return session_; } + inline HttpRequest request() const { return request_; } + inline HttpEnvironment env() const { return env_; } + +private: + WebApplication* app_; + WebSession* session_; + const HttpRequest request_; + const HttpEnvironment env_; + HttpResponse response_; + + void print_impl(const QByteArray& data, const QString& type); +}; + +#endif // ABSTRACTREQUESTHANDLER_H diff --git a/src/webui/extra_translations.h b/src/webui/extra_translations.h new file mode 100644 index 000000000..bd9018ca4 --- /dev/null +++ b/src/webui/extra_translations.h @@ -0,0 +1,75 @@ +/* + * 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 EXTRA_TRANSLATIONS_H +#define EXTRA_TRANSLATIONS_H + +#include + +// Additional translations for Web UI +static const char *__TRANSLATIONS__[] = { + QT_TRANSLATE_NOOP("HttpServer", "File"), + QT_TRANSLATE_NOOP("HttpServer", "Edit"), + QT_TRANSLATE_NOOP("HttpServer", "Help"), + QT_TRANSLATE_NOOP("HttpServer", "Download Torrents from their URL or Magnet link"), + QT_TRANSLATE_NOOP("HttpServer", "Only one link per line"), + QT_TRANSLATE_NOOP("HttpServer", "Download local torrent"), + QT_TRANSLATE_NOOP("HttpServer", "Torrent files were correctly added to download list."), + QT_TRANSLATE_NOOP("HttpServer", "Point to torrent file"), + QT_TRANSLATE_NOOP("HttpServer", "Download"), + QT_TRANSLATE_NOOP("HttpServer", "Are you sure you want to delete the selected torrents from the transfer list and hard disk?"), + QT_TRANSLATE_NOOP("HttpServer", "Download rate limit must be greater than 0 or disabled."), + QT_TRANSLATE_NOOP("HttpServer", "Upload rate limit must be greater than 0 or disabled."), + QT_TRANSLATE_NOOP("HttpServer", "Maximum number of connections limit must be greater than 0 or disabled."), + QT_TRANSLATE_NOOP("HttpServer", "Maximum number of connections per torrent limit must be greater than 0 or disabled."), + QT_TRANSLATE_NOOP("HttpServer", "Maximum number of upload slots per torrent limit must be greater than 0 or disabled."), + QT_TRANSLATE_NOOP("HttpServer", "Unable to save program preferences, qBittorrent is probably unreachable."), + QT_TRANSLATE_NOOP("HttpServer", "Language"), + QT_TRANSLATE_NOOP("HttpServer", "The port used for incoming connections must be greater than 1024 and less than 65535."), + QT_TRANSLATE_NOOP("HttpServer", "The port used for the Web UI must be greater than 1024 and less than 65535."), + QT_TRANSLATE_NOOP("HttpServer", "The Web UI username must be at least 3 characters long."), + QT_TRANSLATE_NOOP("HttpServer", "The Web UI password must be at least 3 characters long."), + QT_TRANSLATE_NOOP("HttpServer", "Save"), + QT_TRANSLATE_NOOP("HttpServer", "qBittorrent client is not reachable"), + QT_TRANSLATE_NOOP("HttpServer", "HTTP Server"), + QT_TRANSLATE_NOOP("HttpServer", "The following parameters are supported:"), + QT_TRANSLATE_NOOP("HttpServer", "Torrent path"), + QT_TRANSLATE_NOOP("HttpServer", "Torrent name"), + QT_TRANSLATE_NOOP("HttpServer", "qBittorrent has been shutdown."), + QT_TRANSLATE_NOOP("HttpServer", "Unable to log in, qBittorrent is probably unreachable."), + QT_TRANSLATE_NOOP("HttpServer", "Invalid Username or Password."), + QT_TRANSLATE_NOOP("HttpServer", "Password"), + QT_TRANSLATE_NOOP("HttpServer", "Login"), + QT_TRANSLATE_NOOP("HttpServer", "qBittorrent web User Interface") +}; + +static const struct { const char *source; const char *comment; } __COMMENTED_TRANSLATIONS__[] = { + QT_TRANSLATE_NOOP3("HttpServer", "Downloaded", "Is the file downloaded or not?") +}; + +#endif // EXTRA_TRANSLATIONS_H diff --git a/src/webui/httpconnection.cpp b/src/webui/httpconnection.cpp index 00ac1c636..196b1f5e7 100644 --- a/src/webui/httpconnection.cpp +++ b/src/webui/httpconnection.cpp @@ -1,5 +1,6 @@ /* - * Bittorrent Client using Qt4 and libtorrent. + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2014 Vladimir Golovnev * Copyright (C) 2006 Ishan Arora and Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -24,661 +25,84 @@ * 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 "httpconnection.h" -#include "httpserver.h" -#include "httprequestheader.h" -#include "httpresponseheader.h" -#include "preferences.h" -#include "btjson.h" -#include "prefjson.h" -#include "qbtsession.h" -#include "misc.h" -#include "fs_utils.h" -#ifndef DISABLE_GUI -#include "iconprovider.h" -#endif #include -#include -#include -#include #include -#include -#include -#include -#include - -#include - -using namespace libtorrent; +#include "httptypes.h" +#include "httpserver.h" +#include "httprequestparser.h" +#include "httpresponsegenerator.h" +#include "webapplication.h" +#include "requesthandler.h" +#include "httpconnection.h" -HttpConnection::HttpConnection(QTcpSocket *socket, HttpServer *parent) - : QObject(parent), m_socket(socket), m_httpserver(parent) +HttpConnection::HttpConnection(QTcpSocket *socket, HttpServer *httpserver) + : QObject(httpserver), m_socket(socket) { m_socket->setParent(this); connect(m_socket, SIGNAL(readyRead()), SLOT(read())); connect(m_socket, SIGNAL(disconnected()), SLOT(deleteLater())); } -HttpConnection::~HttpConnection() { +HttpConnection::~HttpConnection() +{ delete m_socket; } -void HttpConnection::processDownloadedFile(const QString &url, - const QString &file_path) { - qDebug("URL %s successfully downloaded !", qPrintable(url)); - emit torrentReadyToBeDownloaded(file_path, false, url, false); -} - -void HttpConnection::handleDownloadFailure(const QString& url, - const QString& reason) { - std::cerr << "Could not download " << qPrintable(url) << ", reason: " - << qPrintable(reason) << std::endl; -} - void HttpConnection::read() { m_receivedData.append(m_socket->readAll()); - // Parse HTTP request header - const int header_end = m_receivedData.indexOf("\r\n\r\n"); - if (header_end < 0) { - qDebug() << "Partial request: \n" << m_receivedData; + HttpRequest request; + HttpRequestParser::ErrorCode err = HttpRequestParser::parse(m_receivedData, request); + switch (err) + { + case HttpRequestParser::IncompleteRequest: // Partial request waiting for the rest - return; - } - - const QByteArray header = m_receivedData.left(header_end); - m_parser.writeHeader(header); - if (m_parser.isError()) { - qWarning() << Q_FUNC_INFO << "header parsing error"; - m_receivedData.clear(); - m_generator.setStatusLine(400, "Bad Request"); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - - // Parse HTTP request message - if (m_parser.header().hasContentLength()) { - const int expected_length = m_parser.header().contentLength(); - QByteArray message = m_receivedData.mid(header_end + 4, expected_length); - - if (expected_length > 10000000 /* ~10MB */) { - qWarning() << "Bad request: message too long"; - m_generator.setStatusLine(400, "Bad Request"); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - m_receivedData.clear(); - write(); - return; - } - - if (message.length() < expected_length) { - // Message too short, waiting for the rest - qDebug() << "Partial message:\n" << message; - return; - } - - m_parser.writeMessage(message); - m_receivedData = m_receivedData.mid(header_end + 4 + expected_length); - } else { - m_receivedData.clear(); - } - - if (m_parser.isError()) { - qWarning() << Q_FUNC_INFO << "message parsing error"; - m_generator.setStatusLine(400, "Bad Request"); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - } else { - respond(); - } -} - -void HttpConnection::write() + break; + case HttpRequestParser::BadRequest: + write(HttpResponse(400, "Bad Request")); + break; + case HttpRequestParser::NoError: + HttpEnvironment env; + env.clientAddress = m_socket->peerAddress(); + HttpResponse response = RequestHandler(request, env, WebApplication::instance()).run(); + if (acceptsGzipEncoding(request.headers["accept-encoding"])) + response.headers[HEADER_CONTENT_ENCODING] = "gzip"; + write(response); + break; + } +} + +void HttpConnection::write(const HttpResponse& response) { - m_socket->write(m_generator.toByteArray()); + m_socket->write(HttpResponseGenerator::generate(response)); m_socket->disconnectFromHost(); } -void HttpConnection::translateDocument(QString& data) { - static QRegExp regex(QString::fromUtf8("_\\(([\\w\\s?!:\\/\\(\\),%µ&\\-\\.]+)\\)")); - static QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?"); - const std::string contexts[] = {"TransferListFiltersWidget", "TransferListWidget", - "PropertiesWidget", "MainWindow", "HttpServer", - "confirmDeletionDlg", "TrackerList", "TorrentFilesModel", - "options_imp", "Preferences", "TrackersAdditionDlg", - "ScanFoldersModel", "PropTabBar", "TorrentModel", - "downloadFromURL", "misc"}; - const size_t context_count = sizeof(contexts)/sizeof(contexts[0]); - 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) { - size_t context_index = 0; - while(context_index < context_count && translation == word) { -#if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0)) - translation = qApp->translate(contexts[context_index].c_str(), word.constData(), 0, QCoreApplication::UnicodeUTF8, 1); -#else - translation = qApp->translate(contexts[context_index].c_str(), word.constData(), 0, 1); -#endif - ++context_index; - } - } - // Remove keyboard shortcuts - translation.replace(mnemonic, ""); - - data.replace(i, regex.matchedLength(), translation); - i += translation.length(); - } else { - found = false; // no more translatable strings - } - } -} - -void HttpConnection::respond() { - if ((m_socket->peerAddress() != QHostAddress::LocalHost - && m_socket->peerAddress() != QHostAddress::LocalHostIPv6) - || m_httpserver->isLocalAuthEnabled()) { - // Authentication - const QString peer_ip = m_socket->peerAddress().toString(); - const int nb_fail = m_httpserver->NbFailedAttemptsForIp(peer_ip); - if (nb_fail >= MAX_AUTH_FAILED_ATTEMPTS) { - m_generator.setStatusLine(403, "Forbidden"); - m_generator.setMessage(tr("Your IP address has been banned after too many failed authentication attempts.")); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - QString auth = m_parser.header().value("Authorization"); - if (auth.isEmpty()) { - // Return unauthorized header - qDebug("Auth is Empty..."); - m_generator.setStatusLine(401, "Unauthorized"); - m_generator.setValue("WWW-Authenticate", "Digest realm=\""+QString(QBT_REALM)+"\", nonce=\""+m_httpserver->generateNonce()+"\", opaque=\""+m_httpserver->generateNonce()+"\", stale=\"false\", algorithm=\"MD5\", qop=\"auth\""); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - //qDebug("Auth: %s", qPrintable(auth.split(" ").first())); - if (QString::compare(auth.split(" ").first(), "Digest", Qt::CaseInsensitive) != 0 - || !m_httpserver->isAuthorized(auth.toUtf8(), m_parser.header().method())) { - // Update failed attempt counter - m_httpserver->increaseNbFailedAttemptsForIp(peer_ip); - qDebug("client IP: %s (%d failed attempts)", qPrintable(peer_ip), nb_fail); - // Return unauthorized header - m_generator.setStatusLine(401, "Unauthorized"); - m_generator.setValue("WWW-Authenticate", "Digest realm=\""+QString(QBT_REALM)+"\", nonce=\""+m_httpserver->generateNonce()+"\", opaque=\""+m_httpserver->generateNonce()+"\", stale=\"false\", algorithm=\"MD5\", qop=\"auth\""); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - // Client successfully authenticated, reset number of failed attempts - m_httpserver->resetNbFailedAttemptsForIp(peer_ip); - } - QString url = m_parser.url(); - // Favicon - if (url.endsWith("favicon.ico")) { - qDebug("Returning favicon"); - QFile favicon(":/Icons/skin/qbittorrent16.png"); - if (favicon.open(QIODevice::ReadOnly)) { - const QByteArray data = favicon.readAll(); - favicon.close(); - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("png"); - m_generator.setMessage(data); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - } else { - respondNotFound(); - } - return; - } - - QStringList list = url.split('/', QString::SkipEmptyParts); - if (list.contains(".") || list.contains("..")) { - respondNotFound(); - return; - } +bool HttpConnection::acceptsGzipEncoding(const QString& encoding) +{ + int pos = encoding.indexOf("gzip", 0, Qt::CaseInsensitive); + if (pos == -1) + return false; - if (list.isEmpty()) - list.append("index.html"); + // Let's see if there's a qvalue of 0.0 following + if (encoding[pos + 4] != ';') //there isn't, so it accepts gzip anyway + return true; - if (list.size() >= 2) { - if (list[0] == "json") { - if (list[1] == "torrents") { - respondTorrentsJson(); - return; - } - if (list.size() > 2) { - if (list[1] == "propertiesGeneral") { - const QString& hash = list[2]; - respondGenPropertiesJson(hash); - return; - } - if (list[1] == "propertiesTrackers") { - const QString& hash = list[2]; - respondTrackersPropertiesJson(hash); - return; - } - if (list[1] == "propertiesFiles") { - const QString& hash = list[2]; - respondFilesPropertiesJson(hash); - return; - } - } else { - if (list[1] == "preferences") { - respondPreferencesJson(); - return; - } else { - if (list[1] == "transferInfo") { - respondGlobalTransferInfoJson(); - return; - } - } - } - } - if (list[0] == "command") { - const QString& command = list[1]; - if (command == "shutdown") { - qDebug() << "Shutdown request from Web UI"; - // Special case handling for shutdown, we - // need to reply to the Web UI before - // actually shutting down. - m_generator.setStatusLine(200, "OK"); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - qApp->processEvents(); - // Exit application - qApp->exit(); - } else { - respondCommand(command); - m_generator.setStatusLine(200, "OK"); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - } - return; - } - } + //So let's find = and the next comma + pos = encoding.indexOf("=", pos + 4, Qt::CaseInsensitive); + int comma_pos = encoding.indexOf(",", pos, Qt::CaseInsensitive); - // Icons from theme - //qDebug() << "list[0]" << list[0]; - if (list[0] == "theme" && list.size() == 2) { -#ifdef DISABLE_GUI - url = ":/Icons/oxygen/"+list[1]+".png"; -#else - url = IconProvider::instance()->getIconPath(list[1]); -#endif - qDebug() << "There icon:" << url; - } else { - if (list[0] == "images") { - list[0] = "Icons"; - } else { - if (list.last().endsWith(".html")) - list.prepend("html"); - list.prepend("webui"); - } - url = ":/" + list.join("/"); - } - QFile file(url); - if (!file.open(QIODevice::ReadOnly)) { - qDebug("File %s was not found!", qPrintable(url)); - respondNotFound(); - return; - } - QString ext = list.last(); - int index = ext.lastIndexOf('.') + 1; - if (index > 0) - ext.remove(0, index); + QString value; + if (comma_pos == -1) + value = encoding.mid(pos + 1, comma_pos); else - ext.clear(); - QByteArray data = file.readAll(); - file.close(); - - // Translate the page - if (ext == "html" || (ext == "js" && !list.last().startsWith("excanvas"))) { - QString dataStr = QString::fromUtf8(data.constData()); - translateDocument(dataStr); - if (url.endsWith("about.html")) { - dataStr.replace("${VERSION}", VERSION); - } - data = dataStr.toUtf8(); - } - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt(ext); - m_generator.setMessage(data); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} - -void HttpConnection::respondNotFound() { - m_generator.setStatusLine(404, "File not found"); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} + value = encoding.mid(pos + 1, comma_pos - (pos + 1)); -void HttpConnection::respondTorrentsJson() { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("js"); - m_generator.setMessage(btjson::getTorrents()); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} - -void HttpConnection::respondGenPropertiesJson(const QString& hash) { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("js"); - m_generator.setMessage(btjson::getPropertiesForTorrent(hash)); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} - -void HttpConnection::respondTrackersPropertiesJson(const QString& hash) { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("js"); - m_generator.setMessage(btjson::getTrackersForTorrent(hash)); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} - -void HttpConnection::respondFilesPropertiesJson(const QString& hash) { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("js"); - m_generator.setMessage(btjson::getFilesForTorrent(hash)); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} - -void HttpConnection::respondPreferencesJson() { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("js"); - m_generator.setMessage(prefjson::getPreferences()); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} + if (value.toDouble() == 0.0) + return false; -void HttpConnection::respondGlobalTransferInfoJson() { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("js"); - m_generator.setMessage(btjson::getTransferInfo()); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); -} - -void HttpConnection::respondCommand(const QString& command) { - qDebug() << Q_FUNC_INFO << command; - if (command == "download") { - QString urls = m_parser.post("urls"); - QStringList list = urls.split('\n'); - foreach (QString url, list) { - url = url.trimmed(); - if (!url.isEmpty()) { - if (url.startsWith("bc://bt/", Qt::CaseInsensitive)) { - qDebug("Converting bc link to magnet link"); - url = misc::bcLinkToMagnet(url); - } - if (url.startsWith("magnet:", Qt::CaseInsensitive)) { - emit MagnetReadyToBeDownloaded(url); - } else { - qDebug("Downloading url: %s", qPrintable(url)); - emit UrlReadyToBeDownloaded(url); - } - } - } - return; - } - - if (command == "addTrackers") { - QString hash = m_parser.post("hash"); - if (!hash.isEmpty()) { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid() && h.has_metadata()) { - QString urls = m_parser.post("urls"); - QStringList list = urls.split('\n'); - foreach (const QString& url, list) { - announce_entry e(url.toStdString()); - h.add_tracker(e); - } - } - } - return; - } - if (command == "upload") { - qDebug() << Q_FUNC_INFO << "upload"; - const QList& torrents = m_parser.torrents(); - foreach(const QByteArray& torrentContent, torrents) { - // Get a unique filename - QTemporaryFile *tmpfile = new QTemporaryFile(QDir::temp().absoluteFilePath("qBT-XXXXXX.torrent")); - tmpfile->setAutoRemove(false); - if (tmpfile->open()) { - QString filePath = tmpfile->fileName(); - tmpfile->write(torrentContent); - tmpfile->close(); - // XXX: tmpfile needs to be deleted on Windows before using the file - // or it will complain that the file is used by another process. - delete tmpfile; - emit torrentReadyToBeDownloaded(filePath, false, QString(), false); - // Clean up - fsutils::forceRemove(filePath); - } else { - std::cerr << "I/O Error: Could not create temporary file" << std::endl; - delete tmpfile; - return; - } - } - // Prepare response - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("html"); - m_generator.setMessage(QString("")); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - if (command == "resumeall") { - emit resumeAllTorrents(); - return; - } - if (command == "pauseall") { - emit pauseAllTorrents(); - return; - } - if (command == "resume") { - emit resumeTorrent(m_parser.post("hash")); - return; - } - if (command == "setPreferences") { - prefjson::setPreferences(m_parser.post("json")); - return; - } - if (command == "setFilePrio") { - QString hash = m_parser.post("hash"); - int file_id = m_parser.post("id").toInt(); - int priority = m_parser.post("priority").toInt(); - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid() && h.has_metadata()) { - h.file_priority(file_id, priority); - } - return; - } - if (command == "getGlobalUpLimit") { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("html"); - m_generator.setMessage(QByteArray::number(QBtSession::instance()->getSession()->settings().upload_rate_limit)); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - if (command == "getGlobalDlLimit") { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("html"); - m_generator.setMessage(QByteArray::number(QBtSession::instance()->getSession()->settings().download_rate_limit)); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - return; - } - if (command == "getTorrentUpLimit") { - QString hash = m_parser.post("hash"); - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid()) { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("html"); - m_generator.setMessage(QByteArray::number(h.upload_limit())); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - } - return; - } - if (command == "getTorrentDlLimit") { - QString hash = m_parser.post("hash"); - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid()) { - m_generator.setStatusLine(200, "OK"); - m_generator.setContentTypeByExt("html"); - m_generator.setMessage(QByteArray::number(h.download_limit())); - m_generator.setContentEncoding(m_parser.acceptsEncoding()); - write(); - } - return; - } - if (command == "setTorrentUpLimit") { - QString hash = m_parser.post("hash"); - qlonglong limit = m_parser.post("limit").toLongLong(); - if (limit == 0) limit = -1; - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid()) { - h.set_upload_limit(limit); - } - return; - } - if (command == "setTorrentDlLimit") { - QString hash = m_parser.post("hash"); - qlonglong limit = m_parser.post("limit").toLongLong(); - if (limit == 0) limit = -1; - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid()) { - h.set_download_limit(limit); - } - return; - } - if (command == "setGlobalUpLimit") { - qlonglong limit = m_parser.post("limit").toLongLong(); - if (limit == 0) limit = -1; - QBtSession::instance()->setUploadRateLimit(limit); - Preferences::instance()->setGlobalUploadLimit(limit/1024.); - return; - } - if (command == "setGlobalDlLimit") { - qlonglong limit = m_parser.post("limit").toLongLong(); - if (limit == 0) limit = -1; - QBtSession::instance()->setDownloadRateLimit(limit); - Preferences::instance()->setGlobalDownloadLimit(limit/1024.); - return; - } - if (command == "pause") { - emit pauseTorrent(m_parser.post("hash")); - return; - } - if (command == "delete") { - QStringList hashes = m_parser.post("hashes").split("|"); - foreach (const QString &hash, hashes) { - emit deleteTorrent(hash, false); - } - return; - } - if (command == "deletePerm") { - QStringList hashes = m_parser.post("hashes").split("|"); - foreach (const QString &hash, hashes) { - emit deleteTorrent(hash, true); - } - return; - } - if (command == "increasePrio") { - increaseTorrentsPriority(m_parser.post("hashes").split("|")); - return; - } - if (command == "decreasePrio") { - decreaseTorrentsPriority(m_parser.post("hashes").split("|")); - return; - } - if (command == "topPrio") { - foreach (const QString &hash, m_parser.post("hashes").split("|")) { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid()) h.queue_position_top(); - } - return; - } - if (command == "bottomPrio") { - foreach (const QString &hash, m_parser.post("hashes").split("|")) { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (h.is_valid()) h.queue_position_bottom(); - } - return; - } - if (command == "recheck") { - QBtSession::instance()->recheckTorrent(m_parser.post("hash")); - return; - } -} - -void HttpConnection::decreaseTorrentsPriority(const QStringList &hashes) { - qDebug() << Q_FUNC_INFO << hashes; - std::priority_queue, - std::vector >, - std::less > > torrent_queue; - // Sort torrents by priority - foreach (const QString &hash, hashes) { - try { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (!h.is_seed()) { - torrent_queue.push(qMakePair(h.queue_position(), h)); - } - }catch(invalid_handle&) {} - } - // Decrease torrents priority (starting with the ones with lowest priority) - while(!torrent_queue.empty()) { - QTorrentHandle h = torrent_queue.top().second; - try { - h.queue_position_down(); - } catch(invalid_handle& h) {} - torrent_queue.pop(); - } -} - -void HttpConnection::increaseTorrentsPriority(const QStringList &hashes) -{ - qDebug() << Q_FUNC_INFO << hashes; - std::priority_queue, - std::vector >, - std::greater > > torrent_queue; - // Sort torrents by priority - foreach (const QString &hash, hashes) { - try { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - if (!h.is_seed()) { - torrent_queue.push(qMakePair(h.queue_position(), h)); - } - }catch(invalid_handle&) {} - } - // Increase torrents priority (starting with the ones with highest priority) - while(!torrent_queue.empty()) { - QTorrentHandle h = torrent_queue.top().second; - try { - h.queue_position_up(); - } catch(invalid_handle& h) {} - torrent_queue.pop(); - } + return true; } diff --git a/src/webui/httpconnection.h b/src/webui/httpconnection.h index 1af5fcc04..e1a9ed82f 100644 --- a/src/webui/httpconnection.h +++ b/src/webui/httpconnection.h @@ -1,5 +1,6 @@ /* - * Bittorrent Client using Qt4 and libtorrent. + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2014 Vladimir Golovnev * Copyright (C) 2006 Ishan Arora and Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -24,17 +25,14 @@ * 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 HTTPCONNECTION_H #define HTTPCONNECTION_H -#include "httprequestparser.h" -#include "httpresponsegenerator.h" #include +#include "httptypes.h" class HttpServer; @@ -48,46 +46,18 @@ class HttpConnection : public QObject Q_DISABLE_COPY(HttpConnection) public: - HttpConnection(QTcpSocket *m_socket, HttpServer *m_httpserver); + HttpConnection(QTcpSocket* socket, HttpServer* httpserver); ~HttpConnection(); - void translateDocument(QString& data); - -protected slots: - void write(); - void respond(); - void respondTorrentsJson(); - void respondGenPropertiesJson(const QString& hash); - void respondTrackersPropertiesJson(const QString& hash); - void respondFilesPropertiesJson(const QString& hash); - void respondPreferencesJson(); - void respondGlobalTransferInfoJson(); - void respondCommand(const QString& command); - void respondNotFound(); - void processDownloadedFile(const QString& url, const QString& file_path); - void handleDownloadFailure(const QString& url, const QString& reason); - void decreaseTorrentsPriority(const QStringList& hashes); - void increaseTorrentsPriority(const QStringList& hashes); private slots: void read(); -signals: - void UrlReadyToBeDownloaded(const QString& url); - void MagnetReadyToBeDownloaded(const QString& uri); - void torrentReadyToBeDownloaded(const QString&, bool, const QString&, bool); - void deleteTorrent(const QString& hash, bool permanently); - void resumeTorrent(const QString& hash); - void pauseTorrent(const QString& hash); - void increasePrioTorrent(const QString& hash); - void decreasePrioTorrent(const QString& hash); - void resumeAllTorrents(); - void pauseAllTorrents(); - private: + void write(const HttpResponse& response); + + static bool acceptsGzipEncoding(const QString& encoding); + QTcpSocket *m_socket; - HttpServer *m_httpserver; - HttpRequestParser m_parser; - HttpResponseGenerator m_generator; QByteArray m_receivedData; }; diff --git a/src/webui/httpserver.cpp b/src/webui/httpserver.cpp index 7aadb8905..c37c5301c 100644 --- a/src/webui/httpserver.cpp +++ b/src/webui/httpserver.cpp @@ -1,6 +1,8 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006 Ishan Arora and Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * Copyright (C) 2006 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,132 +26,38 @@ * 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 "httpserver.h" -#include "httpconnection.h" -#include "qbtsession.h" -#include -#include -#include -#include - #ifndef QT_NO_OPENSSL #include #else #include #endif +#include "httpconnection.h" +#include "httpserver.h" -using namespace libtorrent; - -const int BAN_TIME = 3600000; // 1 hour - -class UnbanTimer: public QTimer { -public: - UnbanTimer(const QString& peer_ip, QObject *parent): QTimer(parent), - m_peerIp(peer_ip) { - setSingleShot(true); - setInterval(BAN_TIME); - } - - inline const QString& peerIp() const { return m_peerIp; } - -private: - QString m_peerIp; -}; - -void HttpServer::UnbanTimerEvent() { - UnbanTimer* ubantimer = static_cast(sender()); - qDebug("Ban period has expired for %s", qPrintable(ubantimer->peerIp())); - m_clientFailedAttempts.remove(ubantimer->peerIp()); - ubantimer->deleteLater(); -} - -int HttpServer::NbFailedAttemptsForIp(const QString& ip) const { - return m_clientFailedAttempts.value(ip, 0); -} - -void HttpServer::increaseNbFailedAttemptsForIp(const QString& ip) { - const int nb_fail = m_clientFailedAttempts.value(ip, 0) + 1; - m_clientFailedAttempts.insert(ip, nb_fail); - if (nb_fail == MAX_AUTH_FAILED_ATTEMPTS) { - // Max number of failed attempts reached - // Start ban period - UnbanTimer* ubantimer = new UnbanTimer(ip, this); - connect(ubantimer, SIGNAL(timeout()), SLOT(UnbanTimerEvent())); - ubantimer->start(); - } -} - -void HttpServer::resetNbFailedAttemptsForIp(const QString& ip) { - m_clientFailedAttempts.remove(ip); -} - -HttpServer::HttpServer(QObject* parent) : QTcpServer(parent) -{ - - const Preferences* const pref = Preferences::instance(); - - m_username = pref->getWebUiUsername().toUtf8(); - m_passwordSha1 = pref->getWebUiPassword().toUtf8(); - m_localAuthEnabled = pref->isWebUiLocalAuthEnabled(); - - // HTTPS-related +HttpServer::HttpServer(QObject* parent) + : QTcpServer(parent) #ifndef QT_NO_OPENSSL - m_https = pref->isWebUiHttpsEnabled(); - if (m_https) { - m_certificate = QSslCertificate(pref->getWebUiHttpsCertificate()); - m_key = QSslKey(pref->getWebUiHttpsKey(), QSsl::Rsa); - } + , m_https(false) #endif - - // Additional translations for Web UI - QString a = tr("File"); - a = tr("Edit"); - a = tr("Help"); - a = tr("Download Torrents from their URL or Magnet link"); - a = tr("Only one link per line"); - a = tr("Download local torrent"); - a = tr("Torrent files were correctly added to download list."); - a = tr("Point to torrent file"); - a = tr("Download"); - a = tr("Are you sure you want to delete the selected torrents from the transfer list and hard disk?"); - a = tr("Download rate limit must be greater than 0 or disabled."); - a = tr("Upload rate limit must be greater than 0 or disabled."); - a = tr("Maximum number of connections limit must be greater than 0 or disabled."); - a = tr("Maximum number of connections per torrent limit must be greater than 0 or disabled."); - a = tr("Maximum number of upload slots per torrent limit must be greater than 0 or disabled."); - a = tr("Unable to save program preferences, qBittorrent is probably unreachable."); - a = tr("Language"); - a = tr("Downloaded", "Is the file downloaded or not?"); - a = tr("The port used for incoming connections must be greater than 1024 and less than 65535."); - a = tr("The port used for the Web UI must be greater than 1024 and less than 65535."); - a = tr("The Web UI username must be at least 3 characters long."); - a = tr("The Web UI password must be at least 3 characters long."); - a = tr("Save"); - a = tr("qBittorrent client is not reachable"); - a = tr("HTTP Server"); - a = tr("The following parameters are supported:"); - a = tr("Torrent path"); - a = tr("Torrent name"); - a = tr("qBittorrent has been shutdown."); +{ } -HttpServer::~HttpServer() { +HttpServer::~HttpServer() +{ } #ifndef QT_NO_OPENSSL -void HttpServer::enableHttps(const QSslCertificate &certificate, - const QSslKey &key) { +void HttpServer::enableHttps(const QSslCertificate &certificate, const QSslKey &key) +{ m_certificate = certificate; m_key = key; m_https = true; } -void HttpServer::disableHttps() { +void HttpServer::disableHttps() +{ m_https = false; m_certificate.clear(); m_key.clear(); @@ -169,143 +77,21 @@ void HttpServer::incomingConnection(int socketDescriptor) else #endif serverSocket = new QTcpSocket(this); - if (serverSocket->setSocketDescriptor(socketDescriptor)) { + if (serverSocket->setSocketDescriptor(socketDescriptor)) + { #ifndef QT_NO_OPENSSL - if (m_https) { + if (m_https) + { static_cast(serverSocket)->setProtocol(QSsl::AnyProtocol); static_cast(serverSocket)->setPrivateKey(m_key); static_cast(serverSocket)->setLocalCertificate(m_certificate); static_cast(serverSocket)->startServerEncryption(); } #endif - handleNewConnection(serverSocket); - } else { - serverSocket->deleteLater(); - } -} - -void HttpServer::handleNewConnection(QTcpSocket *socket) -{ - HttpConnection *connection = new HttpConnection(socket, this); - //connect connection to QBtSession::instance() - connect(connection, SIGNAL(UrlReadyToBeDownloaded(QString)), QBtSession::instance(), SLOT(downloadUrlAndSkipDialog(QString))); - connect(connection, SIGNAL(MagnetReadyToBeDownloaded(QString)), QBtSession::instance(), SLOT(addMagnetSkipAddDlg(QString))); - connect(connection, SIGNAL(torrentReadyToBeDownloaded(QString, bool, QString, bool)), QBtSession::instance(), SLOT(addTorrent(QString, bool, QString, bool))); - connect(connection, SIGNAL(deleteTorrent(QString, bool)), QBtSession::instance(), SLOT(deleteTorrent(QString, bool))); - connect(connection, SIGNAL(pauseTorrent(QString)), QBtSession::instance(), SLOT(pauseTorrent(QString))); - connect(connection, SIGNAL(resumeTorrent(QString)), QBtSession::instance(), SLOT(resumeTorrent(QString))); - connect(connection, SIGNAL(pauseAllTorrents()), QBtSession::instance(), SLOT(pauseAllTorrents())); - connect(connection, SIGNAL(resumeAllTorrents()), QBtSession::instance(), SLOT(resumeAllTorrents())); -} - -QString HttpServer::generateNonce() const { - QCryptographicHash md5(QCryptographicHash::Md5); - md5.addData(QTime::currentTime().toString("hhmmsszzz").toUtf8()); - md5.addData(":"); - md5.addData(QBT_REALM); - return md5.result().toHex(); -} - -void HttpServer::setAuthorization(const QString& username, - const QString& password_sha1) { - m_username = username.toUtf8(); - m_passwordSha1 = password_sha1.toUtf8(); -} - -// Parse HTTP AUTH string -// http://tools.ietf.org/html/rfc2617 -bool HttpServer::isAuthorized(const QByteArray& auth, - const QString& method) const { - //qDebug("AUTH string is %s", auth.data()); - // Get user name - QRegExp regex_user(".*username=\"([^\"]+)\".*"); // Must be a quoted string - if (regex_user.indexIn(auth) < 0) return false; - QString prop_user = regex_user.cap(1); - //qDebug("AUTH: Proposed username is %s, real username is %s", qPrintable(prop_user), username.data()); - if (prop_user != m_username) { - // User name is invalid, we can reject already - qDebug("AUTH-PROB: Username is invalid"); - return false; + new HttpConnection(serverSocket, this); } - // Get realm - QRegExp regex_realm(".*realm=\"([^\"]+)\".*"); // Must be a quoted string - if (regex_realm.indexIn(auth) < 0) { - qDebug("AUTH-PROB: Missing realm"); - return false; - } - QByteArray prop_realm = regex_realm.cap(1).toUtf8(); - if (prop_realm != QBT_REALM) { - qDebug("AUTH-PROB: Wrong realm"); - return false; - } - // get nonce - QRegExp regex_nonce(".*nonce=[\"]?([\\w=]+)[\"]?.*"); - if (regex_nonce.indexIn(auth) < 0) { - qDebug("AUTH-PROB: missing nonce"); - return false; - } - QByteArray prop_nonce = regex_nonce.cap(1).toUtf8(); - //qDebug("prop nonce is: %s", prop_nonce.data()); - // get uri - QRegExp regex_uri(".*uri=\"([^\"]+)\".*"); - if (regex_uri.indexIn(auth) < 0) { - qDebug("AUTH-PROB: Missing uri"); - return false; - } - QByteArray prop_uri = regex_uri.cap(1).toUtf8(); - //qDebug("prop uri is: %s", prop_uri.data()); - // get response - QRegExp regex_response(".*response=[\"]?([\\w=]+)[\"]?.*"); - if (regex_response.indexIn(auth) < 0) { - qDebug("AUTH-PROB: Missing response"); - return false; - } - QByteArray prop_response = regex_response.cap(1).toUtf8(); - //qDebug("prop response is: %s", prop_response.data()); - // Compute correct reponse - QCryptographicHash md5_ha2(QCryptographicHash::Md5); - md5_ha2.addData(method.toUtf8() + ":" + prop_uri); - QByteArray ha2 = md5_ha2.result().toHex(); - QByteArray response = ""; - if (auth.contains("qop=")) { - QCryptographicHash md5_ha(QCryptographicHash::Md5); - // Get nc - QRegExp regex_nc(".*nc=[\"]?([\\w=]+)[\"]?.*"); - if (regex_nc.indexIn(auth) < 0) { - qDebug("AUTH-PROB: qop but missing nc"); - return false; - } - QByteArray prop_nc = regex_nc.cap(1).toUtf8(); - //qDebug("prop nc is: %s", prop_nc.data()); - QRegExp regex_cnonce(".*cnonce=[\"]?([\\w=]+)[\"]?.*"); - if (regex_cnonce.indexIn(auth) < 0) { - qDebug("AUTH-PROB: qop but missing cnonce"); - return false; - } - QByteArray prop_cnonce = regex_cnonce.cap(1).toUtf8(); - //qDebug("prop cnonce is: %s", prop_cnonce.data()); - QRegExp regex_qop(".*qop=[\"]?(\\w+)[\"]?.*"); - if (regex_qop.indexIn(auth) < 0) { - qDebug("AUTH-PROB: missing qop"); - return false; - } - QByteArray prop_qop = regex_qop.cap(1).toUtf8(); - //qDebug("prop qop is: %s", prop_qop.data()); - md5_ha.addData(m_passwordSha1+":"+prop_nonce+":"+prop_nc+":"+prop_cnonce+":"+prop_qop+":"+ha2); - response = md5_ha.result().toHex(); - } else { - QCryptographicHash md5_ha(QCryptographicHash::Md5); - md5_ha.addData(m_passwordSha1+":"+prop_nonce+":"+ha2); - response = md5_ha.result().toHex(); + else + { + serverSocket->deleteLater(); } - //qDebug("AUTH: comparing reponses: (%d)", static_cast(prop_response == response)); - return prop_response == response; -} - -void HttpServer::setlocalAuthEnabled(bool enabled) { - m_localAuthEnabled = enabled; -} - -bool HttpServer::isLocalAuthEnabled() const { - return m_localAuthEnabled; } diff --git a/src/webui/httpserver.h b/src/webui/httpserver.h index d596d090f..170cceec8 100644 --- a/src/webui/httpserver.h +++ b/src/webui/httpserver.h @@ -1,6 +1,7 @@ /* - * Bittorrent Client using Qt4 and libtorrent. - * Copyright (C) 2006 Ishan Arora and Christophe Dumez + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2014 Vladimir Golovnev + * Copyright (C) 2006 Ishan Arora and Christophe Dumez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -24,50 +25,26 @@ * 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 HTTPSERVER_H #define HTTPSERVER_H -#include #include -#include -#include -#include - #ifndef QT_NO_OPENSSL #include #include #endif -#include "preferences.h" - -class EventManager; - -QT_BEGIN_NAMESPACE -class QTimer; -QT_END_NAMESPACE - -const int MAX_AUTH_FAILED_ATTEMPTS = 5; - -class HttpServer : public QTcpServer { +class HttpServer : public QTcpServer +{ Q_OBJECT Q_DISABLE_COPY(HttpServer) public: HttpServer(QObject* parent = 0); ~HttpServer(); - void setAuthorization(const QString& username, const QString& password_sha1); - bool isAuthorized(const QByteArray& auth, const QString& method) const; - void setlocalAuthEnabled(bool enabled); - bool isLocalAuthEnabled() const; - QString generateNonce() const; - int NbFailedAttemptsForIp(const QString& ip) const; - void increaseNbFailedAttemptsForIp(const QString& ip); - void resetNbFailedAttemptsForIp(const QString& ip); #ifndef QT_NO_OPENSSL void enableHttps(const QSslCertificate &certificate, const QSslKey &key); @@ -81,17 +58,7 @@ private: void incomingConnection(int socketDescriptor); #endif -private slots: - void UnbanTimerEvent(); - -private: - void handleNewConnection(QTcpSocket *socket); - private: - QByteArray m_username; - QByteArray m_passwordSha1; - QHash m_clientFailedAttempts; - bool m_localAuthEnabled; #ifndef QT_NO_OPENSSL bool m_https; QSslCertificate m_certificate; diff --git a/src/webui/jsonutils.h b/src/webui/jsonutils.h index b08cfb0d8..5668b356b 100644 --- a/src/webui/jsonutils.h +++ b/src/webui/jsonutils.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2014 Vladimir Golovnev + * 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 @@ -24,8 +24,6 @@ * 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 : glassez@yandex.ru */ #ifndef JSONUTILS_H diff --git a/src/webui/requesthandler.cpp b/src/webui/requesthandler.cpp new file mode 100644 index 000000000..26633bd4d --- /dev/null +++ b/src/webui/requesthandler.cpp @@ -0,0 +1,588 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2014 Vladimir Golovnev + * 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. + */ + +#include +#ifdef DISABLE_GUI +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#ifndef DISABLE_GUI +#include "iconprovider.h" +#endif +#include "misc.h" +#include "fs_utils.h" +#include "preferences.h" +#include "btjson.h" +#include "prefjson.h" +#include "qbtsession.h" +#include "requesthandler.h" + +using namespace libtorrent; + +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"; + +#define ADD_ACTION(scope, action) actions[#scope][#action] = &RequestHandler::action_##scope##_##action + +QMap > RequestHandler::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(json, torrents); + ADD_ACTION(json, preferences); + ADD_ACTION(json, transferInfo); + ADD_ACTION(json, propertiesGeneral); + ADD_ACTION(json, propertiesTrackers); + ADD_ACTION(json, propertiesFiles); + 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, getTorrentUpLimit); + ADD_ACTION(command, getTorrentDlLimit); + ADD_ACTION(command, setTorrentUpLimit); + ADD_ACTION(command, setTorrentDlLimit); + 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, recheck); + + return actions; +} + +void RequestHandler::action_public_index() +{ + QString path; + if (!args_.isEmpty()) + { + if (args_.back() == "favicon.ico") + path = ":/Icons/skin/qbittorrent16.png"; + else + path = WWW_FOLDER + args_.join("/"); + } + + printFile(path); +} + +void RequestHandler::action_public_webui() +{ + if (!sessionActive()) + printFile(PRIVATE_FOLDER + "login.html"); + else + printFile(PRIVATE_FOLDER + "index.html"); +} + +void RequestHandler::action_public_login() +{ + const Preferences* const pref = Preferences::instance(); + QCryptographicHash md5(QCryptographicHash::Md5); + + md5.addData(request().posts["password"].toLocal8Bit()); + QString pass = md5.result().toHex(); + + if ((request().posts["username"] == pref->getWebUiUsername()) && (pass == pref->getWebUiPassword())) + { + sessionStart(); + print(QByteArray("Ok."), CONTENT_TYPE_TXT); + } + else + { + QString addr = env().clientAddress.toString(); + increaseFailedAttempts(); + qDebug("client IP: %s (%d failed attempts)", qPrintable(addr), failedAttempts()); + print(QByteArray("Fails."), CONTENT_TYPE_TXT); + } +} + +void RequestHandler::action_public_logout() +{ + sessionEnd(); +} + +void RequestHandler::action_public_theme() +{ + if (args_.size() != 1) + { + status(404, "Not Found"); + return; + } + +#ifdef DISABLE_GUI + QString url = ":/Icons/oxygen/" + args_.front() + ".png"; +#else + QString url = IconProvider::instance()->getIconPath(args_.front()); +#endif + qDebug() << Q_FUNC_INFO << "There icon:" << url; + + printFile(url); +} + +void RequestHandler::action_public_images() +{ + const QString path = ":/Icons/" + args_.join("/"); + printFile(path); +} + +void RequestHandler::action_json_torrents() +{ + print(btjson::getTorrents(), CONTENT_TYPE_JS); +} + +void RequestHandler::action_json_preferences() +{ + print(prefjson::getPreferences(), CONTENT_TYPE_JS); +} + +void RequestHandler::action_json_transferInfo() +{ + print(btjson::getTransferInfo(), CONTENT_TYPE_JS); +} + +void RequestHandler::action_json_propertiesGeneral() +{ + print(btjson::getPropertiesForTorrent(args_.front()), CONTENT_TYPE_JS); +} + +void RequestHandler::action_json_propertiesTrackers() +{ + print(btjson::getTrackersForTorrent(args_.front()), CONTENT_TYPE_JS); +} + +void RequestHandler::action_json_propertiesFiles() +{ + print(btjson::getFilesForTorrent(args_.front()), CONTENT_TYPE_JS); +} + +void RequestHandler::action_command_shutdown() +{ + 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(0, qApp, SLOT(quit())); +} + +void RequestHandler::action_command_download() +{ + QString urls = request().posts["urls"]; + QStringList list = urls.split('\n'); + + foreach (QString url, list) + { + url = url.trimmed(); + if (!url.isEmpty()) + { + if (url.startsWith("bc://bt/", Qt::CaseInsensitive)) + { + qDebug("Converting bc link to magnet link"); + url = misc::bcLinkToMagnet(url); + } + else if (url.startsWith("magnet:", Qt::CaseInsensitive)) + { + QBtSession::instance()->addMagnetSkipAddDlg(url); + } + else + { + qDebug("Downloading url: %s", qPrintable(url)); + QBtSession::instance()->downloadUrlAndSkipDialog(url); + } + } + } +} + +void RequestHandler::action_command_upload() +{ + qDebug() << Q_FUNC_INFO; + + foreach(const UploadedFile& torrent, request().files) + { + QString filePath = saveTmpFile(torrent.data); + + if (!filePath.isEmpty()) + { + QBtSession::instance()->addTorrent(filePath); + // Clean up + fsutils::forceRemove(filePath); + print(QLatin1String("")); + } + else + { + qWarning() << "I/O Error: Could not create temporary file"; + } + } +} + +void RequestHandler::action_command_addTrackers() +{ + QString hash = request().posts["hash"]; + + if (!hash.isEmpty()) + { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid() && h.has_metadata()) + { + QString urls = request().posts["urls"]; + QStringList list = urls.split('\n'); + + foreach (const QString& url, list) + { + announce_entry e(url.toStdString()); + h.add_tracker(e); + } + } + } +} + +void RequestHandler::action_command_resumeAll() +{ + QBtSession::instance()->resumeAllTorrents(); +} + +void RequestHandler::action_command_pauseAll() +{ + QBtSession::instance()->pauseAllTorrents(); +} + +void RequestHandler::action_command_resume() +{ + QBtSession::instance()->resumeTorrent(request().posts["hash"]); +} + +void RequestHandler::action_command_pause() +{ + QBtSession::instance()->pauseTorrent(request().posts["hash"]); +} + +void RequestHandler::action_command_setPreferences() +{ + prefjson::setPreferences(request().posts["json"]); +} + +void RequestHandler::action_command_setFilePrio() +{ + QString hash = request().posts["hash"]; + int file_id = request().posts["id"].toInt(); + int priority = request().posts["priority"].toInt(); + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid() && h.has_metadata()) + { + h.file_priority(file_id, priority); + } +} + +void RequestHandler::action_command_getGlobalUpLimit() +{ + print(QByteArray::number(QBtSession::instance()->getSession()->settings().upload_rate_limit)); +} + +void RequestHandler::action_command_getGlobalDlLimit() +{ + print(QByteArray::number(QBtSession::instance()->getSession()->settings().download_rate_limit)); +} + +void RequestHandler::action_command_setGlobalUpLimit() +{ + qlonglong limit = request().posts["limit"].toLongLong(); + if (limit == 0) limit = -1; + + QBtSession::instance()->setUploadRateLimit(limit); + Preferences::instance()->setGlobalUploadLimit(limit/1024.); +} + +void RequestHandler::action_command_setGlobalDlLimit() +{ + qlonglong limit = request().posts["limit"].toLongLong(); + if (limit == 0) limit = -1; + + QBtSession::instance()->setDownloadRateLimit(limit); + Preferences::instance()->setGlobalDownloadLimit(limit/1024.); +} + +void RequestHandler::action_command_getTorrentUpLimit() +{ + QString hash = request().posts["hash"]; + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid()) + { + print(QByteArray::number(h.upload_limit())); + } +} + +void RequestHandler::action_command_getTorrentDlLimit() +{ + QString hash = request().posts["hash"]; + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid()) + { + print(QByteArray::number(h.download_limit())); + } +} + +void RequestHandler::action_command_setTorrentUpLimit() +{ + QString hash = request().posts["hash"]; + qlonglong limit = request().posts["limit"].toLongLong(); + if (limit == 0) limit = -1; + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid()) + { + h.set_upload_limit(limit); + } +} + +void RequestHandler::action_command_setTorrentDlLimit() +{ + QString hash = request().posts["hash"]; + qlonglong limit = request().posts["limit"].toLongLong(); + if (limit == 0) limit = -1; + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid()) + { + h.set_download_limit(limit); + } +} + +void RequestHandler::action_command_delete() +{ + QStringList hashes = request().posts["hashes"].split("|"); + + foreach (const QString &hash, hashes) + { + QBtSession::instance()->deleteTorrent(hash, false); + } +} + +void RequestHandler::action_command_deletePerm() +{ + QStringList hashes = request().posts["hashes"].split("|"); + + foreach (const QString &hash, hashes) + { + QBtSession::instance()->deleteTorrent(hash, true); + } +} + +void RequestHandler::action_command_increasePrio() +{ + QStringList hashes = request().posts["hashes"].split("|"); + std::priority_queue, + std::vector >, + std::greater > > torrent_queue; + + // Sort torrents by priority + foreach (const QString &hash, hashes) + { + try + { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + if (!h.is_seed()) + { + torrent_queue.push(qMakePair(h.queue_position(), h)); + } + } + catch(invalid_handle&) {} + } + + // Increase torrents priority (starting with the ones with highest priority) + while(!torrent_queue.empty()) + { + QTorrentHandle h = torrent_queue.top().second; + + try + { + h.queue_position_up(); + } + catch(invalid_handle&) {} + + torrent_queue.pop(); + } +} + +void RequestHandler::action_command_decreasePrio() +{ + QStringList hashes = request().posts["hashes"].split("|"); + std::priority_queue, + std::vector >, + std::less > > torrent_queue; + + // Sort torrents by priority + foreach (const QString &hash, hashes) + { + try + { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (!h.is_seed()) + { + torrent_queue.push(qMakePair(h.queue_position(), h)); + } + } + catch(invalid_handle&) {} + } + + // Decrease torrents priority (starting with the ones with lowest priority) + while(!torrent_queue.empty()) + { + QTorrentHandle h = torrent_queue.top().second; + + try + { + h.queue_position_down(); + } + catch(invalid_handle&) {} + + torrent_queue.pop(); + } +} + +void RequestHandler::action_command_topPrio() +{ + foreach (const QString &hash, request().posts["hashes"].split("|")) + { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + if (h.is_valid()) h.queue_position_top(); + } +} + +void RequestHandler::action_command_bottomPrio() +{ + foreach (const QString &hash, request().posts["hashes"].split("|")) + { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + if (h.is_valid()) h.queue_position_bottom(); + } +} + +void RequestHandler::action_command_recheck() +{ + QBtSession::instance()->recheckTorrent(request().posts["hash"]); +} + +bool RequestHandler::isPublicScope() +{ + return (scope_ == DEFAULT_SCOPE); +} + +void RequestHandler::processRequest() +{ + if (args_.contains(".") || args_.contains("..")) + { + qDebug() << Q_FUNC_INFO << "Invalid path:" << request().path; + status(404, "Not Found"); + return; + } + + if (!isPublicScope() && !sessionActive()) + { + status(403, "Forbidden"); + return; + } + + if (actions_.value(scope_).value(action_) != 0) + { + (this->*(actions_[scope_][action_]))(); + } + else + { + status(404, "Not Found"); + qDebug() << Q_FUNC_INFO << "Resource not found:" << request().path; + } +} + +void RequestHandler::parsePath() +{ + if(request().path == "/") action_ = WEBUI_ACTION; + + // check action for requested path + QStringList pathItems = request().path.split('/', QString::SkipEmptyParts); + if (!pathItems.empty()) + { + if (actions_.contains(pathItems.front())) + { + scope_ = pathItems.front(); + pathItems.pop_front(); + } + } + + if (!pathItems.empty()) + { + if (actions_[scope_].contains(pathItems.front())) + { + action_ = pathItems.front(); + pathItems.pop_front(); + } + } + + args_ = pathItems; +} + +RequestHandler::RequestHandler(const HttpRequest &request, const HttpEnvironment &env, WebApplication *app) + : AbstractRequestHandler(request, env, app), scope_(DEFAULT_SCOPE), action_(DEFAULT_ACTION) +{ + parsePath(); +} + +QMap > RequestHandler::actions_ = RequestHandler::initializeActions(); diff --git a/src/webui/requesthandler.h b/src/webui/requesthandler.h new file mode 100644 index 000000000..a92f85269 --- /dev/null +++ b/src/webui/requesthandler.h @@ -0,0 +1,100 @@ +/* + * 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 REQUESTHANDLER_H +#define REQUESTHANDLER_H + +#include +#include "httptypes.h" +#include "abstractrequesthandler.h" + +class WebApplication; + +class RequestHandler: public AbstractRequestHandler +{ +public: + RequestHandler( + const HttpRequest& request, const HttpEnvironment& env, + WebApplication* app); + +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_json_torrents(); + void action_json_preferences(); + void action_json_transferInfo(); + void action_json_propertiesGeneral(); + void action_json_propertiesTrackers(); + void action_json_propertiesFiles(); + 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_getTorrentUpLimit(); + void action_command_getTorrentDlLimit(); + void action_command_setTorrentUpLimit(); + void action_command_setTorrentDlLimit(); + 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_recheck(); + + typedef void (RequestHandler::*Action)(); + + QString scope_; + QString action_; + QStringList args_; + + void processRequest(); + + bool isPublicScope(); + void parsePath(); + + static QMap > initializeActions(); + static QMap > actions_; +}; + +#endif // REQUESTHANDLER_H diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp new file mode 100644 index 000000000..2ada7817c --- /dev/null +++ b/src/webui/webapplication.cpp @@ -0,0 +1,309 @@ +/* + * 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. + */ + +#ifdef DISABLE_GUI +#include +#else +#include +#endif +#include +#include +#include +#include +#include "preferences.h" +#include "requesthandler.h" +#include "webapplication.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; +}; + +// WebApplication + +WebApplication::WebApplication(QObject *parent) + : QObject(parent) +{ +} + +WebApplication::~WebApplication() +{ + // cleanup sessions data + foreach (WebSession* session, sessions_.values()) + delete session; +} + +WebApplication *WebApplication::instance() +{ + static WebApplication inst; + return &inst; +} + +void WebApplication::UnbanTimerEvent() +{ + UnbanTimer* ubantimer = static_cast(sender()); + qDebug("Ban period has expired for %s", qPrintable(ubantimer->peerIp().toString())); + clientFailedAttempts_.remove(ubantimer->peerIp()); + ubantimer->deleteLater(); +} + +bool WebApplication::sessionInitialize(AbstractRequestHandler* _this) +{ + if (_this->session_ == 0) + { + QString cookie = _this->request_.headers.value("cookie"); + //qDebug() << Q_FUNC_INFO << "cookie: " << cookie; + + QString sessionId; + const QString SID_START = C_SID + "="; + int pos = cookie.indexOf(SID_START); + if (pos >= 0) + { + pos += SID_START.length(); + int end = cookie.indexOf(QRegExp("[,;]"), pos); + sessionId = cookie.mid(pos, end >= 0 ? end - pos : end); + } + + // TODO: Additional session check + + if (!sessionId.isNull()) + { + if (sessions_.contains(sessionId)) + { + _this->session_ = sessions_[sessionId]; + return true; + } + else + { + qDebug() << Q_FUNC_INFO << "session does not exist!"; + } + } + } + + return false; +} + +bool WebApplication::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!", qPrintable(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")) + { + dataStr.replace("${VERSION}", VERSION); + } + + data = dataStr.toUtf8(); + translatedFiles_[path] = data; // cashing translated file + } + } + + type = CONTENT_TYPE_BY_EXT[ext]; + return true; +} + +QString WebApplication::generateSid() +{ + QString sid; + + qsrand(QDateTime::currentDateTime().toTime_t()); + do + { + const size_t size = 6; + quint32 tmp[size]; + + for (size_t i = 0; i < size; ++i) + tmp[i] = qrand(); + + sid = QByteArray::fromRawData(reinterpret_cast(tmp), sizeof(quint32) * size).toBase64(); + } + while (sessions_.contains(sid)); + + return sid; +} + +void WebApplication::translateDocument(QString& data) +{ + const QRegExp regex(QString::fromUtf8("_\\(([\\w\\s?!:\\/\\(\\),%µ&\\-\\.]+)\\)")); + const QRegExp mnemonic("\\(?&([a-zA-Z]?\\))?"); + const std::string contexts[] = { + "TransferListFiltersWidget", "TransferListWidget", "PropertiesWidget", + "HttpServer", "confirmDeletionDlg", "TrackerList", "TorrentFilesModel", + "options_imp", "Preferences", "TrackersAdditionDlg", "ScanFoldersModel", + "PropTabBar", "TorrentModel", "downloadFromURL", "MainWindow", "misc" + }; + const size_t context_count = sizeof(contexts) / sizeof(contexts[0]); + 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) + { + size_t context_index = 0; + while ((context_index < context_count) && (translation == word)) + { +#if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0)) + translation = qApp->translate(contexts[context_index].c_str(), word.constData(), 0, QCoreApplication::UnicodeUTF8, 1); +#else + translation = qApp->translate(contexts[context_index].c_str(), word.constData(), 0, 1); +#endif + ++context_index; + } + } + // Remove keyboard shortcuts + translation.replace(mnemonic, ""); + + data.replace(i, regex.matchedLength(), translation); + i += translation.length(); + } + else + { + found = false; // no more translatable strings + } + } +} + +bool WebApplication::isBanned(const AbstractRequestHandler *_this) const +{ + return clientFailedAttempts_.value(_this->env_.clientAddress, 0) >= MAX_AUTH_FAILED_ATTEMPTS; +} + +int WebApplication::failedAttempts(const AbstractRequestHandler* _this) const +{ + return clientFailedAttempts_.value(_this->env_.clientAddress, 0); +} + +void WebApplication::resetFailedAttempts(AbstractRequestHandler* _this) +{ + clientFailedAttempts_.remove(_this->env_.clientAddress); +} + +void WebApplication::increaseFailedAttempts(AbstractRequestHandler* _this) +{ + const int nb_fail = clientFailedAttempts_.value(_this->env_.clientAddress, 0) + 1; + + clientFailedAttempts_[_this->env_.clientAddress] = nb_fail; + if (nb_fail == MAX_AUTH_FAILED_ATTEMPTS) + { + // Max number of failed attempts reached + // Start ban period + UnbanTimer* ubantimer = new UnbanTimer(_this->env_.clientAddress, this); + connect(ubantimer, SIGNAL(timeout()), SLOT(UnbanTimerEvent())); + ubantimer->start(); + } +} + +bool WebApplication::sessionStart(AbstractRequestHandler *_this) +{ + if (_this->session_ == 0) + { + _this->session_ = new WebSession(generateSid()); + sessions_[_this->session_->id] = _this->session_; + return true; + } + + return false; +} + +bool WebApplication::sessionEnd(AbstractRequestHandler *_this) +{ + if ((_this->session_ != 0) && (sessions_.contains(_this->session_->id))) + { + sessions_.remove(_this->session_->id); + delete _this->session_; + _this->session_ = 0; + return true; + } + + return false; +} + +QStringMap WebApplication::initializeContentTypeByExtMap() +{ + QStringMap map; + + map["htm"] = CONTENT_TYPE_HTML; + map["html"] = CONTENT_TYPE_HTML; + map["css"] = CONTENT_TYPE_CSS; + map["gif"] = CONTENT_TYPE_GIF; + map["png"] = CONTENT_TYPE_PNG; + map["js"] = CONTENT_TYPE_JS; + + return map; +} + +const QStringMap WebApplication::CONTENT_TYPE_BY_EXT = WebApplication::initializeContentTypeByExtMap(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h new file mode 100644 index 000000000..0e005f0b3 --- /dev/null +++ b/src/webui/webapplication.h @@ -0,0 +1,87 @@ +/* + * 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 WEBAPPLICATION_H +#define WEBAPPLICATION_H + +#include +#include +#include +#include "httptypes.h" + +struct WebSession +{ + const QString id; + + WebSession(const QString& id): id(id) {} +}; + +const QString C_SID = "SID"; // name of session id cookie +const int BAN_TIME = 3600000; // 1 hour +const int MAX_AUTH_FAILED_ATTEMPTS = 5; + +class AbstractRequestHandler; + +class WebApplication: public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(WebApplication) + +public: + WebApplication(QObject* parent = 0); + virtual ~WebApplication(); + + static WebApplication* instance(); + + bool isBanned(const AbstractRequestHandler* _this) const; + int failedAttempts(const AbstractRequestHandler *_this) const; + void resetFailedAttempts(AbstractRequestHandler* _this); + void increaseFailedAttempts(AbstractRequestHandler* _this); + + bool sessionStart(AbstractRequestHandler* _this); + bool sessionEnd(AbstractRequestHandler* _this); + bool sessionInitialize(AbstractRequestHandler* _this); + + bool readFile(const QString &path, QByteArray& data, QString& type); + +private slots: + void UnbanTimerEvent(); + +private: + QMap sessions_; + QHash clientFailedAttempts_; + QMap translatedFiles_; + + QString generateSid(); + static void translateDocument(QString& data); + + static const QStringMap CONTENT_TYPE_BY_EXT; + static QStringMap initializeContentTypeByExtMap(); +}; + +#endif // WEBAPPLICATION_H diff --git a/src/webui/webui.pri b/src/webui/webui.pri index fb893d47b..be4989a22 100644 --- a/src/webui/webui.pri +++ b/src/webui/webui.pri @@ -10,6 +10,7 @@ HEADERS += $$PWD/httpserver.h \ $$PWD/httptypes.h \ $$PWD/extra_translations.h \ $$PWD/webapplication.h \ + $$PWD/abstractrequesthandler.h \ $$PWD/requesthandler.h SOURCES += $$PWD/httpserver.cpp \ @@ -19,6 +20,7 @@ SOURCES += $$PWD/httpserver.cpp \ $$PWD/btjson.cpp \ $$PWD/prefjson.cpp \ $$PWD/webapplication.cpp \ + $$PWD/abstractrequesthandler.cpp \ $$PWD/requesthandler.cpp # QJson JSON parser/serializer for using with Qt4