Messagelib

searchfullhashjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2016-2023 Laurent Montel <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "searchfullhashjob.h"
8 
9 #include <PimCommon/NetworkManager>
10 #include <QJsonDocument>
11 #include <QNetworkAccessManager>
12 #include <QUrlQuery>
13 #include <webengineviewer_debug.h>
14 
15 using namespace WebEngineViewer;
16 
17 WEBENGINEVIEWER_EXPORT bool webengineview_useCompactJson_SearchFullHashJob = true;
18 
19 class WebEngineViewer::SearchFullHashJobPrivate
20 {
21 public:
22  SearchFullHashJobPrivate() = default;
23 
24  Q_REQUIRED_RESULT bool foundExactHash(const QList<QByteArray> &listLongHash);
26  QUrl mUrl;
27  QStringList mDatabaseHashes;
28  QNetworkAccessManager *mNetworkAccessManager = nullptr;
29 };
30 
31 SearchFullHashJob::SearchFullHashJob(QObject *parent)
32  : QObject(parent)
33  , d(new SearchFullHashJobPrivate)
34 {
35  d->mNetworkAccessManager = new QNetworkAccessManager(this);
36  d->mNetworkAccessManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
37  d->mNetworkAccessManager->setStrictTransportSecurityEnabled(true);
38  d->mNetworkAccessManager->enableStrictTransportSecurityStore(true);
39 
40  connect(d->mNetworkAccessManager, &QNetworkAccessManager::finished, this, &SearchFullHashJob::slotCheckUrlFinished);
41  connect(d->mNetworkAccessManager, &QNetworkAccessManager::sslErrors, this, &SearchFullHashJob::slotSslErrors);
42 }
43 
44 SearchFullHashJob::~SearchFullHashJob() = default;
45 
46 void SearchFullHashJob::slotSslErrors(QNetworkReply *reply, const QList<QSslError> &error)
47 {
48  qCDebug(WEBENGINEVIEWER_LOG) << " void SearchFullHashJob::slotSslErrors(QNetworkReply *reply, const QList<QSslError> &error)" << error.count();
49  reply->ignoreSslErrors(error);
50 }
51 
52 void SearchFullHashJob::parse(const QByteArray &replyStr)
53 {
54  /*
55 
56  {
57  "matches": [{
58  "threatType": "MALWARE",
59  "platformType": "WINDOWS",
60  "threatEntryType": "URL",
61  "threat": {
62  "hash": "WwuJdQx48jP-4lxr4y2Sj82AWoxUVcIRDSk1PC9Rf-4="
63  },
64  "threatEntryMetadata": {
65  "entries": [{
66  "key": "bWFsd2FyZV90aHJlYXRfdHlwZQ==", // base64-encoded "malware_threat_type"
67  "value": "TEFORElORw==" // base64-encoded "LANDING"
68  }]
69  },
70  "cacheDuration": "300.000s"
71  }, {
72  "threatType": "SOCIAL_ENGINEERING",
73  "platformType": "WINDOWS",
74  "threatEntryType": "URL",
75  "threat": {
76  "hash": "771MOrRPMn6xPKlCrXx_CrR-wmCk0LgFFoSgGy7zUiA="
77  },
78  "threatEntryMetadata": {
79  "entries": []
80  },
81  "cacheDuration": "300.000s"
82  }],
83  "minimumWaitDuration": "300.000s",
84  "negativeCacheDuration": "300.000s"
85  }
86  */
87  QJsonDocument document = QJsonDocument::fromJson(replyStr);
88  if (document.isNull()) {
89  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::Unknown, d->mUrl);
90  } else {
91  const QVariantMap answer = document.toVariant().toMap();
92  if (answer.isEmpty()) {
93  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::Ok, d->mUrl);
94  return;
95  } else {
96  const QVariantList info = answer.value(QStringLiteral("matches")).toList();
97  // TODO
98  // const QString minimumWaitDuration = answer.value(QStringLiteral("minimumWaitDuration")).toString();
99  // const QString negativeCacheDuration = answer.value(QStringLiteral("negativeCacheDuration")).toString();
100  // Implement multi match ?
101  const auto numberOfInfo{info.count()};
102  if (numberOfInfo == 1) {
103  const QVariantMap map = info.at(0).toMap();
104  const QString threatTypeStr = map[QStringLiteral("threatType")].toString();
105 
106  // const QString cacheDuration = map[QStringLiteral("cacheDuration")].toString();
107 
108  if (threatTypeStr == QLatin1String("MALWARE")) {
109  const QVariantMap urlMap = map[QStringLiteral("threat")].toMap();
110  QList<QByteArray> hashList;
111  QMap<QString, QVariant>::const_iterator urlMapIt = urlMap.cbegin();
112  const QMap<QString, QVariant>::const_iterator urlMapItEnd = urlMap.cend();
113  hashList.reserve(urlMap.count());
114  for (; urlMapIt != urlMapItEnd; ++urlMapIt) {
115  const QByteArray hashStr = urlMapIt.value().toByteArray();
116  hashList << hashStr;
117  }
118 
119  if (d->foundExactHash(hashList)) {
120  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::MalWare, d->mUrl);
121  } else {
122  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::Unknown, d->mUrl);
123  }
124  const QVariantMap threatEntryMetadataMap = map[QStringLiteral("threatEntryMetadata")].toMap();
125  if (!threatEntryMetadataMap.isEmpty()) {
126  // TODO
127  }
128  } else {
129  qCWarning(WEBENGINEVIEWER_LOG) << " SearchFullHashJob::parse threatTypeStr : " << threatTypeStr;
130  }
131  } else if (numberOfInfo == 0) {
132  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::Ok, d->mUrl);
133  } else {
134  qCWarning(WEBENGINEVIEWER_LOG) << " SearchFullHashJob::parse matches multi element : " << info.count();
135  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::Unknown, d->mUrl);
136  }
137  }
138  }
139  deleteLater();
140 }
141 
142 bool SearchFullHashJobPrivate::foundExactHash(const QList<QByteArray> &listLongHash)
143 {
144  const QList<QByteArray> lstLongHash = mHashs.keys();
145  for (const QByteArray &ba : lstLongHash) {
146  if (listLongHash.contains(ba)) {
147  return true;
148  }
149  }
150  return false;
151 }
152 
153 void SearchFullHashJob::slotCheckUrlFinished(QNetworkReply *reply)
154 {
155  parse(reply->readAll());
156  reply->deleteLater();
157 }
158 
159 void SearchFullHashJob::setSearchHashs(const QHash<QByteArray, QByteArray> &hash)
160 {
161  d->mHashs = hash;
162 }
163 
164 QByteArray SearchFullHashJob::jsonRequest() const
165 {
166  /*
167  {
168  "client": {
169  "clientId": "yourcompanyname",
170  "clientVersion": "1.5.2"
171  },
172  "clientStates": [
173  "ChAIARABGAEiAzAwMSiAEDABEAE=",
174  "ChAIAhABGAEiAzAwMSiAEDABEOgH"
175  ],
176  "threatInfo": {
177  "threatTypes": ["MALWARE", "SOCIAL_ENGINEERING"],
178  "platformTypes": ["WINDOWS"],
179  "threatEntryTypes": ["URL"],
180  "threatEntries": [
181  {"hash": "WwuJdQ=="},
182  {"hash": "771MOg=="},
183  {"hash": "5eOrwQ=="}
184  ]
185  }
186  }
187  */
188  QVariantMap clientMap;
189  QVariantMap map;
190 
191  clientMap.insert(QStringLiteral("clientId"), QStringLiteral("KDE"));
192  clientMap.insert(QStringLiteral("clientVersion"), CheckPhishingUrlUtil::versionApps());
193  map.insert(QStringLiteral("client"), clientMap);
194 
195  // clientStates We can support multi database.
196  QVariantList clientStatesList;
197  for (const QString &str : std::as_const(d->mDatabaseHashes)) {
198  if (!str.isEmpty()) {
199  clientStatesList.append(str);
200  }
201  }
202  map.insert(QStringLiteral("clientStates"), clientStatesList);
203 
204  QVariantMap threatMap;
205  QVariantList platformList;
206  platformList.append(QLatin1String("WINDOWS"));
207  threatMap.insert(QStringLiteral("platformTypes"), platformList);
208 
209  const QVariantList threatTypesList = {QStringLiteral("MALWARE")};
210  threatMap.insert(QStringLiteral("threatTypes"), threatTypesList);
211  const QVariantList threatEntryTypesList = {QStringLiteral("URL")};
212  threatMap.insert(QStringLiteral("threatEntryTypes"), threatEntryTypesList);
213 
214  QVariantList threatEntriesList;
215 
216  QVariantMap hashUrlMap;
218  while (i.hasNext()) {
219  i.next();
220  hashUrlMap.insert(QStringLiteral("hash"), i.value());
221  }
222  threatEntriesList.append(hashUrlMap);
223 
224  threatMap.insert(QStringLiteral("threatEntries"), threatEntriesList);
225 
226  map.insert(QStringLiteral("threatInfo"), threatMap);
227 
228  const QJsonDocument postData = QJsonDocument::fromVariant(map);
229  const QByteArray baPostData = postData.toJson(webengineview_useCompactJson_SearchFullHashJob ? QJsonDocument::Compact : QJsonDocument::Indented);
230  return baPostData;
231 }
232 
234 {
235  if (!PimCommon::NetworkManager::self()->isOnline()) {
236  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::BrokenNetwork, d->mUrl);
237  deleteLater();
238  } else if (canStart()) {
240  query.addQueryItem(QStringLiteral("key"), WebEngineViewer::CheckPhishingUrlUtil::apiKey());
241  QUrl safeUrl = QUrl(QStringLiteral("https://safebrowsing.googleapis.com/v4/fullHashes:find"));
242  safeUrl.setQuery(query);
243  QNetworkRequest request(safeUrl);
244  request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
245 
246  const QByteArray baPostData = jsonRequest();
247  // qCDebug(WEBENGINEVIEWER_LOG) << " postData.toJson()" << baPostData;
248  Q_EMIT debugJson(baPostData);
249  QNetworkReply *reply = d->mNetworkAccessManager->post(request, baPostData);
250  connect(reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::errorOccurred), this, &SearchFullHashJob::slotError);
251  } else {
252  Q_EMIT result(WebEngineViewer::CheckPhishingUrlUtil::InvalidUrl, d->mUrl);
253  deleteLater();
254  }
255 }
256 
257 void SearchFullHashJob::slotError(QNetworkReply::NetworkError error)
258 {
259  auto reply = qobject_cast<QNetworkReply *>(sender());
260  qCWarning(WEBENGINEVIEWER_LOG) << " error " << error << " error string : " << reply->errorString();
261  reply->deleteLater();
262  deleteLater();
263 }
264 
265 bool SearchFullHashJob::canStart() const
266 {
267  return !d->mHashs.isEmpty() && !d->mDatabaseHashes.isEmpty() && !d->mUrl.isEmpty();
268 }
269 
270 void SearchFullHashJob::setDatabaseState(const QStringList &hash)
271 {
272  d->mDatabaseHashes = hash;
273 }
274 
275 void SearchFullHashJob::setSearchFullHashForUrl(const QUrl &url)
276 {
277  d->mUrl = url;
278 }
QString errorString() const const
void errorOccurred(QNetworkReply::NetworkError code)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isNull() const const
QVariant toVariant() const const
Q_SCRIPTABLE Q_NOREPLY void start()
QVector< QVariant > parse(const QString &message, const QDateTime &externalIssueDateTime=QDateTime())
bool contains(const T &value) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void reserve(int alloc)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
void finished(QNetworkReply *reply)
void deleteLater()
void sslErrors(QNetworkReply *reply, const QList< QSslError > &errors)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QMap< QString, QVariant > toMap() const const
void setQuery(const QString &query, QUrl::ParsingMode mode)
void ignoreSslErrors(const QList< QSslError > &errors)
QByteArray toJson() const const
QByteArray readAll()
QJsonDocument fromVariant(const QVariant &variant)
QFuture< void > map(Sequence &sequence, MapFunctor function)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Mar 26 2023 04:08:12 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.