Документация
ОС Аврора 5.1.5

Пример кода серверного приложения

Тестовый пример серверного приложения, как и клиентского, является Аврора-приложением. С полным кодом проекта можно ознакомиться по ссылке.

Данная программа считывает настройки проекта из 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 &registarationId,
                                     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 &registarationId,
                                         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);
    }
}

Мы используем cookies для персонализации сайта и его более удобного использования. Вы можете запретить cookies в настройках браузера.

Пожалуйста ознакомьтесь с политикой использования cookies.