Merge pull request #6654 from Chocobo1/persistence

Webui server fixes
This commit is contained in:
sledgehammer999
2017-04-30 16:34:23 +03:00
committed by GitHub
15 changed files with 349 additions and 223 deletions

View File

@@ -1,4 +1,4 @@
find_package(ZLIB REQUIRED)
find_package(ZLIB 1.2.5.2 REQUIRED)
set(QBT_BASE_HEADERS
bittorrent/addtorrentparams.h

View File

@@ -29,14 +29,14 @@
* Contact : chris@qbittorrent.org
*/
#include <QTcpSocket>
#include <QDebug>
#include "connection.h"
#include <QRegExp>
#include "types.h"
#include <QTcpSocket>
#include "irequesthandler.h"
#include "requestparser.h"
#include "responsegenerator.h"
#include "irequesthandler.h"
#include "connection.h"
using namespace Http;
@@ -46,27 +46,33 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
, m_requestHandler(requestHandler)
{
m_socket->setParent(this);
m_idleTimer.start();
connect(m_socket, SIGNAL(readyRead()), SLOT(read()));
connect(m_socket, SIGNAL(disconnected()), SLOT(deleteLater()));
}
Connection::~Connection()
{
m_socket->close();
}
void Connection::read()
{
m_receivedData.append(m_socket->readAll());
m_idleTimer.restart();
m_receivedData.append(m_socket->readAll());
Request request;
RequestParser::ErrorCode err = RequestParser::parse(m_receivedData, request);
RequestParser::ErrorCode err = RequestParser::parse(m_receivedData, request); // TODO: transform request headers to lowercase
switch (err) {
case RequestParser::IncompleteRequest:
// Partial request waiting for the rest
break;
case RequestParser::BadRequest:
sendResponse(Response(400, "Bad Request"));
m_receivedData.clear();
break;
case RequestParser::NoError:
Environment env;
env.clientAddress = m_socket->peerAddress();
@@ -74,25 +80,65 @@ void Connection::read()
if (acceptsGzipEncoding(request.headers["accept-encoding"]))
response.headers[HEADER_CONTENT_ENCODING] = "gzip";
sendResponse(response);
m_receivedData.clear();
break;
}
}
void Connection::sendResponse(const Response &response)
{
m_socket->write(ResponseGenerator::generate(response));
m_socket->disconnectFromHost();
m_socket->write(toByteArray(response));
m_socket->close(); // TODO: remove when HTTP pipelining is supported
}
bool Connection::acceptsGzipEncoding(const QString &encoding)
bool Connection::hasExpired(const qint64 timeout) const
{
QRegExp rx("(gzip)(;q=([^,]+))?");
if (rx.indexIn(encoding) >= 0) {
if (rx.cap(2).size() > 0)
// check if quality factor > 0
return (rx.cap(3).toDouble() > 0);
// if quality factor is not specified, then it's 1
return m_idleTimer.hasExpired(timeout);
}
bool Connection::isClosed() const
{
return (m_socket->state() == QAbstractSocket::UnconnectedState);
}
bool Connection::acceptsGzipEncoding(QString codings)
{
// [rfc7231] 5.3.4. Accept-Encoding
const auto isCodingAvailable = [](const QStringList &list, const QString &encoding) -> bool
{
foreach (const QString &str, list) {
if (!str.startsWith(encoding))
continue;
// without quality values
if (str == encoding)
return true;
// [rfc7231] 5.3.1. Quality Values
const QStringRef substr = str.midRef(encoding.size() + 3); // ex. skip over "gzip;q="
bool ok = false;
const double qvalue = substr.toDouble(&ok);
if (!ok || (qvalue <= 0.0))
return false;
return true;
}
return false;
};
const QStringList list = codings.remove(' ').remove('\t').split(',', QString::SkipEmptyParts);
if (list.isEmpty())
return false;
const bool canGzip = isCodingAvailable(list, QLatin1String("gzip"));
if (canGzip)
return true;
}
const bool canAny = isCodingAvailable(list, QLatin1String("*"));
if (canAny)
return true;
return false;
}

View File

@@ -33,12 +33,12 @@
#ifndef HTTP_CONNECTION_H
#define HTTP_CONNECTION_H
#include <QElapsedTimer>
#include <QObject>
#include "types.h"
QT_BEGIN_NAMESPACE
class QTcpSocket;
QT_END_NAMESPACE
namespace Http
{
@@ -53,16 +53,20 @@ namespace Http
Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = 0);
~Connection();
bool hasExpired(qint64 timeout) const;
bool isClosed() const;
private slots:
void read();
private:
static bool acceptsGzipEncoding(const QString &encoding);
static bool acceptsGzipEncoding(QString codings);
void sendResponse(const Response &response);
QTcpSocket *m_socket;
IRequestHandler *m_requestHandler;
QByteArray m_receivedData;
QElapsedTimer m_idleTimer;
};
}

