Files
thehub/libs/httpengine/socket.cpp
T
2022-09-09 17:45:53 +02:00

422 lines
12 KiB
C++

/* This file is part of Flowee
*
* Copyright (C) 2017 Nathan Osman
* Copyright (C) 2019-2021 Tom Zander <tom@flowee.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* For the full copy of the License see <http://www.gnu.org/licenses/>
*/
#include "parser.h"
#include "socket_p.h"
#include <QFile>
#include <QJsonParseError>
#include <QTcpSocket>
#include <QThread>
#include <cstring>
using namespace HttpEngine;
// Predefined error response requires a simple HTML template to be returned to
// the client describing the error condition
const QString ErrorTemplate =
"<!DOCTYPE html>"
"<html>"
"<head>"
"<meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<title>%1 %2</title>"
"</head>"
"<body>"
"<h1>%1 %2</h1>"
"<p>"
"An error has occurred while trying to display the requested resource. "
"Please contact the website owner if this error persists."
"</p>"
"<hr>"
"<p><em>Flowee HttpEngine %3</em></p>"
"</body>"
"</html>";
SocketPrivate::SocketPrivate(Socket *httpSocket, QTcpSocket *tcpSocket)
: QObject(httpSocket),
socket(tcpSocket),
readState(ReadHeaders),
requestDataRead(0),
requestDataTotal(-1),
writeState(WriteNone),
responseStatusCode(200),
responseStatusReason(statusReason(200)),
q(httpSocket)
{
socket->setParent(this);
connect(socket, &QTcpSocket::readyRead, this, &SocketPrivate::onReadyRead);
connect(socket, &QTcpSocket::bytesWritten, this, &SocketPrivate::onBytesWritten);
connect(socket, &QTcpSocket::readChannelFinished, this, &SocketPrivate::onReadChannelFinished);
connect(socket, &QTcpSocket::disconnected, q, &Socket::disconnected);
// Process anything already received by the socket
onReadyRead();
}
QByteArray SocketPrivate::statusReason(int statusCode) const
{
switch (statusCode) {
case Socket::OK: return "OK";
case Socket::Created: return "CREATED";
case Socket::Accepted: return "ACCEPTED";
case Socket::PartialContent: return "PARTIAL CONTENT";
case Socket::MovedPermanently: return "MOVED PERMANENTLY";
case Socket::Found: return "FOUND";
case Socket::BadRequest: return "BAD REQUEST";
case Socket::Unauthorized: return "UNAUTHORIZED";
case Socket::Forbidden: return "FORBIDDEN";
case Socket::NotFound: return "NOT FOUND";
case Socket::MethodNotAllowed: return "METHOD NOT ALLOWED";
case Socket::Conflict: return "CONFLICT";
case Socket::BadGateway: return "BAD GATEWAY";
case Socket::ServiceUnavailable: return "SERVICE UNAVAILABLE";
case Socket::InternalServerError: return "INTERNAL SERVER ERROR";
case Socket::HttpVersionNotSupported: return "HTTP VERSION NOT SUPPORTED";
default: return "UNKNOWN ERROR";
}
}
void SocketPrivate::onReadyRead()
{
// Append all of the new data to the read buffer
readBuffer.append(socket->readAll());
// If reading headers, return if they could not be read (yet)
if (readState == ReadHeaders && !readHeaders()) {
return;
}
// Read data if in that state, otherwise discard
switch (readState) {
case ReadData:
readData();
break;
case ReadFinished:
readBuffer.clear();
break;
case ReadHeaders:
assert(false);
break;
default:
break;
}
}
void SocketPrivate::onBytesWritten(qint64 bytes)
{
// Check to see if all of the response header was written
if (writeState == WriteHeaders) {
if (responseHeaderRemaining - bytes > 0) {
responseHeaderRemaining -= bytes;
} else {
writeState = WriteData;
bytes -= responseHeaderRemaining;
}
}
// Only emit bytesWritten() for data after the headers
if (writeState == WriteData) {
Q_EMIT q->bytesWritten(bytes);
}
}
void SocketPrivate::onReadChannelFinished()
{
if (requestDataTotal == -1) {
emit q->readChannelFinished();
}
}
bool SocketPrivate::readHeaders()
{
// Check for the double CRLF that signals the end of the headers and
// if it is not found, wait until the next time readyRead is emitted
int index = readBuffer.indexOf("\r\n\r\n");
if (index == -1) {
return false;
}
// Attempt to parse the headers and if a problem is encountered, abort
// the connection (so that no more data is read or written) and return
if (!Parser::parseRequestHeaders(readBuffer.left(index), requestMethod, requestRawPath, requestHeaders) ||
!Parser::parsePath(requestRawPath, requestPath, requestQueryString)) {
q->writeError(Socket::BadRequest);
return false;
}
// Remove the headers from the buffer
readBuffer.remove(0, index + 4);
readState = ReadData;
// If the content-length header is present, use it to determine
// how much data to expect from the socket - not all requests
// use this header - WebSocket requests, for example, do not
if (requestHeaders.contains("Content-Length")) {
requestDataTotal = requestHeaders.value("Content-Length").toLongLong();
}
// Indicate that the headers have been parsed
Q_EMIT q->headersParsed();
return true;
}
void SocketPrivate::readData()
{
// Emit the readyRead() signal if any data is available in the buffer
if (readBuffer.size()) {
Q_EMIT q->readyRead();
}
// Check to see if the specified amount of data has been read from the
// socket, if so, emit the readChannelFinished() signal
if (requestDataTotal != -1 &&
requestDataRead + readBuffer.size() >= requestDataTotal) {
readState = ReadFinished;
Q_EMIT q->readChannelFinished();
}
}
Socket::Socket(QTcpSocket *socket, QObject *parent)
: QIODevice(parent),
d(new SocketPrivate(this, socket))
{
// The device is initially open for both reading and writing
setOpenMode(QIODevice::ReadWrite);
}
qint64 Socket::bytesAvailable() const
{
if (d->readState > SocketPrivate::ReadHeaders) {
return d->readBuffer.size() + QIODevice::bytesAvailable();
} else {
return 0;
}
}
bool Socket::isSequential() const
{
return true;
}
void Socket::close()
{
// Invoke the parent method
QIODevice::close();
d->readState = SocketPrivate::ReadFinished;
d->writeState = SocketPrivate::WriteFinished;
connect(d->socket, &QTcpSocket::disconnected, this, &Socket::deleteLater);
d->socket->close();
}
QHostAddress Socket::peerAddress() const
{
return d->socket->peerAddress();
}
bool Socket::isHeadersParsed() const
{
return d->readState > SocketPrivate::ReadHeaders;
}
Socket::Method Socket::method() const
{
return d->requestMethod;
}
QByteArray Socket::rawPath() const
{
return d->requestRawPath;
}
QString Socket::path() const
{
return d->requestPath;
}
Socket::QueryStringMap Socket::queryString() const
{
return d->requestQueryString;
}
Socket::HeaderMap Socket::headers() const
{
return d->requestHeaders;
}
qint64 Socket::contentLength() const
{
return d->requestDataTotal;
}
bool Socket::readJson(QJsonDocument &document)
{
QJsonParseError error;
document = QJsonDocument::fromJson(readAll(), &error);
if (error.error != QJsonParseError::NoError) {
writeError(Socket::BadRequest);
return false;
}
return true;
}
void Socket::setStatusCode(int statusCode, const QByteArray &statusReason)
{
d->responseStatusCode = statusCode;
d->responseStatusReason = statusReason.isNull() ? d->statusReason(statusCode) : statusReason;
}
void Socket::setHeader(const QByteArray &name, const QByteArray &value, bool replace)
{
if (replace || d->responseHeaders.count(name)) {
d->responseHeaders.replace(name, value);
} else {
d->responseHeaders.replace(name, d->responseHeaders.value(name) + ", " + value);
}
}
void Socket::setHeaders(const HeaderMap &headers)
{
d->responseHeaders = headers;
}
void Socket::writeHeaders()
{
// Use a QByteArray for building the header so that we can later determine
// exactly how many bytes were written
QByteArray header;
// Append the status line
header.append("HTTP/1.0 ");
header.append(QByteArray::number(d->responseStatusCode) + " " + d->responseStatusReason);
header.append("\r\n");
// Append each of the headers followed by a CRLF
for (auto i = d->responseHeaders.constBegin(); i != d->responseHeaders.constEnd(); ++i) {
header.append(i.key());
header.append(": ");
header.append(d->responseHeaders.values(i.key()).join(", "));
header.append("\r\n");
}
// Append an extra CRLF
header.append("\r\n");
d->writeState = SocketPrivate::WriteHeaders;
d->responseHeaderRemaining = header.length();
// Write the header
d->socket->write(header);
}
void Socket::writeRedirect(const QByteArray &path, bool permanent)
{
setStatusCode(permanent ? MovedPermanently : Found);
setHeader("Location", path);
writeHeaders();
close();
}
void Socket::writeError(int statusCode, const QByteArray &statusReason)
{
setStatusCode(statusCode, statusReason);
// Build the template that will be sent to the client
QByteArray data = ErrorTemplate
.arg(d->responseStatusCode)
.arg(d->responseStatusReason.constData())
.arg(HTTPENGINE_VERSION)
.toUtf8();
setHeader("Content-Length", QByteArray::number(data.length()));
setHeader("Content-Type", "text/html");
writeHeaders();
write(data);
close();
}
void Socket::writeJson(const QJsonDocument &document, QJsonDocument::JsonFormat format)
{
QByteArray data = document.toJson(format);
setHeader("Content-Length", QByteArray::number(data.length()));
setHeader("Content-Type", "application/json");
write(data);
close();
}
qint64 Socket::readData(char *data, qint64 maxlen)
{
// Ensure the connection is in the correct state for reading data
if (d->readState == SocketPrivate::ReadHeaders) {
return 0;
}
// Ensure that no more than the requested amount or the size of the buffer is read
qint64 size = qMin(static_cast<qint64>(d->readBuffer.size()), maxlen);
memcpy(data, d->readBuffer.constData(), size);
// Remove the amount that was read from the buffer
d->readBuffer.remove(0, size);
d->requestDataRead += size;
return size;
}
qint64 Socket::writeData(const char *data, qint64 len)
{
// If the response headers have not yet been written, they must
// immediately be written before the data can be
if (d->writeState == SocketPrivate::WriteNone) {
writeHeaders();
}
return d->socket->write(data, len);
}
void HttpEngine::returnTemplatePath(Socket *socket, const QString &templateName, const QString &error)
{
Q_ASSERT(QThread::currentThread() == socket->thread());
QFile helpMessage(":/" + templateName);
if (!helpMessage.open(QIODevice::ReadOnly)) {
// missing file
socket->close();
return;
}
auto data = helpMessage.readAll();
data.replace("%ERROR%", error.toUtf8());
socket->setHeader("Content-Length", QByteArray::number(data.size()));
if (templateName.endsWith(".html"))
socket->setHeader("Content-Type", "text/html");
else
socket->setHeader("Content-Type", "application/json");
socket->setHeader("last-modified", "Fri, 1 Jan 2021 18:33:01 GMT");
socket->writeHeaders();
if (socket->method() != HttpEngine::Socket::HEAD)
socket->write(data);
socket->close();
}