Skip to content

Commit

Permalink
Log HTTP requests and responses
Browse files Browse the repository at this point in the history
  • Loading branch information
TheOneRing committed Jun 8, 2020
1 parent fb3e759 commit d701088
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 32 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/7873
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Change: Option to log HTTP requests and responses

We now allow to log http requests and responses

https://github.com/owncloud/client/issues/7873
1 change: 1 addition & 0 deletions src/libsync/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ set(libsync_SRCS
discovery.cpp
discoveryphase.cpp
filesystem.cpp
httplogger.cpp
logger.cpp
accessmanager.cpp
configfile.cpp
Expand Down
30 changes: 5 additions & 25 deletions src/libsync/abstractnetworkjob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
#include <QCoreApplication>
#include <QAuthenticator>
#include <QMetaEnum>
#include <QRegularExpression>

#include "common/asserts.h"
#include "networkjobs.h"
#include "account.h"
#include "owncloudpropagator.h"
#include "httplogger.h"

#include "creds/abstractcredentials.h"

Expand Down Expand Up @@ -172,10 +174,9 @@ void AbstractNetworkJob::slotFinished()
if (_reply->error() == QNetworkReply::SslHandshakeFailedError) {
qCWarning(lcNetworkJob) << "SslHandshakeFailedError: " << errorString() << " : can be caused by a webserver wanting SSL client certificates";
}

// Qt doesn't yet transparently resend HTTP2 requests, do so here
const auto maxHttp2Resends = 3;
QByteArray verb = requestVerb(*reply());
QByteArray verb = HttpLogger::requestVerb(*reply());
if (_reply->error() == QNetworkReply::ContentReSendError
&& _reply->attribute(QNetworkRequest::HTTP2WasUsedAttribute).toBool()) {

Expand Down Expand Up @@ -425,27 +426,6 @@ QString errorMessage(const QString &baseError, const QByteArray &body)
return msg;
}

QByteArray requestVerb(const QNetworkReply &reply)
{
switch (reply.operation()) {
case QNetworkAccessManager::HeadOperation:
return "HEAD";
case QNetworkAccessManager::GetOperation:
return "GET";
case QNetworkAccessManager::PutOperation:
return "PUT";
case QNetworkAccessManager::PostOperation:
return "POST";
case QNetworkAccessManager::DeleteOperation:
return "DELETE";
case QNetworkAccessManager::CustomOperation:
return reply.request().attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
case QNetworkAccessManager::UnknownOperation:
break;
}
return QByteArray();
}

QString networkReplyErrorString(const QNetworkReply &reply)
{
QString base = reply.errorString();
Expand All @@ -457,15 +437,15 @@ QString networkReplyErrorString(const QNetworkReply &reply)
return base;
}

return AbstractNetworkJob::tr("Server replied \"%1 %2\" to \"%3 %4\"").arg(QString::number(httpStatus), httpReason, requestVerb(reply), reply.request().url().toDisplayString());
return AbstractNetworkJob::tr("Server replied \"%1 %2\" to \"%3 %4\"").arg(QString::number(httpStatus), httpReason, HttpLogger::requestVerb(reply), reply.request().url().toDisplayString());
}

