diff --git a/src/app/application.cpp b/src/app/application.cpp index 640f90985..73470159f 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -32,6 +32,7 @@ #include #include #include + #ifndef DISABLE_GUI #ifdef Q_OS_WIN #include @@ -39,6 +40,7 @@ #endif // Q_OS_WIN #ifdef Q_OS_MAC #include +#include #include #endif // Q_OS_MAC #include "mainwindow.h" @@ -47,8 +49,8 @@ #include #endif // DISABLE_GUI -#if (!defined(DISABLE_GUI) && defined(Q_OS_MAC)) -#include +#ifndef DISABLE_WEBUI +#include "webui.h" #endif #include "application.h" @@ -92,17 +94,6 @@ void Application::processMessage(const QString &message) m_paramsQueue.append(params); } -void Application::cleanup() -{ -#ifndef DISABLE_GUI - delete m_window; -#endif - QBtSession::drop(); - TorrentPersistentData::drop(); - Preferences::drop(); - Logger::drop(); -} - bool Application::sendParams(const QStringList ¶ms) { return sendMessage(params.join(QLatin1String(PARAMS_SEPARATOR))); @@ -159,7 +150,12 @@ int Application::exec(const QStringList ¶ms) // Resume unfinished torrents QBtSession::instance()->startUpTorrents(); +#ifndef DISABLE_WEBUI + m_webui = new WebUI; +#endif + #ifdef DISABLE_GUI +#ifndef DISABLE_WEBUI Preferences* const pref = Preferences::instance(); if (pref->isWebUiEnabled()) { // Display some information to the user @@ -172,9 +168,10 @@ int Application::exec(const QStringList ¶ms) std::cout << qPrintable(tr("This is a security risk, please consider changing your password from program preferences.")) << std::endl; } } +#endif // DISABLE_WEBUI #else m_window = new MainWindow; -#endif +#endif // DISABLE_GUI m_running = true; m_paramsQueue = params + m_paramsQueue; @@ -290,3 +287,17 @@ void Application::initializeTranslation() } #endif } + +void Application::cleanup() +{ +#ifndef DISABLE_GUI + delete m_window; +#endif +#ifndef DISABLE_WEBUI + delete m_webui; +#endif + QBtSession::drop(); + TorrentPersistentData::drop(); + Preferences::drop(); + Logger::drop(); +} diff --git a/src/app/application.h b/src/app/application.h index a3a0ba340..6dd03ac74 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -30,6 +30,7 @@ #ifndef APPLICATION_H #define APPLICATION_H +#include #include #include @@ -42,6 +43,10 @@ class MainWindow; typedef QtSingleCoreApplication BaseApplication; #endif +#ifndef DISABLE_WEBUI +class WebUI; +#endif + class Application : public BaseApplication { Q_OBJECT @@ -69,9 +74,15 @@ private slots: private: bool m_running; + #ifndef DISABLE_GUI QPointer m_window; #endif + +#ifndef DISABLE_WEBUI + QPointer m_webui; +#endif + QTranslator m_qtTranslator; QTranslator m_translator; QStringList m_paramsQueue; diff --git a/src/app/main.cpp b/src/app/main.cpp index 90a4fe739..26605d8a7 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -49,11 +49,8 @@ Q_IMPORT_PLUGIN(qico) #endif // QBT_STATIC_QT #else // DISABLE_GUI #include -#include #endif // DISABLE_GUI -#include "application.h" - #ifdef Q_OS_UNIX #include #include @@ -67,6 +64,8 @@ Q_IMPORT_PLUGIN(qico) #endif //STACKTRACE_WIN #include +#include +#include "application.h" #include "misc.h" #include "preferences.h" #include "logger.h" diff --git a/src/core/core.pri b/src/core/core.pri index 006e147b3..19707a885 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -3,7 +3,6 @@ INCLUDEPATH += $$PWD unix:!macx:dbus: include(qtnotify/qtnotify.pri) include(qtlibtorrent/qtlibtorrent.pri) -include(tracker/tracker.pri) HEADERS += \ $$PWD/misc.h \ @@ -16,10 +15,15 @@ HEADERS += \ $$PWD/smtp.h \ $$PWD/dnsupdater.h \ $$PWD/logger.h \ - $$PWD/httptypes.h \ - $$PWD/httprequestparser.h \ - $$PWD/httpresponsegenerator.h \ - $$PWD/preferences.h + $$PWD/preferences.h \ + $$PWD/qtracker.h \ + $$PWD/http/irequesthandler.h \ + $$PWD/http/connection.h \ + $$PWD/http/requestparser.h \ + $$PWD/http/responsegenerator.h \ + $$PWD/http/server.h \ + $$PWD/http/types.h \ + $$PWD/http/responsebuilder.h SOURCES += \ $$PWD/downloadthread.cpp \ @@ -30,6 +34,10 @@ SOURCES += \ $$PWD/smtp.cpp \ $$PWD/dnsupdater.cpp \ $$PWD/logger.cpp \ - $$PWD/httprequestparser.cpp \ - $$PWD/httpresponsegenerator.cpp \ - $$PWD/preferences.cpp + $$PWD/preferences.cpp \ + $$PWD/qtracker.cpp \ + $$PWD/http/connection.cpp \ + $$PWD/http/requestparser.cpp \ + $$PWD/http/responsegenerator.cpp \ + $$PWD/http/server.cpp \ + $$PWD/http/responsebuilder.cpp diff --git a/src/webui/httpconnection.cpp b/src/core/http/connection.cpp similarity index 73% rename from src/webui/httpconnection.cpp rename to src/core/http/connection.cpp index 3a6208bdb..435d2e73c 100644 --- a/src/webui/httpconnection.cpp +++ b/src/core/http/connection.cpp @@ -31,59 +31,60 @@ #include #include -#include "httptypes.h" -#include "httpserver.h" -#include "httprequestparser.h" -#include "httpresponsegenerator.h" -#include "webapplication.h" -#include "requesthandler.h" -#include "httpconnection.h" +#include "types.h" +#include "requestparser.h" +#include "responsegenerator.h" +#include "irequesthandler.h" +#include "connection.h" -HttpConnection::HttpConnection(QTcpSocket *socket, HttpServer *httpserver) - : QObject(httpserver), m_socket(socket) +using namespace Http; + +Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent) + : QObject(parent) + , m_socket(socket) + , m_requestHandler(requestHandler) { m_socket->setParent(this); connect(m_socket, SIGNAL(readyRead()), SLOT(read())); connect(m_socket, SIGNAL(disconnected()), SLOT(deleteLater())); } -HttpConnection::~HttpConnection() +Connection::~Connection() { - delete m_socket; } -void HttpConnection::read() +void Connection::read() { m_receivedData.append(m_socket->readAll()); - HttpRequest request; - HttpRequestParser::ErrorCode err = HttpRequestParser::parse(m_receivedData, request); + Request request; + RequestParser::ErrorCode err = RequestParser::parse(m_receivedData, request); switch (err) { - case HttpRequestParser::IncompleteRequest: + case RequestParser::IncompleteRequest: // Partial request waiting for the rest break; - case HttpRequestParser::BadRequest: - write(HttpResponse(400, "Bad Request")); + case RequestParser::BadRequest: + sendResponse(Response(400, "Bad Request")); break; - case HttpRequestParser::NoError: - HttpEnvironment env; + case RequestParser::NoError: + Environment env; env.clientAddress = m_socket->peerAddress(); - HttpResponse response = RequestHandler(request, env, WebApplication::instance()).run(); + Response response = m_requestHandler->processRequest(request, env); if (acceptsGzipEncoding(request.headers["accept-encoding"])) response.headers[HEADER_CONTENT_ENCODING] = "gzip"; - write(response); + sendResponse(response); break; } } -void HttpConnection::write(const HttpResponse& response) +void Connection::sendResponse(const Response &response) { - m_socket->write(HttpResponseGenerator::generate(response)); + m_socket->write(ResponseGenerator::generate(response)); m_socket->disconnectFromHost(); } -bool HttpConnection::acceptsGzipEncoding(const QString& encoding) +bool Connection::acceptsGzipEncoding(const QString &encoding) { int pos = encoding.indexOf("gzip", 0, Qt::CaseInsensitive); if (pos == -1) diff --git a/src/webui/httpconnection.h b/src/core/http/connection.h similarity index 79% rename from src/webui/httpconnection.h rename to src/core/http/connection.h index 0b768276a..4d3b28243 100644 --- a/src/webui/httpconnection.h +++ b/src/core/http/connection.h @@ -30,37 +30,42 @@ */ -#ifndef HTTPCONNECTION_H -#define HTTPCONNECTION_H +#ifndef HTTP_CONNECTION_H +#define HTTP_CONNECTION_H #include -#include "httptypes.h" - -class HttpServer; +#include "types.h" QT_BEGIN_NAMESPACE class QTcpSocket; QT_END_NAMESPACE -class HttpConnection : public QObject +namespace Http +{ + +class IRequestHandler; + +class Connection : public QObject { Q_OBJECT - Q_DISABLE_COPY(HttpConnection) + Q_DISABLE_COPY(Connection) public: - HttpConnection(QTcpSocket* socket, HttpServer* httpserver); - ~HttpConnection(); + Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = 0); + ~Connection(); private slots: void read(); private: - void write(const HttpResponse& response); - - static bool acceptsGzipEncoding(const QString& encoding); + static bool acceptsGzipEncoding(const QString &encoding); + void sendResponse(const Response &response); QTcpSocket *m_socket; + IRequestHandler *m_requestHandler; QByteArray m_receivedData; }; -#endif +} + +#endif // HTTP_CONNECTION_H diff --git a/src/core/http/irequesthandler.h b/src/core/http/irequesthandler.h new file mode 100644 index 000000000..2fab8d5c6 --- /dev/null +++ b/src/core/http/irequesthandler.h @@ -0,0 +1,47 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 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 HTTP_IREQUESTHANDLER_H +#define HTTP_IREQUESTHANDLER_H + +#include "types.h" + +namespace Http +{ + +class IRequestHandler +{ +public: + virtual ~IRequestHandler() {} + virtual Response processRequest(const Request &request, const Environment &env) = 0; +}; + +} + +#endif // HTTP_IREQUESTHANDLER_H + diff --git a/src/core/httprequestparser.cpp b/src/core/http/requestparser.cpp similarity index 82% rename from src/core/httprequestparser.cpp rename to src/core/http/requestparser.cpp index 8d0894d32..d056aa539 100644 --- a/src/core/httprequestparser.cpp +++ b/src/core/http/requestparser.cpp @@ -38,7 +38,7 @@ #include #include #include -#include "httprequestparser.h" +#include "requestparser.h" const QByteArray EOL("\r\n"); const QByteArray EOH("\r\n\r\n"); @@ -51,19 +51,21 @@ inline QString unquoted(const QString& str) return str; } -HttpRequestParser::ErrorCode HttpRequestParser::parse(const QByteArray& data, HttpRequest& request, uint maxContentLength) +using namespace Http; + +RequestParser::ErrorCode RequestParser::parse(const QByteArray& data, Request& request, uint maxContentLength) { - return HttpRequestParser(maxContentLength).parseHttpRequest(data, request); + return RequestParser(maxContentLength).parseHttpRequest(data, request); } -HttpRequestParser::HttpRequestParser(uint maxContentLength) - : maxContentLength_(maxContentLength) +RequestParser::RequestParser(uint maxContentLength) + : m_maxContentLength(maxContentLength) { } -HttpRequestParser::ErrorCode HttpRequestParser::parseHttpRequest(const QByteArray& data, HttpRequest& request) +RequestParser::ErrorCode RequestParser::parseHttpRequest(const QByteArray& data, Request& request) { - request_ = HttpRequest(); + m_request = Request(); // Parse HTTP request header const int header_end = data.indexOf(EOH); @@ -81,10 +83,10 @@ HttpRequestParser::ErrorCode HttpRequestParser::parseHttpRequest(const QByteArra // Parse HTTP request message int content_length = 0; - if (request_.headers.contains("content-length")) + if (m_request.headers.contains("content-length")) { - content_length = request_.headers["content-length"].toInt(); - if (content_length > static_cast(maxContentLength_)) + content_length = m_request.headers["content-length"].toInt(); + if (content_length > static_cast(m_maxContentLength)) { qWarning() << Q_FUNC_INFO << "bad request: message too long"; return BadRequest; @@ -108,20 +110,20 @@ HttpRequestParser::ErrorCode HttpRequestParser::parseHttpRequest(const QByteArra // qDebug() << "HTTP Request header:"; // qDebug() << data.left(header_end) << "\n"; - request = request_; + request = m_request; return NoError; } -bool HttpRequestParser::parseStartingLine(const QString &line) +bool RequestParser::parseStartingLine(const QString &line) { const QRegExp rx("^([A-Z]+)\\s+(\\S+)\\s+HTTP/\\d\\.\\d$"); if (rx.indexIn(line.trimmed()) >= 0) { - request_.method = rx.cap(1); + m_request.method = rx.cap(1); QUrl url = QUrl::fromEncoded(rx.cap(2).toLatin1()); - request_.path = url.path(); // Path + m_request.path = url.path(); // Path // Parse GET parameters #if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0)) @@ -132,7 +134,7 @@ bool HttpRequestParser::parseStartingLine(const QString &line) while (i.hasNext()) { QPair pair = i.next(); - request_.gets[pair.first] = pair.second; + m_request.gets[pair.first] = pair.second; } return true; @@ -142,7 +144,7 @@ bool HttpRequestParser::parseStartingLine(const QString &line) return false; } -bool HttpRequestParser::parseHeaderLine(const QString &line, QPair& out) +bool RequestParser::parseHeaderLine(const QString &line, QPair& out) { int i = line.indexOf(QLatin1Char(':')); if (i == -1) @@ -155,7 +157,7 @@ bool HttpRequestParser::parseHeaderLine(const QString &line, QPair HttpRequestParser::splitMultipartData(const QByteArray& data, const QByteArray& boundary) +QList RequestParser::splitMultipartData(const QByteArray& data, const QByteArray& boundary) { QList ret; QByteArray sep = boundary + EOL; @@ -223,14 +225,14 @@ QList HttpRequestParser::splitMultipartData(const QByteArray& data, return ret; } -bool HttpRequestParser::parseContent(const QByteArray& data) +bool RequestParser::parseContent(const QByteArray& data) { // Parse message content - qDebug() << Q_FUNC_INFO << "Content-Length: " << request_.headers["content-length"]; + qDebug() << Q_FUNC_INFO << "Content-Length: " << m_request.headers["content-length"]; qDebug() << Q_FUNC_INFO << "data.size(): " << data.size(); // Parse url-encoded POST data - if (request_.headers["content-type"].startsWith("application/x-www-form-urlencoded")) + if (m_request.headers["content-type"].startsWith("application/x-www-form-urlencoded")) { QUrl url; #if (QT_VERSION < QT_VERSION_CHECK(5, 0, 0)) @@ -243,7 +245,7 @@ bool HttpRequestParser::parseContent(const QByteArray& data) while (i.hasNext()) { QPair pair = i.next(); - request_.posts[pair.first.toLower()] = pair.second; + m_request.posts[pair.first.toLower()] = pair.second; } return true; @@ -268,7 +270,7 @@ Content-Disposition: form-data; name=\"Upload\" Submit Query --cH2ae0GI3KM7GI3Ij5ae0ei4Ij5Ij5-- **/ - QString content_type = request_.headers["content-type"]; + QString content_type = m_request.headers["content-type"]; if (content_type.startsWith("multipart/form-data")) { const QRegExp boundaryRegexQuoted("boundary=\"([ \\w'()+,-\\./:=\\?]+)\""); @@ -309,7 +311,7 @@ Submit Query return false; } -bool HttpRequestParser::parseFormData(const QByteArray& data) +bool RequestParser::parseFormData(const QByteArray& data) { // Parse form data header const int header_end = data.indexOf(EOH); @@ -347,17 +349,17 @@ bool HttpRequestParser::parseFormData(const QByteArray& data) ufile.type = disposition["content-type"]; ufile.data = data.mid(header_end + EOH.length()); - request_.files[disposition["name"]] = ufile; + m_request.files[disposition["name"]] = ufile; } else { - request_.posts[disposition["name"]] = QString::fromUtf8(data.mid(header_end + EOH.length())); + m_request.posts[disposition["name"]] = QString::fromUtf8(data.mid(header_end + EOH.length())); } return true; } -bool HttpRequestParser::parseHeaderValue(const QString& value, QStringMap& out) +bool RequestParser::parseHeaderValue(const QString& value, QStringMap& out) { QStringList items = value.split(QLatin1Char(';')); out[""] = items[0]; diff --git a/src/core/httprequestparser.h b/src/core/http/requestparser.h similarity index 83% rename from src/core/httprequestparser.h rename to src/core/http/requestparser.h index 9169c040f..01ddb7176 100644 --- a/src/core/httprequestparser.h +++ b/src/core/http/requestparser.h @@ -29,24 +29,27 @@ * Contact : chris@qbittorrent.org */ -#ifndef HTTPREQUESTPARSER_H -#define HTTPREQUESTPARSER_H +#ifndef HTTP_REQUESTPARSER_H +#define HTTP_REQUESTPARSER_H -#include "httptypes.h" +#include "types.h" -class HttpRequestParser +namespace Http +{ + +class RequestParser { public: enum ErrorCode { NoError = 0, IncompleteRequest, BadRequest }; // when result != NoError parsed request is undefined // Warning! Header names are converted to lower-case. - static ErrorCode parse(const QByteArray& data, HttpRequest& request, uint maxContentLength = 10000000 /* ~10MB */); + static ErrorCode parse(const QByteArray& data, Request& request, uint maxContentLength = 10000000 /* ~10MB */); private: - HttpRequestParser(uint maxContentLength); + RequestParser(uint maxContentLength); - ErrorCode parseHttpRequest(const QByteArray& data, HttpRequest& request); + ErrorCode parseHttpRequest(const QByteArray& data, Request& request); bool parseHttpHeader(const QByteArray& data); bool parseStartingLine(const QString &line); @@ -57,8 +60,10 @@ private: static bool parseHeaderLine(const QString& line, QPair& out); static bool parseHeaderValue(const QString& value, QStringMap& out); - const uint maxContentLength_; - HttpRequest request_; + const uint m_maxContentLength; + Request m_request; }; -#endif +} + +#endif // HTTP_REQUESTPARSER_H diff --git a/src/core/http/responsebuilder.cpp b/src/core/http/responsebuilder.cpp new file mode 100644 index 000000000..954ed05b5 --- /dev/null +++ b/src/core/http/responsebuilder.cpp @@ -0,0 +1,74 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 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 "responsebuilder.h" + +using namespace Http; + +ResponseBuilder::ResponseBuilder(QObject *parent) + : QObject(parent) +{ +} + +void ResponseBuilder::status(uint code, const QString &text) +{ + m_response.status = ResponseStatus(code, text); +} + +void ResponseBuilder::header(const QString &name, const QString &value) +{ + m_response.headers[name] = value; +} + +void ResponseBuilder::print(const QString &text, const QString &type) +{ + print_impl(text.toUtf8(), type); +} + +void ResponseBuilder::print(const QByteArray &data, const QString &type) +{ + print_impl(data, type); +} + +void ResponseBuilder::clear() +{ + m_response = Response(); +} + +Response ResponseBuilder::response() const +{ + return m_response; +} + +void ResponseBuilder::print_impl(const QByteArray &data, const QString &type) +{ + if (!m_response.headers.contains(HEADER_CONTENT_TYPE)) + m_response.headers[HEADER_CONTENT_TYPE] = type; + + m_response.content += data; +} diff --git a/src/core/http/responsebuilder.h b/src/core/http/responsebuilder.h new file mode 100644 index 000000000..0a3b0d8f1 --- /dev/null +++ b/src/core/http/responsebuilder.h @@ -0,0 +1,60 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 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 HTTP_RESPONSEBUILDER_H +#define HTTP_RESPONSEBUILDER_H + +#include +#include "types.h" + +namespace Http +{ + +class ResponseBuilder : public QObject +{ +public: + explicit ResponseBuilder(QObject *parent = 0); + +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); + void print(const QByteArray &data, const QString &type = CONTENT_TYPE_HTML); + void clear(); + + Response response() const; + +private: + void print_impl(const QByteArray &data, const QString &type); + + Response m_response; +}; + +} + +#endif // HTTP_RESPONSEBUILDER_H diff --git a/src/core/httpresponsegenerator.cpp b/src/core/http/responsegenerator.cpp similarity index 97% rename from src/core/httpresponsegenerator.cpp rename to src/core/http/responsegenerator.cpp index 02842c514..64f0ffbf7 100644 --- a/src/core/httpresponsegenerator.cpp +++ b/src/core/http/responsegenerator.cpp @@ -30,11 +30,13 @@ */ #include -#include "httpresponsegenerator.h" +#include "responsegenerator.h" bool gCompress(QByteArray data, QByteArray& dest_buffer); -QByteArray HttpResponseGenerator::generate(HttpResponse response) +using namespace Http; + +QByteArray ResponseGenerator::generate(Response response) { if (response.headers[HEADER_CONTENT_ENCODING] == "gzip") { diff --git a/src/core/httpresponsegenerator.h b/src/core/http/responsegenerator.h similarity index 87% rename from src/core/httpresponsegenerator.h rename to src/core/http/responsegenerator.h index d06e134c0..43d73d709 100644 --- a/src/core/httpresponsegenerator.h +++ b/src/core/http/responsegenerator.h @@ -30,15 +30,20 @@ */ -#ifndef HTTPRESPONSEGENERATOR_H -#define HTTPRESPONSEGENERATOR_H +#ifndef HTTP_RESPONSEGENERATOR_H +#define HTTP_RESPONSEGENERATOR_H -#include "httptypes.h" +#include "types.h" -class HttpResponseGenerator +namespace Http +{ + +class ResponseGenerator { public: - static QByteArray generate(HttpResponse response); + static QByteArray generate(Response response); }; -#endif +} + +#endif // HTTP_RESPONSEGENERATOR_H diff --git a/src/webui/httpserver.cpp b/src/core/http/server.cpp similarity index 84% rename from src/webui/httpserver.cpp rename to src/core/http/server.cpp index c37c5301c..6430f9745 100644 --- a/src/webui/httpserver.cpp +++ b/src/core/http/server.cpp @@ -33,30 +33,33 @@ #else #include #endif -#include "httpconnection.h" -#include "httpserver.h" +#include "connection.h" +#include "server.h" -HttpServer::HttpServer(QObject* parent) +using namespace Http; + +Server::Server(IRequestHandler *requestHandler, QObject* parent) : QTcpServer(parent) + , m_requestHandler(requestHandler) #ifndef QT_NO_OPENSSL , m_https(false) #endif { } -HttpServer::~HttpServer() +Server::~Server() { } #ifndef QT_NO_OPENSSL -void HttpServer::enableHttps(const QSslCertificate &certificate, const QSslKey &key) +void Server::enableHttps(const QSslCertificate &certificate, const QSslKey &key) { m_certificate = certificate; m_key = key; m_https = true; } -void HttpServer::disableHttps() +void Server::disableHttps() { m_https = false; m_certificate.clear(); @@ -65,9 +68,9 @@ void HttpServer::disableHttps() #endif #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) -void HttpServer::incomingConnection(qintptr socketDescriptor) +void Server::incomingConnection(qintptr socketDescriptor) #else -void HttpServer::incomingConnection(int socketDescriptor) +void Server::incomingConnection(int socketDescriptor) #endif { QTcpSocket *serverSocket; @@ -88,7 +91,7 @@ void HttpServer::incomingConnection(int socketDescriptor) static_cast(serverSocket)->startServerEncryption(); } #endif - new HttpConnection(serverSocket, this); + new Connection(serverSocket, m_requestHandler, this); } else { diff --git a/src/webui/httpserver.h b/src/core/http/server.h similarity index 87% rename from src/webui/httpserver.h rename to src/core/http/server.h index d68728f75..5ee591421 100644 --- a/src/webui/httpserver.h +++ b/src/core/http/server.h @@ -30,8 +30,8 @@ */ -#ifndef HTTPSERVER_H -#define HTTPSERVER_H +#ifndef HTTP_SERVER_H +#define HTTP_SERVER_H #include #ifndef QT_NO_OPENSSL @@ -39,14 +39,20 @@ #include #endif -class HttpServer : public QTcpServer +namespace Http +{ + +class IRequestHandler; +class Connection; + +class Server : public QTcpServer { Q_OBJECT - Q_DISABLE_COPY(HttpServer) + Q_DISABLE_COPY(Server) public: - HttpServer(QObject* parent = 0); - ~HttpServer(); + Server(IRequestHandler *requestHandler, QObject *parent = 0); + ~Server(); #ifndef QT_NO_OPENSSL void enableHttps(const QSslCertificate &certificate, const QSslKey &key); @@ -61,6 +67,7 @@ private: #endif private: + IRequestHandler *m_requestHandler; #ifndef QT_NO_OPENSSL bool m_https; QSslCertificate m_certificate; @@ -68,4 +75,6 @@ private: #endif }; -#endif +} + +#endif // HTTP_SERVER_H diff --git a/src/core/httptypes.h b/src/core/http/types.h similarity index 75% rename from src/core/httptypes.h rename to src/core/http/types.h index 7fd95c310..5857031bb 100644 --- a/src/core/httptypes.h +++ b/src/core/http/types.h @@ -26,8 +26,8 @@ * exception statement from your version. */ -#ifndef HTTPTYPES_H -#define HTTPTYPES_H +#ifndef HTTP_TYPES_H +#define HTTP_TYPES_H #include #include @@ -35,6 +35,9 @@ typedef QMap QStringMap; +namespace Http +{ + const QString HEADER_SET_COOKIE = "Set-Cookie"; const QString HEADER_CONTENT_TYPE = "Content-Type"; const QString HEADER_CONTENT_ENCODING = "Content-Encoding"; @@ -48,43 +51,45 @@ const QString CONTENT_TYPE_JS = "text/javascript; charset=UTF-8"; const QString CONTENT_TYPE_PNG = "image/png"; const QString CONTENT_TYPE_TXT = "text/plain; charset=UTF-8"; -struct HttpEnvironment +struct Environment { - QHostAddress clientAddress; + QHostAddress clientAddress; }; struct UploadedFile { - QString filename; // original filename - QString type; // MIME type - QByteArray data; // File data + QString filename; // original filename + QString type; // MIME type + QByteArray data; // File data }; -struct HttpRequest +struct Request { - QString method; - QString path; - QStringMap headers; - QStringMap gets; - QStringMap posts; - QMap files; + QString method; + QString path; + QStringMap headers; + QStringMap gets; + QStringMap posts; + QMap files; }; -struct HttpResponseStatus +struct ResponseStatus { - uint code; - QString text; + uint code; + QString text; - HttpResponseStatus(uint code = 200, const QString& text = "OK"): code(code), text(text) {} + ResponseStatus(uint code = 200, const QString& text = "OK"): code(code), text(text) {} }; -struct HttpResponse +struct Response { - HttpResponseStatus status; - QStringMap headers; - QByteArray content; + ResponseStatus status; + QStringMap headers; + QByteArray content; - HttpResponse(uint code = 200, const QString& text = "OK"): status(code, text) {} + Response(uint code = 200, const QString& text = "OK"): status(code, text) {} }; -#endif // HTTPTYPES_H +} + +#endif // HTTP_TYPES_H diff --git a/src/core/qtlibtorrent/qbtsession.cpp b/src/core/qtlibtorrent/qbtsession.cpp index cd46234aa..05aff6a20 100644 --- a/src/core/qtlibtorrent/qbtsession.cpp +++ b/src/core/qtlibtorrent/qbtsession.cpp @@ -57,7 +57,6 @@ #include "geoipmanager.h" #endif #include "torrentpersistentdata.h" -#include "httpserver.h" #include "bandwidthscheduler.h" #include #include @@ -79,7 +78,6 @@ #include #include #include -#include "dnsupdater.h" #if LIBTORRENT_VERSION_NUM < 10000 #include @@ -117,7 +115,6 @@ QBtSession::QBtSession() #if LIBTORRENT_VERSION_NUM < 10000 , m_upnp(0), m_natpmp(0) #endif - , m_dynDNSUpdater(0) , m_alertDispatcher(0) { BigRatioTimer = new QTimer(this); @@ -159,6 +156,7 @@ QBtSession::QBtSession() connect(m_scanFolders, SIGNAL(torrentsAdded(QStringList&)), SLOT(addTorrentsFromScanFolder(QStringList&))); // Apply user settings to Bittorrent session configureSession(); + connect(pref, SIGNAL(changed()), SLOT(configureSession())); // Torrent speed monitor m_speedMonitor = new TorrentSpeedMonitor(this); m_torrentStatistics = new TorrentStatistics(this, this); @@ -191,9 +189,6 @@ QBtSession::~QBtSession() { delete downloader; if (bd_scheduler) delete bd_scheduler; - // HTTP Server - if (httpServer) - delete httpServer; delete m_alertDispatcher; delete m_torrentStatistics; qDebug("Deleting the session"); @@ -583,9 +578,6 @@ void QBtSession::configureSession() { }else{ disableIPFilter(); } - // Update Web UI - // Use a QTimer because the function can be called from qBtSession constructor - QTimer::singleShot(0, this, SLOT(initWebUi())); // * Proxy settings proxy_settings proxySettings; if (pref->isProxyEnabled()) { @@ -655,64 +647,6 @@ void QBtSession::configureSession() { qDebug("Session configured"); } -void QBtSession::initWebUi() { - Preferences* const pref = Preferences::instance(); - if (pref->isWebUiEnabled()) { - const quint16 port = pref->getWebUiPort(); - - if (httpServer) { - if (httpServer->serverPort() != port) { - httpServer->close(); - } - } else { - httpServer = new HttpServer(this); - } - -#ifndef QT_NO_OPENSSL - if (pref->isWebUiHttpsEnabled()) { - QSslCertificate cert(pref->getWebUiHttpsCertificate()); - QSslKey key; - const QByteArray raw_key = pref->getWebUiHttpsKey(); - key = QSslKey(raw_key, QSsl::Rsa); - if (!cert.isNull() && !key.isNull()) - httpServer->enableHttps(cert, key); - else - httpServer->disableHttps(); - } else { - httpServer->disableHttps(); - } -#endif - - if (!httpServer->isListening()) { - Logger* const logger = Logger::instance(); - bool success = httpServer->listen(QHostAddress::Any, port); - if (success) - logger->addMessage(tr("The Web UI is listening on port %1").arg(port)); - else - logger->addMessage(tr("Web User Interface Error - Unable to bind Web UI to port %1").arg(port), Log::CRITICAL); - } - // DynDNS - if (pref->isDynDNSEnabled()) { - if (!m_dynDNSUpdater) - m_dynDNSUpdater = new DNSUpdater(this); - else - m_dynDNSUpdater->updateCredentials(); - } else { - if (m_dynDNSUpdater) { - delete m_dynDNSUpdater; - m_dynDNSUpdater = 0; - } - } - } else { - if (httpServer) - delete httpServer; - if (m_dynDNSUpdater) { - delete m_dynDNSUpdater; - m_dynDNSUpdater = 0; - } - } -} - void QBtSession::useAlternativeSpeedsLimit(bool alternative) { qDebug() << Q_FUNC_INFO << alternative; // Save new state to remember it on startup @@ -1495,6 +1429,7 @@ void QBtSession::enableUPnP(bool b) { s->start_upnp(); s->start_natpmp(); #endif + // TODO: Remove dependency from WebUI // Use UPnP/NAT-PMP for Web UI too if (pref->isWebUiEnabled() && pref->useUPnPForWebUIPort()) { const qint16 port = pref->getWebUiPort(); diff --git a/src/core/qtlibtorrent/qbtsession.h b/src/core/qtlibtorrent/qbtsession.h index e856519d1..3edbe6a38 100644 --- a/src/core/qtlibtorrent/qbtsession.h +++ b/src/core/qtlibtorrent/qbtsession.h @@ -90,12 +90,10 @@ namespace libtorrent { class DownloadThread; class FilterParserThread; -class HttpServer; class BandwidthScheduler; class ScanFoldersModel; class TorrentSpeedMonitor; class TorrentStatistics; -class DNSUpdater; class QAlertDispatcher; enum TorrentExportFolder { @@ -209,7 +207,6 @@ public slots: #endif void addMagnetInteractive(const QString& uri); void downloadFromURLList(const QStringList& urls); - void configureSession(); void banIP(QString ip); void recursiveTorrentDownload(const QTorrentHandle &h); void unhideMagnet(const QString &hash); @@ -264,9 +261,9 @@ private slots: void mergeTorrents(QTorrentHandle& h_ex, boost::intrusive_ptr t); void mergeTorrents(QTorrentHandle& h_ex, const QString& magnet_uri); void exportTorrentFile(const QTorrentHandle &h, TorrentExportFolder folder = RegularTorrentExportFolder); - void initWebUi(); void handleIPFilterParsed(int ruleCount); void handleIPFilterError(); + void configureSession(); signals: void addedTorrent(const QTorrentHandle& h); @@ -327,8 +324,6 @@ private: // IP filtering QPointer filterParser; QString filterPath; - // Web UI - QPointer httpServer; QList url_skippingDlg; // GeoIP #ifndef DISABLE_GUI @@ -344,8 +339,6 @@ private: libtorrent::upnp *m_upnp; libtorrent::natpmp *m_natpmp; #endif - // DynDNS - DNSUpdater *m_dynDNSUpdater; QAlertDispatcher* m_alertDispatcher; TorrentStatistics* m_torrentStatistics; }; diff --git a/src/core/tracker/qtracker.cpp b/src/core/qtracker.cpp similarity index 54% rename from src/core/tracker/qtracker.cpp rename to src/core/qtracker.cpp index 0e334ccda..5afad0d50 100644 --- a/src/core/tracker/qtracker.cpp +++ b/src/core/qtracker.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt4 and libtorrent. + * Copyright (C) 2015 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -28,163 +29,171 @@ * Contact : chris@qbittorrent.org */ -#include - +#include #include -#include -#include "qtracker.h" #include "preferences.h" -#include "httpresponsegenerator.h" -#include "httprequestparser.h" +#include "http/server.h" +#include "qtracker.h" -#include +// QPeer +bool QPeer::operator!=(const QPeer &other) const +{ + return qhash() != other.qhash(); +} -using namespace libtorrent; +bool QPeer::operator==(const QPeer &other) const +{ + return qhash() == other.qhash(); +} -QTracker::QTracker(QObject *parent) : - QTcpServer(parent) +QString QPeer::qhash() const { - Q_ASSERT(Preferences::instance()->isTrackerEnabled()); - connect(this, SIGNAL(newConnection()), this, SLOT(handlePeerConnection())); + return ip + ":" + QString::number(port); } -QTracker::~QTracker() { - if (isListening()) { - qDebug("Shutting down the embedded tracker..."); - close(); - } - // TODO: Store the torrent list +libtorrent::entry QPeer::toEntry(bool no_peer_id) const +{ + libtorrent::entry::dictionary_type peer_map; + if (!no_peer_id) + peer_map["id"] = libtorrent::entry(peer_id.toStdString()); + peer_map["ip"] = libtorrent::entry(ip.toStdString()); + peer_map["port"] = libtorrent::entry(port); + + return libtorrent::entry(peer_map); } -void QTracker::handlePeerConnection() +// QTracker + +QTracker::QTracker(QObject *parent) + : Http::ResponseBuilder(parent) + , m_server(new Http::Server(this, this)) { - QTcpSocket *socket; - while((socket = nextPendingConnection())) - { - qDebug("QTracker: New peer connection"); - connect(socket, SIGNAL(readyRead()), SLOT(readRequest())); - } +} + +QTracker::~QTracker() +{ + if (m_server->isListening()) + qDebug("Shutting down the embedded tracker..."); + // TODO: Store the torrent list } bool QTracker::start() { const int listen_port = Preferences::instance()->getTrackerPort(); - // - if (isListening()) { - if (serverPort() == listen_port) { + + if (m_server->isListening()) { + if (m_server->serverPort() == listen_port) { // Already listening on the right port, just return return true; } // Wrong port, closing the server - close(); + m_server->close(); } + qDebug("Starting the embedded tracker..."); // Listen on the predefined port - return listen(QHostAddress::Any, listen_port); + return m_server->listen(QHostAddress::Any, listen_port); } -void QTracker::readRequest() +Http::Response QTracker::processRequest(const Http::Request &request, const Http::Environment &env) { - QTcpSocket *socket = static_cast(sender()); - QByteArray input = socket->readAll(); - //qDebug("QTracker: Raw request:\n%s", input.data()); - 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 (request.method != "GET") { - qDebug("QTracker: Unsupported HTTP request: %s", qPrintable(request.method)); - respondInvalidRequest(socket, 100, "Invalid request type"); - return; - } - if (!request.path.startsWith("/announce", Qt::CaseInsensitive)) { - qDebug("QTracker: Unrecognized path: %s", qPrintable(request.path)); - respondInvalidRequest(socket, 100, "Invalid request type"); - return; - } + clear(); // clear response - // OK, this is a GET request - respondToAnnounceRequest(socket, request.gets); -} + //qDebug("QTracker received the following request:\n%s", qPrintable(parser.toString())); + // Is request a GET request? + if (request.method != "GET") { + qDebug("QTracker: Unsupported HTTP request: %s", qPrintable(request.method)); + status(100, "Invalid request type"); + } + else if (!request.path.startsWith("/announce", Qt::CaseInsensitive)) { + qDebug("QTracker: Unrecognized path: %s", qPrintable(request.path)); + status(100, "Invalid request type"); + } + else { + // OK, this is a GET request + m_request = request; + m_env = env; + respondToAnnounceRequest(); + } -void QTracker::respondInvalidRequest(QTcpSocket *socket, int code, QString msg) -{ - HttpResponse response(code, msg); - socket->write(HttpResponseGenerator::generate(response)); - socket->disconnectFromHost(); + return response(); } -void QTracker::respondToAnnounceRequest(QTcpSocket *socket, - const QMap& get_parameters) +void QTracker::respondToAnnounceRequest() { + const QStringMap &gets = m_request.gets; TrackerAnnounceRequest annonce_req; + // IP - annonce_req.peer.ip = socket->peerAddress().toString(); + annonce_req.peer.ip = m_env.clientAddress.toString(); + // 1. Get info_hash - if (!get_parameters.contains("info_hash")) { + if (!gets.contains("info_hash")) { qDebug("QTracker: Missing info_hash"); - respondInvalidRequest(socket, 101, "Missing info_hash"); + status(101, "Missing info_hash"); return; } - annonce_req.info_hash = get_parameters.value("info_hash"); + annonce_req.info_hash = gets.value("info_hash"); // info_hash cannot be longer than 20 bytes /*if (annonce_req.info_hash.toLatin1().length() > 20) { qDebug("QTracker: Info_hash is not 20 byte long: %s (%d)", qPrintable(annonce_req.info_hash), annonce_req.info_hash.toLatin1().length()); - respondInvalidRequest(socket, 150, "Invalid infohash"); + status(150, "Invalid infohash"); return; }*/ + // 2. Get peer ID - if (!get_parameters.contains("peer_id")) { + if (!gets.contains("peer_id")) { qDebug("QTracker: Missing peer_id"); - respondInvalidRequest(socket, 102, "Missing peer_id"); + status(102, "Missing peer_id"); return; } - annonce_req.peer.peer_id = get_parameters.value("peer_id"); + annonce_req.peer.peer_id = gets.value("peer_id"); // peer_id cannot be longer than 20 bytes /*if (annonce_req.peer.peer_id.length() > 20) { qDebug("QTracker: peer_id is not 20 byte long: %s", qPrintable(annonce_req.peer.peer_id)); - respondInvalidRequest(socket, 151, "Invalid peerid"); + status(151, "Invalid peerid"); return; }*/ + // 3. Get port - if (!get_parameters.contains("port")) { + if (!gets.contains("port")) { qDebug("QTracker: Missing port"); - respondInvalidRequest(socket, 103, "Missing port"); + status(103, "Missing port"); return; } bool ok = false; - annonce_req.peer.port = get_parameters.value("port").toInt(&ok); + annonce_req.peer.port = gets.value("port").toInt(&ok); if (!ok || annonce_req.peer.port < 1 || annonce_req.peer.port > 65535) { qDebug("QTracker: Invalid port number (%d)", annonce_req.peer.port); - respondInvalidRequest(socket, 103, "Missing port"); + status(103, "Missing port"); return; } + // 4. Get event annonce_req.event = ""; - if (get_parameters.contains("event")) { - annonce_req.event = get_parameters.value("event"); + if (gets.contains("event")) { + annonce_req.event = gets.value("event"); qDebug("QTracker: event is %s", qPrintable(annonce_req.event)); } + // 5. Get numwant annonce_req.numwant = 50; - if (get_parameters.contains("numwant")) { - int tmp = get_parameters.value("numwant").toInt(); + if (gets.contains("numwant")) { + int tmp = gets.value("numwant").toInt(); if (tmp > 0) { - qDebug("QTracker: numwant=%d", tmp); + qDebug("QTracker: numwant = %d", tmp); annonce_req.numwant = tmp; } } + // 6. no_peer_id (extension) annonce_req.no_peer_id = false; - if (get_parameters.contains("no_peer_id")) { + if (gets.contains("no_peer_id")) annonce_req.no_peer_id = true; - } + // 7. TODO: support "compact" extension + // Done parsing, now let's reply if (m_torrents.contains(annonce_req.info_hash)) { if (annonce_req.event == "stopped") { @@ -207,33 +216,32 @@ void QTracker::respondToAnnounceRequest(QTcpSocket *socket, } peers[annonce_req.peer.qhash()] = annonce_req.peer; m_torrents[annonce_req.info_hash] = peers; + // Reply - ReplyWithPeerList(socket, annonce_req); + replyWithPeerList(annonce_req); } -void QTracker::ReplyWithPeerList(QTcpSocket *socket, const TrackerAnnounceRequest &annonce_req) +void QTracker::replyWithPeerList(const TrackerAnnounceRequest &annonce_req) { // Prepare the entry for bencoding - entry::dictionary_type reply_dict; - reply_dict["interval"] = entry(ANNOUNCE_INTERVAL); + libtorrent::entry::dictionary_type reply_dict; + reply_dict["interval"] = libtorrent::entry(ANNOUNCE_INTERVAL); QList peers = m_torrents.value(annonce_req.info_hash).values(); - entry::list_type peer_list; + libtorrent::entry::list_type peer_list; foreach (const QPeer & p, peers) { //if (p != annonce_req.peer) peer_list.push_back(p.toEntry(annonce_req.no_peer_id)); } - reply_dict["peers"] = entry(peer_list); - entry reply_entry(reply_dict); + reply_dict["peers"] = libtorrent::entry(peer_list); + libtorrent::entry reply_entry(reply_dict); // bencode std::vector buf; - bencode(std::back_inserter(buf), reply_entry); + libtorrent::bencode(std::back_inserter(buf), reply_entry); QByteArray reply(&buf[0], static_cast(buf.size())); qDebug("QTracker: reply with the following bencoded data:\n %s", reply.constData()); + // HTTP reply - HttpResponse response(200, "OK"); - response.content = reply; - socket->write(HttpResponseGenerator::generate(response)); - socket->disconnectFromHost(); + print(reply, Http::CONTENT_TYPE_TXT); } diff --git a/src/core/tracker/qtracker.h b/src/core/qtracker.h similarity index 64% rename from src/core/tracker/qtracker.h rename to src/core/qtracker.h index bf313ba18..520480d78 100644 --- a/src/core/tracker/qtracker.h +++ b/src/core/qtracker.h @@ -1,5 +1,6 @@ /* - * Bittorrent Client using Qt4 and libtorrent. + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -31,11 +32,33 @@ #ifndef QTRACKER_H #define QTRACKER_H -#include +#include #include +#include "http/types.h" +#include "http/responsebuilder.h" +#include "http/irequesthandler.h" -#include "trackerannouncerequest.h" -#include "qpeer.h" +struct QPeer +{ + QString ip; + QString peer_id; + int port; + + bool operator!=(const QPeer &other) const; + bool operator==(const QPeer &other) const; + QString qhash() const; + libtorrent::entry toEntry(bool no_peer_id) const; +}; + +struct TrackerAnnounceRequest +{ + QString info_hash; + QString event; + int numwant; + QPeer peer; + // Extensions + bool no_peer_id; +}; // static limits const int MAX_TORRENTS = 100; @@ -45,9 +68,11 @@ const int ANNOUNCE_INTERVAL = 1800; // 30min typedef QHash PeerList; typedef QHash TorrentList; -/* Basic Bittorrent tracker implementation in Qt4 */ +namespace Http { class Server; } + +/* Basic Bittorrent tracker implementation in Qt */ /* Following http://wiki.theory.org/BitTorrent_Tracker_Protocol */ -class QTracker : public QTcpServer +class QTracker : public Http::ResponseBuilder, public Http::IRequestHandler { Q_OBJECT Q_DISABLE_COPY(QTracker) @@ -55,18 +80,19 @@ class QTracker : public QTcpServer public: explicit QTracker(QObject *parent = 0); ~QTracker(); - bool start(); -protected slots: - void readRequest(); - void handlePeerConnection(); - void respondInvalidRequest(QTcpSocket *socket, int code, QString msg); - void respondToAnnounceRequest(QTcpSocket *socket, const QMap& get_parameters); - void ReplyWithPeerList(QTcpSocket *socket, const TrackerAnnounceRequest &annonce_req); + bool start(); + Http::Response processRequest(const Http::Request &request, const Http::Environment &env); private: + void respondToAnnounceRequest(); + void replyWithPeerList(const TrackerAnnounceRequest &annonce_req); + + Http::Server *m_server; TorrentList m_torrents; + Http::Request m_request; + Http::Environment m_env; }; #endif // QTRACKER_H diff --git a/src/core/tracker/qpeer.h b/src/core/tracker/qpeer.h deleted file mode 100644 index 05073ee0f..000000000 --- a/src/core/tracker/qpeer.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef QPEER_H -#define QPEER_H - -#include -#include - -struct QPeer { - - bool operator!=(const QPeer &other) const { - return qhash() != other.qhash(); - } - - bool operator==(const QPeer &other) const { - return qhash() == other.qhash(); - } - - QString qhash() const { - return ip+":"+QString::number(port); - } - - libtorrent::entry toEntry(bool no_peer_id) const { - libtorrent::entry::dictionary_type peer_map; - if (!no_peer_id) - peer_map["id"] = libtorrent::entry(peer_id.toStdString()); - peer_map["ip"] = libtorrent::entry(ip.toStdString()); - peer_map["port"] = libtorrent::entry(port); - return libtorrent::entry(peer_map); - } - - QString ip; - QString peer_id; - int port; -}; - -#endif // QPEER_H diff --git a/src/core/tracker/tracker.pri b/src/core/tracker/tracker.pri deleted file mode 100644 index 5b5809a86..000000000 --- a/src/core/tracker/tracker.pri +++ /dev/null @@ -1,9 +0,0 @@ -INCLUDEPATH += $$PWD - -HEADERS += \ - $$PWD/qtracker.h \ - $$PWD/trackerannouncerequest.h \ - $$PWD/qpeer.h - -SOURCES += \ - $$PWD/qtracker.cpp diff --git a/src/core/tracker/trackerannouncerequest.h b/src/core/tracker/trackerannouncerequest.h deleted file mode 100644 index 273ced385..000000000 --- a/src/core/tracker/trackerannouncerequest.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef TRACKERANNOUNCEREQUEST_H -#define TRACKERANNOUNCEREQUEST_H - -#include - -struct TrackerAnnounceRequest { - QString info_hash; - QString event; - int numwant; - QPeer peer; - // Extensions - bool no_peer_id; -}; - -#endif // TRACKERANNOUNCEREQUEST_H diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index a8eb3170c..1adc167bd 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1185,9 +1185,6 @@ void MainWindow::loadPreferences(bool configure_session) IconProvider::instance()->useSystemIconTheme(pref->useSystemIconTheme()); #endif - if (configure_session) - QBtSession::instance()->configureSession(); - #if defined(Q_OS_WIN) || defined(Q_OS_MAC) if (pref->isUpdateCheckEnabled()) checkProgramUpdate(); diff --git a/src/gui/options_imp.cpp b/src/gui/options_imp.cpp index 18f394ff4..cbfa62c8a 100644 --- a/src/gui/options_imp.cpp +++ b/src/gui/options_imp.cpp @@ -70,7 +70,11 @@ options_imp::options_imp(QWidget *parent): tabSelection->item(TAB_CONNECTION)->setIcon(IconProvider::instance()->getIcon("network-wired")); tabSelection->item(TAB_DOWNLOADS)->setIcon(IconProvider::instance()->getIcon("download")); tabSelection->item(TAB_SPEED)->setIcon(IconProvider::instance()->getIcon("chronometer")); +#ifndef DISABLE_WEBUI tabSelection->item(TAB_WEBUI)->setIcon(IconProvider::instance()->getIcon("network-server")); +#else + tabSelection->item(TAB_WEBUI)->setHidden(true); +#endif tabSelection->item(TAB_ADVANCED)->setIcon(IconProvider::instance()->getIcon("preferences-other")); IpFilterRefreshBtn->setIcon(IconProvider::instance()->getIcon("view-refresh")); @@ -229,6 +233,7 @@ options_imp::options_imp(QWidget *parent): connect(spinMaxActiveUploads, SIGNAL(valueChanged(QString)), this, SLOT(enableApplyButton())); connect(spinMaxActiveTorrents, SIGNAL(valueChanged(QString)), this, SLOT(enableApplyButton())); connect(checkIgnoreSlowTorrentsForQueueing, SIGNAL(toggled(bool)), this, SLOT(enableApplyButton())); +#ifndef DISABLE_WEBUI // Web UI tab connect(checkWebUi, SIGNAL(toggled(bool)), this, SLOT(enableApplyButton())); connect(spinWebUiPort, SIGNAL(valueChanged(int)), this, SLOT(enableApplyButton())); @@ -244,6 +249,7 @@ options_imp::options_imp(QWidget *parent): connect(domainNameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); connect(DNSUsernameTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); connect(DNSPasswordTxt, SIGNAL(textChanged(QString)), SLOT(enableApplyButton())); +#endif // Disable apply Button applyButton->setEnabled(false); // Tab selection mecanism diff --git a/src/webui/abstractrequesthandler.cpp b/src/webui/abstractrequesthandler.cpp deleted file mode 100644 index 8753e9409..000000000 --- a/src/webui/abstractrequesthandler.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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) -{ - sessionInitialize(); - if (!sessionActive() && !isAuthNeeded()) - sessionStart(); -} - -HttpResponse AbstractRequestHandler::run() -{ - if (isBanned()) { - status(403, "Forbidden"); - print(QObject::tr("Your IP address has been banned after too many failed authentication attempts."), CONTENT_TYPE_TXT); - } - else { - 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/abstractwebapplication.cpp b/src/webui/abstractwebapplication.cpp new file mode 100644 index 000000000..be73940f9 --- /dev/null +++ b/src/webui/abstractwebapplication.cpp @@ -0,0 +1,413 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include + +#include "preferences.h" +#include "websessiondata.h" +#include "abstractwebapplication.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) + { + } + + void updateTimestamp() + { + timestamp = QDateTime::currentDateTime().toTime_t(); + } +}; + +// AbstractWebApplication + +AbstractWebApplication::AbstractWebApplication(QObject *parent) + : Http::ResponseBuilder(parent) + , session_(0) +{ + QTimer *timer = new QTimer(this); + timer->setInterval(60000); // 1 min. + connect(timer, SIGNAL(timeout()), SLOT(removeInactiveSessions())); +} + +AbstractWebApplication::~AbstractWebApplication() +{ + // cleanup sessions data + qDeleteAll(sessions_); +} + +Http::Response AbstractWebApplication::processRequest(const Http::Request &request, const Http::Environment &env) +{ + request_ = request; + env_ = env; + + clear(); // clear 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 { + processRequest(); + } + + return response(); +} + +void AbstractWebApplication::UnbanTimerEvent() +{ + UnbanTimer* ubantimer = static_cast(sender()); + qDebug("Ban period has expired for %s", qPrintable(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); + } +} + +bool AbstractWebApplication::sessionInitialize() +{ + static const QString SID_START = QLatin1String(C_SID) + QLatin1String("="); + + if (session_ == 0) + { + QString cookie = request_.headers.value("cookie"); + //qDebug() << Q_FUNC_INFO << "cookie: " << cookie; + + QString sessionId; + 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)) + { + 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!", 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; +} + +WebSessionData *AbstractWebApplication::session() +{ + Q_ASSERT(session_ != 0); + return &session_->data; +} + + +QString AbstractWebApplication::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 AbstractWebApplication::translateDocument(QString& data) +{ + const QRegExp regex("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR"); + 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", + "StatusBar" + }; + 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 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() +{ + return (env_.clientAddress != QHostAddress::LocalHost + && env_.clientAddress != QHostAddress::LocalHostIPv6) + || Preferences::instance()->isWebUiLocalAuthEnabled(); +} + +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()); + session_->updateTimestamp(); + sessions_[session_->id] = session_; + + QNetworkCookie cookie(C_SID, session_->id.toUtf8()); + 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, session_->id.toUtf8()); + cookie.setPath(QLatin1String("/")); + cookie.setExpirationDate(QDateTime::currentDateTime()); + + 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(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(); +} + +QStringMap AbstractWebApplication::initializeContentTypeByExtMap() +{ + QStringMap map; + + map["htm"] = Http::CONTENT_TYPE_HTML; + map["html"] = Http::CONTENT_TYPE_HTML; + map["css"] = Http::CONTENT_TYPE_CSS; + map["gif"] = Http::CONTENT_TYPE_GIF; + map["png"] = Http::CONTENT_TYPE_PNG; + map["js"] = Http::CONTENT_TYPE_JS; + + return map; +} + +const QStringMap AbstractWebApplication::CONTENT_TYPE_BY_EXT = AbstractWebApplication::initializeContentTypeByExtMap(); diff --git a/src/webui/abstractrequesthandler.h b/src/webui/abstractwebapplication.h similarity index 51% rename from src/webui/abstractrequesthandler.h rename to src/webui/abstractwebapplication.h index b279d1272..bac977fca 100644 --- a/src/webui/abstractrequesthandler.h +++ b/src/webui/abstractwebapplication.h @@ -26,64 +26,83 @@ * exception statement from your version. */ -#ifndef ABSTRACTREQUESTHANDLER_H -#define ABSTRACTREQUESTHANDLER_H +#ifndef ABSTRACTWEBAPPLICATION_H +#define ABSTRACTWEBAPPLICATION_H -#include -#include "httptypes.h" +#include +#include +#include +#include "http/types.h" +#include "http/responsebuilder.h" +#include "http/irequesthandler.h" -class WebApplication; struct WebSession; +struct WebSessionData; -class AbstractRequestHandler +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 { - friend class WebApplication; + Q_OBJECT + Q_DISABLE_COPY(AbstractWebApplication) public: - AbstractRequestHandler( - const HttpRequest& request, const HttpEnvironment& env, - WebApplication* app); + explicit AbstractWebApplication(QObject *parent = 0); + virtual ~AbstractWebApplication(); - HttpResponse run(); + Http::Response processRequest(const Http::Request &request, const Http::Environment &env); 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(); + 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); + static QString saveTmpFile(const QByteArray &data); + + WebSessionData *session(); + Http::Request request() const { return request_; } + Http::Environment env() const { return env_; } - inline WebSession* session() { return session_; } - inline HttpRequest request() const { return request_; } - inline HttpEnvironment env() const { return env_; } +private slots: + void UnbanTimerEvent(); + void removeInactiveSessions(); private: - WebApplication* app_; - WebSession* session_; - const HttpRequest request_; - const HttpEnvironment env_; - HttpResponse response_; + // Persistent data + QMap sessions_; + QHash clientFailedAttempts_; + QMap translatedFiles_; + + // Current data + WebSession *session_; + Http::Request request_; + Http::Environment env_; + + QString generateSid(); + bool sessionInitialize(); + + static void translateDocument(QString &data); - void print_impl(const QByteArray& data, const QString& type); + static const QStringMap CONTENT_TYPE_BY_EXT; + static QStringMap initializeContentTypeByExtMap(); }; -#endif // ABSTRACTREQUESTHANDLER_H +#endif // ABSTRACTWEBAPPLICATION_H diff --git a/src/webui/prefjson.cpp b/src/webui/prefjson.cpp index 23c2f7e59..61e52a3a2 100644 --- a/src/webui/prefjson.cpp +++ b/src/webui/prefjson.cpp @@ -333,6 +333,6 @@ void prefjson::setPreferences(const QString& json) pref->setDynDNSPassword(m["dyndns_password"].toString()); if (m.contains("dyndns_domain")) pref->setDynDomainName(m["dyndns_domain"].toString()); - // Reload preferences - QBtSession::instance()->configureSession(); + // Save preferences + pref->save(); } diff --git a/src/webui/requesthandler.cpp b/src/webui/requesthandler.cpp deleted file mode 100644 index 595eb5382..000000000 --- a/src/webui/requesthandler.cpp +++ /dev/null @@ -1,716 +0,0 @@ -/* - * 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" -#include "webapplication.h" - -using namespace libtorrent; - -static const int API_VERSION = 2; -static const int API_VERSION_MIN = 2; - -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] = &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(query, torrents); - ADD_ACTION(query, preferences); - ADD_ACTION(query, transferInfo); - ADD_ACTION(query, propertiesGeneral); - ADD_ACTION(query, propertiesTrackers); - ADD_ACTION(query, propertiesFiles); - ADD_ACTION(sync, maindata); - 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, alternativeSpeedLimitsEnabled); - ADD_ACTION(command, toggleAlternativeSpeedLimits); - ADD_ACTION(command, toggleSequentialDownload); - ADD_ACTION(command, toggleFirstLastPiecePrio); - 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); - ADD_ACTION(version, api); - ADD_ACTION(version, api_min); - ADD_ACTION(version, qbittorrent); - - return actions; -} - -#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; \ - } \ - } - -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(); - - bool equalUser = misc::slowEquals(request().posts["username"].toUtf8(), pref->getWebUiUsername().toUtf8()); - bool equalPass = misc::slowEquals(pass.toUtf8(), pref->getWebUiPassword().toUtf8()); - - if (equalUser && equalPass) { - 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() -{ - CHECK_URI(0); - 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); - header(HEADER_CACHE_CONTROL, MAX_AGE_MONTH); -} - -void RequestHandler::action_public_images() -{ - const QString path = ":/icons/" + args_.join("/"); - printFile(path); - header(HEADER_CACHE_CONTROL, MAX_AGE_MONTH); -} - -// GET params: -// - filter (string): all, downloading, completed, paused, active, inactive -// - label (string): torrent label for filtering by it (empty string means "unlabeled"; no "label" param presented means "any label") -// - 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 RequestHandler::action_query_torrents() -{ - CHECK_URI(0); - const QStringMap& gets = request().gets; - - print(btjson::getTorrents( - gets["filter"], gets["label"], gets["sort"], gets["reverse"] == "true", - gets["limit"].toInt(), gets["offset"].toInt() - ), CONTENT_TYPE_JS); -} - -void RequestHandler::action_query_preferences() -{ - CHECK_URI(0); - print(prefjson::getPreferences(), CONTENT_TYPE_JS); -} - -void RequestHandler::action_query_transferInfo() -{ - CHECK_URI(0); - print(btjson::getTransferInfo(), CONTENT_TYPE_JS); -} - -void RequestHandler::action_query_propertiesGeneral() -{ - CHECK_URI(1); - print(btjson::getPropertiesForTorrent(args_.front()), CONTENT_TYPE_JS); -} - -void RequestHandler::action_query_propertiesTrackers() -{ - CHECK_URI(1); - print(btjson::getTrackersForTorrent(args_.front()), CONTENT_TYPE_JS); -} - -void RequestHandler::action_query_propertiesFiles() -{ - CHECK_URI(1); - print(btjson::getFilesForTorrent(args_.front()), CONTENT_TYPE_JS); -} - -// GET param: -// - rid (int): last response id -void RequestHandler::action_sync_maindata() -{ - CHECK_URI(0); - print(btjson::getSyncMainData(request().gets["rid"].toInt(), session()->syncMainDataLastResponse, session()->syncMainDataLastAcceptedResponse)); -} - -void RequestHandler::action_version_api() -{ - CHECK_URI(0); - print(QString::number(API_VERSION), CONTENT_TYPE_TXT); -} - -void RequestHandler::action_version_api_min() -{ - CHECK_URI(0); - print(QString::number(API_VERSION_MIN), CONTENT_TYPE_TXT); -} - -void RequestHandler::action_version_qbittorrent() -{ - CHECK_URI(0); - print(QString(VERSION), CONTENT_TYPE_TXT); -} - -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. - - CHECK_URI(0); - QTimer::singleShot(0, qApp, SLOT(quit())); -} - -void RequestHandler::action_command_download() -{ - CHECK_URI(0); - CHECK_PARAMETERS("urls"); - 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; - CHECK_URI(0); - - foreach(const UploadedFile& torrent, request().files) { - QString filePath = saveTmpFile(torrent.data); - - if (!filePath.isEmpty()) { - QTorrentHandle h = QBtSession::instance()->addTorrent(filePath); - if (!h.is_valid()) { - status(415, "Internal Server Error"); - print(QObject::tr("Error: '%1' is not a valid torrent file.\n").arg(torrent.filename), CONTENT_TYPE_TXT); - } - // Clean up - fsutils::forceRemove(filePath); - } - else { - qWarning() << "I/O Error: Could not create temporary file"; - status(500, "Internal Server Error"); - print(QObject::tr("I/O Error: Could not create temporary file."), CONTENT_TYPE_TXT); - } - } -} - -void RequestHandler::action_command_addTrackers() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "urls"); - 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() -{ - CHECK_URI(0); - QBtSession::instance()->resumeAllTorrents(); -} - -void RequestHandler::action_command_pauseAll() -{ - CHECK_URI(0); - QBtSession::instance()->pauseAllTorrents(); -} - -void RequestHandler::action_command_resume() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - QBtSession::instance()->resumeTorrent(request().posts["hash"]); -} - -void RequestHandler::action_command_pause() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - QBtSession::instance()->pauseTorrent(request().posts["hash"]); -} - -void RequestHandler::action_command_setPreferences() -{ - CHECK_URI(0); - CHECK_PARAMETERS("json"); - prefjson::setPreferences(request().posts["json"]); -} - -void RequestHandler::action_command_setFilePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "id" << "priority"); - 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() -{ - CHECK_URI(0); - print(QByteArray::number(QBtSession::instance()->getSession()->settings().upload_rate_limit)); -} - -void RequestHandler::action_command_getGlobalDlLimit() -{ - CHECK_URI(0); - print(QByteArray::number(QBtSession::instance()->getSession()->settings().download_rate_limit)); -} - -void RequestHandler::action_command_setGlobalUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - QBtSession::instance()->setUploadRateLimit(limit); - if (Preferences::instance()->isAltBandwidthEnabled()) - Preferences::instance()->setAltGlobalUploadLimit(limit / 1024.); - else - Preferences::instance()->setGlobalUploadLimit(limit / 1024.); -} - -void RequestHandler::action_command_setGlobalDlLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("limit"); - qlonglong limit = request().posts["limit"].toLongLong(); - if (limit == 0) limit = -1; - - QBtSession::instance()->setDownloadRateLimit(limit); - if (Preferences::instance()->isAltBandwidthEnabled()) - Preferences::instance()->setAltGlobalDownloadLimit(limit / 1024.); - else - Preferences::instance()->setGlobalDownloadLimit(limit / 1024.); -} - -void RequestHandler::action_command_getTorrentUpLimit() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "limit"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash" << "limit"); - 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_toggleAlternativeSpeedLimits() -{ - CHECK_URI(0); - QBtSession::instance()->useAlternativeSpeedsLimit(!Preferences::instance()->isAltBandwidthEnabled()); -} - -void RequestHandler::action_command_alternativeSpeedLimitsEnabled() -{ - CHECK_URI(0); - print(QByteArray::number(Preferences::instance()->isAltBandwidthEnabled())); -} - -void RequestHandler::action_command_toggleSequentialDownload() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split("|"); - foreach (const QString &hash, hashes) { - try { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - h.toggleSequentialDownload(); - } - catch(invalid_handle&) {} - } -} - -void RequestHandler::action_command_toggleFirstLastPiecePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split("|"); - foreach (const QString &hash, hashes) { - try { - QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); - h.toggleFirstLastPiecePrio(); - } - catch(invalid_handle&) {} - } -} - -void RequestHandler::action_command_delete() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split("|"); - foreach (const QString &hash, hashes) - QBtSession::instance()->deleteTorrent(hash, false); -} - -void RequestHandler::action_command_deletePerm() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - QStringList hashes = request().posts["hashes"].split("|"); - foreach (const QString &hash, hashes) - QBtSession::instance()->deleteTorrent(hash, true); -} - -void RequestHandler::action_command_increasePrio() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hashes"); - 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() -{ - CHECK_URI(0); - CHECK_PARAMETERS("hash"); - QBtSession::instance()->recheckTorrent(request().posts["hash"]); -} - -bool RequestHandler::isPublicScope() -{ - return (scope_ == DEFAULT_SCOPE || scope_ == VERSION_INFO); -} - -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 deleted file mode 100644 index 015f245e8..000000000 --- a/src/webui/requesthandler.h +++ /dev/null @@ -1,108 +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 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_query_torrents(); - void action_query_preferences(); - void action_query_transferInfo(); - void action_query_propertiesGeneral(); - void action_query_propertiesTrackers(); - void action_query_propertiesFiles(); - void action_sync_maindata(); - 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_alternativeSpeedLimitsEnabled(); - void action_command_toggleAlternativeSpeedLimits(); - void action_command_toggleSequentialDownload(); - void action_command_toggleFirstLastPiecePrio(); - 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(); - void action_version_api(); - void action_version_api_min(); - void action_version_qbittorrent(); - - 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 index 67ce61421..f7140e96c 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -26,285 +26,691 @@ * exception statement from your version. */ -#ifdef DISABLE_GUI +#include #include -#else -#include -#endif -#include #include -#include -#include +#include +#include +#include +#include +#ifndef DISABLE_GUI +// TODO: Drop GUI dependency! +#include "iconprovider.h" +#endif +#include "misc.h" +#include "fs_utils.h" #include "preferences.h" -#include "requesthandler.h" +#include "btjson.h" +#include "prefjson.h" +#include "qbtsession.h" +#include "websessiondata.h" #include "webapplication.h" -// UnbanTimer +using namespace libtorrent; -class UnbanTimer: public QTimer +static const int API_VERSION = 2; +static const int API_VERSION_MIN = 2; + +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() { -public: - UnbanTimer(const QHostAddress& peer_ip, QObject *parent) - : QTimer(parent), m_peerIp(peer_ip) - { - setSingleShot(true); - setInterval(BAN_TIME); - } + 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, propertiesFiles); + ADD_ACTION(sync, maindata); + 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, alternativeSpeedLimitsEnabled); + ADD_ACTION(command, toggleAlternativeSpeedLimits); + ADD_ACTION(command, toggleSequentialDownload); + ADD_ACTION(command, toggleFirstLastPiecePrio); + 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); + ADD_ACTION(version, api); + ADD_ACTION(version, api_min); + ADD_ACTION(version, qbittorrent); + + return actions; +} - inline const QHostAddress& peerIp() const { return m_peerIp; } +#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; \ + } \ + } -private: - QHostAddress m_peerIp; -}; +void WebApplication::action_public_index() +{ + QString path; -// WebApplication + if (!args_.isEmpty()) { + if (args_.back() == "favicon.ico") + path = ":/icons/skin/qbittorrent16.png"; + else + path = WWW_FOLDER + args_.join("/"); + } -WebApplication::WebApplication(QObject *parent) - : QObject(parent) + printFile(path); +} + +void WebApplication::action_public_webui() { + if (!sessionActive()) + printFile(PRIVATE_FOLDER + "login.html"); + else + printFile(PRIVATE_FOLDER + "index.html"); } -WebApplication::~WebApplication() +void WebApplication::action_public_login() { - // cleanup sessions data - foreach (WebSession* session, sessions_.values()) - delete session; + const Preferences* const pref = Preferences::instance(); + QCryptographicHash md5(QCryptographicHash::Md5); + + md5.addData(request().posts["password"].toLocal8Bit()); + QString pass = md5.result().toHex(); + + bool equalUser = misc::slowEquals(request().posts["username"].toUtf8(), pref->getWebUiUsername().toUtf8()); + bool equalPass = misc::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)", qPrintable(addr), failedAttempts()); + print(QByteArray("Fails."), Http::CONTENT_TYPE_TXT); + } } -WebApplication *WebApplication::instance() +void WebApplication::action_public_logout() { - static WebApplication inst; - return &inst; + CHECK_URI(0); + sessionEnd(); } -void WebApplication::UnbanTimerEvent() +void WebApplication::action_public_theme() { - UnbanTimer* ubantimer = static_cast(sender()); - qDebug("Ban period has expired for %s", qPrintable(ubantimer->peerIp().toString())); - clientFailedAttempts_.remove(ubantimer->peerIp()); - ubantimer->deleteLater(); + 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); + header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); } -bool WebApplication::sessionInitialize(AbstractRequestHandler* _this) +void WebApplication::action_public_images() { - if (_this->session_ == 0) - { - QString cookie = _this->request_.headers.value("cookie"); - //qDebug() << Q_FUNC_INFO << "cookie: " << cookie; + const QString path = ":/icons/" + args_.join("/"); + printFile(path); + header(Http::HEADER_CACHE_CONTROL, MAX_AGE_MONTH); +} - 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); - } +// GET params: +// - filter (string): all, downloading, completed, paused, active, inactive +// - label (string): torrent label for filtering by it (empty string means "unlabeled"; no "label" param presented means "any label") +// - 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; - // 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; - } + print(btjson::getTorrents( + gets["filter"], gets["label"], gets["sort"], gets["reverse"] == "true", + gets["limit"].toInt(), gets["offset"].toInt() + ), Http::CONTENT_TYPE_JS); +} + +void WebApplication::action_query_preferences() +{ + CHECK_URI(0); + print(prefjson::getPreferences(), Http::CONTENT_TYPE_JS); +} + +void WebApplication::action_query_transferInfo() +{ + CHECK_URI(0); + print(btjson::getTransferInfo(), Http::CONTENT_TYPE_JS); +} + +void WebApplication::action_query_propertiesGeneral() +{ + CHECK_URI(1); + print(btjson::getPropertiesForTorrent(args_.front()), Http::CONTENT_TYPE_JS); +} + +void WebApplication::action_query_propertiesTrackers() +{ + CHECK_URI(1); + print(btjson::getTrackersForTorrent(args_.front()), Http::CONTENT_TYPE_JS); +} + +void WebApplication::action_query_propertiesFiles() +{ + CHECK_URI(1); + print(btjson::getFilesForTorrent(args_.front()), Http::CONTENT_TYPE_JS); +} - data = file.readAll(); - file.close(); +// 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)); +} - // Translate the file - if ((ext == "html") || ((ext == "js") && !path.endsWith("excanvas-compressed.js"))) - { - QString dataStr = QString::fromUtf8(data.constData()); - translateDocument(dataStr); +void WebApplication::action_version_api() +{ + CHECK_URI(0); + print(QString::number(API_VERSION), Http::CONTENT_TYPE_TXT); +} - if (path.endsWith("about.html")) - { - dataStr.replace("${VERSION}", VERSION); - } +void WebApplication::action_version_api_min() +{ + CHECK_URI(0); + print(QString::number(API_VERSION_MIN), Http::CONTENT_TYPE_TXT); +} - data = dataStr.toUtf8(); - translatedFiles_[path] = data; // cashing translated file +void WebApplication::action_version_qbittorrent() +{ + CHECK_URI(0); + print(QString(VERSION), Http::CONTENT_TYPE_TXT); +} + +void WebApplication::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. + + CHECK_URI(0); + QTimer::singleShot(0, qApp, SLOT(quit())); +} + +void WebApplication::action_command_download() +{ + CHECK_URI(0); + CHECK_PARAMETERS("urls"); + 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); + } + } } - } - - 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("QBT_TR\\((([^\\)]|\\)(?!QBT_TR))+)\\)QBT_TR"); - 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", - "StatusBar" - }; - 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; +} + +void WebApplication::action_command_upload() +{ + qDebug() << Q_FUNC_INFO; + CHECK_URI(0); + + foreach(const Http::UploadedFile& torrent, request().files) { + QString filePath = saveTmpFile(torrent.data); + + if (!filePath.isEmpty()) { + QTorrentHandle h = QBtSession::instance()->addTorrent(filePath); + if (!h.is_valid()) { + status(415, "Internal Server Error"); + print(QObject::tr("Error: '%1' is not a valid torrent file.\n").arg(torrent.filename), Http::CONTENT_TYPE_TXT); + } + // Clean up + fsutils::forceRemove(filePath); + } + else { + qWarning() << "I/O Error: Could not create temporary file"; + status(500, "Internal Server Error"); + print(QObject::tr("I/O Error: Could not create temporary file."), Http::CONTENT_TYPE_TXT); } - } - // Remove keyboard shortcuts - translation.replace(mnemonic, ""); + } +} - data.replace(i, regex.matchedLength(), translation); - i += translation.length(); +void WebApplication::action_command_addTrackers() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash" << "urls"); + 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 WebApplication::action_command_resumeAll() +{ + CHECK_URI(0); + QBtSession::instance()->resumeAllTorrents(); +} + +void WebApplication::action_command_pauseAll() +{ + CHECK_URI(0); + QBtSession::instance()->pauseAllTorrents(); +} + +void WebApplication::action_command_resume() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash"); + QBtSession::instance()->resumeTorrent(request().posts["hash"]); +} + +void WebApplication::action_command_pause() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash"); + QBtSession::instance()->pauseTorrent(request().posts["hash"]); +} + +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 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 WebApplication::action_command_getGlobalUpLimit() +{ + CHECK_URI(0); + print(QByteArray::number(QBtSession::instance()->getSession()->settings().upload_rate_limit)); +} + +void WebApplication::action_command_getGlobalDlLimit() +{ + CHECK_URI(0); + print(QByteArray::number(QBtSession::instance()->getSession()->settings().download_rate_limit)); +} + +void WebApplication::action_command_setGlobalUpLimit() +{ + CHECK_URI(0); + CHECK_PARAMETERS("limit"); + qlonglong limit = request().posts["limit"].toLongLong(); + if (limit == 0) limit = -1; + + QBtSession::instance()->setUploadRateLimit(limit); + if (Preferences::instance()->isAltBandwidthEnabled()) + Preferences::instance()->setAltGlobalUploadLimit(limit / 1024.); else - { - found = false; // no more translatable strings + Preferences::instance()->setGlobalUploadLimit(limit / 1024.); +} + +void WebApplication::action_command_setGlobalDlLimit() +{ + CHECK_URI(0); + CHECK_PARAMETERS("limit"); + qlonglong limit = request().posts["limit"].toLongLong(); + if (limit == 0) limit = -1; + + QBtSession::instance()->setDownloadRateLimit(limit); + if (Preferences::instance()->isAltBandwidthEnabled()) + Preferences::instance()->setAltGlobalDownloadLimit(limit / 1024.); + else + Preferences::instance()->setGlobalDownloadLimit(limit / 1024.); +} + +void WebApplication::action_command_getTorrentUpLimit() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash"); + QString hash = request().posts["hash"]; + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid()) + print(QByteArray::number(h.upload_limit())); +} + +void WebApplication::action_command_getTorrentDlLimit() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash"); + QString hash = request().posts["hash"]; + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + + if (h.is_valid()) + print(QByteArray::number(h.download_limit())); +} + +void WebApplication::action_command_setTorrentUpLimit() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash" << "limit"); + 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 WebApplication::action_command_setTorrentDlLimit() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hash" << "limit"); + 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 WebApplication::action_command_toggleAlternativeSpeedLimits() +{ + CHECK_URI(0); + QBtSession::instance()->useAlternativeSpeedsLimit(!Preferences::instance()->isAltBandwidthEnabled()); +} + +void WebApplication::action_command_alternativeSpeedLimitsEnabled() +{ + CHECK_URI(0); + print(QByteArray::number(Preferences::instance()->isAltBandwidthEnabled())); +} + +void WebApplication::action_command_toggleSequentialDownload() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + QStringList hashes = request().posts["hashes"].split("|"); + foreach (const QString &hash, hashes) { + try { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + h.toggleSequentialDownload(); + } + catch(invalid_handle&) {} } - } } -bool WebApplication::isBanned(const AbstractRequestHandler *_this) const +void WebApplication::action_command_toggleFirstLastPiecePrio() { - return clientFailedAttempts_.value(_this->env_.clientAddress, 0) >= MAX_AUTH_FAILED_ATTEMPTS; + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + QStringList hashes = request().posts["hashes"].split("|"); + foreach (const QString &hash, hashes) { + try { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + h.toggleFirstLastPiecePrio(); + } + catch(invalid_handle&) {} + } } -int WebApplication::failedAttempts(const AbstractRequestHandler* _this) const +void WebApplication::action_command_delete() { - return clientFailedAttempts_.value(_this->env_.clientAddress, 0); + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + QStringList hashes = request().posts["hashes"].split("|"); + foreach (const QString &hash, hashes) + QBtSession::instance()->deleteTorrent(hash, false); } -void WebApplication::resetFailedAttempts(AbstractRequestHandler* _this) +void WebApplication::action_command_deletePerm() { - clientFailedAttempts_.remove(_this->env_.clientAddress); + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + QStringList hashes = request().posts["hashes"].split("|"); + foreach (const QString &hash, hashes) + QBtSession::instance()->deleteTorrent(hash, true); } -void WebApplication::increaseFailedAttempts(AbstractRequestHandler* _this) +void WebApplication::action_command_increasePrio() { - const int nb_fail = clientFailedAttempts_.value(_this->env_.clientAddress, 0) + 1; + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + 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 WebApplication::action_command_decreasePrio() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + 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 WebApplication::action_command_topPrio() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + foreach (const QString &hash, request().posts["hashes"].split("|")) { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + if (h.is_valid()) h.queue_position_top(); + } +} - 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(); - } +void WebApplication::action_command_bottomPrio() +{ + CHECK_URI(0); + CHECK_PARAMETERS("hashes"); + foreach (const QString &hash, request().posts["hashes"].split("|")) { + QTorrentHandle h = QBtSession::instance()->getTorrentHandle(hash); + if (h.is_valid()) h.queue_position_bottom(); + } } -bool WebApplication::sessionStart(AbstractRequestHandler *_this) +void WebApplication::action_command_recheck() { - if (_this->session_ == 0) - { - _this->session_ = new WebSession(generateSid()); - sessions_[_this->session_->id] = _this->session_; - return true; - } + CHECK_URI(0); + CHECK_PARAMETERS("hash"); + QBtSession::instance()->recheckTorrent(request().posts["hash"]); +} - return false; +bool WebApplication::isPublicScope() +{ + return (scope_ == DEFAULT_SCOPE || scope_ == VERSION_INFO); } -bool WebApplication::sessionEnd(AbstractRequestHandler *_this) +void WebApplication::processRequest() { - if ((_this->session_ != 0) && (sessions_.contains(_this->session_->id))) - { - sessions_.remove(_this->session_->id); - delete _this->session_; - _this->session_ = 0; - return true; - } + scope_ = DEFAULT_SCOPE; + action_ = DEFAULT_ACTION; + + parsePath(); + + 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; + } - return false; + 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; + } } -QStringMap WebApplication::initializeContentTypeByExtMap() +void WebApplication::parsePath() { - QStringMap map; + 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(); + } + } - 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; + if (!pathItems.empty()) { + if (actions_[scope_].contains(pathItems.front())) { + action_ = pathItems.front(); + pathItems.pop_front(); + } + } + + args_ = pathItems; +} - return map; +WebApplication::WebApplication(QObject *parent) + : AbstractWebApplication(parent) +{ } -const QStringMap WebApplication::CONTENT_TYPE_BY_EXT = WebApplication::initializeContentTypeByExtMap(); +QMap > WebApplication::actions_ = WebApplication::initializeActions(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index f35f67009..b4b6b9b3f 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -29,61 +29,77 @@ #ifndef WEBAPPLICATION_H #define WEBAPPLICATION_H -#include -#include -#include -#include "httptypes.h" -#include +#include +#include "abstractwebapplication.h" -struct WebSession +class WebApplication: public AbstractWebApplication { - const QString id; - QVariantMap syncMainDataLastResponse; - QVariantMap syncMainDataLastAcceptedResponse; - 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(); + explicit WebApplication(QObject* parent = 0); private: - QMap sessions_; - QHash clientFailedAttempts_; - QMap translatedFiles_; - - QString generateSid(); - static void translateDocument(QString& data); - - static const QStringMap CONTENT_TYPE_BY_EXT; - static QStringMap initializeContentTypeByExtMap(); + // 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_propertiesFiles(); + void action_sync_maindata(); + 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_alternativeSpeedLimitsEnabled(); + void action_command_toggleAlternativeSpeedLimits(); + void action_command_toggleSequentialDownload(); + void action_command_toggleFirstLastPiecePrio(); + 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(); + void action_version_api(); + void action_version_api_min(); + void action_version_qbittorrent(); + + typedef void (WebApplication::*Action)(); + + QString scope_; + QString action_; + QStringList args_; + + void processRequest(); + + bool isPublicScope(); + void parsePath(); + + static QMap > initializeActions(); + static QMap > actions_; }; #endif // WEBAPPLICATION_H diff --git a/src/webui/websessiondata.h b/src/webui/websessiondata.h new file mode 100644 index 000000000..fe7b4eeec --- /dev/null +++ b/src/webui/websessiondata.h @@ -0,0 +1,41 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 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 WEBSESSIONDATA +#define WEBSESSIONDATA + +#include + +struct WebSessionData +{ + QVariantMap syncMainDataLastResponse; + QVariantMap syncMainDataLastAcceptedResponse; +}; + +#endif // WEBSESSIONDATA + diff --git a/src/webui/webui.cpp b/src/webui/webui.cpp new file mode 100644 index 000000000..6cafe0b4b --- /dev/null +++ b/src/webui/webui.cpp @@ -0,0 +1,102 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 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 "webui.h" +#include "http/server.h" +#include "webapplication.h" +#include "dnsupdater.h" +#include "preferences.h" +#include "logger.h" + +WebUI::WebUI(QObject *parent) : QObject(parent) +{ + init(); + connect(Preferences::instance(), SIGNAL(changed()), SLOT(init())); +} + +void WebUI::init() +{ + Preferences* const pref = Preferences::instance(); + Logger* const logger = Logger::instance(); + + if (pref->isWebUiEnabled()) { + const quint16 port = pref->getWebUiPort(); + + if (httpServer_) { + if (httpServer_->serverPort() != port) + httpServer_->close(); + } + else { + webapp_ = new WebApplication(this); + httpServer_ = new Http::Server(webapp_, this); + } + +#ifndef QT_NO_OPENSSL + if (pref->isWebUiHttpsEnabled()) { + QSslCertificate cert(pref->getWebUiHttpsCertificate()); + QSslKey key; + key = QSslKey(pref->getWebUiHttpsKey(), QSsl::Rsa); + if (!cert.isNull() && !key.isNull()) + httpServer_->enableHttps(cert, key); + else + httpServer_->disableHttps(); + } + else { + httpServer_->disableHttps(); + } +#endif + + if (!httpServer_->isListening()) { + bool success = httpServer_->listen(QHostAddress::Any, port); + if (success) + logger->addMessage(tr("The Web UI is listening on port %1").arg(port)); + else + logger->addMessage(tr("Web User Interface Error - Unable to bind Web UI to port %1").arg(port), Log::CRITICAL); + } + + // DynDNS + if (pref->isDynDNSEnabled()) { + if (!dynDNSUpdater_) + dynDNSUpdater_ = new DNSUpdater(this); + else + dynDNSUpdater_->updateCredentials(); + } + else { + if (dynDNSUpdater_) + delete dynDNSUpdater_; + } + } + else { + if (httpServer_) + delete httpServer_; + if (webapp_) + delete webapp_; + if (dynDNSUpdater_) + delete dynDNSUpdater_; + } +} diff --git a/src/webui/webui.h b/src/webui/webui.h new file mode 100644 index 000000000..2abc6b924 --- /dev/null +++ b/src/webui/webui.h @@ -0,0 +1,55 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015 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 WEBUI_H +#define WEBUI_H + +#include +#include + +namespace Http { class Server; } +class DNSUpdater; +class AbstractWebApplication; + +class WebUI : public QObject +{ + Q_OBJECT + +public: + explicit WebUI(QObject *parent = 0); + +private slots: + void init(); + +private: + QPointer httpServer_; + QPointer dynDNSUpdater_; + QPointer webapp_; +}; + +#endif // WEBUI_H diff --git a/src/webui/webui.pri b/src/webui/webui.pri index ba94e2297..1c36c4a9c 100644 --- a/src/webui/webui.pri +++ b/src/webui/webui.pri @@ -1,30 +1,25 @@ INCLUDEPATH += $$PWD -HEADERS += $$PWD/httpserver.h \ - $$PWD/httpconnection.h \ - $$PWD/btjson.h \ - $$PWD/prefjson.h \ - $$PWD/jsonutils.h \ - $$PWD/extra_translations.h \ - $$PWD/webapplication.h \ - $$PWD/abstractrequesthandler.h \ - $$PWD/requesthandler.h \ - $$PWD/qtorrentfilter.h +HEADERS += \ + $$PWD/webui.h \ + $$PWD/btjson.h \ + $$PWD/prefjson.h \ + $$PWD/jsonutils.h \ + $$PWD/extra_translations.h \ + $$PWD/webapplication.h \ + $$PWD/qtorrentfilter.h \ + $$PWD/websessiondata.h \ + $$PWD/abstractwebapplication.h -SOURCES += $$PWD/httpserver.cpp \ - $$PWD/httpconnection.cpp \ - $$PWD/httprequestparser.cpp \ - $$PWD/httpresponsegenerator.cpp \ - $$PWD/btjson.cpp \ - $$PWD/prefjson.cpp \ - $$PWD/webapplication.cpp \ - $$PWD/abstractrequesthandler.cpp \ - $$PWD/requesthandler.cpp \ - $$PWD/qtorrentfilter.cpp +SOURCES += \ + $$PWD/webui.cpp \ + $$PWD/btjson.cpp \ + $$PWD/prefjson.cpp \ + $$PWD/webapplication.cpp \ + $$PWD/qtorrentfilter.cpp \ + $$PWD/abstractwebapplication.cpp # QJson JSON parser/serializer for using with Qt4 -lessThan(QT_MAJOR_VERSION, 5) { - include(qjson/qjson.pri) -} +lessThan(QT_MAJOR_VERSION, 5): include(qjson/qjson.pri) RESOURCES += $$PWD/webui.qrc