View File

@@ -29,39 +29,79 @@
* Contact : chris@qbittorrent.org
*/
#include "base/utils/gzip.h"
#include "responsegenerator.h"
using namespace Http;
#include <QDateTime>
QByteArray ResponseGenerator::generate(Response response)
#include "base/utils/gzip.h"
QByteArray Http::toByteArray(Response response)
{
if (response.headers[HEADER_CONTENT_ENCODING] == "gzip") {
// A gzip seems to have 23 bytes overhead.
// Also "Content-Encoding: gzip\r\n" is 26 bytes long
// So we only benefit from gzip if the message is bigger than 23+26 = 49
// If the message is smaller than 49 bytes we actually send MORE data if we gzip
QByteArray dest_buf;
if ((response.content.size() > 49) && (Utils::Gzip::compress(response.content, dest_buf)))
response.content = dest_buf;
else
response.headers.remove(HEADER_CONTENT_ENCODING);
}
compressContent(response);
if (response.content.length() > 0)
response.headers[HEADER_CONTENT_LENGTH] = QString::number(response.content.length());
response.headers[HEADER_CONTENT_LENGTH] = QString::number(response.content.length());
response.headers[HEADER_DATE] = httpDate();
QString ret(QLatin1String("HTTP/1.1 %1 %2\r\n%3\r\n"));
QByteArray buf;
buf.reserve(10 * 1024);
QString header;
foreach (const QString& key, response.headers.keys())
header += QString("%1: %2\r\n").arg(key).arg(response.headers[key]);
// Status Line
buf += QString("HTTP/%1 %2 %3")
.arg("1.1", // TODO: depends on request
QString::number(response.status.code),
response.status.text)
.toLatin1()
.append(CRLF);
ret = ret.arg(response.status.code).arg(response.status.text).arg(header);
// Header Fields
for (auto i = response.headers.constBegin(); i != response.headers.constEnd(); ++i)
buf += QString("%1: %2").arg(i.key(), i.value()).toLatin1().append(CRLF);
// qDebug() << Q_FUNC_INFO;
// qDebug() << "HTTP Response header:";
// qDebug() << ret;
// the first empty line
buf += CRLF;
return ret.toUtf8() + response.content;
// message body // TODO: support HEAD request
buf += response.content;
return buf;
}
QString Http::httpDate()
{
// [RFC 7231] 7.1.1.1. Date/Time Formats
// example: "Sun, 06 Nov 1994 08:49:37 GMT"
return QLocale::c().toString(QDateTime::currentDateTimeUtc(), QLatin1String("ddd, dd MMM yyyy HH:mm:ss"))
.append(QLatin1String(" GMT"));
}
void Http::compressContent(Response &response)
{
if (response.headers.value(HEADER_CONTENT_ENCODING) != QLatin1String("gzip"))
return;
response.headers.remove(HEADER_CONTENT_ENCODING);
// for very small files, compressing them only wastes cpu cycles
const int contentSize = response.content.size();
if (contentSize <= 1024) // 1 kb
return;
// filter out known hard-to-compress types
const QString contentType = response.headers[HEADER_CONTENT_TYPE];
if ((contentType == CONTENT_TYPE_GIF) || (contentType == CONTENT_TYPE_PNG))
return;
// try compressing
bool ok = false;
const QByteArray compressedData = Utils::Gzip::compress(response.content, 6, &ok);
if (!ok)
return;
// "Content-Encoding: gzip\r\n" is 24 bytes long
if ((compressedData.size() + 24) >= contentSize)
return;
response.content = compressedData;
response.headers[HEADER_CONTENT_ENCODING] = QLatin1String("gzip");
}

View File

@@ -37,11 +37,9 @@
namespace Http
{
class ResponseGenerator
{
public:
static QByteArray generate(Response response);
};
QByteArray toByteArray(Response response);
QString httpDate();
void compressContent(Response &response);
}
#endif // HTTP_RESPONSEGENERATOR_H

View File

