Browse Source

Merge pull request #6654 from Chocobo1/persistence

Webui server fixes
adaptive-webui-19844
sledgehammer999 8 years ago committed by GitHub
parent
commit
0a5bb6685f
  1. 100
      .travis.yml
  2. 18
      configure
  3. 2
      configure.ac
  4. 2
      src/base/CMakeLists.txt
  5. 82
      src/base/http/connection.cpp
  6. 10
      src/base/http/connection.h
  7. 102
      src/base/http/responsegenerator.cpp
  8. 8
      src/base/http/responsegenerator.h
  9. 50
      src/base/http/server.cpp
  10. 4
      src/base/http/server.h
  11. 39
      src/base/http/types.h
  12. 4
      src/base/net/downloadhandler.cpp
  13. 6
      src/base/net/geoipmanager.cpp
  14. 139
      src/base/utils/gzip.cpp
  15. 4
      src/base/utils/gzip.h

100
.travis.yml

@ -33,16 +33,15 @@ notifications:
on_success: change on_success: change
on_failure: change on_failure: change
# container-based builds
#sudo: false
cache: cache:
ccache: true ccache: true
directories: directories:
- $HOME/hombebrew_cache - $HOME/hombebrew_cache
# opt-in Ubuntu Trusty # opt-in Ubuntu Trusty
sudo: required
dist: trusty dist: trusty
# container-based builds
sudo: false
addons: addons:
coverity_scan: coverity_scan:
@ -62,7 +61,7 @@ addons:
- sourceline: 'ppa:beineri/opt-qt551-trusty' - sourceline: 'ppa:beineri/opt-qt551-trusty'
- sourceline: 'ppa:adrozdoff/cmake' - sourceline: 'ppa:adrozdoff/cmake'
packages: packages:
# packages list: https://github.com/travis-ci/apt-package-whitelist/blob/master/ubuntu-precise # packages list: https://github.com/travis-ci/apt-package-whitelist/blob/master/ubuntu-precise
- autoconf - autoconf
- automake - automake
- cmake - cmake
@ -74,6 +73,7 @@ addons:
# Qt 5.5.1 # Qt 5.5.1
- qt55base - qt55base
- qt55tools - qt55tools
before_install: before_install:
# only allow specific build for coverity scan, others will stop # only allow specific build for coverity scan, others will stop
- if [ "$TRAVIS_BRANCH" = "$coverity_branch" ] && ! [ "$TRAVIS_OS_NAME" = "linux" -a "$lt_branch" = "RC_1_0" -a "$gui" = true -a "$build_system" = "qmake" ]; then exit ; fi - if [ "$TRAVIS_BRANCH" = "$coverity_branch" ] && ! [ "$TRAVIS_OS_NAME" = "linux" -a "$lt_branch" = "RC_1_0" -a "$gui" = true -a "$build_system" = "qmake" ]; then exit ; fi
@ -102,54 +102,51 @@ before_install:
- echo $build_system - echo $build_system
- echo $ltconf - echo $ltconf
- echo $qbtconf - echo $qbtconf
install: install:
- | #- |
if [ "$TRAVIS_OS_NAME" = "linux" ]; then #if [ "$TRAVIS_OS_NAME" = "linux" ]; then
# build libtorrent from source # build libtorrent from source
#if [ "$lt_branch" != "dist" ]; then #if [ "$lt_branch" != "dist" ]; then
#cd "$HOME" && pwd && git clone --depth 1 https://github.com/arvidn/libtorrent.git --branch $lt_branch ; #cd "$HOME" && pwd && git clone --depth 1 https://github.com/arvidn/libtorrent.git --branch $lt_branch
#cd libtorrent && ./autotool.sh && ./configure $ltconf && make install ; #cd libtorrent && ./autotool.sh && ./configure $ltconf && make install
#fi ; #fi
#fi
# ccache
if [ "$TRAVIS_BRANCH" != "$coverity_branch" ]; then
dpkg-query -L ccache && export use_ccache=true ;
ccache -V && ccache --show-stats && ccache --zero-stats ;
fi ;
fi
- | - |
if [ "$TRAVIS_OS_NAME" = "osx" ]; then if [ "$TRAVIS_OS_NAME" = "osx" ]; then
wget https://builds.shiki.hu/homebrew/version ; # dependencies
brew update > /dev/null
brew install colormake ccache zlib
PATH="/usr/local/opt/ccache/libexec:$PATH"
brew link --force zlib
brew outdated "pkg-config" || brew upgrade "pkg-config"
wget https://builds.shiki.hu/homebrew/version
if ! cmp --quiet "version" "$HOME/hombebrew_cache/version" ; then if ! cmp --quiet "version" "$HOME/hombebrew_cache/version" ; then
echo "Cached files are different from server. Downloading new ones." ; echo "Cached files are different from server. Downloading new ones."
# First delete old files # First delete old files
rm -r "$HOME/hombebrew_cache" ; rm -r "$HOME/hombebrew_cache"
mkdir "$HOME/hombebrew_cache"; mkdir "$HOME/hombebrew_cache"
cp "version" $HOME/hombebrew_cache ; cp "version" $HOME/hombebrew_cache
cd "$HOME/hombebrew_cache" ; cd "$HOME/hombebrew_cache"
wget https://builds.shiki.hu/homebrew/libtorrent-rasterbar.rb ; wget https://builds.shiki.hu/homebrew/libtorrent-rasterbar.rb
wget https://builds.shiki.hu/homebrew/libtorrent-rasterbar-1.0.10.el_capitan.bottle.tar.gz ; wget https://builds.shiki.hu/homebrew/libtorrent-rasterbar-1.0.10.el_capitan.bottle.tar.gz
wget https://builds.shiki.hu/homebrew/qt5.rb ; wget https://builds.shiki.hu/homebrew/qt5.rb
wget https://builds.shiki.hu/homebrew/qt5-5.7.1_1.el_capitan.bottle.tar.gz ; wget https://builds.shiki.hu/homebrew/qt5-5.7.1_1.el_capitan.bottle.tar.gz
fi fi
# dependencies
brew update > /dev/null ;
brew install colormake ccache ;
if [ "$build_system" = "cmake" ]; then
brew unlink cmake
brew install cmake;
fi
brew outdated "pkg-config" || brew upgrade "pkg-config" ;
# Copy custom libtorrent bottle to homebrew's cache so it can find and install it # Copy custom libtorrent bottle to homebrew's cache so it can find and install it
# Also install our custom libtorrent formula by passing the local path to it # Also install our custom libtorrent formula by passing the local path to it
# These 2 files are restored from Travis' cache. # These 2 files are restored from Travis' cache.
cp "$HOME/hombebrew_cache/libtorrent-rasterbar-1.0.10.el_capitan.bottle.tar.gz" "$(brew --cache)" ; cp "$HOME/hombebrew_cache/libtorrent-rasterbar-1.0.10.el_capitan.bottle.tar.gz" "$(brew --cache)"
brew install "$HOME/hombebrew_cache/libtorrent-rasterbar.rb" ; brew install "$HOME/hombebrew_cache/libtorrent-rasterbar.rb"
if [ "$build_system" = "cmake" ]; then if [ "$build_system" = "cmake" ]; then
brew install qt5 ; brew unlink cmake
brew link --force qt5 ; brew install cmake
brew install qt5
brew link --force qt5
ln -s /usr/local/opt/qt/mkspecs /usr/local/mkspecs ln -s /usr/local/opt/qt/mkspecs /usr/local/mkspecs
ln -s /usr/local/opt/qt/plugins /usr/local/plugins ln -s /usr/local/opt/qt/plugins /usr/local/plugins
else else
@ -157,18 +154,17 @@ install:
# Copy custom qt5 bottle to homebrew's cache so it can find and install it # Copy custom qt5 bottle to homebrew's cache so it can find and install it
# Also install our custom qt5 formula by passing the local path to it # Also install our custom qt5 formula by passing the local path to it
# These 2 files are restored from Travis' cache. # These 2 files are restored from Travis' cache.
cp "$HOME/hombebrew_cache/qt5-5.7.1_1.el_capitan.bottle.tar.gz" "$(brew --cache)" ; cp "$HOME/hombebrew_cache/qt5-5.7.1_1.el_capitan.bottle.tar.gz" "$(brew --cache)"
brew install "$HOME/hombebrew_cache/qt5.rb" ; brew install "$HOME/hombebrew_cache/qt5.rb"
brew link --force qt5 ; brew link --force qt5
fi fi
MY_CMAKE_OPENSSL_HINT="-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl/" MY_CMAKE_OPENSSL_HINT="-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl/"
fi
# ccache - |
if [ "$TRAVIS_BRANCH" != "$coverity_branch" ]; then if [ "$TRAVIS_BRANCH" != "$coverity_branch" ]; then
export PATH="/usr/local/opt/ccache/libexec:$PATH" && export use_ccache=true ; export use_ccache=true
ccache -V && ccache --show-stats && ccache --zero-stats ; ccache -V && ccache --show-stats && ccache --zero-stats
fi ;
fi fi
script: script:
@ -184,9 +180,9 @@ script:
if [ "$build_system" = "qmake" ]; then if [ "$build_system" = "qmake" ]; then
./bootstrap.sh && ./configure $qbtconf ./bootstrap.sh && ./configure $qbtconf
if [ "$TRAVIS_OS_NAME" = "osx" ]; then if [ "$TRAVIS_OS_NAME" = "osx" ]; then
sed -i "" -e "s/^\(CC.*&&\).*$/\1 $CC/" src/Makefile ; # workaround for Qt & ccache: https://bugreports.qt.io/browse/QTBUG-31034 sed -i "" -e "s/^\(CC.*&&\).*$/\1 $CC/" src/Makefile # workaround for Qt & ccache: https://bugreports.qt.io/browse/QTBUG-31034
sed -i "" -e "s/^\(CXX.*&&\).*$/\1 $CXX/" src/Makefile ; sed -i "" -e "s/^\(CXX.*&&\).*$/\1 $CXX/" src/Makefile
sed -i "" -e 's/^\(CXXFLAGS.*\)$/\1 -Wno-unused-local-typedefs -Wno-inconsistent-missing-override/' src/Makefile ; sed -i "" -e 's/^\(CXXFLAGS.*\)$/\1 -Wno-unused-local-typedefs -Wno-inconsistent-missing-override/' src/Makefile
fi fi
fi fi
- make && make install - make && make install
@ -196,8 +192,8 @@ after_success:
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then cd "$qbt_path/bin" ; fi - if [ "$TRAVIS_OS_NAME" = "linux" ]; then cd "$qbt_path/bin" ; fi
- | - |
if [ "$TRAVIS_OS_NAME" = "osx" ]; then if [ "$TRAVIS_OS_NAME" = "osx" ]; then
macdeployqt "$TRAVIS_BUILD_DIR/src/$qbt_exe.app" ; macdeployqt "$TRAVIS_BUILD_DIR/src/$qbt_exe.app"
cd "$TRAVIS_BUILD_DIR/src/$qbt_exe.app/Contents/MacOS" ; cd "$TRAVIS_BUILD_DIR/src/$qbt_exe.app/Contents/MacOS"
fi fi
- ./$qbt_exe --version - ./$qbt_exe --version

