File indexing completed on 2024-05-19 05:49:16

0001 /*
0002     SPDX-FileCopyrightText: 2007 Nicolas Ternisien <nicolas.ternisien@gmail.com>
0003     SPDX-FileCopyrightText: 2015 Vyacheslav Matyushin
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "journaldNetworkAnalyzer.h"
0009 #include "journaldConfiguration.h"
0010 #include "ksystemlogConfig.h"
0011 #include "ksystemlog_debug.h"
0012 #include "logFile.h"
0013 #include "logViewModel.h"
0014 
0015 #include <QJsonArray>
0016 #include <QJsonDocument>
0017 #include <QJsonObject>
0018 #include <QJsonParseError>
0019 #include <QRegularExpression>
0020 
0021 #include <KLocalizedString>
0022 
0023 JournaldNetworkAnalyzer::JournaldNetworkAnalyzer(LogMode *mode, const JournaldAnalyzerOptions &options)
0024     : JournaldAnalyzer(mode)
0025 {
0026     mAddress = options.address;
0027 
0028     connect(&mNetworkManager, &QNetworkAccessManager::sslErrors, this, &JournaldNetworkAnalyzer::sslErrors);
0029 
0030     auto *configuration = mode->logModeConfiguration<JournaldConfiguration *>();
0031 
0032     mBaseUrl = QStringLiteral("%1://%2:%3/").arg(mAddress.https ? QStringLiteral("https") : QStringLiteral("http")).arg(mAddress.address).arg(mAddress.port);
0033 
0034     mEntriesUrlUpdating = mBaseUrl + QStringLiteral("entries");
0035     mEntriesUrlFull = mEntriesUrlUpdating;
0036 
0037     QString filterPrefix;
0038     if (configuration->displayCurrentBootOnly()) {
0039         mEntriesUrlUpdating.append(QStringLiteral("?boot&follow"));
0040         mEntriesUrlFull.append(QStringLiteral("?boot"));
0041         filterPrefix = QStringLiteral("&");
0042     } else {
0043         mEntriesUrlUpdating.append(QStringLiteral("?follow"));
0044         filterPrefix = QStringLiteral("?");
0045     }
0046 
0047     if (!options.filter.isEmpty()) {
0048         mEntriesUrlUpdating.append(QStringLiteral("&") + options.filter);
0049         mEntriesUrlFull.append(filterPrefix + options.filter);
0050     }
0051 
0052     mSyslogIdUrl = mBaseUrl + QStringLiteral("fields/SYSLOG_IDENTIFIER");
0053     mSystemdUnitsUrl = mBaseUrl + QStringLiteral("fields/_SYSTEMD_UNIT");
0054 
0055     mFilterName = options.filter.section(QChar::fromLatin1('='), 1);
0056 
0057     mReply = nullptr;
0058 }
0059 
0060 void JournaldNetworkAnalyzer::watchLogFiles(bool enabled)
0061 {
0062     if (enabled) {
0063         sendRequest(RequestType::SyslogIds);
0064     } else {
0065         mCursor.clear();
0066         updateStatus(QString());
0067         if (mReply) {
0068             mReply->abort();
0069             mReply->deleteLater();
0070             mReply = nullptr;
0071         }
0072     }
0073 }
0074 
0075 QStringList JournaldNetworkAnalyzer::units() const
0076 {
0077     return mSystemdUnits;
0078 }
0079 
0080 QStringList JournaldNetworkAnalyzer::syslogIdentifiers() const
0081 {
0082     return mSyslogIdentifiers;
0083 }
0084 
0085 void JournaldNetworkAnalyzer::httpFinished()
0086 {
0087     QByteArray data = mReply->readAll();
0088     if (mCurrentRequest == RequestType::EntriesFull) {
0089         if (data.size()) {
0090             parseEntries(data, FullRead);
0091             updateStatus(i18n("Connected"));
0092         }
0093         if (!mCursor.isEmpty()) {
0094             sendRequest(RequestType::EntriesUpdate);
0095         } else {
0096             qCWarning(KSYSTEMLOG) << "Network journal analyzer failed to extract cursor string. "
0097                                      "Journal updates will be unavailable.";
0098         }
0099     } else {
0100         QString const identifiersString = QString::fromUtf8(data);
0101         QStringList const identifiersList = identifiersString.split(QChar::fromLatin1('\n'), Qt::SkipEmptyParts);
0102         switch (mCurrentRequest) {
0103         case RequestType::SyslogIds:
0104             mSyslogIdentifiers = identifiersList;
0105             mSyslogIdentifiers.sort();
0106             sendRequest(RequestType::Units);
0107             break;
0108         case RequestType::Units: {
0109             mSystemdUnits = identifiersList;
0110             mSystemdUnits.sort();
0111             auto *journalLogMode = dynamic_cast<JournaldLogMode *>(mLogMode);
0112             JournalFilters filters;
0113             filters.syslogIdentifiers = mSyslogIdentifiers;
0114             filters.systemdUnits = mSystemdUnits;
0115             journalLogMode->updateJournalFilters(mAddress, filters);
0116             // Regenerate the "Logs" submenu to include new syslog identifiers and systemd units.
0117             Q_EMIT mLogMode->menuChanged();
0118             sendRequest(RequestType::EntriesFull);
0119             break;
0120         }
0121         default:
0122             break;
0123         }
0124     }
0125 }
0126 
0127 void JournaldNetworkAnalyzer::httpReadyRead()
0128 {
0129     if (mCurrentRequest == RequestType::EntriesUpdate) {
0130         QByteArray data = mReply->readAll();
0131         parseEntries(data, UpdatingRead);
0132     }
0133 }
0134 
0135 void JournaldNetworkAnalyzer::httpError(QNetworkReply::NetworkError code)
0136 {
0137     if (mParsingPaused) {
0138         return;
0139     }
0140 
0141     if (code == QNetworkReply::OperationCanceledError) {
0142         return;
0143     }
0144 
0145     updateStatus(i18n("Connection error"));
0146     qCWarning(KSYSTEMLOG) << "Network journald connection error:" << code;
0147 }
0148 
0149 void JournaldNetworkAnalyzer::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
0150 {
0151     Q_UNUSED(errors)
0152     reply->ignoreSslErrors();
0153 }
0154 
0155 void JournaldNetworkAnalyzer::parseEntries(QByteArray &data, Analyzer::ReadingMode readingMode)
0156 {
0157     if (mParsingPaused) {
0158         qCDebug(KSYSTEMLOG) << "Parsing is paused, discarding journald entries.";
0159         return;
0160     }
0161 
0162     QList<QByteArray> items = data.split('{');
0163     QList<JournalEntry> entries;
0164     for (int i = 0; i < items.size(); i++) {
0165         QByteArray &item = items[i];
0166         if (item.isEmpty()) {
0167             continue;
0168         }
0169         item.prepend('{');
0170         QJsonParseError jsonError{};
0171         QJsonDocument const doc = QJsonDocument::fromJson(item, &jsonError);
0172         if (jsonError.error != 0) {
0173             continue;
0174         }
0175         QJsonObject object = doc.object();
0176 
0177         if ((readingMode == FullRead) && (i == items.size() - 1)) {
0178             mCursor = object[QStringLiteral("__CURSOR")].toString();
0179             break;
0180         }
0181 
0182         JournalEntry entry;
0183         auto timestampUsec = object[QStringLiteral("__REALTIME_TIMESTAMP")].toVariant().value<quint64>();
0184         entry.date.setMSecsSinceEpoch(timestampUsec / 1000);
0185         entry.message = object[QStringLiteral("MESSAGE")].toString();
0186         if (entry.message.isEmpty()) {
0187             // MESSAGE field contains a JSON array of bytes.
0188             QByteArray stringBytes;
0189             QJsonArray a = object[QStringLiteral("MESSAGE")].toArray();
0190             for (int i = 0; i < a.size(); i++) {
0191                 stringBytes.append(a[i].toVariant().value<char>());
0192             }
0193             entry.message = QString::fromUtf8(stringBytes);
0194         }
0195         entry.message.remove(QRegularExpression(QLatin1String(ConsoleColorEscapeSequence)));
0196         entry.priority = object[QStringLiteral("PRIORITY")].toVariant().value<int>();
0197         entry.bootID = object[QStringLiteral("_BOOT_ID")].toString();
0198         QString unit = object[QStringLiteral("SYSLOG_IDENTIFIER")].toString();
0199         if (unit.isEmpty()) {
0200             unit = object[QStringLiteral("_SYSTEMD_UNIT")].toString();
0201         }
0202         entry.unit = unit;
0203 
0204         entries << entry;
0205     }
0206 
0207     if (entries.empty()) {
0208         qCDebug(KSYSTEMLOG) << "Received no entries.";
0209     } else {
0210         mInsertionLocking.lock();
0211         mLogViewModel->startingMultipleInsertions();
0212 
0213         if (FullRead == readingMode) {
0214             Q_EMIT statusBarChanged(i18n("Reading journald entries..."));
0215             // Start displaying the loading bar.
0216             Q_EMIT readFileStarted(*mLogMode, LogFile(), 0, 1);
0217         }
0218 
0219         // Add journald entries to the model.
0220         int const entriesInserted = updateModel(entries, readingMode);
0221 
0222         mLogViewModel->endingMultipleInsertions(readingMode, entriesInserted);
0223 
0224         if (FullRead == readingMode) {
0225             Q_EMIT statusBarChanged(i18n("Journald entries loaded successfully."));
0226 
0227             // Stop displaying the loading bar.
0228             Q_EMIT readEnded();
0229         }
0230 
0231         // Inform LogManager that new lines have been added.
0232         Q_EMIT logUpdated(entriesInserted);
0233 
0234         mInsertionLocking.unlock();
0235     }
0236 }
0237 
0238 void JournaldNetworkAnalyzer::sendRequest(RequestType requestType)
0239 {
0240     if (mReply) {
0241         mReply->deleteLater();
0242     }
0243 
0244     mCurrentRequest = requestType;
0245 
0246     QNetworkRequest request;
0247     QString url;
0248 
0249     switch (requestType) {
0250     case RequestType::SyslogIds:
0251         url = mSyslogIdUrl;
0252         break;
0253     case RequestType::Units:
0254         url = mSystemdUnitsUrl;
0255         break;
0256     case RequestType::EntriesFull: {
0257         url = mEntriesUrlFull;
0258         int const entries = KSystemLogConfig::maxLines();
0259         request.setRawHeader("Accept", "application/json");
0260         request.setRawHeader("Range", QStringLiteral("entries=:-%1:%2").arg(entries - 1).arg(entries).toUtf8());
0261     } break;
0262     case RequestType::EntriesUpdate:
0263         url = mEntriesUrlUpdating;
0264         request.setRawHeader("Accept", "application/json");
0265         request.setRawHeader("Range", QStringLiteral("entries=%1").arg(mCursor).toUtf8());
0266     default:
0267         break;
0268     }
0269 
0270     request.setUrl(QUrl(url));
0271     qCDebug(KSYSTEMLOG) << "Journal network analyzer requested" << url;
0272     mReply = mNetworkManager.get(request);
0273     connect(mReply, &QNetworkReply::finished, this, &JournaldNetworkAnalyzer::httpFinished);
0274     connect(mReply, &QNetworkReply::readyRead, this, &JournaldNetworkAnalyzer::httpReadyRead);
0275     connect(mReply, &QNetworkReply::errorOccurred, this, &JournaldNetworkAnalyzer::httpError);
0276 }
0277 
0278 void JournaldNetworkAnalyzer::updateStatus(const QString &status)
0279 {
0280     QString newStatus = mBaseUrl;
0281     if (!mFilterName.isEmpty()) {
0282         newStatus += QLatin1String(" - ") + mFilterName;
0283     }
0284     if (!status.isEmpty()) {
0285         newStatus += QLatin1String(" - ") + status;
0286     }
0287     Q_EMIT statusChanged(newStatus);
0288 }
0289 
0290 #include "moc_journaldNetworkAnalyzer.cpp"