/* * Copyright (C) by Hannah von Reth * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License * for more details. */ #include "httplogger.h" #include "common/chronoelapsedtimer.h" #include #include #include #include #include #include #include #include using namespace std::chrono; using namespace Qt::Literals::StringLiterals; namespace { Q_LOGGING_CATEGORY(lcNetworkHttp, "sync.httplogger", QtWarningMsg) const qint64 PeekSize = 1024 * 1024; bool isTextBody(const QString &s) { static const QRegularExpression regexp(QStringLiteral("^(text/.*?|(application/(xml|.*?json|x-www-form-urlencoded)(;|$)))")); return regexp.match(s).hasMatch(); } struct HttpContext { HttpContext(const QNetworkRequest &request) : originalUrl(request.url().toString()) , lastUrl(request.url()) , id(QString::fromUtf8(request.rawHeader(QByteArrayLiteral("X-Request-ID")))) { } void addRedirect(const QUrl &url) { lastUrl = url; redirectUrls.append(url.toString()); } const QString originalUrl; QUrl lastUrl; QStringList redirectUrls; const QString id; OCC::Utility::ChronoElapsedTimer timer; bool send = false; bool receivedBeforeSent = false; }; void logHttp(const QByteArray &verb, HttpContext *ctx, QJsonObject &&header, QIODevice *device, bool cached = false) { static const bool redact = !qEnvironmentVariableIsSet("QSFERA_HTTPLOGGER_NO_REDACT"); const auto reply = qobject_cast(device); const auto contentLength = device ? device->size() : 0; if (redact) { const auto authKey = "authorization"_L1; const QString auth = header.value(authKey).toString(); if (!auth.isEmpty()) { header.insert(authKey, auth.startsWith(QStringLiteral("Bearer ")) ? QStringLiteral("Bearer [redacted]") : QStringLiteral("Basic [redacted]")); } } QJsonObject info{{QStringLiteral("method"), QString::fromUtf8(verb)}, {QStringLiteral("id"), ctx->id}, {QStringLiteral("url"), ctx->originalUrl}}; if (!ctx->redirectUrls.isEmpty()) { info.insert(QStringLiteral("redirects"), QJsonArray::fromStringList(ctx->redirectUrls)); } if (reply) { // respond QJsonObject replyInfo{{QStringLiteral("status"), reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()}, {QStringLiteral("cached"), reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()}, // downcast to int, this is json {QStringLiteral("duration"), static_cast(duration_cast(ctx->timer.duration()).count())}, // {QStringLiteral("durationString"), QDebug::toString(ctx->timer)}, {QStringLiteral("version"), QStringLiteral("HTTP %1").arg( reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool() ? QStringLiteral("1.1") : QStringLiteral("2"))}}; if (reply->error() != QNetworkReply::NoError) { replyInfo.insert(QStringLiteral("error"), reply->errorString()); } info.insert(QStringLiteral("reply"), replyInfo); } else { // request was not actually sent but will be returned from cache info.insert(QStringLiteral("cached"), cached); } QJsonObject body = {{QStringLiteral("length"), contentLength}}; if (contentLength > 0) { const QString contentType = header.value("content-type"_L1).toString(); if (isTextBody(contentType)) { if (!device->isOpen()) { Q_ASSERT(dynamic_cast(device)); // should we close it again? device->open(QIODevice::ReadOnly); } Q_ASSERT(device->pos() == 0); QString data = QString::fromUtf8(device->peek(PeekSize)); if (PeekSize < contentLength) { data += QStringLiteral("...(%1 bytes elided)").arg(QString::number(contentLength - PeekSize)); } body[QStringLiteral("data")] = data; } else { body[QStringLiteral("data")] = QStringLiteral("%1 bytes of %2 data").arg(QString::number(contentLength), contentType); } } qCInfo(lcNetworkHttp).noquote() << (reply ? "RESPONSE" : "REQUEST") << ctx->id << QJsonDocument{QJsonObject{{reply ? QStringLiteral("response") : QStringLiteral("request"), QJsonObject{{QStringLiteral("info"), info}, {QStringLiteral("header"), header}, {QStringLiteral("body"), body}}}}} .toJson(QJsonDocument::Compact); } } namespace OCC { void HttpLogger::logRequest(QNetworkReply *reply, QNetworkAccessManager::Operation operation, QIODevice *device) { if (!lcNetworkHttp().isInfoEnabled()) { return; } auto ctx = std::make_unique(reply->request()); // device should still exist, lets still use a qpointer to ensure we have valid data const auto logSend = [ctx = ctx.get(), operation, reply, device = QPointer(device), deviceRaw = device](bool cached = false) { Q_ASSERT(!deviceRaw || device); if (!ctx->send) { ctx->send = true; ctx->timer.reset(); } else { // this is a redirect if (ctx->lastUrl != reply->url()) { ctx->addRedirect(reply->url()); } else if (ctx->receivedBeforeSent) { qCWarning(lcNetworkHttp) << u"Request:" << ctx->id << u" was sent after we received a result"; } else { qCWarning(lcNetworkHttp) << u"Request:" << ctx->id << u" resending"; } } const auto request = reply->request(); QJsonObject header; for (const auto &key : request.rawHeaderList()) { header[QString::fromUtf8(key)] = QString::fromUtf8(request.rawHeader(key)); } logHttp(requestVerb(operation, request), ctx, std::move(header), device, cached); }; QObject::connect(reply, &QNetworkReply::requestSent, reply, logSend, Qt::DirectConnection); QObject::connect( reply, &QNetworkReply::finished, reply, [reply, ctx = std::move(ctx), logSend] { if (!ctx->send) { ctx->receivedBeforeSent = true; logSend(reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()); } ctx->timer.stop(); QJsonObject header; for (const auto &[key, value] : reply->rawHeaderPairs()) { header[QString::fromUtf8(key)] = QString::fromUtf8(value); } logHttp(requestVerb(*reply), ctx.get(), std::move(header), reply); }, Qt::DirectConnection); } QByteArray HttpLogger::requestVerb(QNetworkAccessManager::Operation operation, const QNetworkRequest &request) { switch (operation) { case QNetworkAccessManager::HeadOperation: return QByteArrayLiteral("HEAD"); case QNetworkAccessManager::GetOperation: return QByteArrayLiteral("GET"); case QNetworkAccessManager::PutOperation: return QByteArrayLiteral("PUT"); case QNetworkAccessManager::PostOperation: return QByteArrayLiteral("POST"); case QNetworkAccessManager::DeleteOperation: return QByteArrayLiteral("DELETE"); case QNetworkAccessManager::CustomOperation: return request.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray(); case QNetworkAccessManager::UnknownOperation: break; } Q_UNREACHABLE(); } }