1 /*
2     SPDX-FileCopyrightText: 2007 Nicolas Ternisien <nicolas.ternisien@gmail.com>
3     SPDX-FileCopyrightText: 2015 Vyacheslav Matyushin
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "journaldNetworkAnalyzer.h"
9 #include "journaldConfiguration.h"
10 #include "ksystemlogConfig.h"
11 #include "ksystemlog_debug.h"
12 #include "logFile.h"
13 #include "logViewModel.h"
14 
15 #include <QJsonArray>
16 #include <QJsonDocument>
17 #include <QJsonObject>
18 #include <QJsonParseError>
19 #include <QRegularExpression>
20 
21 #include <KLocalizedString>
22 
JournaldNetworkAnalyzer(LogMode * mode,const JournaldAnalyzerOptions & options)23 JournaldNetworkAnalyzer::JournaldNetworkAnalyzer(LogMode *mode, const JournaldAnalyzerOptions &options)
24     : JournaldAnalyzer(mode)
25 {
26     mAddress = options.address;
27 
28     connect(&mNetworkManager, &QNetworkAccessManager::sslErrors, this, &JournaldNetworkAnalyzer::sslErrors);
29 
30     auto *configuration = mode->logModeConfiguration<JournaldConfiguration *>();
31 
32     mBaseUrl = QStringLiteral("%1://%2:%3/").arg(mAddress.https ? QStringLiteral("https") : QStringLiteral("http")).arg(mAddress.address).arg(mAddress.port);
33 
34     mEntriesUrlUpdating = mBaseUrl + QStringLiteral("entries");
35     mEntriesUrlFull = mEntriesUrlUpdating;
36 
37     QString filterPrefix;
38     if (configuration->displayCurrentBootOnly()) {
39         mEntriesUrlUpdating.append(QStringLiteral("?boot&follow"));
40         mEntriesUrlFull.append(QStringLiteral("?boot"));
41         filterPrefix = QStringLiteral("&");
42     } else {
43         mEntriesUrlUpdating.append(QStringLiteral("?follow"));
44         filterPrefix = QStringLiteral("?");
45     }
46 
47     if (!options.filter.isEmpty()) {
48         mEntriesUrlUpdating.append(QStringLiteral("&") + options.filter);
49         mEntriesUrlFull.append(filterPrefix + options.filter);
50     }
51 
52     mSyslogIdUrl = mBaseUrl + QStringLiteral("fields/SYSLOG_IDENTIFIER");
53     mSystemdUnitsUrl = mBaseUrl + QStringLiteral("fields/_SYSTEMD_UNIT");
54 
55     mFilterName = options.filter.section(QChar::fromLatin1('='), 1);
56 
57     mReply = nullptr;
58 }
59 
watchLogFiles(bool enabled)60 void JournaldNetworkAnalyzer::watchLogFiles(bool enabled)
61 {
62     if (enabled) {
63         sendRequest(RequestType::SyslogIds);
64     } else {
65         mCursor.clear();
66         updateStatus(QString());
67         if (mReply) {
68             mReply->abort();
69             mReply->deleteLater();
70             mReply = nullptr;
71         }
72     }
73 }
74 
units() const75 QStringList JournaldNetworkAnalyzer::units() const
76 {
77     return mSystemdUnits;
78 }
79 
syslogIdentifiers() const80 QStringList JournaldNetworkAnalyzer::syslogIdentifiers() const
81 {
82     return mSyslogIdentifiers;
83 }
84 
httpFinished()85 void JournaldNetworkAnalyzer::httpFinished()
86 {
87     QByteArray data = mReply->readAll();
88     if (mCurrentRequest == RequestType::EntriesFull) {
89         if (data.size()) {
90             parseEntries(data, FullRead);
91             updateStatus(i18n("Connected"));
92         }
93         if (!mCursor.isEmpty()) {
94             sendRequest(RequestType::EntriesUpdate);
95         } else {
96             qCWarning(KSYSTEMLOG) << "Network journal analyzer failed to extract cursor string. "
97                                      "Journal updates will be unavailable.";
98         }
99     } else {
100         QString identifiersString = QString::fromUtf8(data);
101         QStringList identifiersList = identifiersString.split(QChar::fromLatin1('\n'), Qt::SkipEmptyParts);
102         switch (mCurrentRequest) {
103         case RequestType::SyslogIds:
104             mSyslogIdentifiers = identifiersList;
105             mSyslogIdentifiers.sort();
106             sendRequest(RequestType::Units);
107             break;
108         case RequestType::Units: {
109             mSystemdUnits = identifiersList;
110             mSystemdUnits.sort();
111             auto *journalLogMode = dynamic_cast<JournaldLogMode *>(mLogMode);
112             JournalFilters filters;
113             filters.syslogIdentifiers = mSyslogIdentifiers;
114             filters.systemdUnits = mSystemdUnits;
115             journalLogMode->updateJournalFilters(mAddress, filters);
116             // Regenerate the "Logs" submenu to include new syslog identifiers and systemd units.
117             Q_EMIT mLogMode->menuChanged();
118             sendRequest(RequestType::EntriesFull);
119             break;
120         }
121         default:
122             break;
123         }
124     }
125 }
126 
httpReadyRead()127 void JournaldNetworkAnalyzer::httpReadyRead()
128 {
129     if (mCurrentRequest == RequestType::EntriesUpdate) {
130         QByteArray data = mReply->readAll();
131         parseEntries(data, UpdatingRead);
132     }
133 }
134 
httpError(QNetworkReply::NetworkError code)135 void JournaldNetworkAnalyzer::httpError(QNetworkReply::NetworkError code)
136 {
137     if (mParsingPaused) {
138         return;
139     }
140 
141     if (code == QNetworkReply::OperationCanceledError) {
142         return;
143     }
144 
145     updateStatus(i18n("Connection error"));
146     qCWarning(KSYSTEMLOG) << "Network journald connection error:" << code;
147 }
148 
sslErrors(QNetworkReply * reply,const QList<QSslError> & errors)149 void JournaldNetworkAnalyzer::sslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
150 {
151     Q_UNUSED(errors)
152     reply->ignoreSslErrors();
153 }
154 
parseEntries(QByteArray & data,Analyzer::ReadingMode readingMode)155 void JournaldNetworkAnalyzer::parseEntries(QByteArray &data, Analyzer::ReadingMode readingMode)
156 {
157     if (mParsingPaused) {
158         qCDebug(KSYSTEMLOG) << "Parsing is paused, discarding journald entries.";
159         return;
160     }
161 
162     QList<QByteArray> items = data.split('{');
163     QList<JournalEntry> entries;
164     for (int i = 0; i < items.size(); i++) {
165         QByteArray &item = items[i];
166         if (item.isEmpty()) {
167             continue;
168         }
169         item.prepend('{');
170         QJsonParseError jsonError{};
171         QJsonDocument doc = QJsonDocument::fromJson(item, &jsonError);
172         if (jsonError.error != 0) {
173             continue;
174         }
175         QJsonObject object = doc.object();
176 
177         if ((readingMode == FullRead) && (i == items.size() - 1)) {
178             mCursor = object[QStringLiteral("__CURSOR")].toString();
179             break;
180         }
181 
182         JournalEntry entry;
183         auto timestampUsec = object[QStringLiteral("__REALTIME_TIMESTAMP")].toVariant().value<quint64>();
184         entry.date.setMSecsSinceEpoch(timestampUsec / 1000);
185         entry.message = object[QStringLiteral("MESSAGE")].toString();
186         if (entry.message.isEmpty()) {
187             // MESSAGE field contains a JSON array of bytes.
188             QByteArray stringBytes;
189             QJsonArray a = object[QStringLiteral("MESSAGE")].toArray();
190             for (int i = 0; i < a.size(); i++) {
191                 stringBytes.append(a[i].toVariant().value<char>());
192             }
193             entry.message = QString::fromUtf8(stringBytes);
194         }
195         entry.message.remove(QRegularExpression(QLatin1String(ConsoleColorEscapeSequence)));
196         entry.priority = object[QStringLiteral("PRIORITY")].toVariant().value<int>();
197         entry.bootID = object[QStringLiteral("_BOOT_ID")].toString();
198         QString unit = object[QStringLiteral("SYSLOG_IDENTIFIER")].toString();
199         if (unit.isEmpty()) {
200             unit = object[QStringLiteral("_SYSTEMD_UNIT")].toString();
201         }
202         entry.unit = unit;
203 
204         entries << entry;
205     }
206 
207     if (entries.empty()) {
208         qCDebug(KSYSTEMLOG) << "Received no entries.";
209     } else {
210         mInsertionLocking.lock();
211         mLogViewModel->startingMultipleInsertions();
212 
213         if (FullRead == readingMode) {
214             Q_EMIT statusBarChanged(i18n("Reading journald entries..."));
215             // Start displaying the loading bar.
216             Q_EMIT readFileStarted(*mLogMode, LogFile(), 0, 1);
217         }
218 
219         // Add journald entries to the model.
220         int entriesInserted = updateModel(entries, readingMode);
221 
222         mLogViewModel->endingMultipleInsertions(readingMode, entriesInserted);
223 
224         if (FullRead == readingMode) {
225             Q_EMIT statusBarChanged(i18n("Journald entries loaded successfully."));
226 
227             // Stop displaying the loading bar.
228             Q_EMIT readEnded();
229         }
230 
231         // Inform LogManager that new lines have been added.
232         Q_EMIT logUpdated(entriesInserted);
233 
234         mInsertionLocking.unlock();
235     }
236 }
237 
sendRequest(RequestType requestType)238 void JournaldNetworkAnalyzer::sendRequest(RequestType requestType)
239 {
240     if (mReply) {
241         mReply->deleteLater();
242     }
243 
244     mCurrentRequest = requestType;
245 
246     QNetworkRequest request;
247     QString url;
248 
249     switch (requestType) {
250     case RequestType::SyslogIds:
251         url = mSyslogIdUrl;
252         break;
253     case RequestType::Units:
254         url = mSystemdUnitsUrl;
255         break;
256     case RequestType::EntriesFull: {
257         url = mEntriesUrlFull;
258         int entries = KSystemLogConfig::maxLines();
259         request.setRawHeader("Accept", "application/json");
260         request.setRawHeader("Range", QStringLiteral("entries=:-%1:%2").arg(entries - 1).arg(entries).toUtf8());
261     } break;
262     case RequestType::EntriesUpdate:
263         url = mEntriesUrlUpdating;
264         request.setRawHeader("Accept", "application/json");
265         request.setRawHeader("Range", QStringLiteral("entries=%1").arg(mCursor).toUtf8());
266     default:
267         break;
268     }
269 
270     request.setUrl(QUrl(url));
271     qCDebug(KSYSTEMLOG) << "Journal network analyzer requested" << url;
272     mReply = mNetworkManager.get(request);
273     connect(mReply, &QNetworkReply::finished, this, &JournaldNetworkAnalyzer::httpFinished);
274     connect(mReply, &QNetworkReply::readyRead, this, &JournaldNetworkAnalyzer::httpReadyRead);
275     connect(mReply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::errorOccurred), this, &JournaldNetworkAnalyzer::httpError);
276 }
277 
updateStatus(const QString & status)278 void JournaldNetworkAnalyzer::updateStatus(const QString &status)
279 {
280     QString newStatus = mBaseUrl;
281     if (!mFilterName.isEmpty()) {
282         newStatus += QLatin1String(" - ") + mFilterName;
283     }
284     if (!status.isEmpty()) {
285         newStatus += QLatin1String(" - ") + status;
286     }
287     Q_EMIT statusChanged(newStatus);
288 }
289