18
configure vendored

@ -5188,12 +5188,12 @@ if test -n "$zlib_CFLAGS"; then
pkg_cv_zlib_CFLAGS="$zlib_CFLAGS" pkg_cv_zlib_CFLAGS="$zlib_CFLAGS"
elif test -n "$PKG_CONFIG"; then elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \ if test -n "$PKG_CONFIG" && \
{ { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"zlib\""; } >&5 { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"zlib >= 1.2.5.2\""; } >&5
($PKG_CONFIG --exists --print-errors "zlib") 2>&5 ($PKG_CONFIG --exists --print-errors "zlib >= 1.2.5.2") 2>&5
ac_status=$? ac_status=$?
$as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then test $ac_status = 0; }; then
pkg_cv_zlib_CFLAGS=`$PKG_CONFIG --cflags "zlib" 2>/dev/null` pkg_cv_zlib_CFLAGS=`$PKG_CONFIG --cflags "zlib >= 1.2.5.2" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes test "x$?" != "x0" && pkg_failed=yes
else else
pkg_failed=yes pkg_failed=yes
@ -5205,12 +5205,12 @@ if test -n "$zlib_LIBS"; then
pkg_cv_zlib_LIBS="$zlib_LIBS" pkg_cv_zlib_LIBS="$zlib_LIBS"
elif test -n "$PKG_CONFIG"; then elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \ if test -n "$PKG_CONFIG" && \
{ { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"zlib\""; } >&5 { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"zlib >= 1.2.5.2\""; } >&5
($PKG_CONFIG --exists --print-errors "zlib") 2>&5 ($PKG_CONFIG --exists --print-errors "zlib >= 1.2.5.2") 2>&5
ac_status=$? ac_status=$?
$as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then test $ac_status = 0; }; then
pkg_cv_zlib_LIBS=`$PKG_CONFIG --libs "zlib" 2>/dev/null` pkg_cv_zlib_LIBS=`$PKG_CONFIG --libs "zlib >= 1.2.5.2" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes test "x$?" != "x0" && pkg_failed=yes
else else
pkg_failed=yes pkg_failed=yes
@ -5231,14 +5231,14 @@ else
_pkg_short_errors_supported=no _pkg_short_errors_supported=no
fi fi
if test $_pkg_short_errors_supported = yes; then if test $_pkg_short_errors_supported = yes; then
zlib_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "zlib" 2>&1` zlib_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "zlib >= 1.2.5.2" 2>&1`
else else
zlib_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "zlib" 2>&1` zlib_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "zlib >= 1.2.5.2" 2>&1`
fi fi
# Put the nasty error message in config.log where it belongs # Put the nasty error message in config.log where it belongs
echo "$zlib_PKG_ERRORS" >&5 echo "$zlib_PKG_ERRORS" >&5
as_fn_error $? "Package requirements (zlib) were not met: as_fn_error $? "Package requirements (zlib >= 1.2.5.2) were not met:
$zlib_PKG_ERRORS $zlib_PKG_ERRORS

