// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea.
*
* nymea is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* nymea 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/*!
  \class nymeaserver::HttpRequest
  \brief Represents a HTTP request from a client to the nymea \l{WebServer}.

  \ingroup api
  \inmodule core

  This class holds the header and the payload data of a network request from a client to the \l{WebServer}.

  \note RFC 7231 HTTP/1.1 Semantics and Content -> \l{http://tools.ietf.org/html/rfc7231}{http://tools.ietf.org/html/rfc7231}

*/

/*! \enum nymeaserver::HttpRequest::RequestMethod

    This enum type describes the method of a \l{HttpRequest}. Following methods are allowed/handled:

    \value Get
        Represents the HTTP/1.1 GET method.
    \value Post
        Represents the HTTP/1.1 POST method.
    \value Put
        Represents the HTTP/1.1 PUT method.
    \value Delete
        Represents the HTTP/1.1 DELETE method.
    \value Options
        Represents the HTTP/1.1 OPTIONS method.
    \value Unhandled
        Represents every other method which is not handled.
*/

/*! \fn QDebug nymeaserver::operator<< (QDebug debug, const HttpRequest &httpRequest);
    Writes the \l{HttpRequest} \a httpRequest to the given \a debug. This method gets used just for debugging.
*/

#include "httprequest.h"
#include "loggingcategories.h"

#include <QUrlQuery>
#include <QRegularExpression>

/*! Construct an empty \l{HttpRequest}. */
HttpRequest::HttpRequest() :
    m_rawData(QByteArray()),
    m_valid(false),
    m_isComplete(false)
{
}

/*! Construct a \l{HttpRequest} with the given \a rawData. The \a rawData will be parsed in this constructor. You can check
    if the data is valid with \l{isValid()}. You can check if the request is complete with \l{isComplete}.

    \sa isValid(), isComplete()
*/
HttpRequest::HttpRequest(QByteArray rawData) :
    m_rawData(rawData),
    m_valid(false),
    m_isComplete(false)
{
    validate();
}

/*! Returns the raw header of this request.*/
QByteArray HttpRequest::rawHeader() const
{
    return m_rawHeader;
}

/*! Returns the list of raw header as key and value pairs.*/
QHash<QByteArray, QByteArray> HttpRequest::rawHeaderList() const
{
    return m_rawHeaderList;
}

/*! Returns the \l{RequestMethod} of this request.

  \sa RequestMethod
*/
HttpRequest::RequestMethod HttpRequest::method() const
{
    return m_method;
}

/*! Returns the method as human readable string.*/
QString HttpRequest::methodString() const
{
    return m_methodString;
}

/*! Returns the HTTP version of this \l{HttpRequest}.*/
QByteArray HttpRequest::httpVersion() const
{
    return m_httpVersion;
}

/*! Returns the URL of this \l{HttpRequest}.*/
QUrl HttpRequest::url() const
{
    return m_url;
}

/*! Returns the URL query of this \l{HttpRequest}.*/
QUrlQuery HttpRequest::urlQuery() const
{
    return m_urlQuery;
}

/*! Returns the payload (content) of this \l{HttpRequest}.*/
QByteArray HttpRequest::payload() const
{
    return m_payload;
}

/*! Returns true if this \l{HttpRequest} is valid. A HTTP request is valid if the header and the payload were paresed successfully without errors.*/
bool HttpRequest::isValid() const
{
    return m_valid;
}

/*! Returns true if this \l{HttpRequest} is complete. A HTTP request is complete if "Content-Length" header value matches the actual payload size. Bigger packages will be sent in multiple TCP packages. */
bool HttpRequest::isComplete() const
{
    return m_isComplete;
}

/*! Returns true if this \l{HttpRequest} has a payload.*/
bool HttpRequest::hasPayload() const
{
    return !m_payload.isEmpty();
}

/*! Appends the given \a data to the current raw data of this \l{HttpRequest}.
 *  This method will be used if a \l{HttpRequest} is not complete yet.
 *
 *  \sa isComplete()
*/
void HttpRequest::appendData(const QByteArray &data)
{
    m_rawData.append(data);
    validate();
}

