feature/api-controller-improvements (#567)

* added error handler for api controller
* while downloading the config from the api, the Connecting status is now displayed
* added a button to delete container config for api servers
* added crc check to avoid re-import of api configs
* fixed currentIndex of serversMenuContent after DefaultServerIndexChanged
* added closing the import window after re-importing the config from api
This commit is contained in:
Nethius 2024-02-09 23:23:26 +05:00 committed by GitHub
parent dba05aab07
commit e0863a58aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 275 additions and 113 deletions

View file

@ -91,12 +91,11 @@ void AmneziaApplication::init()
initControllers();
#ifdef Q_OS_ANDROID
if(!AndroidController::initLogging()) {
if (!AndroidController::initLogging()) {
qFatal("Android logging initialization failed");
}
AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs());
connect(m_settings.get(), &Settings::saveLogsChanged,
AndroidController::instance(), &AndroidController::setSaveLogs);
connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs);
connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
[this](Vpn::ConnectionState state) {
@ -331,8 +330,8 @@ void AmneziaApplication::initModels()
m_clientManagementModel.reset(new ClientManagementModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get());
connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked,
m_serversModel.get(), &ServersModel::clearCachedProfile);
connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(),
&ServersModel::clearCachedProfile);
connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this,
[this](const QString &clientId, const QString &clientName, const DockerContainer container,
@ -370,12 +369,13 @@ void AmneziaApplication::initControllers()
m_settings, m_configurator));
m_engine->rootContext()->setContextProperty("ExportController", m_exportController.get());
m_settingsController.reset(new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_settings));
m_settingsController.reset(
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_settings));
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
if (m_settingsController->isAutoConnectEnabled() && m_serversModel->getDefaultServerIndex() >= 0) {
QTimer::singleShot(1000, this, [this]() { m_connectionController->openConnection(); });
}
connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled , m_serversModel.get(),
connect(m_settingsController.get(), &SettingsController::amneziaDnsToggled, m_serversModel.get(),
&ServersModel::toggleAmneziaDns);
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
@ -384,6 +384,11 @@ void AmneziaApplication::initControllers()
m_systemController.reset(new SystemController(m_settings));
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
m_cloudController.reset(new ApiController(m_serversModel, m_containersModel));
m_engine->rootContext()->setContextProperty("ApiController", m_cloudController.get());
m_apiController.reset(new ApiController(m_serversModel, m_containersModel));
m_engine->rootContext()->setContextProperty("ApiController", m_apiController.get());
connect(m_apiController.get(), &ApiController::updateStarted, this,
[this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Connecting); });
connect(m_apiController.get(), &ApiController::errorOccurred, this,
[this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); });
connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(), &ConnectionController::toggleConnection);
}

View file

@ -121,7 +121,7 @@ private:
QScopedPointer<SettingsController> m_settingsController;
QScopedPointer<SitesController> m_sitesController;
QScopedPointer<SystemController> m_systemController;
QScopedPointer<ApiController> m_cloudController;
QScopedPointer<ApiController> m_apiController;
};
#endif // AMNEZIA_APPLICATION_H

View file

@ -88,7 +88,11 @@ namespace amnezia
ImportInvalidConfigError = 900,
// Android errors
AndroidError = 1000
AndroidError = 1000,
// Api errors
ApiConfigDownloadError = 1100,
ApiConfigAlreadyAdded = 1101
};
} // namespace amnezia

View file

