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

View File

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