Skip to content

Commit 5f41505

Browse files
committed
Store big web requests on disk
1 parent 19100c9 commit 5f41505

File tree

5 files changed

+135
-16
lines changed

5 files changed

+135
-16
lines changed

domain-server/src/DomainGatekeeper.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#include <openssl/x509.h>
1717
#include <random>
1818

19+
#include <QDataStream>
20+
1921
#include <AccountManager.h>
2022
#include <Assignment.h>
2123

domain-server/src/DomainServer.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,8 @@ DomainServer::~DomainServer() {
384384
_contentManager->terminate();
385385
}
386386

387+
DependencyManager::destroy<AccountManager>();
388+
387389
// cleanup the AssetClient thread
388390
DependencyManager::destroy<AssetClient>();
389391
_assetClientThread.quit();

ice-server/src/IceServer.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <openssl/x509.h>
1515

16+
#include <QtCore/QDataStream>
1617
#include <QtCore/QJsonDocument>
1718
#include <QtCore/QTimer>
1819
#include <QtNetwork/QNetworkReply>

libraries/embedded-webserver/src/HTTPConnection.cpp

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
#include "HTTPConnection.h"
1313

14+
#include <assert.h>
15+
1416
#include <QBuffer>
1517
#include <QCryptographicHash>
1618
#include <QTcpSocket>
@@ -29,6 +31,88 @@ const char* HTTPConnection::StatusCode404 = "404 Not Found";
2931
const char* HTTPConnection::StatusCode500 = "500 Internal server error";
3032
const char* HTTPConnection::DefaultContentType = "text/plain; charset=ISO-8859-1";
3133

34+
35+
class MemoryStorage : public HTTPConnection::Storage {
36+
public:
37+
static std::unique_ptr<MemoryStorage> make(qint64 size);
38+
virtual ~MemoryStorage() = default;
39+
40+
const QByteArray& content() const override { return _array; }
41+
qint64 bytesLeftToWrite() const override { return _array.size() - _bytesWritten; }
42+
void write(const QByteArray& data) override;
43+
44+
private:
45+
MemoryStorage(qint64 size) { _array.resize(size); }
46+
47+
QByteArray _array;
48+
qint64 _bytesWritten { 0 };
49+
};
50+
51+
std::unique_ptr<MemoryStorage> MemoryStorage::make(qint64 size) {
52+
return std::unique_ptr<MemoryStorage>(new MemoryStorage(size));
53+
}
54+
55+
void MemoryStorage::write(const QByteArray& data) {
56+
assert(data.size() <= bytesLeftToWrite());
57+
memcpy(_array.data() + _bytesWritten, data.data(), data.size());
58+
_bytesWritten += data.size();
59+
}
60+
61+
62+
class FileStorage : public HTTPConnection::Storage {
63+
public:
64+
static std::unique_ptr<FileStorage> make(qint64 size);
65+
virtual ~FileStorage();
66+
67+
const QByteArray& content() const override { return _wrapperArray; };
68+
qint64 bytesLeftToWrite() const override { return _mappedMemorySize - _bytesWritten; }
69+
void write(const QByteArray& data) override;
70+
71+
private:
72+
FileStorage(std::unique_ptr<QTemporaryFile> file, uchar* mapped, qint64 size);
73+
74+
// Byte array is const because any edit will trigger a deep copy
75+
// and pull all the data we want to keep on disk in memory.
76+
const QByteArray _wrapperArray;
77+
std::unique_ptr<QTemporaryFile> _file;
78+
79+
uchar* const _mappedMemoryAddress { nullptr };
80+
const qint64 _mappedMemorySize { 0 };
81+
qint64 _bytesWritten { 0 };
82+
};
83+
84+
std::unique_ptr<FileStorage> FileStorage::make(qint64 size) {
85+
auto file = std::unique_ptr<QTemporaryFile>(new QTemporaryFile());
86+
file->open(); // Open for resize
87+
file->resize(size);
88+
auto mapped = file->map(0, size); // map the entire file
89+
90+
return std::unique_ptr<FileStorage>(new FileStorage(std::move(file), mapped, size));
91+
}
92+
93+
// Use QByteArray::fromRawData to avoid a new allocation and access the already existing
94+
// memory directly as long as all operations on the array are const.
95+
FileStorage::FileStorage(std::unique_ptr<QTemporaryFile> file, uchar* mapped, qint64 size) :
96+
_wrapperArray(QByteArray::fromRawData(reinterpret_cast<char*>(mapped), size)),
97+
_file(std::move(file)),
98+
_mappedMemoryAddress(mapped),
99+
_mappedMemorySize(size)
100+
{
101+
}
102+
103+
FileStorage::~FileStorage() {
104+
_file->unmap(_mappedMemoryAddress);
105+
_file->close();
106+
}
107+
108+
void FileStorage::write(const QByteArray& data) {
109+
assert(data.size() <= bytesLeftToWrite());
110+
// We write directly to the mapped memory
111+
memcpy(_mappedMemoryAddress + _bytesWritten, data.data(), data.size());
112+
_bytesWritten += data.size();
113+
}
114+
115+
32116
HTTPConnection::HTTPConnection(QTcpSocket* socket, HTTPManager* parentManager) :
33117
QObject(parentManager),
34118
_parentManager(parentManager),
@@ -61,7 +145,7 @@ QHash<QString, QString> HTTPConnection::parseUrlEncodedForm() {
61145
return QHash<QString, QString>();
62146
}
63147

