Start authenticator work
parent
84905bcd12
commit
cac4a1491b
32
README.md
32
README.md
|
|
@ -26,6 +26,38 @@ If you want to start the proxy server from the build directory, you need to expo
|
|||
$ ./server/nymea-remoteproxy -c ../nymea-remoteproxy/tests/test-certificate.crt -k ../nymea-remoteproxy/tests/test-certificate.key
|
||||
|
||||
|
||||
## AWS SDK
|
||||
|
||||
Get the latest source code and build dependecies
|
||||
|
||||
$ apt update
|
||||
$ apt install git build-essential cmake libcurl4-openssl-dev libssl-dev uuid-dev zlib1g-dev libpulse-dev
|
||||
|
||||
$ git clone https://github.com/aws/aws-sdk-cpp.git
|
||||
|
||||
Create the build and install folder
|
||||
|
||||
$ cd aws-sdk-cpp
|
||||
$ mkdir -p build/install
|
||||
$ cd build
|
||||
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_ONLY="lambda" -DCMAKE_INSTALL_PREFIX=$(pwd)/install ../
|
||||
$ make -j$(nproc)
|
||||
|
||||
Install build output into install directory
|
||||
|
||||
$ make install
|
||||
|
||||
#### Building debian package
|
||||
|
||||
$ git clone https://github.com/aws/aws-sdk-cpp.git
|
||||
$ cd aws-sdk-cpp
|
||||
|
||||
$ git clone git@gitlab.guh.io:cloud/aws-sdk-cpp-debian.git debian
|
||||
|
||||
$ crossbuilder
|
||||
|
||||
|
||||
# Install
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ AwsAuthenticator::AwsAuthenticator(QObject *parent) :
|
|||
|
||||
}
|
||||
|
||||
AwsAuthenticator::~AwsAuthenticator()
|
||||
{
|
||||
qCDebug(dcAuthenticator()) << "Shutting down" << name();
|
||||
}
|
||||
|
||||
QString AwsAuthenticator::name() const
|
||||
{
|
||||
return "AWS authenticator";
|
||||
|
|
@ -19,6 +24,9 @@ AuthenticationReply *AwsAuthenticator::authenticate(ProxyClient *proxyClient)
|
|||
{
|
||||
qCDebug(dcAuthenticator()) << name() << "Start authenticating" << proxyClient << "using token" << proxyClient->token();
|
||||
AuthenticationReply *reply = createAuthenticationReply(proxyClient, this);
|
||||
|
||||
// TODO: start authentication request
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ class AwsAuthenticator : public Authenticator
|
|||
Q_OBJECT
|
||||
public:
|
||||
explicit AwsAuthenticator(QObject *parent = nullptr);
|
||||
~AwsAuthenticator() override = default;
|
||||
~AwsAuthenticator() override;
|
||||
|
||||
QString name() const override;
|
||||
|
||||
private:
|
||||
|
||||
|
||||
public slots:
|
||||
AuthenticationReply *authenticate(ProxyClient *proxyClient) override;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
#include "sigv4utils.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QCryptographicHash>
|
||||
#include <QMessageAuthenticationCode>
|
||||
#include <QtDebug>
|
||||
#include <QUrlQuery>
|
||||
#include <QList>
|
||||
|
||||
SigV4Utils::SigV4Utils()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getCurrentDateTime()
|
||||
{
|
||||
return QDateTime::currentDateTime().toUTC().toString("yyyyMMddThhmmssZ").toUtf8();
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getCanonicalQueryString(const QNetworkRequest &request, const QByteArray &accessKeyId, const QByteArray &secretAccessKey, const QByteArray &sessionToken, const QByteArray ®ion, const QByteArray &service, const QByteArray &payload)
|
||||
{
|
||||
QByteArray algorithm = "AWS4-HMAC-SHA256";
|
||||
QByteArray dateTime = getCurrentDateTime();
|
||||
QByteArray credentialScope = getCredentialScope(algorithm, dateTime, region, service);
|
||||
|
||||
QByteArray canonicalQueryString;
|
||||
canonicalQueryString += "X-Amz-Algorithm=AWS4-HMAC-SHA256";
|
||||
canonicalQueryString += "&X-Amz-Credential=" + QByteArray(accessKeyId + '/' + credentialScope).toPercentEncoding();
|
||||
canonicalQueryString += "&X-Amz-Date=" + dateTime;
|
||||
if (request.rawHeaderList().count() > 0){
|
||||
canonicalQueryString += "&X-Amz-SignedHeaders=" + request.rawHeaderList().join(';').toLower();
|
||||
}
|
||||
|
||||
QByteArray canonicalRequest = getCanonicalRequest(QNetworkAccessManager::GetOperation, request, payload);
|
||||
|
||||
QByteArray stringToSign = getStringToSign(canonicalRequest, dateTime, region, service);
|
||||
QByteArray signature = getSignature(stringToSign, secretAccessKey, dateTime, region, service);
|
||||
|
||||
canonicalQueryString += "&X-Amz-Signature=" + signature;
|
||||
|
||||
if (!sessionToken.isEmpty()) {
|
||||
canonicalQueryString += "&X-Amz-Security-Token=" + sessionToken.toPercentEncoding();
|
||||
}
|
||||
|
||||
return canonicalQueryString;
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getSignatureKey(const QByteArray &key, const QByteArray &date, const QByteArray ®ion, const QByteArray &service)
|
||||
{
|
||||
QCryptographicHash::Algorithm hashAlgorithm = QCryptographicHash::Sha256;
|
||||
return QMessageAuthenticationCode::hash("aws4_request",
|
||||
QMessageAuthenticationCode::hash(service,
|
||||
QMessageAuthenticationCode::hash(region,
|
||||
QMessageAuthenticationCode::hash(date, "AWS4"+key,
|
||||
hashAlgorithm), hashAlgorithm), hashAlgorithm), hashAlgorithm);
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getCanonicalRequest(QNetworkAccessManager::Operation operation, const QNetworkRequest &request, const QByteArray &payload)
|
||||
{
|
||||
QByteArray canonicalRequest;
|
||||
|
||||
QByteArray method;
|
||||
switch (operation) {
|
||||
case QNetworkAccessManager::GetOperation:
|
||||
method = "GET";
|
||||
break;
|
||||
case QNetworkAccessManager::PostOperation:
|
||||
method = "POST";
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT_X(false, "Network operation not implemented", "SigV4Utils");
|
||||
}
|
||||
QByteArray uri = request.url().path().toUtf8();
|
||||
QUrlQuery query(request.url());
|
||||
QList<QPair<QString, QString> > queryItems = query.queryItems();
|
||||
QStringList queryItemStrings;
|
||||
for (int i = 0; i < queryItems.count(); i++) {
|
||||
QPair<QString, QString> queryItem = queryItems.at(i);
|
||||
queryItemStrings.append(queryItem.first + '=' + queryItem.second);
|
||||
}
|
||||
queryItemStrings.sort(Qt::CaseInsensitive);
|
||||
|
||||
QByteArray canonicalQueryString = queryItemStrings.join('&').toUtf8();
|
||||
|
||||
QByteArray canonicalHeaders;
|
||||
foreach(const QByteArray &headerName, request.rawHeaderList()) {
|
||||
canonicalHeaders += headerName.toLower() + ':' + request.rawHeader(headerName) + '\n';
|
||||
}
|
||||
|
||||
QByteArray payloadHash = QCryptographicHash::hash(payload, QCryptographicHash::Sha256).toHex();
|
||||
|
||||
canonicalRequest = method + '\n' + uri + '\n' + canonicalQueryString + '\n' + canonicalHeaders + '\n' + request.rawHeaderList().join(';').toLower() + '\n' + payloadHash;
|
||||
return canonicalRequest;
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getCredentialScope(const QByteArray &algorithm, const QByteArray &dateTime, const QByteArray ®ion, const QByteArray &service)
|
||||
{
|
||||
Q_UNUSED(algorithm)
|
||||
QByteArray credentialScope = dateTime.left(8) + '/' + region + '/' + service + "/aws4_request";
|
||||
return credentialScope;
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getStringToSign(const QByteArray &canonicalRequest, const QByteArray &dateTime, const QByteArray ®ion, const QByteArray &service)
|
||||
{
|
||||
QByteArray algorithm = "AWS4-HMAC-SHA256";
|
||||
QByteArray credentialScope = getCredentialScope(algorithm, dateTime, region, service);
|
||||
|
||||
QByteArray stringToSign = algorithm + '\n' + dateTime + '\n' + credentialScope + '\n' + QCryptographicHash::hash(canonicalRequest, QCryptographicHash::Sha256).toHex();
|
||||
return stringToSign;
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getSignature(const QByteArray &stringToSign, const QByteArray &secretAccessKey, const QByteArray &dateTime, const QString ®ion, const QString &service)
|
||||
{
|
||||
QByteArray signingKey = getSignatureKey(secretAccessKey, dateTime.left(8), region.toUtf8(), service.toUtf8());
|
||||
QByteArray signature = QMessageAuthenticationCode::hash(stringToSign, signingKey, QCryptographicHash::Sha256).toHex();
|
||||
return signature;
|
||||
}
|
||||
|
||||
QByteArray SigV4Utils::getAuthorizationHeader(const QByteArray &accessKeyId, const QByteArray &dateTime, const QString ®ion, const QString &service, const QNetworkRequest &request, const QByteArray &signature)
|
||||
{
|
||||
QByteArray authHeader = "AWS4-HMAC-SHA256 Credential=" + accessKeyId + '/' + dateTime.left(8) + '/' + region.toUtf8() + '/' + service.toUtf8() + '/' + "aws4_request, SignedHeaders=" + request.rawHeaderList().join(';').toLower() + ", Signature=" + signature;
|
||||
return authHeader;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#ifndef SIGV4UTILS_H
|
||||
#define SIGV4UTILS_H
|
||||
|
||||
#include <QString>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class SigV4Utils
|
||||
{
|
||||
public:
|
||||
SigV4Utils();
|
||||
|
||||
static QByteArray getCurrentDateTime();
|
||||
|
||||
|
||||
static QByteArray getCanonicalQueryString(const QNetworkRequest &request, const QByteArray &accessKeyId, const QByteArray &secretAccessKey, const QByteArray &sessionToken, const QByteArray ®ion, const QByteArray &service, const QByteArray &payload);
|
||||
static QByteArray getCanonicalRequest(QNetworkAccessManager::Operation operation, const QNetworkRequest &request, const QByteArray &payload);
|
||||
static QByteArray getCanonicalHeaders(const QNetworkRequest &request);
|
||||
static QByteArray getCredentialScope(const QByteArray &algorithm, const QByteArray &dateTime, const QByteArray ®ion, const QByteArray &service);
|
||||
static QByteArray getStringToSign(const QByteArray &canonicalRequest, const QByteArray &dateTime, const QByteArray ®ion, const QByteArray &service);
|
||||
static QByteArray getSignatureKey(const QByteArray &key, const QByteArray &date, const QByteArray ®ion, const QByteArray &service);
|
||||
static QByteArray getSignature(const QByteArray &stringToSign, const QByteArray &secretAccessKey, const QByteArray &dateTime, const QString ®ion, const QString &service);
|
||||
static QByteArray getAuthorizationHeader(const QByteArray &accessKeyId, const QByteArray &dateTime, const QString ®ion, const QString &service, const QNetworkRequest &request, const QByteArray &signature);
|
||||
|
||||
};
|
||||
|
||||
#endif // SIGV4UTILS_H
|
||||
|
|
@ -3,6 +3,12 @@ include(../nymea-remoteproxy.pri)
|
|||
TEMPLATE = lib
|
||||
TARGET = nymea-remoteproxy
|
||||
|
||||
# -L/home/timon/guh/development/cloud/aws-sdk-cpp/build/install/lib
|
||||
# -laws-cpp-sdk-access-management \
|
||||
# -laws-cpp-sdk-cognito-identity \
|
||||
# -laws-cpp-sdk-iam \
|
||||
# -laws-cpp-sdk-kinesis\
|
||||
|
||||
HEADERS += \
|
||||
engine.h \
|
||||
loggingcategories.h \
|
||||
|
|
@ -19,7 +25,8 @@ HEADERS += \
|
|||
authentication/awsauthenticator.h \
|
||||
authentication/authenticationreply.h \
|
||||
proxyconfiguration.h \
|
||||
tunnelconnection.h
|
||||
tunnelconnection.h \
|
||||
authentication/sigv4utils.h
|
||||
|
||||
SOURCES += \
|
||||
engine.cpp \
|
||||
|
|
@ -37,7 +44,8 @@ SOURCES += \
|
|||
authentication/awsauthenticator.cpp \
|
||||
authentication/authenticationreply.cpp \
|
||||
proxyconfiguration.cpp \
|
||||
tunnelconnection.cpp
|
||||
tunnelconnection.cpp \
|
||||
authentication/sigv4utils.cpp
|
||||
|
||||
|
||||
# install header file with relative subdirectory
|
||||
|
|
|
|||
|
|
@ -1,10 +1,132 @@
|
|||
#include "loggingcategories.h"
|
||||
#include "proxyconfiguration.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace remoteproxy {
|
||||
|
||||
ProxyConfiguration::ProxyConfiguration(QObject *parent) : QObject(parent)
|
||||
ProxyConfiguration::ProxyConfiguration(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool ProxyConfiguration::loadConfiguration(const QString &fileName)
|
||||
{
|
||||
QFileInfo fileInfo(fileName);
|
||||
|
||||
if (!fileInfo.exists()) {
|
||||
qCWarning(dcApplication()) << "Could not find configuration file" << fileName;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fileInfo.isReadable()) {
|
||||
qCWarning(dcApplication()) << "Cannot read configuration file" << fileName;
|
||||
return false;
|
||||
}
|
||||
|
||||
QSettings settings(fileName, QSettings::IniFormat);
|
||||
|
||||
settings.beginGroup("General");
|
||||
setWriteLogFile(settings.value("writeLogs", false).toBool());
|
||||
setLogFileName(settings.value("logFile", "/var/log/nymea-remoteproxy.log").toString());
|
||||
setSslCertificateFileName(settings.value("certificate", "/etc/ssl/certs/ssl-cert-snakeoil.pem").toString());
|
||||
setSslCertificateKeyFileName(settings.value("certificateKey", "/etc/ssl/private/ssl-cert-snakeoil.key").toString());
|
||||
settings.endGroup();
|
||||
|
||||
settings.beginGroup("WebSocketServer");
|
||||
setWebSocketServerHost(QHostAddress(settings.value("host", "127.0.0.1").toString()));
|
||||
setWebSocketServerPort(static_cast<quint16>(settings.value("port", 1212).toInt()));
|
||||
settings.endGroup();
|
||||
|
||||
settings.beginGroup("TcpServer");
|
||||
setWebSocketServerHost(QHostAddress(settings.value("host", "127.0.0.1").toString()));
|
||||
setWebSocketServerPort(static_cast<quint16>(settings.value("port", 1213).toInt()));
|
||||
settings.endGroup();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProxyConfiguration::writeLogFile() const
|
||||
{
|
||||
return m_writeLogFile;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setWriteLogFile(bool enabled)
|
||||
{
|
||||
m_writeLogFile = enabled;
|
||||
}
|
||||
|
||||
QString ProxyConfiguration::logFileName() const
|
||||
{
|
||||
return m_logFileName;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setLogFileName(const QString &logFileName)
|
||||
{
|
||||
m_logFileName = logFileName;
|
||||
}
|
||||
|
||||
QString ProxyConfiguration::sslCertificateFileName() const
|
||||
{
|
||||
return m_sslCertificateFileName;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setSslCertificateFileName(const QString &fileName)
|
||||
{
|
||||
m_logFileName = fileName;
|
||||
}
|
||||
|
||||
QString ProxyConfiguration::sslCertificateKeyFileName() const
|
||||
{
|
||||
return m_sslCertificateKeyFileName;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setSslCertificateKeyFileName(const QString &fileName)
|
||||
{
|
||||
m_sslCertificateKeyFileName = fileName;
|
||||
}
|
||||
|
||||
QHostAddress ProxyConfiguration::webSocketServerHost() const
|
||||
{
|
||||
return m_webSocketServerHost;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setWebSocketServerHost(const QHostAddress &address)
|
||||
{
|
||||
m_webSocketServerHost = address;
|
||||
}
|
||||
|
||||
quint16 ProxyConfiguration::webSocketServerPort() const
|
||||
{
|
||||
return m_webSocketServerPort;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setWebSocketServerPort(quint16 port)
|
||||
{
|
||||
m_webSocketServerPort = port;
|
||||
}
|
||||
|
||||
QHostAddress ProxyConfiguration::tcpServerHost() const
|
||||
{
|
||||
return m_tcpServerHost;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setTcpServerHost(const QHostAddress &address)
|
||||
{
|
||||
m_tcpServerHost = address;
|
||||
}
|
||||
|
||||
quint16 ProxyConfiguration::tcpServerPort() const
|
||||
{
|
||||
return m_tcpServerPort;
|
||||
}
|
||||
|
||||
void ProxyConfiguration::setTcpServerPort(quint16 port)
|
||||
{
|
||||
m_tcpServerPort = port;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#define PROXYCONFIGURATION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
#include <QHostAddress>
|
||||
|
||||
namespace remoteproxy {
|
||||
|
||||
|
|
@ -11,9 +13,49 @@ class ProxyConfiguration : public QObject
|
|||
public:
|
||||
explicit ProxyConfiguration(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
bool loadConfiguration(const QString &fileName);
|
||||
|
||||
public slots:
|
||||
// General
|
||||
bool writeLogFile() const;
|
||||
void setWriteLogFile(bool enabled);
|
||||
|
||||
QString logFileName() const;
|
||||
void setLogFileName(const QString &logFileName);
|
||||
|
||||
QString sslCertificateFileName() const;
|
||||
void setSslCertificateFileName(const QString &fileName);
|
||||
|
||||
QString sslCertificateKeyFileName() const;
|
||||
void setSslCertificateKeyFileName(const QString &fileName);
|
||||
|
||||
// WebSocketServer
|
||||
QHostAddress webSocketServerHost() const;
|
||||
void setWebSocketServerHost(const QHostAddress &address);
|
||||
|
||||
quint16 webSocketServerPort() const;
|
||||
void setWebSocketServerPort(quint16 port);
|
||||
|
||||
// TcpServer
|
||||
QHostAddress tcpServerHost() const;
|
||||
void setTcpServerHost(const QHostAddress &address);
|
||||
|
||||
quint16 tcpServerPort() const;
|
||||
void setTcpServerPort(quint16 port);
|
||||
|
||||
private:
|
||||
// General
|
||||
bool m_writeLogFile = false;
|
||||
QString m_logFileName = "/var/log/nymea-remoteproxy.log";
|
||||
QString m_sslCertificateFileName = "/etc/ssl/certs/ssl-cert-snakeoil.pem";
|
||||
QString m_sslCertificateKeyFileName = "/etc/ssl/private/ssl-cert-snakeoil.key";
|
||||
|
||||
// WebSocketServer
|
||||
QHostAddress m_webSocketServerHost = QHostAddress::LocalHost;
|
||||
quint16 m_webSocketServerPort = 1212;
|
||||
|
||||
// TcpServer
|
||||
QHostAddress m_tcpServerHost = QHostAddress::LocalHost;
|
||||
quint16 m_tcpServerPort = 1213;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ CONFIG += c++11 console
|
|||
QMAKE_CXXFLAGS *= -Werror -std=c++11 -g
|
||||
QMAKE_LFLAGS *= -std=c++11
|
||||
|
||||
INCLUDEPATH += /home/timon/guh/development/cloud/aws-sdk-cpp/build/install/include
|
||||
LIBS += -L/home/timon/guh/development/cloud/aws-sdk-cpp/build/install/lib -laws-cpp-sdk-core -laws-cpp-sdk-lambda
|
||||
|
||||
top_srcdir=$$PWD
|
||||
top_builddir=$$shadowed($$PWD)
|
||||
|
||||
|
|
|
|||
|
|
@ -124,16 +124,14 @@ int main(int argc, char *argv[])
|
|||
|
||||
QCommandLineOption certOption(QStringList() << "c" <<"certificate", "The path to the SSL certificate used for "
|
||||
"this proxy server.", "certificate");
|
||||
certOption.setDefaultValue("/etc/ssl/certs/ssl-cert-snakeoil.pem");
|
||||
parser.addOption(certOption);
|
||||
|
||||
QCommandLineOption certKeyOption(QStringList() << "k" << "certificate-key", "The path to the SSL certificate key "
|
||||
"used for this proxy server.", "certificate-key");
|
||||
certKeyOption.setDefaultValue("/etc/ssl/private/ssl-cert-snakeoil.key");
|
||||
parser.addOption(certKeyOption);
|
||||
|
||||
QCommandLineOption authenticationUrlOption(QStringList() << "a" << "authentication-server",
|
||||
"The server url of the AWS authentication server.", "url", "https://127.0.0.1");
|
||||
parser.addOption(authenticationUrlOption);
|
||||
|
||||
QCommandLineOption verboseOption(QStringList() << "v" << "verbose", "Print more verbose.");
|
||||
parser.addOption(verboseOption);
|
||||
|
||||
|
|
@ -221,13 +219,6 @@ int main(int argc, char *argv[])
|
|||
exit(-1);
|
||||
}
|
||||
|
||||
// Authentication server url
|
||||
QUrl authenticationServerUrl(parser.value(authenticationUrlOption));
|
||||
if (!authenticationServerUrl.isValid()) {
|
||||
qCCritical(dcApplication()) << "Invalid authentication server url:" << parser.value(authenticationUrlOption);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
qCDebug(dcApplication()) << "==============================================";
|
||||
qCDebug(dcApplication()) << "Starting" << application.applicationName() << application.applicationVersion();
|
||||
qCDebug(dcApplication()) << "==============================================";
|
||||
|
|
@ -243,7 +234,6 @@ int main(int argc, char *argv[])
|
|||
Engine::instance()->setWebSocketServerHostAddress(serverHostAddress);
|
||||
Engine::instance()->setWebSocketServerPort(static_cast<quint16>(port));
|
||||
Engine::instance()->setSslConfiguration(sslConfiguration);
|
||||
Engine::instance()->setAuthenticationServerUrl(authenticationServerUrl);
|
||||
Engine::instance()->start();
|
||||
|
||||
return application.exec();
|
||||
|
|
|
|||
Loading…
Reference in New Issue