401 lines · 14.0 KB · C++
RawDownload
////  HTTPConnection.cpp//  libraries/embedded-webserver/src////  Created by Stephen Birarda on 1/16/14.//  Copyright 2014 High Fidelity, Inc.//  Copyright 2022 Overte e.V.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "HTTPConnection.h" #include <assert.h> #include <QBuffer>#include <QCryptographicHash>#include <QTcpSocket>#include <QUrlQuery> #include "EmbeddedWebserverLogging.h"#include "HTTPManager.h" const char* HTTPConnection::StatusCode200 = "200 OK";const char* HTTPConnection::StatusCode204 = "204 No Content";const char* HTTPConnection::StatusCode301 = "301 Moved Permanently";const char* HTTPConnection::StatusCode302 = "302 Found";const char* HTTPConnection::StatusCode400 = "400 Bad Request";const char* HTTPConnection::StatusCode401 = "401 Unauthorized";const char* HTTPConnection::StatusCode403 = "403 Forbidden";const char* HTTPConnection::StatusCode404 = "404 Not Found";const char* HTTPConnection::StatusCode500 = "500 Internal server error";const char* HTTPConnection::DefaultContentType = "text/plain; charset=ISO-8859-1";  class MemoryStorage : public HTTPConnection::Storage {public:    static std::unique_ptr<MemoryStorage> make(qint64 size);    virtual ~MemoryStorage() = default;     const QByteArray& content() const override { return _array; }    qint64 bytesLeftToWrite() const override { return _array.size() - _bytesWritten; }    void write(const QByteArray& data) override; private:    MemoryStorage(qint64 size) { _array.resize(size); }     QByteArray _array;    qint64 _bytesWritten { 0 };}; std::unique_ptr<MemoryStorage> MemoryStorage::make(qint64 size) {    return std::unique_ptr<MemoryStorage>(new MemoryStorage(size));} void MemoryStorage::write(const QByteArray& data) {    assert(data.size() <= bytesLeftToWrite());    memcpy(_array.data() + _bytesWritten, data.data(), data.size());    _bytesWritten += data.size();}  class FileStorage : public HTTPConnection::Storage {public:    static std::unique_ptr<FileStorage> make(qint64 size);    virtual ~FileStorage();     const QByteArray& content() const override { return _wrapperArray; };    qint64 bytesLeftToWrite() const override { return _mappedMemorySize - _bytesWritten; }    void write(const QByteArray& data) override; private:    FileStorage(std::unique_ptr<QTemporaryFile> file, uchar* mapped, qint64 size);     // Byte array is const because any edit will trigger a deep copy    // and pull all the data we want to keep on disk in memory.    const QByteArray _wrapperArray;    std::unique_ptr<QTemporaryFile> _file;     uchar* const _mappedMemoryAddress { nullptr };    const qint64 _mappedMemorySize { 0 };    qint64 _bytesWritten { 0 };}; std::unique_ptr<FileStorage> FileStorage::make(qint64 size) {    auto file = std::unique_ptr<QTemporaryFile>(new QTemporaryFile());    file->open(); // Open for resize    file->resize(size);    auto mapped = file->map(0, size); // map the entire file     return std::unique_ptr<FileStorage>(new FileStorage(std::move(file), mapped, size));} // Use QByteArray::fromRawData to avoid a new allocation and access the already existing// memory directly as long as all operations on the array are const.FileStorage::FileStorage(std::unique_ptr<QTemporaryFile> file, uchar* mapped, qint64 size) :    _wrapperArray(QByteArray::fromRawData(reinterpret_cast<char*>(mapped), size)),    _file(std::move(file)),    _mappedMemoryAddress(mapped),    _mappedMemorySize(size){} FileStorage::~FileStorage() {    _file->unmap(_mappedMemoryAddress);    _file->close();} void FileStorage::write(const QByteArray& data) {    assert(data.size() <= bytesLeftToWrite());    // We write directly to the mapped memory    memcpy(_mappedMemoryAddress + _bytesWritten, data.data(), data.size());    _bytesWritten += data.size();}  HTTPConnection::HTTPConnection(QTcpSocket* socket, HTTPManager* parentManager) :    QObject(parentManager),    _parentManager(parentManager),    _socket(socket),    _address(socket->peerAddress()){    // take over ownership of the socket    _socket->setParent(this);     // connect initial slots    connect(socket, &QAbstractSocket::readyRead, this, &HTTPConnection::readRequest);    connect(socket, &QAbstractSocket::errorOccurred, this, &HTTPConnection::deleteLater);    connect(socket, &QAbstractSocket::disconnected, this, &HTTPConnection::deleteLater);} HTTPConnection::~HTTPConnection() {    // log the destruction    if (_socket->error() != QAbstractSocket::UnknownSocketError        && _socket->error() != QAbstractSocket::RemoteHostClosedError) {        qCDebug(embeddedwebserver) << _socket->errorString() << "-" << _socket->error();    }} QHash<QString, QString> HTTPConnection::parseUrlEncodedForm() {    // make sure we have the correct MIME type    QList<QByteArray> elements = requestHeader("Content-Type").split(';');     QString contentType = elements.at(0).trimmed();    if (contentType != "application/x-www-form-urlencoded") {        return QHash<QString, QString>();    }     QUrlQuery form { _requestContent->content() };    QHash<QString, QString> pairs;    for (auto pair : form.queryItems()) {        auto key = QUrl::fromPercentEncoding(pair.first.toLatin1().replace('+', ' '));        auto value = QUrl::fromPercentEncoding(pair.second.toLatin1().replace('+', ' '));        pairs[key] = value;    }     return pairs;} QList<FormData> HTTPConnection::parseFormData() const {    // make sure we have the correct MIME type    QList<QByteArray> elements = requestHeader("Content-Type").split(';');     QString contentType = elements.at(0).trimmed();     if (contentType != "multipart/form-data") {        return QList<FormData>();    }     // retrieve the boundary marker    QByteArray boundary;    for (int ii = 1, nn = elements.size(); ii < nn; ii++) {        QByteArray element = elements.at(ii).trimmed();        if (element.startsWith("boundary")) {            boundary = element.mid(element.indexOf('=') + 1).trimmed();            break;        }    }     QByteArray start = "--" + boundary;    QByteArray end = "\r\n--" + boundary + "--\r\n";     QList<FormData> data;    QBuffer buffer(const_cast<QByteArray*>(&_requestContent->content()));    buffer.open(QIODevice::ReadOnly);    while (buffer.canReadLine()) {        QByteArray line = buffer.readLine().trimmed();        if (line == start) {            FormData datum;            while (buffer.canReadLine()) {                QByteArray line = buffer.readLine().trimmed();                if (line.isEmpty()) {                    // content starts after this line                    int idx = _requestContent->content().indexOf(end, buffer.pos());                    if (idx == -1) {                        qWarning() << "Missing end boundary." << _address;                        return data;                    }                    datum.second = QByteArray(_requestContent->content().data() + buffer.pos(),                                                           idx - buffer.pos());                    data.append(datum);                    buffer.seek(idx + end.length());                 } else {                    // it's a header element                    int idx = line.indexOf(':');                    if (idx == -1) {                        qWarning() << "Invalid header line." << _address << line;                        continue;                    }                    datum.first.insert(line.left(idx).trimmed(), line.mid(idx + 1).trimmed());                }            }        }    }     return data;} void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) {    respondWithStatusAndHeaders(code, contentType, headers, content.size());     _socket->write(content);     _socket->disconnectFromHost();     // make sure we receive no further read notifications    disconnect(_socket, &QTcpSocket::readyRead, this, nullptr);} void HTTPConnection::respond(const char* code, std::unique_ptr<QIODevice> device, const char* contentType, const Headers& headers) {    _responseDevice = std::move(device);     if (_responseDevice->isSequential()) {        qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported";        respondWithStatusAndHeaders(StatusCode500, contentType, headers, 0);        _socket->disconnect(SIGNAL(readyRead()), this);        _socket->disconnectFromHost();        return;    }     int totalToBeWritten = _responseDevice->size();    respondWithStatusAndHeaders(code, contentType, headers, totalToBeWritten);     if (_responseDevice->atEnd()) {        _socket->disconnectFromHost();    } else {        connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable {            constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10;            if (!_responseDevice->atEnd()) {                totalToBeWritten -= _socket->write(_responseDevice->read(HTTP_RESPONSE_CHUNK_SIZE));                if (_responseDevice->atEnd()) {                    _socket->disconnectFromHost();                    disconnect(_socket, &QTcpSocket::bytesWritten, this, nullptr);                }            }        });     }     // make sure we receive no further read notifications    disconnect(_socket, &QTcpSocket::readyRead, this, nullptr);} void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 contentLength) {    _socket->write("HTTP/1.1 ");     _socket->write(code);    _socket->write("\r\n");     for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd();            it != end; it++) {        _socket->write(it.key());        _socket->write(": ");        _socket->write(it.value());        _socket->write("\r\n");    }     if (contentLength > 0) {        _socket->write("Content-Length: ");        _socket->write(QByteArray::number(contentLength));        _socket->write("\r\n");         _socket->write("Content-Type: ");        _socket->write(contentType);        _socket->write("\r\n");    }    _socket->write("Connection: close\r\n\r\n");} void HTTPConnection::readRequest() {    if (!_socket->canReadLine()) {        return;    }    if (!_requestUrl.isEmpty()) {        qDebug() << "Request URL was already set";        return;    }    // parse out the method and resource    QByteArray line = _socket->readLine().trimmed();    if (line.startsWith("HEAD")) {        _requestOperation = QNetworkAccessManager::HeadOperation;     } else if (line.startsWith("GET")) {        _requestOperation = QNetworkAccessManager::GetOperation;     } else if (line.startsWith("PUT")) {        _requestOperation = QNetworkAccessManager::PutOperation;     } else if (line.startsWith("POST")) {        _requestOperation = QNetworkAccessManager::PostOperation;     } else if (line.startsWith("DELETE")) {        _requestOperation = QNetworkAccessManager::DeleteOperation;     } else {        qWarning() << "Unrecognized HTTP operation." << _address << line;        respond("400 Bad Request", "Unrecognized operation.");        return;    }    int idx = line.indexOf(' ') + 1;    _requestUrl.setUrl(line.mid(idx, line.lastIndexOf(' ') - idx));     // switch to reading the header    _socket->disconnect(this, SLOT(readRequest()));    connect(_socket, SIGNAL(readyRead()), SLOT(readHeaders()));     // read any headers immediately available    readHeaders();} void HTTPConnection::readHeaders() {    while (_socket->canReadLine()) {        QByteArray line = _socket->readLine();        QByteArray trimmed = line.trimmed();        if (trimmed.isEmpty()) {            _socket->disconnect(this, SLOT(readHeaders()));             QByteArray clength = requestHeader("Content-Length");            if (clength.isEmpty()) {                _requestContent = MemoryStorage::make(0);                _parentManager->handleHTTPRequest(this, _requestUrl);             } else {                bool success = false;                auto length = clength.toInt(&success);                if (!success) {                    qWarning() << "Invalid header." << _address << trimmed;                    respond("400 Bad Request", "The header was malformed.");                    return;                }                 // Storing big requests in memory gets expensive, especially on servers                // with limited memory. So we store big requests in a temporary file on disk                // and map it to faster read/write access.                static const int MAX_CONTENT_SIZE_IN_MEMORY = 10 * 1000 * 1000;                if (length < MAX_CONTENT_SIZE_IN_MEMORY) {                    _requestContent = MemoryStorage::make(length);                } else {                    _requestContent = FileStorage::make(length);                }                 connect(_socket, SIGNAL(readyRead()), SLOT(readContent()));                 // read any content immediately available                readContent();            }            return;        }        char first = line.at(0);        if (first == ' ' || first == '\t') { // continuation            _requestHeaders[_lastRequestHeader].append(trimmed);            continue;        }        int idx = trimmed.indexOf(':');        if (idx == -1) {            qWarning() << "Invalid header." << _address << trimmed;            respond("400 Bad Request", "The header was malformed.");            return;        }        _lastRequestHeader = trimmed.left(idx).toLower();        QByteArray& value = _requestHeaders[_lastRequestHeader];        if (!value.isEmpty()) {            value.append(", ");        }        value.append(trimmed.mid(idx + 1).trimmed());    }} void HTTPConnection::readContent() {    auto size = std::min(_socket->bytesAvailable(), _requestContent->bytesLeftToWrite());     _requestContent->write(_socket->read(size));     if (_requestContent->bytesLeftToWrite() == 0) {        _socket->disconnect(this, SLOT(readContent()));         _parentManager->handleHTTPRequest(this, _requestUrl);    }}