64-
QUrlQuery form { _requestContent };
148+
QUrlQuery form { _requestContent->content() };
65149
QHash<QString, QString> pairs;
66150
for (auto pair : form.queryItems()) {
67151
auto key = QUrl::fromPercentEncoding(pair.first.toLatin1().replace('+', ' '));
@@ -96,7 +180,7 @@ QList<FormData> HTTPConnection::parseFormData() const {
96180
QByteArray end = "\r\n--" + boundary + "--\r\n";
97181

98182
QList<FormData> data;
99-
QBuffer buffer(const_cast<QByteArray*>(&_requestContent));
183+
QBuffer buffer(const_cast<QByteArray*>(&_requestContent->content()));
100184
buffer.open(QIODevice::ReadOnly);
101185
while (buffer.canReadLine()) {
102186
QByteArray line = buffer.readLine().trimmed();
@@ -106,12 +190,13 @@ QList<FormData> HTTPConnection::parseFormData() const {
106190
QByteArray line = buffer.readLine().trimmed();
107191
if (line.isEmpty()) {
108192
// content starts after this line
109-
int idx = _requestContent.indexOf(end, buffer.pos());
193+
int idx = _requestContent->content().indexOf(end, buffer.pos());
110194
if (idx == -1) {
111195
qWarning() << "Missing end boundary." << _address;
112196
return data;
113197
}
114-
datum.second = _requestContent.mid(buffer.pos(), idx - buffer.pos());
198+
datum.second = QByteArray::fromRawData(_requestContent->content().data() + buffer.pos(),
199+
idx - buffer.pos());
115200
data.append(datum);
116201
buffer.seek(idx + end.length());
117202

@@ -255,7 +340,24 @@ void HTTPConnection::readHeaders() {
255340
_parentManager->handleHTTPRequest(this, _requestUrl);
256341

257342
} else {
258-
_requestContent.resize(clength.toInt());
343+
bool success = false;
344+
auto length = clength.toInt(&success);
345+
if (!success) {
346+
qWarning() << "Invalid header." << _address << trimmed;
347+
respond("400 Bad Request", "The header was malformed.");
348+
return;
349+
}
350+
351+
// Storing big requests in memory gets expensive, especially on servers
352+
// with limited memory. So we store big requests in a temporary file on disk
353+
// and map it to faster read/write access.
354+
static const int MAX_CONTENT_SIZE_IN_MEMORY = 10 * 1000 * 1000;
355+
if (length < MAX_CONTENT_SIZE_IN_MEMORY) {
356+
_requestContent = MemoryStorage::make(length);
357+
} else {
358+
_requestContent = FileStorage::make(length);
359+
}
360+
259361
connect(_socket, SIGNAL(readyRead()), SLOT(readContent()));
260362

261363
// read any content immediately available
@@ -284,12 +386,13 @@ void HTTPConnection::readHeaders() {
284386
}
285387

286388
void HTTPConnection::readContent() {
287-
int size = _requestContent.size();
288-
if (_socket->bytesAvailable() < size) {
289-
return;
290-
}
291-
_socket->read(_requestContent.data(), size);
292-
_socket->disconnect(this, SLOT(readContent()));
389+
auto size = std::min(_socket->bytesAvailable(), _requestContent->bytesLeftToWrite());
390+
391+
_requestContent->write(_socket->read(size));
293392

294-
_parentManager->handleHTTPRequest(this, _requestUrl.path());
393+
if (_requestContent->bytesLeftToWrite() == 0) {
394+
_socket->disconnect(this, SLOT(readContent()));
395+
396+
_parentManager->handleHTTPRequest(this, _requestUrl.path());
397+
}
295398
}

libraries/embedded-webserver/src/HTTPConnection.h

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
#ifndef hifi_HTTPConnection_h
1717
#define hifi_HTTPConnection_h
1818

19-
#include <QDataStream>
2019
#include <QHash>
21-
#include <QtNetwork/QHostAddress>
2220
#include <QIODevice>
2321
#include <QList>
22+
#include <QtNetwork/QHostAddress>
2423
#include <QtNetwork/QNetworkAccessManager>
2524
#include <QObject>
2625
#include <QPair>
26+
#include <QTemporaryFile>
2727
#include <QUrl>
2828

2929
#include <memory>
@@ -57,6 +57,17 @@ class HTTPConnection : public QObject {
5757
/// WebSocket close status codes.
5858
enum ReasonCode { NoReason = 0, NormalClosure = 1000, GoingAway = 1001 };
5959

60+
class Storage {
61+
public:
62+
Storage() = default;
63+
virtual ~Storage() = default;
64+
65+
virtual const QByteArray& content() const = 0;
66+
67+
virtual qint64 bytesLeftToWrite() const = 0;
68+
virtual void write(const QByteArray& data) = 0;
69+
};
70+
6071
/// Initializes the connection.
6172
HTTPConnection(QTcpSocket* socket, HTTPManager* parentManager);
6273

@@ -76,7 +87,7 @@ class HTTPConnection : public QObject {
7687
QByteArray requestHeader(const QString& key) const { return _requestHeaders.value(key.toLower().toLocal8Bit()); }
7788

7889
/// Returns a reference to the request content.
79-
const QByteArray& requestContent() const { return _requestContent; }
90+
const QByteArray& requestContent() const { return _requestContent->content(); }
8091

8192
/// Parses the request content as form data, returning a list of header/content pairs.
8293
QList<FormData> parseFormData() const;
@@ -129,7 +140,7 @@ protected slots:
129140
QByteArray _lastRequestHeader;
130141

131142
/// The content of the request.
132-
QByteArray _requestContent;
143+
std::unique_ptr<Storage> _requestContent;
133144

134145
/// Response content
135146
std::unique_ptr<QIODevice> _responseDevice;

0 commit comments

Comments
 (0)