@@ -30,8 +30,10 @@
#include "server.h"
#include <QMutableListIterator>
#include <QNetworkProxy>
#include <QStringList>
#include <QTimer>
#ifndef QT_NO_OPENSSL
#include <QSslSocket>
@@ -41,6 +43,10 @@
#include "connection.h"
static const int KEEP_ALIVE_DURATION = 7; // seconds
static const int CONNECTIONS_LIMIT = 500;
static const int CONNECTIONS_SCAN_INTERVAL = 2; // seconds
using namespace Http;
Server::Server(IRequestHandler *requestHandler, QObject *parent)
@@ -54,6 +60,10 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent)
#ifndef QT_NO_OPENSSL
QSslSocket::setDefaultCiphers(safeCipherList());
#endif
QTimer *dropConnectionTimer = new QTimer(this);
connect(dropConnectionTimer, &QTimer::timeout, this, &Server::dropTimedOutConnection);
dropConnectionTimer->start(CONNECTIONS_SCAN_INTERVAL * 1000);
}
Server::~Server()
@@ -62,6 +72,8 @@ Server::~Server()
void Server::incomingConnection(qintptr socketDescriptor)
{
if (m_connections.size() >= CONNECTIONS_LIMIT) return;
QTcpSocket *serverSocket;
#ifndef QT_NO_OPENSSL
if (m_https)
@@ -70,20 +82,34 @@ void Server::incomingConnection(qintptr socketDescriptor)
#endif
serverSocket = new QTcpSocket(this);
if (serverSocket->setSocketDescriptor(socketDescriptor)) {
#ifndef QT_NO_OPENSSL
if (m_https) {
static_cast<QSslSocket *>(serverSocket)->setProtocol(QSsl::SecureProtocols);
static_cast<QSslSocket *>(serverSocket)->setPrivateKey(m_key);
static_cast<QSslSocket *>(serverSocket)->setLocalCertificateChain(m_certificates);
static_cast<QSslSocket *>(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone);
static_cast<QSslSocket *>(serverSocket)->startServerEncryption();
}
#endif
new Connection(serverSocket, m_requestHandler, this);
if (!serverSocket->setSocketDescriptor(socketDescriptor)) {
delete serverSocket;
return;
}
else {
serverSocket->deleteLater();
#ifndef QT_NO_OPENSSL
if (m_https) {
static_cast<QSslSocket *>(serverSocket)->setProtocol(QSsl::SecureProtocols);
static_cast<QSslSocket *>(serverSocket)->setPrivateKey(m_key);
static_cast<QSslSocket *>(serverSocket)->setLocalCertificateChain(m_certificates);
static_cast<QSslSocket *>(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone);
static_cast<QSslSocket *>(serverSocket)->startServerEncryption();
}
#endif
Connection *c = new Connection(serverSocket, m_requestHandler, this);
m_connections.append(c);
}
void Server::dropTimedOutConnection()
{
QMutableListIterator<Connection *> i(m_connections);
while (i.hasNext()) {
auto connection = i.next();
if (connection->isClosed() || connection->hasExpired(KEEP_ALIVE_DURATION)) {
delete connection;
i.remove();
}
}
}

View File

@@ -60,10 +60,14 @@ namespace Http
void disableHttps();
#endif
private slots:
void dropTimedOutConnection();
private:
void incomingConnection(qintptr socketDescriptor);
IRequestHandler *m_requestHandler;
QList<Connection *> m_connections; // for tracking persistence connections
#ifndef QT_NO_OPENSSL
QList<QSslCipher> safeCipherList() const;

View File

@@ -29,32 +29,35 @@
#ifndef HTTP_TYPES_H
#define HTTP_TYPES_H
#include <QString>
#include <QMap>
#include <QHostAddress>
#include <QString>
#include <QVector>
#include "base/types.h"
namespace Http
{
const QString HEADER_SET_COOKIE = "Set-Cookie";
const QString HEADER_CONTENT_TYPE = "Content-Type";
const QString HEADER_CONTENT_ENCODING = "Content-Encoding";
const QString HEADER_CONTENT_LENGTH = "Content-Length";
const QString HEADER_CACHE_CONTROL = "Cache-Control";
const QString HEADER_X_FRAME_OPTIONS = "X-Frame-Options";
const QString HEADER_X_XSS_PROTECTION = "X-XSS-Protection";
const QString HEADER_X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
const QString HEADER_CONTENT_SECURITY_POLICY = "Content-Security-Policy";
const char HEADER_CACHE_CONTROL[] = "Cache-Control";
const char HEADER_CONTENT_ENCODING[] = "Content-Encoding";
const char HEADER_CONTENT_LENGTH[] = "Content-Length";
const char HEADER_CONTENT_SECURITY_POLICY[] = "Content-Security-Policy";
const char HEADER_CONTENT_TYPE[] = "Content-Type";
const char HEADER_DATE[] = "Date";
const char HEADER_SET_COOKIE[] = "Set-Cookie";
const char HEADER_X_CONTENT_TYPE_OPTIONS[] = "X-Content-Type-Options";
const char HEADER_X_FRAME_OPTIONS[] = "X-Frame-Options";
const char HEADER_X_XSS_PROTECTION[] = "X-XSS-Protection";
const QString CONTENT_TYPE_CSS = "text/css; charset=UTF-8";
const QString CONTENT_TYPE_GIF = "image/gif";
const QString CONTENT_TYPE_HTML = "text/html; charset=UTF-8";
const QString CONTENT_TYPE_JS = "application/javascript; charset=UTF-8";
const QString CONTENT_TYPE_JSON = "application/json";
const QString CONTENT_TYPE_PNG = "image/png";
const QString CONTENT_TYPE_TXT = "text/plain; charset=UTF-8";
const char CONTENT_TYPE_CSS[] = "text/css; charset=UTF-8";
const char CONTENT_TYPE_GIF[] = "image/gif";
const char CONTENT_TYPE_HTML[] = "text/html; charset=UTF-8";
const char CONTENT_TYPE_JS[] = "application/javascript; charset=UTF-8";
const char CONTENT_TYPE_JSON[] = "application/json";
const char CONTENT_TYPE_PNG[] = "image/png";
const char CONTENT_TYPE_TXT[] = "text/plain; charset=UTF-8";
// portability: "\r\n" doesn't guarantee mapping to the correct value
const char CRLF[] = {0x0D, 0x0A, '\0'};
struct Environment
{

View File

@@ -92,8 +92,8 @@ void DownloadHandler::processFinishedDownload()
// Success
QByteArray replyData = m_reply->readAll();
if (m_reply->rawHeader("Content-Encoding") == "gzip") {
// uncompress gzip reply
Utils::Gzip::uncompress(replyData, replyData);
// decompress gzip reply
replyData = Utils::Gzip::decompress(replyData);
}
if (m_saveToFile) {

View File

@@ -416,8 +416,10 @@ void GeoIPManager::downloadFinished(const QString &url, QByteArray data)
{
Q_UNUSED(url);
if (!Utils::Gzip::uncompress(data, data)) {
Logger::instance()->addMessage(tr("Could not uncompress GeoIP database file."), Log::WARNING);
bool ok = false;
data = Utils::Gzip::decompress(data, &ok);
if (!ok) {
Logger::instance()->addMessage(tr("Could not decompress GeoIP database file."), Log::WARNING);
return;
}

View File

@@ -27,116 +27,123 @@
* exception statement from your version.
*/
#include <QByteArray>
#include <zlib.h>
#include "gzip.h"
bool Utils::Gzip::compress(QByteArray src, QByteArray &dest)
{
static const int BUFSIZE = 128 * 1024;
char tmpBuf[BUFSIZE];
int ret;
#include <QByteArray>
dest.clear();
#ifndef ZLIB_CONST
#define ZLIB_CONST // make z_stream.next_in const
#endif
#include <zlib.h>
QByteArray Utils::Gzip::compress(const QByteArray &data, const int level, bool *ok)
{
if (ok) *ok = false;
if (data.isEmpty())
return {};
const int BUFSIZE = 128 * 1024;
char tmpBuf[BUFSIZE] = {0};
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.next_in = reinterpret_cast<uchar *>(src.data());
strm.avail_in = src.length();
strm.next_out = reinterpret_cast<uchar *>(tmpBuf);
strm.next_in = reinterpret_cast<const Bytef *>(data.constData());
strm.avail_in = uInt(data.size());
strm.next_out = reinterpret_cast<Bytef *>(tmpBuf);
strm.avail_out = BUFSIZE;
// windowBits = 15 + 16 to enable gzip
// From the zlib manual: windowBits can also be greater than 15 for optional gzip encoding. Add 16 to windowBits
// to write a simple gzip header and trailer around the compressed data instead of a zlib wrapper.
ret = deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
int result = deflateInit2(&strm, level, Z_DEFLATED, (15 + 16), 9, Z_DEFAULT_STRATEGY);
if (result != Z_OK)
return {};
if (ret != Z_OK)
return false;
QByteArray output;
output.reserve(deflateBound(&strm, data.size()));
while (strm.avail_in != 0) {
ret = deflate(&strm, Z_NO_FLUSH);
if (ret != Z_OK)
return false;
// feed to deflate
while (strm.avail_in > 0) {
result = deflate(&strm, Z_NO_FLUSH);
if (strm.avail_out == 0) {
dest.append(tmpBuf, BUFSIZE);
strm.next_out = reinterpret_cast<uchar *>(tmpBuf);
strm.avail_out = BUFSIZE;
}
}
int deflateRes = Z_OK;
while (deflateRes == Z_OK) {
if (strm.avail_out == 0) {
dest.append(tmpBuf, BUFSIZE);
strm.next_out = reinterpret_cast<uchar *>(tmpBuf);
strm.avail_out = BUFSIZE;
if (result != Z_OK) {
deflateEnd(&strm);
return {};
}
deflateRes = deflate(&strm, Z_FINISH);
output.append(tmpBuf, (BUFSIZE - strm.avail_out));
strm.next_out = reinterpret_cast<Bytef *>(tmpBuf);
strm.avail_out = BUFSIZE;
}
if (deflateRes != Z_STREAM_END)
return false;
// flush the rest from deflate
while (result != Z_STREAM_END) {
result = deflate(&strm, Z_FINISH);
output.append(tmpBuf, (BUFSIZE - strm.avail_out));
strm.next_out = reinterpret_cast<Bytef *>(tmpBuf);
strm.avail_out = BUFSIZE;
}
dest.append(tmpBuf, BUFSIZE - strm.avail_out);
deflateEnd(&strm);
return true;
if (ok) *ok = true;
return output;
}
bool Utils::Gzip::uncompress(QByteArray src, QByteArray &dest)
QByteArray Utils::Gzip::decompress(const QByteArray &data, bool *ok)
{
dest.clear();
if (ok) *ok = false;
if (src.size() <= 4) {
qWarning("uncompress: Input data is truncated");
return false;
}
if (data.isEmpty())
return {};
const int BUFSIZE = 1024 * 1024;
char tmpBuf[BUFSIZE] = {0};
z_stream strm;
static const int CHUNK_SIZE = 1024;
char out[CHUNK_SIZE];
/* allocate inflate state */
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = static_cast<uint>(src.size());
strm.next_in = reinterpret_cast<uchar *>(src.data());
strm.next_in = reinterpret_cast<const Bytef *>(data.constData());
strm.avail_in = uInt(data.size());
strm.next_out = reinterpret_cast<Bytef *>(tmpBuf);
strm.avail_out = BUFSIZE;
const int windowBits = 15;
const int ENABLE_ZLIB_GZIP = 32;
// windowBits must be greater than or equal to the windowBits value provided to deflateInit2() while compressing
// Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection
int result = inflateInit2(&strm, (15 + 32));
if (result != Z_OK)
return {};
int ret = inflateInit2(&strm, windowBits | ENABLE_ZLIB_GZIP); // gzip decoding
if (ret != Z_OK)
return false;
QByteArray output;
// from lzbench, level 9 average compression ratio is: 31.92%, which decompression ratio is: 1 / 0.3192 = 3.13
output.reserve(data.size() * 3);
// run inflate()
do {
strm.avail_out = CHUNK_SIZE;
strm.next_out = reinterpret_cast<uchar *>(out);
// run inflate
while (true) {
result = inflate(&strm, Z_NO_FLUSH);
ret = inflate(&strm, Z_NO_FLUSH);
Q_ASSERT(ret != Z_STREAM_ERROR); // state not clobbered
switch (ret) {
case Z_NEED_DICT:
case Z_DATA_ERROR:
case Z_MEM_ERROR:
inflateEnd(&strm);
return false;
if (result == Z_STREAM_END) {
output.append(tmpBuf, (BUFSIZE - strm.avail_out));
break;
}
dest.append(out, CHUNK_SIZE - strm.avail_out);
}
while (!strm.avail_out);
if (result != Z_OK) {
inflateEnd(&strm);
return {};
}
output.append(tmpBuf, (BUFSIZE - strm.avail_out));
strm.next_out = reinterpret_cast<Bytef *>(tmpBuf);
strm.avail_out = BUFSIZE;
}
// clean up and return
inflateEnd(&strm);
return true;
if (ok) *ok = true;
return output;
}

View File

@@ -36,8 +36,8 @@ namespace Utils
{
namespace Gzip
{
bool compress(QByteArray src, QByteArray &dest);
bool uncompress(QByteArray src, QByteArray &dest);
QByteArray compress(const QByteArray &data, int level = 6, bool *ok = nullptr);
QByteArray decompress(const QByteArray &data, bool *ok = nullptr);
}
}