// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include "qhttpserverhttp1protocolhandler_p.h"

#include <QtCore/qloggingcategory.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qthread.h>
#include <QtCore/qpointer.h>
#include <QtHttpServer/qabstracthttpserver.h>
#include <QtHttpServer/qhttpserverrequest.h>
#include <QtHttpServer/qhttpserverresponder.h>
#include <QtHttpServer/qhttpserverresponse.h>
#if QT_CONFIG(localserver)
#include <QtNetwork/qlocalsocket.h>
#endif
#include <QtNetwork/qtcpsocket.h>

#include <private/qabstracthttpserver_p.h>
#include <private/qhttpserverliterals_p.h>
#include <private/qhttpserverrequest_p.h>

QT_BEGIN_NAMESPACE

Q_STATIC_LOGGING_CATEGORY(lcHttpServerHttp1Handler, "qt.httpserver.http1handler")

// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
static const std::map<QHttpServerResponder::StatusCode, QByteArray> statusString{
#define XX(name, string) { QHttpServerResponder::StatusCode::name, QByteArrayLiteral(string) }
    XX(Continue, "Continue"),
    XX(SwitchingProtocols, "Switching Protocols"),
    XX(Processing, "Processing"),
    XX(Ok, "OK"),
    XX(Created, "Created"),
    XX(Accepted, "Accepted"),
    XX(NonAuthoritativeInformation, "Non-Authoritative Information"),
    XX(NoContent, "No Content"),
    XX(ResetContent, "Reset Content"),
    XX(PartialContent, "Partial Content"),
    XX(MultiStatus, "Multi-Status"),
    XX(AlreadyReported, "Already Reported"),
    XX(IMUsed, "I'm Used"),
    XX(MultipleChoices, "Multiple Choices"),
    XX(MovedPermanently, "Moved Permanently"),
    XX(Found, "Found"),
    XX(SeeOther, "See Other"),
    XX(NotModified, "Not Modified"),
    XX(UseProxy, "Use Proxy"),
    XX(TemporaryRedirect, "Temporary Redirect"),
    XX(PermanentRedirect, "Permanent Redirect"),
    XX(BadRequest, "Bad Request"),
    XX(Unauthorized, "Unauthorized"),
    XX(PaymentRequired, "Payment Required"),
    XX(Forbidden, "Forbidden"),
    XX(NotFound, "Not Found"),
    XX(MethodNotAllowed, "Method Not Allowed"),
    XX(NotAcceptable, "Not Acceptable"),
    XX(ProxyAuthenticationRequired, "Proxy Authentication Required"),
    XX(RequestTimeout, "Request Timeout"),
    XX(Conflict, "Conflict"),
    XX(Gone, "Gone"),
    XX(LengthRequired, "Length Required"),
    XX(PreconditionFailed, "Precondition Failed"),
    XX(PayloadTooLarge, "Request Entity Too Large"),
    XX(UriTooLong, "Request-URI Too Long"),
    XX(UnsupportedMediaType, "Unsupported Media Type"),
    XX(RequestRangeNotSatisfiable, "Requested Range Not Satisfiable"),
    XX(ExpectationFailed, "Expectation Failed"),
    XX(ImATeapot, "I'm a teapot"),
    XX(MisdirectedRequest, "Misdirected Request"),
    XX(UnprocessableEntity, "Unprocessable Entity"),
    XX(Locked, "Locked"),
    XX(FailedDependency, "Failed Dependency"),
    XX(UpgradeRequired, "Upgrade Required"),
    XX(PreconditionRequired, "Precondition Required"),
    XX(TooManyRequests, "Too Many Requests"),
    XX(RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"),
    XX(UnavailableForLegalReasons, "Unavailable For Legal Reasons"),
    XX(InternalServerError, "Internal Server Error"),
    XX(NotImplemented, "Not Implemented"),
    XX(BadGateway, "Bad Gateway"),
    XX(ServiceUnavailable, "Service Unavailable"),
    XX(GatewayTimeout, "Gateway Timeout"),
    XX(HttpVersionNotSupported, "HTTP Version Not Supported"),
    XX(VariantAlsoNegotiates, "Variant Also Negotiates"),
    XX(InsufficientStorage, "Insufficient Storage"),
    XX(LoopDetected, "Loop Detected"),
    XX(NotExtended, "Not Extended"),
    XX(NetworkAuthenticationRequired, "Network Authentication Required"),
    XX(NetworkConnectTimeoutError, "Network Connect Timeout Error"),
#undef XX
};

template <qint64 BUFFERSIZE = 128 * 1024>
struct QHttpServerHttp1IOChunkedTransfer
{
    // TODO This is not the fastest implementation, as it does read & write
    // in a sequential fashion, but these operation could potentially overlap.
    // TODO Can we implement it without the buffer? Direct write to the target buffer
    // would be great.

    static constexpr qint64 bufferSize = BUFFERSIZE;
    static constexpr qint64 targetWriteBufferSaturation = bufferSize / 2;
    char buffer[BUFFERSIZE];
    qint64 beginIndex = -1;
    qint64 endIndex = -1;
    QPointer<QIODevice> source;
    const QPointer<QIODevice> sink;
    const QMetaObject::Connection bytesWrittenConnection;
    const QMetaObject::Connection readyReadConnection;
    const QMetaObject::Connection readChannelFinished;
    QPointer<QHttpServerHttp1ProtocolHandler> handler;
    bool inRead = false;
    bool gotReadChannelFinished = false;
    bool writingIsComplete = false;
    bool useHttp1_1 = false;

    QHttpServerHttp1IOChunkedTransfer(QIODevice *input, QIODevice *output, bool http1_1,
                                      QHttpServerHttp1ProtocolHandler *callback) :
          source(input),
          sink(output),
          bytesWrittenConnection(connectToBytesWritten(this, output)),
          readyReadConnection(QObject::connect(source.data(), &QIODevice::readyRead, source.data(),
                                               [this]() { readFromInput(); })),
          readChannelFinished(QObject::connect(source.data(), &QIODevice::readChannelFinished,
                                               source.data(),
                                               [this]() { readChannelFinishedHandler(); })),
          handler(callback),
          useHttp1_1(http1_1)
    {
        QObject::connect(sink.data(), &QObject::destroyed, source.data(), &QObject::deleteLater);
        QObject::connect(source.data(), &QObject::destroyed, source.data(), [this]() {
            delete this;
        });
        readFromInput();
    }

    ~QHttpServerHttp1IOChunkedTransfer()
    {
        QObject::disconnect(bytesWrittenConnection);
        QObject::disconnect(readyReadConnection);
        QObject::disconnect(readChannelFinished);
    }

    static QMetaObject::Connection connectToBytesWritten(QHttpServerHttp1IOChunkedTransfer *that,
                                                         QIODevice *device)
    {
        auto send = [that]() { that->writeToOutput(); };
#if QT_CONFIG(ssl)
        if (auto *sslSocket = qobject_cast<QSslSocket *>(device)) {
            return QObject::connect(sslSocket, &QSslSocket::encryptedBytesWritten, sslSocket,
                                    std::move(send));
        }
#endif
        return QObject::connect(device, &QIODevice::bytesWritten, device, std::move(send));
    }

    inline bool isBufferEmpty()
    {
        Q_ASSERT(beginIndex <= endIndex);
        return beginIndex == endIndex;
    }

    void readChannelFinishedHandler()
    {
        gotReadChannelFinished = true;
        readFromInput();
    }

    void readFromInput()
    {
        if (inRead)
            return;
        if (source.isNull())
            return;

        if (!isBufferEmpty()) // We haven't consumed all the data yet.
            return;
        QScopedValueRollback inReadGuard(inRead, true);

        while (isBufferEmpty()) {
            beginIndex = 0;
            endIndex = source->read(buffer, bufferSize);
            if (endIndex < 0) {
                endIndex = 0; // Mark the buffer as empty
                if (!source->isSequential()) {
                    qCWarning(lcHttpServerHttp1Handler, "Error reading chunk: %ls",
                              qUtf16Printable(source->errorString()));
                    return;
                }
            }
            if (endIndex == 0) { // Nothing was read
                if (!writingIsComplete
                    && ((!source->isSequential() && source->atEnd()) || gotReadChannelFinished))
                    completeWriting();
                return;
            }
            memset(buffer + endIndex, 0, sizeof(buffer) - std::size_t(endIndex));
            writeToOutput();
        }
    }

    void writeToOutput()
    {
        if (sink.isNull() || source.isNull())
            return;
        if (isBufferEmpty())
            return;

        // If downstream has enough data to write already,
        // don't bother writing more now. That would only lead to
        // higher, unnecessary memory usage.
        if (sink->bytesToWrite() >= targetWriteBufferSaturation)
            return;
#if QT_CONFIG(ssl)
        if (auto *sslSocket = qobject_cast<QSslSocket *>(sink.data())) {
            const qint64 budget = targetWriteBufferSaturation - sink->bytesToWrite();
            if (sslSocket->encryptedBytesToWrite() >= budget)
                return;
        }
#endif

        if (useHttp1_1 && source->isSequential()) {
            sink->write(QByteArray::number(endIndex - beginIndex, 16));
            sink->write("\r\n");
        }
        const auto writtenBytes = sink->write(buffer + beginIndex, endIndex - beginIndex);
        if (writtenBytes < 0) {
            qCWarning(lcHttpServerHttp1Handler, "Error writing chunk: %ls",
                      qUtf16Printable(sink->errorString()));
            return;
        }
        if (useHttp1_1 && source->isSequential())
            sink->write("\r\n");
        beginIndex += writtenBytes;
        if (isBufferEmpty() && !inRead)
            readFromInput();
    }

    void completeWriting()
    {
        if (sink.isNull())
            return;
        Q_ASSERT(!source.isNull());
        Q_ASSERT(isBufferEmpty());

        if (source->isSequential()) {
            if (useHttp1_1)
                sink->write("0\r\n\r\n");
            else
                sink->close();
        }
        source->deleteLater();
        writingIsComplete = true;
        if (!handler.isNull())
            handler->completeWriting();
    }
};

QHttpServerHttp1ProtocolHandler::QHttpServerHttp1ProtocolHandler(QAbstractHttpServer *server,
                                                                 QIODevice *socket,
                                                                 QHttpServerRequestFilter *filter)
    : QHttpServerStream(socket, server),
      server(server),
      socket(socket),
      tcpSocket(qobject_cast<QTcpSocket *>(socket)),
#if QT_CONFIG(localserver)
      localSocket(qobject_cast<QLocalSocket*>(socket)),
#endif
      m_filter(filter)
{
    socket->setParent(this);

    if (tcpSocket) {
        qCDebug(lcHttpServerHttp1Handler) << "Connection from:" << tcpSocket->peerAddress();
        connect(socket, &QTcpSocket::readyRead,
                this, &QHttpServerHttp1ProtocolHandler::handleReadyRead);
        connect(tcpSocket, &QTcpSocket::disconnected,
                this, &QHttpServerHttp1ProtocolHandler::socketDisconnected);
#if QT_CONFIG(localserver)
    } else if (localSocket) {
        qCDebug(lcHttpServerHttp1Handler) << "Connection from:" << localSocket->serverName();
        connect(socket, &QLocalSocket::readyRead,
                this, &QHttpServerHttp1ProtocolHandler::handleReadyRead);
        connect(localSocket, &QLocalSocket::disconnected,
                this, &QHttpServerHttp1ProtocolHandler::socketDisconnected);
#endif
    }

    lastActiveTimer.start();
}

void QHttpServerHttp1ProtocolHandler::responderDestroyed()
{
    Q_ASSERT(QThread::currentThread() == thread());
    if (protocolChanged) {
        deleteLater();
        return;
    }
    Q_ASSERT(handlingRequest);
    handlingRequest = false;
    if (state == TransferState::Ready)
        resumeListening();
}

void QHttpServerHttp1ProtocolHandler::resumeListening()
{
    if (tcpSocket) {
        if (tcpSocket->state() != QAbstractSocket::ConnectedState) {
            deleteLater();
        } else {
            connect(tcpSocket, &QTcpSocket::readyRead,
                    this, &QHttpServerHttp1ProtocolHandler::handleReadyRead);
            QMetaObject::invokeMethod(tcpSocket, &QTcpSocket::readyRead, Qt::QueuedConnection);
        }
#if QT_CONFIG(localserver)
    } else if (localSocket) {
        if (localSocket->state() != QLocalSocket::ConnectedState) {
            deleteLater();
        } else {
            connect(localSocket, &QLocalSocket::readyRead,
                    this, &QHttpServerHttp1ProtocolHandler::handleReadyRead);
            QMetaObject::invokeMethod(localSocket, &QLocalSocket::readyRead, Qt::QueuedConnection);
        }
#endif
    }
}

void QHttpServerHttp1ProtocolHandler::startHandlingRequest()
{
    handlingRequest = true;
}

void QHttpServerHttp1ProtocolHandler::socketDisconnected()
{
    if (!handlingRequest)
        deleteLater();
}

void QHttpServerHttp1ProtocolHandler::handleReadyRead()
{
    if (handlingRequest || state != TransferState::Ready)
        return;

    lastActiveTimer.restart();

    if (!socket->isTransactionStarted())
        socket->startTransaction();

    auto requestReceived = parser.parse(socket);
    if (!requestReceived) {
        if (tcpSocket)
            tcpSocket->disconnectFromHost();
#if QT_CONFIG(localserver)
        else if (localSocket)
            localSocket->disconnectFromServer();
#endif
        return;
    }

#if QT_CONFIG(ssl)
    auto request = QHttpServerRequest::create(parser, sslConfiguration);
#else
    auto request = QHttpServerRequest::create(parser);
#endif

    if (parser.state != QHttpServerParser::State::AllDone)
        return; // Partial read

    qCDebug(lcHttpServerHttp1Handler) << "Request:" << request;
    useHttp1_1 = request.d->minorVersion == 1;

    QHttpServerResponder responder(this);

#if defined(QT_WEBSOCKETS_LIB)
    if (auto *tcpSocket = qobject_cast<QTcpSocket*>(socket)) {
        if (parser.upgrade) { // Upgrade
            const auto &upgradeValue = request.value(QByteArrayLiteral("upgrade"));
            if (upgradeValue.compare(QByteArrayLiteral("websocket"), Qt::CaseInsensitive) == 0) {
                const auto upgradeResponse = server->verifyWebSocketUpgrade(request);
                static const auto signal =
                        QMetaMethod::fromSignal(&QAbstractHttpServer::newWebSocketConnection);
                if (server->isSignalConnected(signal)
                    && upgradeResponse.type()
                            != QHttpServerWebSocketUpgradeResponse::ResponseType::PassToNext) {
                    if (upgradeResponse.type()
                        == QHttpServerWebSocketUpgradeResponse::ResponseType::Accept) {
                        // Socket will now be managed by websocketServer
                        protocolChanged = true;
                        socket->disconnect();
                        socket->rollbackTransaction();
                        socket->setParent(nullptr);
                        server->d_func()->websocketServer.handleConnection(tcpSocket);
                        Q_EMIT socket->readyRead();
                    } else {
                        qCDebug(lcHttpServerHttp1Handler, "WebSocket upgrade denied: %ls",
                                qUtf16Printable(QLatin1StringView(upgradeResponse.denyMessage())));
                        QByteArray buffer;
                        buffer.append("HTTP/1.1 ");
                        buffer.append(QByteArray::number(quint32(upgradeResponse.denyStatus())));
                        buffer.append(" ");
                        buffer.append(upgradeResponse.denyMessage());
                        buffer.append("\r\n\r\n");
                        tcpSocket->write(buffer);
                    }
                } else {
                    if (!server->isSignalConnected(signal)) {
                        qCWarning(lcHttpServerHttp1Handler,
                                  "WebSocket received but no slots connected to "
                                  "QWebSocketServer::newConnection");
                    }
                    server->missingHandler(request, responder);
                    tcpSocket->disconnectFromHost();
                }
                return;
            }
        }
    }
#endif // QT_WEBSOCKETS_LIB

    socket->commitTransaction();

    QHostAddress peerAddress = tcpSocket ? tcpSocket->peerAddress()
                                         : QHostAddress::LocalHost;
    if (!m_filter->isRequestAllowed(peerAddress)) {
        responder.sendResponse(
                QHttpServerResponse(QHttpServerResponder::StatusCode::Forbidden));
    }
    if (!m_filter->isRequestWithinRate(peerAddress)) {
        responder.sendResponse(
                QHttpServerResponse(QHttpServerResponder::StatusCode::TooManyRequests));
    } else if (!server->handleRequest(request, responder)) {
        server->missingHandler(request, responder);
    }

    if (handlingRequest || state != TransferState::Ready)
        disconnect(socket, &QIODevice::readyRead, this, &QHttpServerHttp1ProtocolHandler::handleReadyRead);
    else if (socket->bytesAvailable() > 0)
        QMetaObject::invokeMethod(socket, &QIODevice::readyRead, Qt::QueuedConnection);
}

void QHttpServerHttp1ProtocolHandler::write(const QByteArray &body, const QHttpHeaders &headers,
                                        QHttpServerResponder::StatusCode status, quint32 streamId)
{
    Q_UNUSED(streamId);
    Q_ASSERT(state == TransferState::Ready);
    writeStatusAndHeaders(status, headers);
    write(body);
    state = TransferState::Ready;
}

void QHttpServerHttp1ProtocolHandler::write(QHttpServerResponder::StatusCode status, quint32 streamId)
{
    Q_UNUSED(streamId);
    Q_ASSERT(state == TransferState::Ready);
    QHttpHeaders headers;
    headers.append(QHttpHeaders::WellKnownHeader::ContentType,
                   QHttpServerLiterals::contentTypeXEmpty());
    headers.append(QHttpHeaders::WellKnownHeader::ContentLength, "0");
    writeStatusAndHeaders(status, headers);
    state = TransferState::Ready;
}

void QHttpServerHttp1ProtocolHandler::write(QIODevice *data, const QHttpHeaders &headers,
                                        QHttpServerResponder::StatusCode status, quint32 streamId)
{
    Q_UNUSED(streamId);
    Q_ASSERT(state == TransferState::Ready);
    std::unique_ptr<QIODevice, QScopedPointerDeleteLater> input(data);

    input->setParent(nullptr);
    if (!input->isOpen()) {
        if (!input->open(QIODevice::ReadOnly)) {
            // TODO Add developer error handling
            qCDebug(lcHttpServerHttp1Handler, "500: Could not open device %ls",
                    qUtf16Printable(input->errorString()));
            write(QHttpServerResponder::StatusCode::InternalServerError, 0);
            return;
        }
    } else if (!(input->openMode() & QIODevice::ReadOnly)) {
        // TODO Add developer error handling
        qCDebug(lcHttpServerHttp1Handler) << "500: Device is opened in a wrong mode"
                                          << input->openMode();
        write(QHttpServerResponder::StatusCode::InternalServerError, 0);
        return;
    }

    QHttpHeaders allHeaders(headers);
    if (input->isSequential()) {
        if (useHttp1_1)
            allHeaders.append(QHttpHeaders::WellKnownHeader::TransferEncoding, "chunked");
        else
            allHeaders.append(QHttpHeaders::WellKnownHeader::Connection, "close");
    } else { // Non-sequential QIODevice should know its data size
        allHeaders.append(QHttpHeaders::WellKnownHeader::ContentLength,
                          QByteArray::number(input->size()));
    }
    writeStatusAndHeaders(status, allHeaders);

    state = TransferState::IODeviceTransferBegun;
    // input takes ownership of the QHttpServerHttp1IOChunkedTransfer pointer inside his constructor
    new QHttpServerHttp1IOChunkedTransfer<>(input.release(), socket, useHttp1_1, this);
}

void QHttpServerHttp1ProtocolHandler::writeBeginChunked(const QHttpHeaders &headers,
                                                    QHttpServerResponder::StatusCode status,
                                                    quint32 streamId)
{
    Q_UNUSED(streamId);
    Q_ASSERT(state == TransferState::Ready);
    QHttpHeaders allHeaders(headers);
    allHeaders.append(QHttpHeaders::WellKnownHeader::TransferEncoding, "chunked");
    writeStatusAndHeaders(status, allHeaders);
    state = TransferState::ChunkedTransferBegun;
}

void QHttpServerHttp1ProtocolHandler::writeChunk(const QByteArray &data, quint32 streamId)
{
    Q_UNUSED(streamId);
    Q_ASSERT(state == TransferState::ChunkedTransferBegun);

    if (data.length() == 0) {
        qCWarning(lcHttpServerHttp1Handler, "Chunk must have length > 0");
        return;
    }

    write(QByteArray::number(data.length(), 16));
    write("\r\n");
    write(data);
    write("\r\n");
}

void QHttpServerHttp1ProtocolHandler::writeEndChunked(const QByteArray &data,
                                                      const QHttpHeaders &trailers,
                                                      quint32 streamId)
{
    Q_UNUSED(streamId);
    Q_ASSERT(state == TransferState::ChunkedTransferBegun);
    writeChunk(data, 0);
    write("0\r\n");
    for (qsizetype i = 0; i < trailers.size(); ++i) {
        const auto name = trailers.nameAt(i);
        const auto value = trailers.valueAt(i);
        writeHeader({ name.data(), name.size() }, value.toByteArray());
    }
    write("\r\n");
    state = TransferState::Ready;
}

void QHttpServerHttp1ProtocolHandler::writeStatusAndHeaders(QHttpServerResponder::StatusCode status,
                                                        const QHttpHeaders &headers)
{
    Q_ASSERT(state == TransferState::Ready);
    QByteArray payload;
    payload.append(useHttp1_1 ? "HTTP/1.1 " : "HTTP/1.0 ");
    payload.append(QByteArray::number(quint32(status)));
    const auto it = statusString.find(status);
    if (it != statusString.end()) {
        payload.append(" ");
        payload.append(statusString.at(status));
    }
    payload.append("\r\n");

    for (qsizetype i = 0; i < headers.size(); ++i) {
        const auto name = headers.nameAt(i);
        payload.append(QByteArrayView(name.data(), name.size()) + ": "
                       + headers.valueAt(i).toByteArray() + "\r\n");
    }
    payload.append("\r\n");
    write(payload);
    state = TransferState::HeadersSent;
}

void QHttpServerHttp1ProtocolHandler::writeHeader(const QByteArray &key, const QByteArray &value)
{
    write(key + ": " + value + "\r\n");
}

void QHttpServerHttp1ProtocolHandler::write(const QByteArray &ba)
{
    Q_ASSERT(QThread::currentThread() == thread());
    socket->write(ba);
}

void QHttpServerHttp1ProtocolHandler::write(const char *body, qint64 size)
{
    Q_ASSERT(QThread::currentThread() == thread());
    socket->write(body, size);
}

void QHttpServerHttp1ProtocolHandler::completeWriting()
{
    Q_ASSERT(state == TransferState::IODeviceTransferBegun);
    state = TransferState::Ready;
    if (!handlingRequest)
        resumeListening();
}

void QHttpServerHttp1ProtocolHandler::checkKeepAliveTimeout()
{
    if (handlingRequest || state != TransferState::Ready)
        return;

    if (lastActiveTimer.durationElapsed() > server->configuration().keepAliveTimeout()) {
        if (tcpSocket)
            tcpSocket->abort();
#if QT_CONFIG(localserver)
        else if (localSocket)
            localSocket->abort();
#endif
    }
}

QT_END_NAMESPACE