2
configure.ac

@ -166,7 +166,7 @@ PKG_CHECK_MODULES(libtorrent,
LIBS="$libtorrent_LIBS $LIBS"]) LIBS="$libtorrent_LIBS $LIBS"])
PKG_CHECK_MODULES(zlib, PKG_CHECK_MODULES(zlib,
[zlib], [zlib >= 1.2.5.2],
[CPPFLAGS="$zlib_CFLAGS $CPPFLAGS" [CPPFLAGS="$zlib_CFLAGS $CPPFLAGS"
LIBS="$zlib_LIBS $LIBS"]) LIBS="$zlib_LIBS $LIBS"])

2
src/base/CMakeLists.txt

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

82
src/base/http/connection.cpp

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

10
src/base/http/connection.h

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

102
src/base/http/responsegenerator.cpp

@ -29,39 +29,79 @@
* Contact : chris@qbittorrent.org * Contact : chris@qbittorrent.org
*/ */
#include "base/utils/gzip.h"
#include "responsegenerator.h" #include "responsegenerator.h"
using namespace Http; #include <QDateTime>
#include "base/utils/gzip.h"
QByteArray ResponseGenerator::generate(Response response) QByteArray Http::toByteArray(Response response)
{ {
if (response.headers[HEADER_CONTENT_ENCODING] == "gzip") { compressContent(response);
// A gzip seems to have 23 bytes overhead.
// Also "Content-Encoding: gzip\r\n" is 26 bytes long response.headers[HEADER_CONTENT_LENGTH] = QString::number(response.content.length());
// So we only benefit from gzip if the message is bigger than 23+26 = 49 response.headers[HEADER_DATE] = httpDate();
// If the message is smaller than 49 bytes we actually send MORE data if we gzip
QByteArray dest_buf; QByteArray buf;
if ((response.content.size() > 49) && (Utils::Gzip::compress(response.content, dest_buf))) buf.reserve(10 * 1024);
response.content = dest_buf;
else // Status Line
response.headers.remove(HEADER_CONTENT_ENCODING); buf += QString("HTTP/%1 %2 %3")
} .arg("1.1", // TODO: depends on request
QString::number(response.status.code),
if (response.content.length() > 0) response.status.text)
response.headers[HEADER_CONTENT_LENGTH] = QString::number(response.content.length()); .toLatin1()
.append(CRLF);
QString ret(QLatin1String("HTTP/1.1 %1 %2\r\n%3\r\n"));
// Header Fields
QString header; for (auto i = response.headers.constBegin(); i != response.headers.constEnd(); ++i)
foreach (const QString& key, response.headers.keys()) buf += QString("%1: %2").arg(i.key(), i.value()).toLatin1().append(CRLF);
header += QString("%1: %2\r\n").arg(key).arg(response.headers[key]);
// the first empty line
ret = ret.arg(response.status.code).arg(response.status.text).arg(header); buf += CRLF;
// qDebug() << Q_FUNC_INFO; // message body // TODO: support HEAD request
// qDebug() << "HTTP Response header:"; buf += response.content;
// qDebug() << ret;
return buf;
return ret.toUtf8() + response.content; }
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");
} }

8
src/base/http/responsegenerator.h

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

50
src/base/http/server.cpp

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

4
src/base/http/server.h

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

39
src/base/http/types.h

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

4
src/base/net/downloadhandler.cpp

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

6
src/base/net/geoipmanager.cpp

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

139
src/base/utils/gzip.cpp

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

4
src/base/utils/gzip.h

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

Loading…
Cancel
Save