swift
apiserverconnection.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2019 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QJsonArray>
7 #include <QJsonObject>
8 #include <QPointer>
9 #include <QScopedPointer>
10 #include <QUrl>
11 #include <QUrlQuery>
12 
13 #include "qjsonwebtoken/qjsonwebtoken.h"
14 
15 #include "config/buildconfig.h"
16 #include "misc/logmessage.h"
18 #include "misc/stringutils.h"
19 
20 using namespace swift::misc;
21 using namespace swift::misc::network;
22 using namespace swift::config;
23 
24 namespace swift::core::afv::connection
25 {
26  const QStringList &CApiServerConnection::getLogCategories()
27  {
28  static const QStringList cats { CLogCategories::audio(), CLogCategories::vatsimSpecific() };
29  return cats;
30  }
31 
32  CApiServerConnection::CApiServerConnection(const QString &address, QObject *parent)
33  : QObject(parent), m_addressUrl(address)
34  {
35  CLogMessage(this).debug(u"ApiServerConnection instantiated");
36  }
37 
38  void CApiServerConnection::connectTo(const QString &username, const QString &password, const QString &client,
39  const QUuid &networkVersion, ConnectionCallback callback)
40  {
41  if (isShuttingDown()) { return; }
42 
43  m_username = username;
44  m_password = password;
45  m_client = client;
46  m_networkVersion = networkVersion;
47  m_isAuthenticated = false;
48 
49  QUrl url(m_addressUrl);
50  url.setPath("/api/v1/auth");
51 
52  QJsonObject obj { { "username", username },
53  { "password", password },
54  { "networkversion", networkVersion.toString() },
55  { "client", client } };
56 
57  QNetworkRequest request(url);
58  QPointer<CApiServerConnection> myself(this);
59  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
60 
61  // posted in QAM thread, reply is nullptr if called from another thread
62  sApp->postToNetwork(request, CApplication::NoLogRequestId, QJsonDocument(obj).toJson(),
63  { this, [=](QNetworkReply *nwReply) {
64  // called in "this" thread
65  const QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
66  if (!myself || isShuttingDown()) // cppcheck-suppress knownConditionTrueFalse
67  {
68  return;
69  }
70 
71  this->logRequestDuration(reply.data(), "authentication");
72  if (reply->error() != QNetworkReply::NoError)
73  {
74  this->logReplyErrorMessage(reply.data(), "authentication error");
75  callback(false);
76  return;
77  }
78 
79  // JWT authentication token
80  m_serverToUserOffsetMs = 0;
81  m_expiryLocalUtc = QDateTime(); // clean up
82 
83  m_jwt = reply->readAll().trimmed();
84  qint64 lifeTimeSecs = -1;
85  qint64 serverToUserOffsetSecs = -1;
86  do {
87  const QString jwtToken(m_jwt);
88  const QJsonWebToken token = QJsonWebToken::fromTokenAndSecret(jwtToken, "");
89 
90  // get decoded header and payload
91  // QString strHeader = token.getHeaderQStr();
92  // QString strPayload = token.getPayloadQStr();
93  const QJsonDocument doc = token.getPayloadJDoc();
94  if (doc.isEmpty() || !doc.isObject()) { break; }
95  const qint64 validFromSecs = doc.object().value("nbf").toInt(-1);
96  if (validFromSecs < 0) { break; }
97  const qint64 localSecsSinceEpoch = QDateTime::currentSecsSinceEpoch();
98  serverToUserOffsetSecs = validFromSecs - localSecsSinceEpoch;
99  const qint64 serverExpirySecs = doc.object().value("exp").toInt();
100  const qint64 expiryLocalUtc = serverExpirySecs - serverToUserOffsetSecs;
101  lifeTimeSecs = expiryLocalUtc - localSecsSinceEpoch;
102  }
103  while (false);
104 
105  if (lifeTimeSecs > 0)
106  {
107  m_serverToUserOffsetMs = serverToUserOffsetSecs * 1000;
108  m_expiryLocalUtc = QDateTime::currentDateTimeUtc().addSecs(lifeTimeSecs);
109  m_isAuthenticated = true;
110  }
111 
112  // connected, callback
113  callback(m_isAuthenticated);
114  } });
115  }
116 
118  {
119  return this->postNoRequest<PostCallsignResponseDto>("/api/v1/users/" + m_username + "/callsigns/" + callsign);
120  }
121 
122  void CApiServerConnection::removeCallsign(const QString &callsign)
123  {
124  this->deleteResource("/api/v1/users/" + m_username + "/callsigns/" + callsign);
125  }
126 
127  void CApiServerConnection::updateTransceivers(const QString &callsign, const QVector<TransceiverDto> &transceivers)
128  {
129  if (!this->sendToNetworkIfAuthenticated()) { return; }
130  QJsonArray array;
131  for (const TransceiverDto &tx : transceivers) { array.append(tx.toJson()); }
132  this->postNoResponse("/api/v1/users/" + m_username + "/callsigns/" + callsign + "/transceivers",
133  QJsonDocument(array));
134  }
135 
137  {
138  m_isAuthenticated = false;
139  m_jwt.clear();
140  }
141 
143  {
144  const QVector<StationDto> stations = this->getAsVector<StationDto>("/api/v1/stations/aliased");
145  return stations;
146  }
147 
148  bool CApiServerConnection::setUrl(const QString &url)
149  {
150  if (stringCompare(m_addressUrl, url, Qt::CaseInsensitive)) { return false; }
151  m_addressUrl = url;
152  return true;
153  }
154 
155  QByteArray CApiServerConnection::getWithResponse(const QNetworkRequest &request)
156  {
157  if (isShuttingDown()) { return {}; }
158 
159  QPointer<QEventLoop> loop(this->newEventLoop());
160  QByteArray receivedData;
161 
162  // posted in QAM thread, reply is nullptr if called from another thread
163  sApp->getFromNetwork(request,
164  { this, [=, &receivedData](QNetworkReply *nwReply) {
165  const QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
166 
167  // called in "this" thread
168  if (loop && !isShuttingDown())
169  {
170  this->logRequestDuration(reply.data());
171  if (reply->error() == QNetworkReply::NoError) { receivedData = reply->readAll(); }
172  else { this->logReplyErrorMessage(reply.data()); }
173  }
174  if (loop) { loop->exit(); }
175  } });
176 
177  if (loop) { loop->exec(); }
178  return receivedData;
179  }
180 
181  QByteArray CApiServerConnection::postWithResponse(const QNetworkRequest &request, const QByteArray &data)
182  {
183  if (isShuttingDown()) { return {}; }
184 
185  QPointer<QEventLoop> loop(this->newEventLoop());
186  QByteArray receivedData;
187 
188  // posted in QAM thread, reply is nullptr if called from another thread
190  { this, [=, &receivedData](QNetworkReply *nwReply) {
191  const QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
192 
193  // called in "this" thread
194  if (loop && !isShuttingDown())
195  {
196  this->logRequestDuration(reply.data());
197  if (reply->error() == QNetworkReply::NoError) { receivedData = reply->readAll(); }
198  else { this->logReplyErrorMessage(reply.data()); }
199  }
200  if (loop) { loop->exit(); }
201  } });
202 
203  if (loop) { loop->exec(); }
204  return receivedData;
205  }
206 
207  void CApiServerConnection::postNoResponse(const QString &resource, const QJsonDocument &json)
208  {
209  if (isShuttingDown()) { return; }
210  this->checkExpiry();
211 
212  QUrl url(m_addressUrl);
213  url.setPath(resource);
214  QNetworkRequest request(url);
215  request.setRawHeader("Authorization", "Bearer " + m_jwt);
216  request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
217 
218  // posted in QAM thread, reply is nullptr if called from another thread
219  sApp->postToNetwork(request, CApplication::NoLogRequestId, json.toJson(),
220  { this, [=](QNetworkReply *nwReply) {
221  // called in "this" thread
222  const QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
223  if (isShuttingDown()) { return; }
224  this->logRequestDuration(reply.data());
225  if (reply->error() != QNetworkReply::NoError)
226  {
227  this->logReplyErrorMessage(reply.data());
228  }
229  } });
230  }
231 
232  void CApiServerConnection::deleteResource(const QString &resource)
233  {
234  if (isShuttingDown()) { return; }
235 
236  QUrl url(m_addressUrl);
237  url.setPath(resource);
238 
239  QNetworkRequest request(url);
240  request.setRawHeader("Authorization", "Bearer " + m_jwt);
241 
242  // posted in QAM thread
244  { this, [=](QNetworkReply *nwReply) {
245  // called in "this" thread
246  const QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(
247  nwReply);
248  if (isShuttingDown()) { return; }
249  this->logRequestDuration(reply.data());
250  if (reply->error() != QNetworkReply::NoError)
251  {
252  this->logReplyErrorMessage(reply.data());
253  }
254  } });
255  }
256 
257  void CApiServerConnection::checkExpiry()
258  {
259  if (!m_expiryLocalUtc.isValid() || QDateTime::currentDateTimeUtc() > m_expiryLocalUtc.addSecs(-5 * 60))
260  {
261  QPointer<CApiServerConnection> myself(this);
262  this->connectTo(m_username, m_password, m_client, m_networkVersion,
263  { this, [=](bool authenticated) {
264  if (!myself) { return; } // cppcheck-suppress knownConditionTrueFalse
265  CLogMessage(this).info(u"API server authenticated '%1': %2")
266  << m_username << boolToYesNo(authenticated);
267  } });
268  }
269  }
270 
271  void CApiServerConnection::logReplyErrorMessage(const QNetworkReply *reply, const QString &addMsg)
272  {
273  if (!reply) { return; }
274  if (addMsg.isEmpty())
275  {
276  CLogMessage(this).warning(u"AFV network error for '%1' '%2': '%3'")
277  << reply->url().toString() << CNetworkUtils::networkOperationToString(reply->operation())
278  << reply->errorString();
279  }
280  else
281  {
282  CLogMessage(this).warning(u"AFV network error (%1) for '%2' '%3': '%4'")
283  << addMsg << reply->url().toString() << CNetworkUtils::networkOperationToString(reply->operation())
284  << reply->errorString();
285  }
286  }
287 
288  void CApiServerConnection::logRequestDuration(const QNetworkReply *reply, const QString &addMsg)
289  {
290  if (!CBuildConfig::isLocalDeveloperDebugBuild()) { return; }
291  if (!reply) { return; }
292 
293  const qint64 d = CNetworkUtils::requestDuration(reply);
294  if (d < 0) { return; }
295  if (addMsg.isEmpty())
296  {
297  CLogMessage(this).info(u"AFV network request for '%1': %2ms") << reply->url().toString() << d;
298  }
299  else
300  {
301  CLogMessage(this).info(u"AFV network request (%1) for '%2': '%3'")
302  << addMsg << reply->url().toString() << d;
303  }
304  }
305 
306  QEventLoop *CApiServerConnection::newEventLoop()
307  {
308  QEventLoop *loop = new QEventLoop(this);
309  if (sApp)
310  {
311  QObject::connect(sApp, &CApplication::aboutToShutdown, loop, &QEventLoop::quit, Qt::QueuedConnection);
312  }
313  return loop;
314  }
315 
316  bool CApiServerConnection::sendToNetworkIfAuthenticated() const { return m_isAuthenticated && !isShuttingDown(); }
317 
318  bool CApiServerConnection::isShuttingDown() { return !sApp || sApp->isShuttingDown(); }
319 
320 } // namespace swift::core::afv::connection
SWIFT_CORE_EXPORT swift::core::CApplication * sApp
Single instance of application object.
Definition: application.cpp:71
void aboutToShutdown()
About to shutdown.
QNetworkReply * getFromNetwork(const swift::misc::network::CUrl &url, const CallbackSlot &callback, int maxRedirects=DefaultMaxRedirects)
Request to get network reply.
static constexpr int NoLogRequestId
network request without logging
Definition: application.h:413
QNetworkReply * postToNetwork(const QNetworkRequest &request, int logId, const QByteArray &data, const CallbackSlot &callback)
Post to network.
bool isShuttingDown() const
Is application shutting down?
QNetworkReply * deleteResourceFromNetwork(const QNetworkRequest &request, int logId, const CallbackSlot &callback, int maxRedirects=DefaultMaxRedirects)
Request to delete a network resource from network, supporting swift::misc::network::CUrlLog.
void removeCallsign(const QString &callsign)
Remove callsign from network.
PostCallsignResponseDto addCallsign(const QString &callsign)
Add callsign to network.
void forceDisconnect()
Force disconnect from network.
QVector< StationDto > getAllAliasedStations()
All aliased stations.
void connectTo(const QString &username, const QString &password, const QString &client, const QUuid &networkVersion, ConnectionCallback callback)
Connect to network.
void updateTransceivers(const QString &callsign, const QVector< TransceiverDto > &transceivers)
Update transceivers.
static const QString & vatsimSpecific()
VATSIM specific.
static const QString & audio()
Audio related.
Definition: logcategories.h:52
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & debug()
Set the severity to debug.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
Callable wrapper for a member function with function signature F.
Definition: slot.h:62
Free functions in swift::misc.
SWIFT_MISC_EXPORT bool stringCompare(const QString &c1, const QString &c2, Qt::CaseSensitivity cs)
String compare.
const std::string & boolToYesNo(bool t)
Yes/no from bool.
Definition: qtfreeutils.h:129
Callsign DTO.
Definition: dto.h:87
Transceiver DTO.
Definition: dto.h:117