void HttpRequest::validate()
{
    m_isComplete = true; m_valid = false;

    // Parese the HTTP request. The request is invalid, until the end of the parse process.
    if (m_rawData.isEmpty())
        return;

    // split the data into header and payload
    int headerEndIndex = m_rawData.indexOf("\r\n\r\n");
    if (headerEndIndex < 0) {
        qCWarning(dcWebServer()) << "Could not parse end of HTTP header (empty line between header and body):" << m_rawData;
        return;
    }

    m_rawHeader = m_rawData.left(headerEndIndex);
    m_payload = m_rawData.right(m_rawData.length() - headerEndIndex).simplified();

    // parse status line
    QStringList headerLines = QString(m_rawHeader).split(QRegularExpression("\r\n"));
    QString statusLine = headerLines.takeFirst();
    QStringList statusLineTokens = statusLine.split(QRegularExpression("[ \r\n][ \r\n]*"));
    if (statusLineTokens.count() != 3) {
        qCWarning(dcWebServer()) << "Could not parse HTTP status line:" << statusLine;
        return;
    }

    // verify http version
    m_httpVersion = statusLineTokens.at(2).toUtf8().simplified();
    if (!m_httpVersion.contains("HTTP")) {
        qCWarning(dcWebServer()) << "Unknown HTTP version:" << m_httpVersion;
        return;
    }
    m_methodString = statusLineTokens.at(0).simplified();
    m_method = getRequestMethodType(m_methodString);

    m_url = QUrl("http://example.com" + statusLineTokens.at(1).simplified());

    if (m_url.hasQuery())
        m_urlQuery = QUrlQuery(m_url.query());

    // verify header formating
    foreach (const QString &line, headerLines) {
        if (!line.contains(":")) {
            qCWarning(dcWebServer()) << "Invalid HTTP header:" << line;
            return;
        }
        int index = line.indexOf(":");
        QByteArray key = line.left(index).toUtf8().simplified();
        QByteArray value = line.right(line.count() - index - 1).toUtf8().simplified();
        m_rawHeaderList.insert(key, value);
    }

    // check User-Agent
    if (!m_rawHeaderList.contains("User-Agent"))
        qCDebug(dcWebServer()) << "User-Agent header is missing";


    // verify content length with actual payload
    if (m_rawHeaderList.contains("Content-Length")) {
        bool ok = false;
        int contentLength = m_rawHeaderList.value("Content-Length").toInt(&ok);
        if (!ok) {
            qCWarning(dcWebServer()) << "Could not parse Content-Length.";
            return;
        }
        // check if we have all data
        if (m_payload.size() < contentLength) {
            qCDebug(dcWebServer()) << "Request incomplete:";
            qCDebug(dcWebServer()) << "   -> Content-Length:" << contentLength;
            qCDebug(dcWebServer()) << "   -> Payload size  :" << payload().size();
            m_isComplete = false;
            return;
        }
        // check if the content length bigger than header Content-Length
        if (m_payload.size() > contentLength) {
            qCWarning(dcWebServer()) << "Payload size greater than header Content-Length:";
            qCWarning(dcWebServer()) << "   -> Content-Length:" << contentLength;
            qCWarning(dcWebServer()) << "   -> Payload size  :" << payload().size();
            m_isComplete = true;
            return;
        }

    }
    m_valid = true;
}

HttpRequest::RequestMethod HttpRequest::getRequestMethodType(const QString &methodString)
{
    if (methodString == "GET") {
        return RequestMethod::Get;
    } else if (methodString == "POST") {
        return RequestMethod::Post;
    } else if (methodString == "PUT") {
        return RequestMethod::Put;
    } else if (methodString == "DELETE") {
        return RequestMethod::Delete;
    } else if (methodString == "OPTIONS") {
        return RequestMethod::Options;
    }
    qCWarning(dcWebServer()) << "Method" << methodString << "will not be handled.";
    return RequestMethod::Unhandled;
}

QDebug operator<<(QDebug debug, const HttpRequest &httpRequest)
{
    QDebugStateSaver saver(debug);
    debug << "HttpRequest:" << '\n';
    debug << qUtf8Printable(httpRequest.rawHeader());
    debug << qUtf8Printable(httpRequest.payload());
    return debug;
}

