Revision control

++ /// Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #include "interceptors.h"
++ #include "clientguide.qpb.h"
++
++ #include <QtProtobuf/QAbstractProtobufSerializer>
++
++ #include <QtGrpc/QAbstractGrpcChannel>
++ #include <QtGrpc/QGrpcCallOptions>
++ #include <QtGrpc/QGrpcStatus>
++
++ #include <QtCore/QDebug>
++ #include <QtCore/QDateTime>
++ #include <QtCore/QUuid>
++ #include <QtCore/QMultiHash>
++
++ using namespace Qt::Literals::StringLiterals;
++
++ //! [metrics-2]
++ MetricsInterceptor::~MetricsInterceptor()
++ {
++ Q_ASSERT(m_activeRPCs.isEmpty());
++ const auto keys = m_metrics.keys();
++ for (const auto &k : keys) {
++ qInfo() << "Metrics for:" << k;
++ const auto &m = m_metrics[k];
++ qInfo() << " Bytes received :" << m.bytesReceived;
++ qInfo() << " Failed RPCs :" << m.failed;
++ qInfo() << " Completed RPCs :" << m.completed;
++ qInfo() << " Completed Durations (ms):" << m.durations;
++ }
++ }
++ //! [metrics-2]
++
++ //! [metrics-1]
++ QGrpcStartInterceptor::Continuation
++ MetricsInterceptor::onStart(QGrpcInterceptionContext &context,
++ QProtobufMessage & /*message*/,
++ QGrpcCallOptions &callOptions)
++ {
++ m_activeRPCs.insert(context.operationId(), std::chrono::steady_clock::now());
++ callOptions.addMetadata("x-trace-id"_L1, QUuid::createUuid().toByteArray());
++
++ return Continuation::Proceed;
++ }
++
++ void MetricsInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
++ QByteArray &messageData)
++ {
++ m_metrics[context.descriptor()].bytesReceived += messageData.size();
++ }
++
++ void MetricsInterceptor::onFinished(QGrpcInterceptionContext &context,
++ QGrpcStatus &status)
++ {
++ const auto it = m_activeRPCs.find(context.operationId());
++ Q_ASSERT(it != m_activeRPCs.cend());
++
++ const auto duration = Ms(Clock::now() - it.value()).count();
++ m_activeRPCs.erase(it);
++
++ auto &metrics = m_metrics[context.descriptor()];
++ if (!status.isOk()) {
++ ++metrics.failed;
++ return;
++ }
++ ++metrics.completed;
++ metrics.durations.append(duration);
++ }
++ //! [metrics-1]
++
++ //! [auth-1]
++ TokenProvider::TokenProvider() { refresh(); }
++ QByteArray TokenProvider::token() { return m_token; }
++ void TokenProvider::refresh()
++ {
++ // NOTE: This is NOT a real JWT or secure token.
++ // It only mimics the structure: header.payload.signature
++ // The payload is just a timestamp used for expiry checks.
++ auto header = R"({"alg":"none","typ":"JWT"})"_ba; // fake header
++ auto payload = QByteArray::number(now()); // fake "iat"
++ auto signature = "no_signature"_ba; // fake signing
++
++ m_token = header + '.' + payload + '.' + signature;
++ }
++ //! [auth-1]
++
++ //! [auth-2]
++ QGrpcStartInterceptor::Continuation
++ AuthInterceptor::onStart(QGrpcInterceptionContext & /*context*/,
++ QProtobufMessage & /*messasge*/,
++ QGrpcCallOptions &callOptions)
++ {
++ constexpr QByteArrayView AuthKey("authorization");
++
++ if (!callOptions.metadata(QtGrpc::MultiValue).contains(AuthKey))
++ callOptions.addMetadata(AuthKey, "Bearer " + m_tokenProvider.token());
++
++ return Continuation::Proceed;
++ }
++
++ void AuthInterceptor::onTrailingMetadata(QGrpcInterceptionContext & /*context*/,
++ QMultiHash<QByteArray, QByteArray> &metadata)
++ {
++ constexpr QByteArrayView AuthErrorKey("www-authenticate");
++ const auto it = metadata.constFind(AuthErrorKey);
++ if (it == metadata.cend())
++ return;
++ m_serverAuthHint = it.value();
++ }
++
++ void AuthInterceptor::onFinished(QGrpcInterceptionContext & /*context*/, QGrpcStatus &status)
++ {
++ // Only handle Unauthenticated status codes.
++ if (status.code() != QtGrpc::StatusCode::Unauthenticated)
++ return;
++
++ qDebug("[AuthInterceptor] Refreshing token for next call");
++ // NOTE: Simplified for example. Real tokens are obtained via proper auth flow.
++ m_tokenProvider.refresh();
++ if (!m_serverAuthHint.isEmpty()) {
++ status = QGrpcStatus{ status.code(), status.message() + u", Hint: "_s + m_serverAuthHint };
++ m_serverAuthHint.clear();
++ }
++ }
++ //! [auth-2]
++
++ namespace {
++
++ //! [sanitizing-1]
++ qsizetype getTotalBytes(const QMultiHash<QByteArray, QByteArray> &md)
++ {
++ qsizetype total = 0;
++ for (const auto &[k, v] : md.asKeyValueRange())
++ total += k.size() + v.size();
++ return total;
++ }
++
++ template <typename T>
++ void sanitizeMessage(T &request)
++ {
++ const auto num = request.num();
++ if (num < 0 || num > 5)
++ request.setNum(qBound(0, num, 5));
++
++ const auto time = request.time();
++ const auto currentTime = now();
++ if (time < 0 || time > currentTime)
++ request.setTime(qMax(0, currentTime));
++ }
++ //! [sanitizing-1]
++
++ } // namespace
++
++ //! [sanitizing-2]
++ QGrpcStartInterceptor::Continuation
++ SanitizingInterceptor::onStart(QGrpcInterceptionContext &context, QProtobufMessage &message,
++ QGrpcCallOptions &callOptions)
++ {
++ constexpr quint16 MaxMetadata = 1024 * 8; // 8 KiB
++ auto totalMdBytes = getTotalBytes(callOptions.metadata(QtGrpc::MultiValue));
++ if (totalMdBytes > MaxMetadata) {
++ qDebug() << "[Sanitizing] Aborting call with metadata size of" << totalMdBytes << "bytes";
++ return Continuation::Drop;
++ }
++
++ onWriteMessage(context, message);
++
++ return Continuation::Proceed;
++ }
++
++ void SanitizingInterceptor::onWriteMessage(QGrpcInterceptionContext & /*context*/,
++ QProtobufMessage &message)
++ {
++ // For handling more complex scenarios with various Request message types,
++ // it would make sense to branch on the context.descriptor() appropriately.
++ if (auto *request = qprotobufmessage_cast<client::guide::Request *>(&message))
++ sanitizeMessage(*request);
++ }
++
++ void SanitizingInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
++ QByteArray &messageData)
++ {
++ // messageData can only be of type client::guide::Response. For more
++ // complex handling add appropriate handling with context.descriptor().
++ // Note that the deserialization and serialization introduces overhead.
++ client::guide::Response response;
++ auto serializer = context.channel().serializer();
++ if (serializer->deserialize(&response, messageData))
++ return;
++ sanitizeMessage(response);
++ messageData = serializer->serialize(&response);
++ }
++ //! [sanitizing-2]