@ -64,6 +64,10 @@ QString errorString(ErrorCode code) {
// Android errors
case (AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
// Api errors
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case(InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;

View file

@ -85,6 +85,8 @@ namespace amnezia
constexpr char splitTunnelSites[] = "splitTunnelSites";
constexpr char splitTunnelType[] = "splitTunnelType";
constexpr char crc[] = "crc";
}
namespace protocols

View file

@ -3,9 +3,11 @@
#include <QEventLoop>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QtConcurrent>
#include "configurators/openvpn_configurator.h"
#include "configurators/wireguard_configurator.h"
#include "core/errorstrings.h"
namespace
{
@ -28,7 +30,8 @@ ApiController::ApiController(const QSharedPointer<ServersModel> &serversModel,
{
}
void ApiController::processCloudConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config)
void ApiController::processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData,
QString &config)
{
if (protocol == configKey::cloak) {
config.replace("<key>", "<key>\n");
@ -64,73 +67,91 @@ QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiCont
return obj;
}
bool ApiController::updateServerConfigFromApi()
void ApiController::updateServerConfigFromApi()
{
QtConcurrent::run([this]() {
auto serverConfig = m_serversModel->getDefaultServerConfig();
auto containerConfig = serverConfig.value(config_key::containers).toArray();
bool isConfigUpdateStarted = false;
if (serverConfig.value(config_key::configVersion).toInt() && containerConfig.isEmpty()) {
emit updateStarted();
isConfigUpdateStarted = true;
QNetworkAccessManager manager;
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization",
"Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint.replace("https", "http")); // todo remove
QString protocol = serverConfig.value(configKey::protocol).toString();
auto apiPayloadData = generateApiPayloadData(protocol);
QByteArray requestBody = QJsonDocument(fillApiPayload(protocol, apiPayloadData)).toJson();
QScopedPointer<QNetworkReply> reply;
reply.reset(manager.post(request, requestBody));
QEventLoop wait;
QObject::connect(reply.get(), &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec();
if (reply->error() == QNetworkReply::NoError) {
QString contents = QString::fromUtf8(reply->readAll());
auto data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString();
data.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(data.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray ba_uncompressed = qUncompress(ba);
if (!ba_uncompressed.isEmpty()) {
ba = ba_uncompressed;
}
QString configStr = ba;
processApiConfig(protocol, apiPayloadData, configStr);
QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig.insert(config_key::dns1, apiConfig.value(config_key::dns1));
serverConfig.insert(config_key::dns2, apiConfig.value(config_key::dns2));
serverConfig.insert(config_key::containers, apiConfig.value(config_key::containers));
serverConfig.insert(config_key::hostName, apiConfig.value(config_key::hostName));
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
serverConfig.insert(config_key::defaultContainer, defaultContainer);
m_serversModel->editServer(serverConfig);
emit m_serversModel->defaultContainerChanged(ContainerProps::containerFromString(defaultContainer));
} else {
qDebug() << reply->error();
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
emit errorOccurred(errorString(ApiConfigDownloadError));
return;
}
}
emit updateFinished(isConfigUpdateStarted);
return;
});
}
void ApiController::clearApiConfig()
{
auto serverConfig = m_serversModel->getDefaultServerConfig();
auto containerConfig = serverConfig.value(config_key::containers).toArray();
serverConfig.remove(config_key::dns1);
serverConfig.remove(config_key::dns2);
serverConfig.remove(config_key::containers);
serverConfig.remove(config_key::hostName);
if (serverConfig.value(config_key::configVersion).toInt() && containerConfig.isEmpty()) {
QNetworkAccessManager manager;
serverConfig.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None));
QNetworkRequest request;
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization",
"Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint.replace("https", "http")); // todo remove
QString protocol = serverConfig.value(configKey::protocol).toString();
auto apiPayloadData = generateApiPayloadData(protocol);
QByteArray requestBody = QJsonDocument(fillApiPayload(protocol, apiPayloadData)).toJson();
QScopedPointer<QNetworkReply> reply;
reply.reset(manager.post(request, requestBody));
QEventLoop wait;
QObject::connect(reply.get(), &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec();
if (reply->error() == QNetworkReply::NoError) {
QString contents = QString::fromUtf8(reply->readAll());
auto data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString();
data.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(data.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray ba_uncompressed = qUncompress(ba);
if (!ba_uncompressed.isEmpty()) {
ba = ba_uncompressed;
}
QString configStr = ba;
processCloudConfig(protocol, apiPayloadData, configStr);
QJsonObject cloudConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig.insert("cloudConfig", cloudConfig);
serverConfig.insert(config_key::dns1, cloudConfig.value(config_key::dns1));
serverConfig.insert(config_key::dns2, cloudConfig.value(config_key::dns2));
serverConfig.insert(config_key::containers, cloudConfig.value(config_key::containers));
serverConfig.insert(config_key::hostName, cloudConfig.value(config_key::hostName));
auto defaultContainer = cloudConfig.value(config_key::defaultContainer).toString();
serverConfig.insert(config_key::defaultContainer, defaultContainer);
m_serversModel->editServer(serverConfig);
emit m_serversModel->defaultContainerChanged(ContainerProps::containerFromString(defaultContainer));
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll()); //todo remove debug output
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
emit errorOccurred(tr("Error when retrieving configuration from cloud server"));
return false;
}
}
return true;
m_serversModel->editServer(serverConfig);
}

View file

@ -16,9 +16,13 @@ public:
const QSharedPointer<ContainersModel> &containersModel, QObject *parent = nullptr);
public slots:
bool updateServerConfigFromApi();
void updateServerConfigFromApi();
void clearApiConfig();
signals:
void updateStarted();
void updateFinished(bool isConfigUpdateStarted);
void errorOccurred(const QString &errorMessage);
private:
@ -31,7 +35,7 @@ private:
ApiPayloadData generateApiPayloadData(const QString &protocol);
QJsonObject fillApiPayload(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData);
void processCloudConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config);
void processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config);
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;

View file

@ -25,6 +25,11 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
void ConnectionController::openConnection()
{
if (!m_containersModel->isAnyContainerInstalled()) {
emit noInstalledContainers();
return;
}
int serverIndex = m_serversModel->getDefaultServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
@ -129,6 +134,17 @@ QString ConnectionController::connectionStateText() const
return m_connectionStateText;
}
void ConnectionController::toggleConnection(bool skipConnectionInProgressCheck)
{
if (!skipConnectionInProgressCheck && isConnectionInProgress()) {
closeConnection();
} else if (isConnected()) {
closeConnection();
} else {
openConnection();
}
}
bool ConnectionController::isConnectionInProgress() const
{
return m_isConnectionInProgress;

View file

@ -26,6 +26,8 @@ public:
QString connectionStateText() const;
public slots:
void toggleConnection(bool skipConnectionInProgressCheck);
void openConnection();
void closeConnection();
@ -45,6 +47,8 @@ signals:
void connectionErrorOccurred(const QString &errorMessage);
void reconnectWithUpdatedContainer(const QString &message);
void noInstalledContainers();
private:
Vpn::ConnectionState getCurrentConnectionState();

View file

@ -123,12 +123,19 @@ void ImportController::importConfig()
credentials.userName = m_config.value(config_key::userName).toString();
credentials.secretData = m_config.value(config_key::password).toString();
if (credentials.isValid()
|| m_config.contains(config_key::containers)
|| m_config.contains(config_key::configVersion)) { // todo
if (credentials.isValid() || m_config.contains(config_key::containers)) {
m_serversModel->addServer(m_config);
emit importFinished();
} else if (m_config.contains(config_key::configVersion)) {
quint16 crc = qChecksum(QJsonDocument(m_config).toJson());
if (m_serversModel->isServerFromApiAlreadyExists(crc)) {
emit importErrorOccurred(errorString(ErrorCode::ApiConfigAlreadyAdded), true);
} else {
m_config.insert(config_key::crc, crc);
m_serversModel->addServer(m_config);
emit importFinished();
}
} else {
qDebug() << "Failed to import profile";
qDebug().noquote() << QJsonDocument(m_config).toJson();

View file

@ -39,7 +39,7 @@ public slots:
signals:
void importFinished();
void importErrorOccurred(const QString &errorMessage);
void importErrorOccurred(const QString &errorMessage, bool goToPageHome = false);
void qrDecodingFinished();

View file

@ -53,7 +53,7 @@ QVariant ContainersModel::data(const int index, int role) const
return data(modelIndex, role);
}
void ContainersModel::updateModel(QJsonArray &containers)
void ContainersModel::updateModel(const QJsonArray &containers)
{
beginResetModel();
m_containers.clear();

View file

@ -40,7 +40,7 @@ public:
QVariant data(const int index, int role) const;
public slots:
void updateModel(QJsonArray &containers);
void updateModel(const QJsonArray &containers);
DockerContainer getDefaultContainer();
void setDefaultContainer(const int containerIndex);

View file

@ -331,6 +331,11 @@ QJsonObject ServersModel::getDefaultServerConfig()
return m_servers.at(m_defaultServerIndex).toObject();
}
QJsonObject ServersModel::getCurrentlyProcessedServerConfig()
{
return m_servers.at(m_currentlyProcessedServerIndex).toObject();
}
void ServersModel::reloadContainerConfig()
{
QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject();
@ -544,3 +549,18 @@ bool ServersModel::isDefaultServerFromApi()
return m_settings->server(m_defaultServerIndex).value(config_key::configVersion).toInt();
}
bool ServersModel::isCurrentlyProcessedServerFromApi()
{
return m_settings->server(m_currentlyProcessedServerIndex).value(config_key::configVersion).toInt();
}
bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc)
{
for (const auto &server : qAsConst(m_servers)) {
if (static_cast<quint16>(server.toObject().value(config_key::crc).toInt()) == crc) {
return true;
}
}
return false;
}

View file

@ -78,6 +78,7 @@ public slots:
bool isAmneziaDnsContainerInstalled(const int serverIndex);
QJsonObject getDefaultServerConfig();
QJsonObject getCurrentlyProcessedServerConfig();
void reloadContainerConfig();
void updateContainerConfig(const int containerIndex, const QJsonObject config);
@ -99,6 +100,9 @@ public slots:
void toggleAmneziaDns(bool enabled);
bool isDefaultServerFromApi();
bool isCurrentlyProcessedServerFromApi();
bool isServerFromApiAlreadyExists(const quint16 crc);
protected:
QHash<int, QByteArray> roleNames() const override;
@ -109,7 +113,7 @@ signals:
void defaultServerNameChanged();
void defaultServerDescriptionChanged();
void containersUpdated(QJsonArray &containers);
void containersUpdated(const QJsonArray &containers);
void defaultContainerChanged(const int containerIndex);
private:

View file

@ -138,26 +138,8 @@ Button {
}
onClicked: {
if (!ApiController.updateServerConfigFromApi()) {
return
}
if (!ContainersModel.isAnyContainerInstalled()) {
PageController.setTriggeredBtConnectButton(true)
ServersModel.currentlyProcessedIndex = ServersModel.getDefaultServerIndex()
InstallController.setShouldCreateServer(false)
PageController.goToPage(PageEnum.PageSetupWizardEasy)
return
}
if (ConnectionController.isConnectionInProgress) {
ConnectionController.closeConnection()
} else if (ConnectionController.isConnected) {
ConnectionController.closeConnection()
} else {
ConnectionController.openConnection()
if (!ConnectionController.isConnectionInProgress) {
ApiController.updateServerConfigFromApi()
}
}
}

View file

@ -401,6 +401,13 @@ PageType {
model: ServersModel
currentIndex: ServersModel.defaultIndex
Connections {
target: ServersModel
function onDefaultServerIndexChanged(serverIndex) {
serversMenuContent.currentIndex = serverIndex
}
}
clip: true
interactive: false
@ -429,19 +436,19 @@ PageType {
text: name
descriptionText: {
var description = ""
var fullDescription = ""
if (hasWriteAccess) {
if (SettingsController.isAmneziaDnsEnabled()
&& ServersModel.isAmneziaDnsContainerInstalled(index)) {
description += "Amnezia DNS | "
fullDescription += "Amnezia DNS | "
}
} else {
if (containsAmneziaDns) {
description += "Amnezia DNS | "
fullDescription += "Amnezia DNS | "
}
}
return description += hostName
return fullDescription += serverDescription
}
checked: index === serversMenuContent.currentIndex

View file

@ -225,7 +225,69 @@ PageType {
DividerType {
visible: content.isServerWithWriteAccess
}
}
LabelWithButtonType {
visible: content.isServerWithWriteAccess
Layout.fillWidth: true
text: qsTr("Clear server from Amnezia software")
textColor: "#EB5757"
clickedFunction: function() {
questionDrawer.headerText = qsTr("Do you want to clear server from Amnezia software?")
questionDrawer.descriptionText = qsTr("All containers will be deleted on the server. This means that configuration files, keys and certificates will be deleted.")
questionDrawer.yesButtonText = qsTr("Continue")
questionDrawer.noButtonText = qsTr("Cancel")
questionDrawer.yesButtonFunction = function() {
questionDrawer.visible = false
PageController.goToPage(PageEnum.PageDeinstalling)
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
ConnectionController.closeConnection()
}
InstallController.removeAllContainers()
}
questionDrawer.noButtonFunction = function() {
questionDrawer.visible = false
}
questionDrawer.visible = true
}
}
DividerType {
visible: content.isServerWithWriteAccess
}
LabelWithButtonType {
visible: ServersModel.isCurrentlyProcessedServerFromApi()
Layout.fillWidth: true
text: qsTr("Reset API config")
textColor: "#EB5757"
clickedFunction: function() {
questionDrawer.headerText = qsTr("Do you want to reset API config?")
questionDrawer.descriptionText = ""
questionDrawer.yesButtonText = qsTr("Continue")
questionDrawer.noButtonText = qsTr("Cancel")
questionDrawer.yesButtonFunction = function() {
questionDrawer.visible = false
PageController.showBusyIndicator(true)
ApiController.clearApiConfig()
PageController.showBusyIndicator(false)
}
questionDrawer.noButtonFunction = function() {
questionDrawer.visible = false
}
questionDrawer.visible = true
}
}
DividerType {
visible: ServersModel.isCurrentlyProcessedServerFromApi()
}
QuestionDrawer {
id: questionDrawer

View file

@ -18,8 +18,12 @@ PageType {
Connections {
target: ImportController
function onImportErrorOccurred(errorMessage) {
PageController.closePage()
function onImportErrorOccurred(errorMessage, goToPageHome) {
if (goToPageHome) {
PageController.goToStartPage()
} else {
PageController.closePage()
}
PageController.showErrorMessage(errorMessage)
}

View file

@ -111,6 +111,22 @@ PageType {
PageController.showNotificationMessage(message)
PageController.closePage()
}
function onNoInstalledContainers() {
PageController.setTriggeredBtConnectButton(true)
ServersModel.currentlyProcessedIndex = ServersModel.getDefaultServerIndex()
InstallController.setShouldCreateServer(false)
PageController.goToPage(PageEnum.PageSetupWizardEasy)
}
}
Connections {
target: ApiController
function onErrorOccurred(errorMessage) {
PageController.showErrorMessage(errorMessage)
}
}
StackViewType {