void AbstractNetworkJob::retry()
{
ENFORCE(_reply);
auto req = _reply->request();
QUrl requestedUrl = req.url();
QByteArray verb = requestVerb(*_reply);
QByteArray verb = HttpLogger::requestVerb(*_reply);
qCInfo(lcNetworkJob) << "Restarting" << verb << requestedUrl;
resetTimeout();
if (_requestBody) {
Expand Down
6 changes: 0 additions & 6 deletions src/libsync/abstractnetworkjob.h
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,6 @@ QString OWNCLOUDSYNC_EXPORT extractErrorMessage(const QByteArray &errorResponse)
/** Builds a error message based on the error and the reply body. */
QString OWNCLOUDSYNC_EXPORT errorMessage(const QString &baseError, const QByteArray &body);

/** Helper to construct the HTTP verb used in the request
*
* Returns an empty QByteArray for UnknownOperation.
*/
QByteArray OWNCLOUDSYNC_EXPORT requestVerb(const QNetworkReply &reply);

/** Nicer errorString() for QNetworkReply
*
* By default QNetworkReply::errorString() often produces messages like
Expand Down
6 changes: 5 additions & 1 deletion src/libsync/accessmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "cookiejar.h"
#include "accessmanager.h"
#include "common/utility.h"
#include "httplogger.h"

namespace OCC {

Expand Down Expand Up @@ -82,8 +83,11 @@ QNetworkReply *AccessManager::createRequest(QNetworkAccessManager::Operation op,

newRequest.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, http2EnabledEnv);
}
HttpLogger::logRequest(newRequest, op, outgoingData);

return QNetworkAccessManager::createRequest(op, newRequest, outgoingData);
const auto reply = QNetworkAccessManager::createRequest(op, newRequest, outgoingData);
HttpLogger::logReplyOnFinished(reply);
return reply;
}

} // namespace OCC
129 changes: 129 additions & 0 deletions src/libsync/httplogger.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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 <QRegularExpression>
#include <QLoggingCategory>
#include <QBuffer>

namespace {
Q_LOGGING_CATEGORY(lcNetworkJobHttp, "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();
}

void logHttp(bool isRequest, const QByteArray &verb, const QString &url, const QString &contentType, const qint64 &contentLength, const QList<QNetworkReply::RawHeaderPair> &header, QIODevice *device)
{
QString msg;
QTextStream stream(&msg);
if (isRequest) {
stream << "Request: " << verb;
} else {
stream << "Response: " << verb;
}
stream << " " << url << " Header: { ";
for (const auto &it : header) {
if (it.first == "Authorization") {
stream << it.first << ": [redacted], ";
} else {
stream << it.first << ": " << it.second << ", ";
}
}
stream << "} Data: [";
if (contentLength > 0) {
if (isTextBody(contentType)) {
if (!device->isOpen()) {
Q_ASSERT(dynamic_cast<QBuffer *>(device));
// should we close it again?
device->open(QIODevice::ReadOnly);
}
Q_ASSERT(device->pos() == 0);
stream << device->peek(PeekSize);
} else {
stream << "Binary Data";
}
}
stream << "]";
qCInfo(lcNetworkJobHttp) << msg;
}
}


namespace OCC {


void HttpLogger::logReplyOnFinished(const QNetworkReply *reply)
{
if (!lcNetworkJobHttp().isInfoEnabled()) {
return;
}
QObject::connect(reply, &QNetworkReply::finished, reply, [reply] {
::logHttp(false,
requestVerb(*reply),
reply->url().toString(),
reply->header(QNetworkRequest::ContentTypeHeader).toString(),
reply->header(QNetworkRequest::ContentLengthHeader).toInt(),
reply->rawHeaderPairs(),
const_cast<QNetworkReply *>(reply));
});
}

void HttpLogger::logRequest(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, QIODevice *device)
{
if (!lcNetworkJobHttp().isInfoEnabled()) {
return;
}
const auto keys = request.rawHeaderList();
QList<QNetworkReply::RawHeaderPair> header;
header.reserve(keys.size());
for (const auto &key : keys) {
header << qMakePair(key, request.rawHeader(key));
}
::logHttp(true,
requestVerb(operation, request),
request.url().toString(),
request.header(QNetworkRequest::ContentTypeHeader).toString(),
device ? device->size() : 0,
header,
device);
}

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();
}

}
35 changes: 35 additions & 0 deletions src/libsync/httplogger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once

#include "owncloudlib.h"

#include <QNetworkReply>
#include <QUrl>

namespace OCC {
namespace HttpLogger {
void OWNCLOUDSYNC_EXPORT logReplyOnFinished(const QNetworkReply *reply);
void OWNCLOUDSYNC_EXPORT logRequest(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, QIODevice *device);

/**
* Helper to construct the HTTP verb used in the request
*/
QByteArray OWNCLOUDSYNC_EXPORT requestVerb(QNetworkAccessManager::Operation operation, const QNetworkRequest &request);
inline QByteArray requestVerb(const QNetworkReply &reply)
{
return requestVerb(reply.operation(), reply.request());
}
}
}

0 comments on commit d701088

Please sign in to comment.