// // HTTPConnection.cpp // libraries/embedded-webserver/src // // Created by Stephen Birarda on 1/16/14. // Copyright 2014 High Fidelity, Inc. // // 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 #include #include #include #include #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 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::make(qint64 size) { return std::unique_ptr(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 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 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 _file; uchar* const _mappedMemoryAddress { nullptr }; const qint64 _mappedMemorySize { 0 }; qint64 _bytesWritten { 0 }; }; std::unique_ptr FileStorage::make(qint64 size) { auto file = std::unique_ptr(new QTemporaryFile()); file->open(); // Open for resize file->resize(size); auto mapped = file->map(0, size); // map the entire file return std::unique_ptr(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 file, uchar* mapped, qint64 size) : _wrapperArray(QByteArray::fromRawData(reinterpret_cast(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, SIGNAL(readyRead()), SLOT(readRequest())); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(deleteLater())); connect(socket, SIGNAL(disconnected()), SLOT(deleteLater())); } HTTPConnection::~HTTPConnection() { // log the destruction if (_socket->error() != QAbstractSocket::UnknownSocketError && _socket->error() != QAbstractSocket::RemoteHostClosedError) { qCDebug(embeddedwebserver) << _socket->errorString() << "-" << _socket->error(); } } QHash HTTPConnection::parseUrlEncodedForm() { // make sure we have the correct MIME type QList elements = requestHeader("Content-Type").split(';'); QString contentType = elements.at(0).trimmed(); if (contentType != "application/x-www-form-urlencoded") { return QHash(); } QUrlQuery form { _requestContent->content() }; QHash 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 HTTPConnection::parseFormData() const { // make sure we have the correct MIME type QList elements = requestHeader("Content-Type").split(';'); QString contentType = elements.at(0).trimmed(); if (contentType != "multipart/form-data") { return QList(); } // 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 data; QBuffer buffer(const_cast(&_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::fromRawData(_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 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()) { _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.path()); } }