Пример кода серверного приложения
Тестовый пример серверного приложения, как и клиентского, является Аврора-приложением. С полным кодом проекта можно ознакомиться по ссылке.
Данная программа считывает настройки проекта из yaml-файла, загруженного через графический интерфейс, делает запрос на получение токена доступа и в заключительной части отправляет push-уведомление.
До запуска примера необходимо скопировать на устройство или эмулятор конфигурационный файл с настройками сервер Сервиса уведомлений.
Графический интерфейс приложения состоит из двух страниц.
На главной странице приложения настраивается registrationId
,
получаемый от мобильного приложения после регистрации в push-демоне.
Например, клиент Push Receiver позволяет скопировать
этот идентификатор через меню.
На дополнительной прикреплённой странице настраиваются параметры push-сервера, которые можно импортировать из конфигурационного yaml-файла. Настройки сервера сохраняются между перезапусками приложения.
Таким образом, при первом запуске приложения на первой странице необходимо указать
registrationId
, полученный клиентом.
Далее нужно перейти на прикреплённую страницу и настроить конфигурационные параметры.
Потом можно вернуться на первую страницу, ввести текст для уведомления,
и через верхнее меню отправить push-уведомление клиенту.
Архитектура примера серверного приложения:
- ru.omp.PushSender.cpp — главный файл приложения, стартовая точка;
- Класс
O2AuroraPushServer
содержит основную функциональность сервера: загружает настройки из файла, полученает токен и отправляет push-уведомления; - qml-код для графического интерфейса (в данной статье не приводится, его можно изучить самостоятельно).
Объявление O2AuroraPushServer
:
#include <QObject>
#include "o2.h"
// Disclamer: "God object" antipattern to simplify the minimal example
// Aurora OS Push Server's dialect of OAuth 2.0
class O2AuroraPushServer : public O2
{
Q_OBJECT
public:
explicit O2AuroraPushServer(QObject *parent = nullptr);
Q_INVOKABLE void readSettingFile(const QString &filePath);
Q_INVOKABLE void sendPushMessage(const QString &apiUrl,
const QString &projectId,
const QString &clientId,
const QString &privateKeyId,
const QString &privateKey,
const QString &audience,
const QString &scope,
const QString &tokenUrl,
const QString ®istarationId,
const QString &textMessage);
signals:
void serverError(const QString &serverError);
void serverAnswer(const QString &serverAnswer);
void apiUrlReady(const QString &apiUrl);
void projectIdReady(const QString &projectId);
void clientIdReady(const QString &clientId);
void privateKeyIdReady(const QString &privateKeyId);
void privateKeyReady(const QString &privateKey);
void audienceReady(const QString &audience);
void scopeReady(const QString &scope);
void tokenUrlReady(const QString &tokenUrl);
public slots:
void onSendPushReplyFinished();
void onTokenReplyFinished() override;
void link() override;
private:
QByteArray signSHA256withRSA(const QByteArray &data, const QByteArray &privateKey);
QByteArray createJWT();
QString generateToken(int randomStringLength);
void sendPushRequest();
void readYamlSettingFile(const QString &filePath);
private:
QString m_apiUrl { QString::null };
QString m_projectId { QString::null };
QString m_clientId { QString::null };
QString m_privateKeyId { QString::null };
QString m_privateKey { QString::null };
QString m_audience { QString::null };
QString m_scope { QString::null };
QString m_registarationId { QString::null };
QString m_textMessage { QString::null };
int m_timeToLive { 0 };
};
Инициализация сервера в конструкторе:
O2AuroraPushServer::O2AuroraPushServer(QObject *parent)
: O2(parent), m_timeToLive(7200)
{
connect(this, &O2AuroraPushServer::linkingSucceeded,
this, &O2AuroraPushServer::sendPushRequest);
connect(this, &O2AuroraPushServer::tokenUrlChanged, [this]() {
emit tokenUrlReady(tokenUrl());
});
}
После успешного соединения с сервером и получения токена следует отправка push-уведомления.
Изменение URL для получения токена можно отследить с помощью сигнала tokenUrlReady
.
Чтение данных из yaml-файла, полученного через графический интерфейс:
void O2AuroraPushServer::readSettingFile(const QString &filePath)
{
if (filePath.endsWith(QStringLiteral(".yml")) || filePath.endsWith(QStringLiteral(".yaml")))
readYamlSettingFile(filePath);
else
qDebug() << Q_FUNC_INFO << "Could not recognize settings file format";
}
void O2AuroraPushServer::readYamlSettingFile(const QString &filePath)
{
QFile inputFile(filePath);
if (!inputFile.open(QIODevice::ReadOnly))
return;
QByteArray yaml = inputFile.readAll();
inputFile.close();
QtYAML::DocumentList docs = QtYAML::Document::fromYaml(yaml);
QtYAML::Mapping pnsMapping = docs.first().mapping()[QStringLiteral("push_notification_system")].toMapping();
QString tokenEndpoint = pnsMapping[QStringLiteral("token_url")].toString();
setTokenUrl(tokenEndpoint);
setRefreshTokenUrl(tokenEndpoint);
emit projectIdReady(pnsMapping[QStringLiteral("project_id")].toString());
emit apiUrlReady(pnsMapping[QStringLiteral("api_url")].toString());
emit clientIdReady(pnsMapping[QStringLiteral("client_id")].toString());
emit scopeReady(pnsMapping[QStringLiteral("scopes")].toString());
emit audienceReady(pnsMapping[QStringLiteral("audience")].toString());
emit privateKeyIdReady(pnsMapping[QStringLiteral("key_id")].toString());
emit privateKeyReady(pnsMapping[QStringLiteral("private_key")].toString());
}
Запрос токена инициируется, когда пользователь выбирает пункт меню для отправки push-уведомления:
void O2AuroraPushServer::sendPushMessage(const QString &apiUrl,
const QString &projectId,
const QString &clientId,
const QString &privateKeyId,
const QString &privateKey,
const QString &audience,
const QString &scope,
const QString &tokenUrl,
const QString ®istarationId,
const QString &textMessage)
{
m_apiUrl = apiUrl;
m_projectId = projectId;
m_clientId = clientId;
m_privateKeyId = privateKeyId;
m_privateKey = privateKey;
m_audience = audience;
m_scope = scope;
m_registarationId = registarationId;
m_textMessage = textMessage;
setTokenUrl(tokenUrl);
setRefreshTokenUrl(tokenUrl);
setRequestUrl(QStringLiteral("%1projects/%2/messages").arg(m_apiUrl, projectId));
setClientSecret(privateKey);
setScope(scope);
setGrantType(QStringLiteral("client_credentials"));
O0SettingsStore *store = new O0SettingsStore(O2_ENCRYPTION_KEY);
store->setGroupKey(QStringLiteral("aurorapushserver"));
setStore(store);
qDebug() << Q_FUNC_INFO << "manually resetting linked state";
setLinked(false);
link();
}
void O2AuroraPushServer::link()
{
qDebug() << Q_FUNC_INFO;
if (linked()) {
qDebug() << Q_FUNC_INFO << "Linked already";
emit linkingSucceeded();
return;
}
setLinked(false);
setToken("");
setTokenSecret("");
setExtraTokens(QVariantMap());
setRefreshToken(QStringLiteral());
setExpires(0);
qDebug() << Q_FUNC_INFO << "tokenUrl_:" << tokenUrl_;
QByteArray jwt = createJWT();
qDebug() << Q_FUNC_INFO << "jwt:" << jwt;
QMap<QString, QString> parameters;
parameters.insert(O2_OAUTH2_GRANT_TYPE, QStringLiteral("client_credentials"));
parameters.insert(QStringLiteral("audience"), m_audience);
parameters.insert(QStringLiteral("scope"), m_scope);
parameters.insert(QStringLiteral("client_assertion_type"), QStringLiteral("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"));
parameters.insert(QStringLiteral("client_assertion"), jwt);
QByteArray data = buildRequestBody(parameters);
QNetworkRequest tokenRequest(tokenUrl_);
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QNetworkReply *tokenReply = manager_->post(tokenRequest, data);
timedReplies_.add(tokenReply);
connect(tokenReply, &QNetworkReply::finished,
this, &O2AuroraPushServer::onTokenReplyFinished, Qt::QueuedConnection);
connect(tokenReply, static_cast<void(QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, &O2AuroraPushServer::onTokenReplyError, Qt::QueuedConnection);
}
QByteArray O2AuroraPushServer::createJWT()
{
const qint64 issuedAt = QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000;
const qint64 expiresAt = issuedAt + m_timeToLive;
const QString jwtHeader = QStringLiteral(R"({"kid":"%1","alg":"RS256"})").arg(m_privateKeyId);
const QString jwtClaimSet = QStringLiteral(R"({"iss":"%1","sub":"%2","iat":%3,"exp":%4,"jti":"%5","aud":"%6"})")
.arg(m_clientId, m_clientId, QString::number(issuedAt), QString::number(expiresAt), generateToken(36), tokenUrl_.url());
QByteArray jwt = jwtHeader.toUtf8().toBase64(QByteArray::Base64UrlEncoding);
jwt.append(u'.');
jwt.append(jwtClaimSet.toUtf8().toBase64(QByteArray::Base64UrlEncoding));
QByteArray sign = signSHA256withRSA(jwt,m_privateKey.toLatin1());
if (sign.isEmpty())
return QByteArrayLiteral("");
jwt.append(u'.');
jwt.append(sign.toBase64(QByteArray::Base64UrlEncoding));
return jwt;
}
QByteArray O2AuroraPushServer::signSHA256withRSA(const QByteArray &data, const QByteArray &privateKey)
{
BIO *b = BIO_new_mem_buf(privateKey.constData(), privateKey.length());
if (b == nullptr)
return QByteArrayLiteral("");
RSA *r = PEM_read_bio_RSAPrivateKey(b, nullptr, nullptr, nullptr);
if (r == nullptr) {
BIO_free(b);
return QByteArrayLiteral("");
}
QScopedPointer<unsigned char, QScopedPointerPodDeleter> sig; // NOLINT
sig.reset(reinterpret_cast<unsigned char*>(malloc(static_cast<size_t>(RSA_size(r)))));
if (sig.isNull()) {
BIO_free(b);
RSA_free(r);
return QByteArrayLiteral("");
}
QByteArray digest = QCryptographicHash::hash(data, QCryptographicHash::Sha256);
unsigned int sigLen = 0;
int rc = RSA_sign(NID_sha256, reinterpret_cast<const unsigned char *>(digest.constData()),
static_cast<quint32>(digest.length()), sig.data(), &sigLen, r);
QByteArray result;
if (rc == 1)
result = QByteArray(reinterpret_cast<char *>(sig.data()), static_cast<int>(sigLen));
BIO_free(b);
RSA_free(r);
return result;
}
QString O2AuroraPushServer::generateToken(int randomStringLength)
{
static const QString possibleCharacters(QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"));
qsrand(static_cast<unsigned int>(QTime::currentTime().msec())); // NOTE: QRandomGenerator in future Qt versions
QString randomString;
for (int i = 0; i < randomStringLength; ++i) {
int index = qrand() % possibleCharacters.length();
QChar nextChar = possibleCharacters.at(index);
randomString.append(nextChar);
}
return randomString;
}
void O2AuroraPushServer::onTokenReplyFinished()
{
QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(sender());
QByteArray replyData = tokenReply->readAll();
if (tokenReply->error() == QNetworkReply::NoError) {
QJsonDocument doc = QJsonDocument::fromJson(replyData);
const QJsonObject rootObject = doc.object();
QVariantMap reply;
for (const QString &key : rootObject.keys())
reply.insert(key, rootObject[key].toVariant());
setToken(reply.value(QStringLiteral("access_token")).toString());
setExpires(reply.value(QStringLiteral("expires_in")).toInt());
setLinked(true);
emit linkingSucceeded();
} else {
qWarning() << Q_FUNC_INFO << tokenReply->errorString();
emit serverError(tokenReply->errorString() + ": " +replyData);
}
}
Отправка push-уведомления происходит после успешного соединения с сервером и получения токена:
void O2AuroraPushServer::sendPushRequest()
{
QJsonObject body
{
{ "target", m_registarationId },
{ "ttl", "2h" },
{ "type", "device" },
{
"notification", {
{
{ "title", "OMP Sender Test" },
{ "message", m_textMessage },
{
"data", {
{
{ "action", "command" },
{ "another_key", "value" },
}
},
},
}
},
},
};
QNetworkRequest sendPushRequest(requestUrl());
sendPushRequest.setRawHeader(QByteArrayLiteral("Authorization"), QStringLiteral("Bearer %1").arg(token()).toUtf8());
sendPushRequest.setRawHeader(QByteArrayLiteral("Accept"), QByteArrayLiteral("application/json"));
QNetworkReply *sendPushReply = manager_->post(sendPushRequest, QJsonDocument(body).toJson(QJsonDocument::Compact).toStdString().c_str());
timedReplies_.add(sendPushReply);
connect(sendPushReply, &QNetworkReply::finished,
this, &O2AuroraPushServer::onSendPushReplyFinished, Qt::QueuedConnection);
}
void O2AuroraPushServer::onSendPushReplyFinished()
{
QNetworkReply *sendPushReply = qobject_cast<QNetworkReply *>(sender());
QByteArray replyData = sendPushReply->readAll();
if (sendPushReply->error() == QNetworkReply::NoError) {
emit serverAnswer(replyData);
} else {
qWarning() << Q_FUNC_INFO << sendPushReply->errorString();
emit serverError(sendPushReply->errorString() + ": " +replyData);
}
}