Ref T570, prepared ApiServerConnection for "threaded AFV client"

* use get/post/deleteFromNetwork function in sApp (calling QAM in coorect thread)
* removed timer, use QDateTime::currentMSecsSinceEpoch - one member less causing issues in threaded environment
* using CLogMessage instead of qDebug/qWarning
* unified log messages
This commit is contained in:
Klaus Basan
2019-10-03 19:45:20 +02:00
committed by Mat Sutcliffe
parent ea8e27cc9b
commit 61d82ab780
2 changed files with 231 additions and 117 deletions

View File

@@ -7,13 +7,20 @@
*/
#include "apiserverconnection.h"
#include "blackmisc/network/networkutils.h"
#include "blackmisc/network/external/qjsonwebtoken.h"
#include "blackmisc/logmessage.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QUrl>
#include <QUrlQuery>
#include <QScopedPointer>
#include <QMetaEnum>
using namespace BlackMisc;
using namespace BlackMisc::Network;
namespace BlackCore
{
@@ -23,19 +30,19 @@ namespace BlackCore
{
ApiServerConnection::ApiServerConnection(const QString &address, QObject *parent) :
QObject(parent),
m_address(address),
m_watch(new QElapsedTimer)
m_address(address)
{
qDebug() << "ApiServerConnection instantiated";
CLogMessage(this).debug(u"ApiServerConnection instantiated");
}
bool ApiServerConnection::connectTo(const QString &username, const QString &password, const QUuid &networkVersion)
{
if (isShuttingDown()) { return false; }
m_username = username;
m_password = password;
m_networkVersion = networkVersion;
m_networkVersion = networkVersion;
m_isAuthenticated = false;
m_watch->start();
QUrl url(m_address);
url.setPath("/api/v1/auth");
@@ -47,66 +54,76 @@ namespace BlackCore
{"networkversion", networkVersion.toString()},
};
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
QEventLoop loop;
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nam->post(request, QJsonDocument(obj).toJson()));
while (! reply->isFinished()) { loop.exec(); }
QEventLoop loop(sApp);
qDebug() << "POST api/v1/auth (" << m_watch->elapsed() << "ms)";
if (reply->error() != QNetworkReply::NoError)
// posted in QAM thread
QNetworkReply *reply = sApp->postToNetwork(request, CApplication::NoLogRequestId, QJsonDocument(obj).toJson(),
{
qWarning() << reply->errorString();
return false;
}
this, [ & ](QNetworkReply * nwReply)
{
// called in "this" thread
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
if (isShuttingDown()) { return; }
this->logRequestDuration(reply.data(), "authentication");
if (reply->error() != QNetworkReply::NoError)
{
this->logReplyErrorMessage(reply.data(), "authentication error");
loop.exit();
return;
}
// JWT authentication token
m_serverToUserOffsetMs = 0;
m_expiryLocalUtc = QDateTime(); // clean up
// JWT authentication token
m_serverToUserOffsetMs = 0;
m_expiryLocalUtc = QDateTime(); // clean up
m_jwt = reply->readAll().trimmed();
qint64 lifeTimeSecs = -1;
qint64 serverToUserOffsetSecs = -1;
do
{
const QString jwtToken(m_jwt);
QJsonWebToken token = QJsonWebToken::fromTokenAndSecret(jwtToken, "");
m_jwt = reply->readAll().trimmed();
qint64 lifeTimeSecs = -1;
qint64 serverToUserOffsetSecs = -1;
do
{
const QString jwtToken(m_jwt);
QJsonWebToken token = QJsonWebToken::fromTokenAndSecret(jwtToken, "");
// get decoded header and payload
// QString strHeader = token.getHeaderQStr();
// QString strPayload = token.getPayloadQStr();
const QJsonDocument doc = token.getPayloadJDoc();
if (doc.isEmpty() || !doc.isObject()) { break; }
const qint64 validFromSecs = doc.object().value("nbf").toInt(-1);
if (validFromSecs < 0) { break; }
const qint64 localSecsSinceEpoch = QDateTime::currentSecsSinceEpoch();
serverToUserOffsetSecs = validFromSecs - localSecsSinceEpoch;
const qint64 serverExpirySecs = doc.object().value("exp").toInt();
const qint64 expiryLocalUtc = serverExpirySecs - serverToUserOffsetSecs;
lifeTimeSecs = expiryLocalUtc - localSecsSinceEpoch;
}
while (false);
// get decoded header and payload
// QString strHeader = token.getHeaderQStr();
// QString strPayload = token.getPayloadQStr();
const QJsonDocument doc = token.getPayloadJDoc();
if (doc.isEmpty() || !doc.isObject()) { break; }
const qint64 validFromSecs = doc.object().value("nbf").toInt(-1);
if (validFromSecs < 0) { break; }
const qint64 localSecsSinceEpoch = QDateTime::currentSecsSinceEpoch();
serverToUserOffsetSecs = validFromSecs - localSecsSinceEpoch;
const qint64 serverExpirySecs = doc.object().value("exp").toInt();
const qint64 expiryLocalUtc = serverExpirySecs - serverToUserOffsetSecs;
lifeTimeSecs = expiryLocalUtc - localSecsSinceEpoch;
}
while (false);
if (lifeTimeSecs > 0)
{
m_serverToUserOffsetMs = serverToUserOffsetSecs * 1000;
m_expiryLocalUtc = QDateTime::currentDateTimeUtc().addSecs(lifeTimeSecs);
m_isAuthenticated = true;
}
if (lifeTimeSecs > 0)
{
m_serverToUserOffsetMs = serverToUserOffsetSecs * 1000;
m_expiryLocalUtc = QDateTime::currentDateTimeUtc().addSecs(lifeTimeSecs);
m_isAuthenticated = true;
}
loop.exit();
}
});
if (reply) { loop.exec(); }
return m_isAuthenticated;
}
PostCallsignResponseDto ApiServerConnection::addCallsign(const QString &callsign)
{
return postNoRequest<PostCallsignResponseDto>("/api/v1/users/" + m_username + "/callsigns/" + callsign);
return this->postNoRequest<PostCallsignResponseDto>("/api/v1/users/" + m_username + "/callsigns/" + callsign);
}
void ApiServerConnection::removeCallsign(const QString &callsign)
{
deleteResource("/api/v1/users/" + m_username + "/callsigns/" + callsign);
this->deleteResource("/api/v1/users/" + m_username + "/callsigns/" + callsign);
}
void ApiServerConnection::updateTransceivers(const QString &callsign, const QVector<TransceiverDto> &transceivers)
@@ -116,8 +133,7 @@ namespace BlackCore
{
array.append(tx.toJson());
}
postNoResponse("/api/v1/users/" + m_username + "/callsigns/" + callsign + "/transceivers", QJsonDocument(array));
this->postNoResponse("/api/v1/users/" + m_username + "/callsigns/" + callsign + "/transceivers", QJsonDocument(array));
}
void ApiServerConnection::forceDisconnect()
@@ -128,64 +144,142 @@ namespace BlackCore
QVector<StationDto> ApiServerConnection::getAllAliasedStations()
{
getAsVector<StationDto>("/api/v1/stations/aliased");
this->getAsVector<StationDto>("/api/v1/stations/aliased");
return {};
}
QByteArray ApiServerConnection::getWithResponse(const QNetworkRequest &request)
{
if (isShuttingDown()) { return {}; }
QEventLoop loop(sApp);
QByteArray receivedData;
// posted in QAM thread
QNetworkReply *reply = sApp->getFromNetwork(request,
{
this, [ & ](QNetworkReply * nwReply)
{
// called in "this" thread
if (!isShuttingDown())
{
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
if (isShuttingDown()) { return; }
this->logRequestDuration(reply.data());
if (reply->error() == QNetworkReply::NoError)
{
receivedData = reply->readAll();
}
else
{
this->logReplyErrorMessage(reply.data());
}
}
loop.exit();
}
});
if (!reply) { return {}; }
loop.exec();
return receivedData;
}
QByteArray ApiServerConnection::postWithResponse(const QNetworkRequest &request, const QByteArray &data)
{
if (isShuttingDown()) { return {}; }
QEventLoop loop(sApp);
QByteArray receivedData;
// posted in QAM thread
QNetworkReply *reply = sApp->postToNetwork(request, CApplication::NoLogRequestId, data,
{
this, [ & ](QNetworkReply * nwReply)
{
// called in "this" thread
if (!isShuttingDown())
{
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
if (isShuttingDown()) { return; }
this->logRequestDuration(reply.data());
if (reply->error() == QNetworkReply::NoError)
{
receivedData = reply->readAll();
}
else
{
this->logReplyErrorMessage(reply.data());
}
}
loop.exit();
}
});
if (!reply) { return {}; }
loop.exec();
return receivedData;
}
void ApiServerConnection::postNoResponse(const QString &resource, const QJsonDocument &json)
{
if (isShuttingDown()) { return; } // avoid crash
if (! m_isAuthenticated)
if (!m_isAuthenticated)
{
qDebug() << "Not authenticated";
CLogMessage(this).debug(u"AFV not authenticated");
return;
}
checkExpiry();
this->checkExpiry();
m_watch->start();
QUrl url(m_address);
url.setPath(resource);
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
QEventLoop loop;
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_jwt);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nam->post(request, json.toJson()));
while (! reply->isFinished()) { loop.exec(); }
qDebug() << "POST" << resource << "(" << m_watch->elapsed() << "ms)";
if (reply->error() != QNetworkReply::NoError)
// posted in QAM thread
sApp->postToNetwork(request, CApplication::NoLogRequestId, json.toJson(),
{
qWarning() << "POST" << resource << "failed:" << reply->errorString();
return;
}
this, [ & ](QNetworkReply * nwReply)
{
// called in "this" thread
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
if (isShuttingDown()) { return; }
this->logRequestDuration(reply.data());
if (reply->error() != QNetworkReply::NoError)
{
this->logReplyErrorMessage(reply.data());
}
}
});
}
void ApiServerConnection::deleteResource(const QString &resource)
{
if (! m_isAuthenticated) { return; }
if (isShuttingDown()) { return; }
if (!m_isAuthenticated) { return; }
m_watch->start();
QUrl url(m_address);
url.setPath(resource);
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
QEventLoop loop;
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_jwt);
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nam->deleteResource(request));
while (! reply->isFinished()) { loop.exec(); }
qDebug() << "DELETE" << resource << "(" << m_watch->elapsed() << "ms)";
if (reply->error() != QNetworkReply::NoError)
// posted in QAM thread
sApp->deleteResourceFromNetwork(request, CApplication::NoLogRequestId,
{
qWarning() << "DELETE" << resource << "failed:" << reply->errorString();
}
this, [ & ](QNetworkReply * nwReply)
{
// called in "this" thread
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nwReply);
if (isShuttingDown()) { return; }
this->logRequestDuration(reply.data());
if (reply->error() != QNetworkReply::NoError)
{
this->logReplyErrorMessage(reply.data());
}
}
});
}
void ApiServerConnection::checkExpiry()
@@ -196,6 +290,34 @@ namespace BlackCore
}
}
void ApiServerConnection::logReplyErrorMessage(const QNetworkReply *reply, const QString &addMsg)
{
if (!reply) { return; }
if (addMsg.isEmpty())
{
CLogMessage(this).warning(u"AFV network error for '%1' '%2': '%3'") << reply->url().toString() << CNetworkUtils::networkOperationToString(reply->operation()) << reply->errorString();
}
else
{
CLogMessage(this).warning(u"AFV network error (%1) for '%2' '%3': '%4'") << addMsg << reply->url().toString() << CNetworkUtils::networkOperationToString(reply->operation()) << reply->errorString();
}
}
void ApiServerConnection::logRequestDuration(const QNetworkReply *reply, const QString &addMsg)
{
if (!reply) { return; }
const qint64 d = CNetworkUtils::requestDuration(reply);
if (d < 0) { return; }
if (addMsg.isEmpty())
{
CLogMessage(this).info(u"AFV network request for '%1': %2ms") << reply->url().toString() << d;
}
else
{
CLogMessage(this).info(u"AFV network request (%1) for '%2': '%3'") << addMsg << reply->url().toString() << d;
}
}
bool ApiServerConnection::isShuttingDown()
{
return !sApp || sApp->isShuttingDown();

View File

@@ -68,91 +68,84 @@ namespace BlackCore
QVector<StationDto> getAllAliasedStations();
private:
//! Post to resource
template<typename TResponse>
TResponse postNoRequest(const QString &resource)
{
if (!m_isAuthenticated)
{
qDebug() << "Not authenticated";
CLogMessage(this).debug(u"AFV not authenticated");
return {};
}
checkExpiry();
this->checkExpiry();
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
m_watch->start();
QUrl url(m_address);
url.setPath(resource);
QEventLoop loop;
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_jwt);
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nam->post(request, QByteArray()));
while (! reply->isFinished()) { loop.exec(); }
qDebug() << "POST" << resource << "(" << m_watch->elapsed() << "ms)";
if (reply->error() != QNetworkReply::NoError)
{
qWarning() << "POST" << resource << "failed:" << reply->errorString();
return {};
}
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
TResponse response = TResponse::fromJson(doc.object());
const QByteArray receivedData = this->postWithResponse(request);
const QJsonDocument doc = QJsonDocument::fromJson(receivedData);
const TResponse response = TResponse::fromJson(doc.object());
return response;
}
//! Get resource and return as vector
template<typename TResponse>
QVector<TResponse> getAsVector(const QString &resource)
{
if (! m_isAuthenticated)
{
qDebug() << "Not authenticated";
CLogMessage(this).debug(u"AFV not authenticated");
return {};
}
checkExpiry();
this->checkExpiry();
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
m_watch->start();
QUrl url(m_address);
url.setPath(resource);
QEventLoop loop;
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_jwt);
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(nam->get(request));
while (! reply->isFinished()) { loop.exec(); }
qDebug() << "GET" << resource << "(" << m_watch->elapsed() << "ms)";
if (reply->error() != QNetworkReply::NoError)
{
qWarning() << "GET" << resource << "failed:" << reply->errorString();
return {};
}
const QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
const QByteArray receivedData = this->getWithResponse(request);
const QJsonDocument jsonDoc = QJsonDocument::fromJson(receivedData);
QVector<TResponse> dtos;
if (jsonDoc.isArray())
{
QJsonArray rootArray = jsonDoc.array();
for (auto o : rootArray)
{
QJsonObject d = o.toObject();
TResponse dto = TResponse::fromJson(d);
const QJsonObject d = o.toObject();
const TResponse dto = TResponse::fromJson(d);
dtos.push_back(dto);
}
}
return dtos;
}
//! Pseudo synchronous post request returning data
QByteArray getWithResponse(const QNetworkRequest &request);
//! Pseudo synchronous post request returning data
QByteArray postWithResponse(const QNetworkRequest &request, const QByteArray &data = {});
//! Post but do NOT wait for response
void postNoResponse(const QString &resource, const QJsonDocument &json);
//! Delete and do NOT wait for response
void deleteResource(const QString &resource);
//! Session expired, then re-login
void checkExpiry();
//! Message if reply has error
void logReplyErrorMessage(const QNetworkReply *reply, const QString &addMsg = {});
//! Message if reply has error
void logRequestDuration(const QNetworkReply *reply, const QString &addMsg = {});
//! Application shutting down
static bool isShuttingDown();
const QString m_address;
@@ -163,7 +156,6 @@ namespace BlackCore
QDateTime m_expiryLocalUtc;
qint64 m_serverToUserOffsetMs;
bool m_isAuthenticated = false;
QElapsedTimer *m_watch = nullptr;
};
} // ns
} // ns