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