(rowCount()))
return QVariant();
- QJsonObject service = m_services.at(index.row()).toObject();
- QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
- auto serviceType = service.value(configKey::serviceType).toString();
+ auto apiServiceData = m_services.at(index.row());
+ auto serviceType = apiServiceData.type;
+ auto isServiceAvailable = apiServiceData.isServiceAvailable;
switch (role) {
case NameRole: {
- return serviceInfo.value(configKey::name).toString();
+ return apiServiceData.serviceInfo.name;
}
case CardDescriptionRole: {
- auto speed = serviceInfo.value(configKey::speed).toString();
+ auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) {
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
"Works for any sites. Speed up to %1 MBit/s")
.arg(speed);
} else if (serviceType == serviceType::amneziaFree){
QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. ");
- if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
+ if (isServiceAvailable) {
description += tr("Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.");
}
return description;
@@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case IsServiceAvailableRole: {
if (serviceType == serviceType::amneziaFree) {
- if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
+ if (isServiceAvailable) {
return false;
}
}
return true;
}
case SpeedRole: {
- auto speed = serviceInfo.value(configKey::speed).toString();
- return tr("%1 MBit/s").arg(speed);
+ return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
}
- case WorkPeriodRole: {
- auto timelimit = serviceInfo.value(configKey::timelimit).toString();
- if (timelimit == "0") {
+ case TimeLimitRole: {
+ auto timeLimit = apiServiceData.serviceInfo.timeLimit;
+ if (timeLimit == "0") {
return "";
}
- return tr("%1 days").arg(timelimit);
+ return tr("%1 days").arg(timeLimit);
}
case RegionRole: {
- return serviceInfo.value(configKey::region).toString();
+ return apiServiceData.serviceInfo.region;
}
case FeaturesRole: {
if (serviceType == serviceType::amneziaPremium) {
@@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
}
case PriceRole: {
- auto price = serviceInfo.value(configKey::price).toString();
+ auto price = apiServiceData.serviceInfo.price;
if (price == "free") {
return tr("Free");
}
return tr("%1 $/month").arg(price);
}
+ case EndDateRole: {
+ return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
+ }
}
return QVariant();
@@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
{
beginResetModel();
- m_countryCode = data.value(configKey::userCountryCode).toString();
- m_services = data.value(configKey::services).toArray();
- if (m_services.isEmpty()) {
- QJsonObject service;
- service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo));
- service.insert(configKey::serviceType, data.value(configKey::serviceType));
+ m_services.clear();
- m_services.push_back(service);
+ m_countryCode = data.value(configKey::userCountryCode).toString();
+ auto services = data.value(configKey::services).toArray();
+
+ if (services.isEmpty()) {
+ m_services.push_back(getApiServicesData(data));
m_selectedServiceIndex = 0;
+ } else {
+ for (const auto &service : services) {
+ m_services.push_back(getApiServicesData(service.toObject()));
+ }
}
endResetModel();
@@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index)
QJsonObject ApiServicesModel::getSelectedServiceInfo()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::serviceInfo).toObject();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.serviceInfo.object;
}
QString ApiServicesModel::getSelectedServiceType()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::serviceType).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.type;
}
QString ApiServicesModel::getSelectedServiceProtocol()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::serviceProtocol).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.protocol;
}
QString ApiServicesModel::getSelectedServiceName()
{
- auto modelIndex = index(m_selectedServiceIndex, 0);
- return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.serviceInfo.name;
}
QJsonArray ApiServicesModel::getSelectedServiceCountries()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::availableCountries).toArray();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.availableCountries;
}
QString ApiServicesModel::getCountryCode()
@@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode()
QString ApiServicesModel::getStoreEndpoint()
{
- QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
- return service.value(configKey::storeEndpoint).toString();
+ auto service = m_services.at(m_selectedServiceIndex);
+ return service.storeEndpoint;
}
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
@@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const
roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsServiceAvailableRole] = "isServiceAvailable";
roles[SpeedRole] = "speed";
- roles[WorkPeriodRole] = "workPeriod";
+ roles[TimeLimitRole] = "timeLimit";
roles[RegionRole] = "region";
roles[FeaturesRole] = "features";
roles[PriceRole] = "price";
+ roles[EndDateRole] = "endDate";
return roles;
}
+
+ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data)
+{
+ auto serviceInfo = data.value(configKey::serviceInfo).toObject();
+ auto serviceType = data.value(configKey::serviceType).toString();
+ auto serviceProtocol = data.value(configKey::serviceProtocol).toString();
+ auto availableCountries = data.value(configKey::availableCountries).toArray();
+
+ auto subscriptionObject = data.value(configKey::subscription).toObject();
+
+ ApiServicesData serviceData;
+ serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString();
+ serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString();
+ serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString();
+ serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
+ serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
+
+ serviceData.type = serviceType;
+ serviceData.protocol = serviceProtocol;
+
+ serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString();
+
+ if (serviceInfo.value(configKey::isAvailable).isBool()) {
+ serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool();
+ } else {
+ serviceData.isServiceAvailable = true;
+ }
+
+ serviceData.serviceInfo.object = serviceInfo;
+ serviceData.availableCountries = availableCountries;
+
+ serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString();
+
+ return serviceData;
+}
diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h
index 49918940..c96a49ab 100644
--- a/client/ui/models/apiServicesModel.h
+++ b/client/ui/models/apiServicesModel.h
@@ -3,6 +3,7 @@
#include
#include
+#include
class ApiServicesModel : public QAbstractListModel
{
@@ -15,10 +16,11 @@ public:
ServiceDescriptionRole,
IsServiceAvailableRole,
SpeedRole,
- WorkPeriodRole,
+ TimeLimitRole,
RegionRole,
FeaturesRole,
- PriceRole
+ PriceRole,
+ EndDateRole
};
explicit ApiServicesModel(QObject *parent = nullptr);
@@ -48,8 +50,40 @@ protected:
QHash roleNames() const override;
private:
+ struct ServiceInfo
+ {
+ QString name;
+ QString speed;
+ QString timeLimit;
+ QString region;
+ QString price;
+
+ QJsonObject object;
+ };
+
+ struct Subscription
+ {
+ QString endDate;
+ };
+
+ struct ApiServicesData
+ {
+ bool isServiceAvailable;
+
+ QString type;
+ QString protocol;
+ QString storeEndpoint;
+
+ ServiceInfo serviceInfo;
+ Subscription subscription;
+
+ QJsonArray availableCountries;
+ };
+
+ ApiServicesData getApiServicesData(const QJsonObject &data);
+
QString m_countryCode;
- QJsonArray m_services;
+ QVector m_services;
int m_selectedServiceIndex;
};
diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp
index 7445d60f..f07eae71 100644
--- a/client/ui/models/clientManagementModel.cpp
+++ b/client/ui/models/clientManagementModel.cpp
@@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
error = getOpenVpnClients(container, credentials, serverController, count);
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
error = getWireGuardClients(container, credentials, serverController, count);
+ } else if (container == DockerContainer::Xray) {
+ error = getXrayClients(container, credentials, serverController, count);
}
if (error != ErrorCode::NoError) {
endResetModel();
@@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta
}
return error;
}
+ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
+ const QSharedPointer &serverController, int &count)
+{
+ ErrorCode error = ErrorCode::NoError;
+
+ const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
+ const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error);
+ if (error != ErrorCode::NoError) {
+ logger.error() << "Failed to get the xray server config file from the server";
+ return error;
+ }
+
+ QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8());
+ if (serverConfig.isNull()) {
+ logger.error() << "Failed to parse xray server config JSON";
+ return ErrorCode::InternalError;
+ }
+
+ if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) {
+ logger.error() << "Invalid xray server config structure";
+ return ErrorCode::InternalError;
+ }
+
+ const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject();
+ if (!inbound.contains("settings")) {
+ logger.error() << "Missing settings in xray inbound config";
+ return ErrorCode::InternalError;
+ }
+
+ const QJsonObject settings = inbound["settings"].toObject();
+ if (!settings.contains("clients")) {
+ logger.error() << "Missing clients in xray settings config";
+ return ErrorCode::InternalError;
+ }
+
+ const QJsonArray clients = settings["clients"].toArray();
+ for (const auto &clientValue : clients) {
+ const QJsonObject clientObj = clientValue.toObject();
+ if (!clientObj.contains("id")) {
+ logger.error() << "Missing id in xray client config";
+ continue;
+ }
+ QString clientId = clientObj["id"].toString();
+
+ QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
+ xrayDefaultUuid.replace("\n", "");
+
+ if (!isClientExists(clientId) && clientId != xrayDefaultUuid) {
+ QJsonObject client;
+ client[configKey::clientId] = clientId;
+
+ QJsonObject userData;
+ userData[configKey::clientName] = QString("Client %1").arg(count);
+ client[configKey::userData] = userData;
+
+ m_clientsTable.push_back(client);
+ count++;
+ }
+ }
+
+ return error;
+}
ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer &serverController, std::vector &data)
@@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c
const QSharedPointer &serverController)
{
Proto protocol;
- if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
- protocol = Proto::OpenVpn;
- } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
- protocol = ContainerProps::defaultProtocol(container);
- } else {
- return ErrorCode::NoError;
+ switch (container) {
+ case DockerContainer::ShadowSocks:
+ case DockerContainer::Cloak:
+ protocol = Proto::OpenVpn;
+ break;
+ case DockerContainer::OpenVpn:
+ case DockerContainer::WireGuard:
+ case DockerContainer::Awg:
+ case DockerContainer::Xray:
+ protocol = ContainerProps::defaultProtocol(container);
+ break;
+ default:
+ return ErrorCode::NoError;
}
auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig);
+ return appendClient(protocolConfig, clientName, container, credentials, serverController);
+}
- return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController);
+ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container,
+ const ServerCredentials &credentials, const QSharedPointer &serverController)
+{
+ QString clientId;
+ if (container == DockerContainer::Xray) {
+ if (!protocolConfig.contains("outbounds")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonArray outbounds = protocolConfig.value("outbounds").toArray();
+ if (outbounds.isEmpty()) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject outbound = outbounds[0].toObject();
+ if (!outbound.contains("settings")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject settings = outbound["settings"].toObject();
+ if (!settings.contains("vnext")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonArray vnext = settings["vnext"].toArray();
+ if (vnext.isEmpty()) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject vnextObj = vnext[0].toObject();
+ if (!vnextObj.contains("users")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonArray users = vnextObj["users"].toArray();
+ if (users.isEmpty()) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject user = users[0].toObject();
+ if (!user.contains("id")) {
+ return ErrorCode::InternalError;
+ }
+ clientId = user["id"].toString();
+ } else {
+ clientId = protocolConfig.value(config_key::clientId).toString();
+ }
+
+ return appendClient(clientId, clientName, container, credentials, serverController);
}
ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
@@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain
auto client = m_clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
- if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
- errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
- } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
- errorCode = revokeWireGuard(row, container, credentials, serverController);
+ switch(container)
+ {
+ case DockerContainer::OpenVpn:
+ case DockerContainer::ShadowSocks:
+ case DockerContainer::Cloak: {
+ errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
+ break;
+ }
+ case DockerContainer::WireGuard:
+ case DockerContainer::Awg: {
+ errorCode = revokeWireGuard(row, container, credentials, serverController);
+ break;
+ }
+ case DockerContainer::Xray: {
+ errorCode = revokeXray(row, container, credentials, serverController);
+ break;
+ }
+ default: {
+ logger.error() << "Internal error: received unexpected container type";
+ return ErrorCode::InternalError;
+ }
}
if (errorCode == ErrorCode::NoError) {
@@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
}
Proto protocol;
- if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
- protocol = Proto::OpenVpn;
- } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
- protocol = ContainerProps::defaultProtocol(container);
- } else {
- return ErrorCode::NoError;
+
+ switch(container)
+ {
+ case DockerContainer::ShadowSocks:
+ case DockerContainer::Cloak: {
+ protocol = Proto::OpenVpn;
+ break;
+ }
+ case DockerContainer::OpenVpn:
+ case DockerContainer::WireGuard:
+ case DockerContainer::Awg:
+ case DockerContainer::Xray: {
+ protocol = ContainerProps::defaultProtocol(container);
+ break;
+ }
+ default: {
+ logger.error() << "Internal error: received unexpected container type";
+ return ErrorCode::InternalError;
+ }
}
auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig);
+ QString clientId;
+ if (container == DockerContainer::Xray) {
+ if (!protocolConfig.contains("outbounds")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonArray outbounds = protocolConfig.value("outbounds").toArray();
+ if (outbounds.isEmpty()) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject outbound = outbounds[0].toObject();
+ if (!outbound.contains("settings")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject settings = outbound["settings"].toObject();
+ if (!settings.contains("vnext")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonArray vnext = settings["vnext"].toArray();
+ if (vnext.isEmpty()) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject vnextObj = vnext[0].toObject();
+ if (!vnextObj.contains("users")) {
+ return ErrorCode::InternalError;
+ }
+ QJsonArray users = vnextObj["users"].toArray();
+ if (users.isEmpty()) {
+ return ErrorCode::InternalError;
+ }
+ QJsonObject user = users[0].toObject();
+ if (!user.contains("id")) {
+ return ErrorCode::InternalError;
+ }
+ clientId = user["id"].toString();
+ } else {
+ clientId = protocolConfig.value(config_key::clientId).toString();
+ }
+
int row;
bool clientExists = false;
- QString clientId = protocolConfig.value(config_key::clientId).toString();
for (row = 0; row < rowCount(); row++) {
auto client = m_clientsTable.at(row).toObject();
if (clientId == client.value(configKey::clientId).toString()) {
@@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
return errorCode;
}
- if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
+ switch (container)
+ {
+ case DockerContainer::OpenVpn:
+ case DockerContainer::ShadowSocks:
+ case DockerContainer::Cloak: {
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
- } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
- errorCode = revokeWireGuard(row, container, credentials, serverController);
+ break;
}
+ case DockerContainer::WireGuard:
+ case DockerContainer::Awg: {
+ errorCode = revokeWireGuard(row, container, credentials, serverController);
+ break;
+ }
+ case DockerContainer::Xray: {
+ errorCode = revokeXray(row, container, credentials, serverController);
+ break;
+ }
+ default:
+ logger.error() << "Internal error: received unexpected container type";
+ return ErrorCode::InternalError;
+ }
+
return errorCode;
}
@@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont
return ErrorCode::NoError;
}
+ErrorCode ClientManagementModel::revokeXray(const int row,
+ const DockerContainer container,
+ const ServerCredentials &credentials,
+ const QSharedPointer &serverController)
+{
+ ErrorCode error = ErrorCode::NoError;
+
+ // Get server config
+ const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
+ const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error);
+ if (error != ErrorCode::NoError) {
+ logger.error() << "Failed to get the xray server config file";
+ return error;
+ }
+
+ QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8());
+ if (serverConfig.isNull()) {
+ logger.error() << "Failed to parse xray server config JSON";
+ return ErrorCode::InternalError;
+ }
+
+ // Get client ID to remove
+ auto client = m_clientsTable.at(row).toObject();
+ QString clientId = client.value(configKey::clientId).toString();
+
+ // Remove client from server config
+ QJsonObject configObj = serverConfig.object();
+ if (!configObj.contains("inbounds")) {
+ logger.error() << "Missing inbounds in xray config";
+ return ErrorCode::InternalError;
+ }
+
+ QJsonArray inbounds = configObj["inbounds"].toArray();
+ if (inbounds.isEmpty()) {
+ logger.error() << "Empty inbounds array in xray config";
+ return ErrorCode::InternalError;
+ }
+
+ QJsonObject inbound = inbounds[0].toObject();
+ if (!inbound.contains("settings")) {
+ logger.error() << "Missing settings in xray inbound config";
+ return ErrorCode::InternalError;
+ }
+
+ QJsonObject settings = inbound["settings"].toObject();
+ if (!settings.contains("clients")) {
+ logger.error() << "Missing clients in xray settings";
+ return ErrorCode::InternalError;
+ }
+
+ QJsonArray clients = settings["clients"].toArray();
+ if (clients.isEmpty()) {
+ logger.error() << "Empty clients array in xray config";
+ return ErrorCode::InternalError;
+ }
+
+ for (int i = 0; i < clients.size(); ++i) {
+ QJsonObject clientObj = clients[i].toObject();
+ if (clientObj.contains("id") && clientObj["id"].toString() == clientId) {
+ clients.removeAt(i);
+ break;
+ }
+ }
+
+ // Update server config
+ settings["clients"] = clients;
+ inbound["settings"] = settings;
+ inbounds[0] = inbound;
+ configObj["inbounds"] = inbounds;
+
+ // Upload updated config
+ error = serverController->uploadTextFileToContainer(
+ container,
+ credentials,
+ QJsonDocument(configObj).toJson(),
+ serverConfigPath
+ );
+ if (error != ErrorCode::NoError) {
+ logger.error() << "Failed to upload updated xray config";
+ return error;
+ }
+
+ // Remove from local table
+ beginRemoveRows(QModelIndex(), row, row);
+ m_clientsTable.removeAt(row);
+ endRemoveRows();
+
+ // Update clients table file on server
+ const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson();
+ QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
+ .arg(ContainerProps::containerTypeToString(container));
+
+ error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
+ if (error != ErrorCode::NoError) {
+ logger.error() << "Failed to upload the clientsTable file";
+ }
+
+ // Restart container
+ QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
+ error = serverController->runScript(
+ credentials,
+ serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container))
+ );
+ if (error != ErrorCode::NoError) {
+ logger.error() << "Failed to restart xray container";
+ return error;
+ }
+
+ return error;
+}
+
QHash ClientManagementModel::roleNames() const
{
QHash roles;
@@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const
roles[DataSentRole] = "dataSent";
roles[AllowedIpsRole] = "allowedIps";
return roles;
-}
+}
\ No newline at end of file
diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h
index 60132abe..989120a9 100644
--- a/client/ui/models/clientManagementModel.h
+++ b/client/ui/models/clientManagementModel.h
@@ -40,6 +40,8 @@ public slots:
const QSharedPointer &serverController);
ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig,
const QString &clientName, const QSharedPointer &serverController);
+ ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container,
+ const ServerCredentials &credentials, const QSharedPointer &serverController);
ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
const ServerCredentials &credentials, const QSharedPointer &serverController);
ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials,
@@ -64,11 +66,15 @@ private:
const QSharedPointer &serverController);
ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer &serverController);
+ ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials,
+ const QSharedPointer &serverController);
ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer &serverController, int &count);
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer &serverController, int &count);
+ ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
+ const QSharedPointer &serverController, int &count);
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
const QSharedPointer &serverController, std::vector &data);
diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp
index 7dd76b84..b72b10c3 100644
--- a/client/ui/models/servers_model.cpp
+++ b/client/ui/models/servers_model.cpp
@@ -22,7 +22,7 @@ namespace
constexpr char serviceProtocol[] = "service_protocol";
constexpr char publicKeyInfo[] = "public_key";
- constexpr char endDate[] = "end_date";
+ constexpr char expiresAt[] = "expires_at";
}
}
@@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent)
emit ServersModel::defaultServerNameChanged();
updateDefaultServerContainersModel();
});
+
+ connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
+ connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
}
int ServersModel::rowCount(const QModelIndex &parent) const
@@ -685,7 +688,7 @@ QVariant ServersModel::getProcessedServerData(const QString roleString)
return {};
}
-bool ServersModel::setProcessedServerData(const QString& roleString, const QVariant& value)
+bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value)
{
const auto roles = roleNames();
for (auto it = roles.begin(); it != roles.end(); it++) {
@@ -693,7 +696,7 @@ bool ServersModel::setProcessedServerData(const QString& roleString, const QVari
return setData(m_processedServerIndex, value, it.key());
}
}
-
+
return false;
}
@@ -736,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
- const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
- if (endDate.isEmpty()) {
- publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
+ const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
+ if (expiresAt.isEmpty()) {
+ publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
serverConfig.insert(configKey::apiConfig, apiConfig);
editServer(serverConfig, serverIndex);
@@ -746,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
return false;
}
- auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
- if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
+ auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
+ if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
return true;
}
return false;
diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h
index c15a5b51..78bc22cc 100644
--- a/client/ui/models/servers_model.h
+++ b/client/ui/models/servers_model.h
@@ -129,6 +129,9 @@ protected:
signals:
void processedServerIndexChanged(const int index);
+ // emitted when the processed server index or processed server data is changed
+ void processedServerChanged();
+
void defaultServerIndexChanged(const int index);
void defaultServerNameChanged();
void defaultServerDescriptionChanged();
diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml
index 482b5217..4277d735 100644
--- a/client/ui/qml/Controls2/CardWithIconsType.qml
+++ b/client/ui/qml/Controls2/CardWithIconsType.qml
@@ -169,6 +169,7 @@ Button {
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
+ enabled: root.enabled
onEntered: {
backgroundRect.color = root.hoveredColor
diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml
index dd38b0f4..dd097a1a 100644
--- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml
+++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml
@@ -51,8 +51,14 @@ PageType {
imageSource: "qrc:/images/controls/download.svg"
checked: index === ApiCountryModel.currentIndex
+ checkable: !ConnectionController.isConnected
onClicked: {
+ if (ConnectionController.isConnected) {
+ PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection"))
+ return
+ }
+
if (index !== ApiCountryModel.currentIndex) {
PageController.showBusyIndicator(true)
var prevIndex = ApiCountryModel.currentIndex
diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
index 73fdc551..db17196a 100644
--- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
+++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml
@@ -49,12 +49,15 @@ PageType {
}
LabelWithImageType {
+ property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable")
+
Layout.fillWidth: true
Layout.margins: 16
imageSource: "qrc:/images/controls/history.svg"
- leftText: qsTr("Work period")
- rightText: ApiServicesModel.getSelectedServiceData("workPeriod")
+ leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period")
+ rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate")
+ : ApiServicesModel.getSelectedServiceData("workPeriod")
visible: rightText !== ""
}
diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml
index e17cec8f..3172d31b 100644
--- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml
+++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml
@@ -25,7 +25,7 @@ PageType {
readonly property int pageSettingsApiServerInfo: 3
readonly property int pageSettingsApiLanguageList: 4
- property var server
+ property var processedServer
Connections {
target: PageController
@@ -35,10 +35,18 @@ PageType {
}
}
+ Connections {
+ target: ServersModel
+
+ function onProcessedServerChanged() {
+ root.processedServer = proxyServersModel.get(0)
+ }
+ }
+
SortFilterProxyModel {
id: proxyServersModel
objectName: "proxyServersModel"
-
+
sourceModel: ServersModel
filters: [
ValueFilter {
@@ -48,7 +56,7 @@ PageType {
]
Component.onCompleted: {
- root.server = proxyServersModel.get(0)
+ root.processedServer = proxyServersModel.get(0)
}
}
@@ -65,8 +73,8 @@ PageType {
objectName: "backButton"
backButtonFunction: function() {
- if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo
- && ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
+ if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo &&
+ root.processedServer.isCountrySelectionAvailable) {
nestedStackView.currentIndex = root.pageSettingsApiLanguageList
} else {
PageController.closePage()
@@ -83,18 +91,23 @@ PageType {
Layout.rightMargin: 16
Layout.bottomMargin: 10
- actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg"
+ actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg"
+ : "qrc:/images/controls/edit-3.svg"
- headerText: root.server.name
+ headerText: root.processedServer.name
descriptionText: {
- if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) {
- return ApiServicesModel.getSelectedServiceData("serviceDescription")
- } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) {
- return root.server.serverDescription
- } else if (ServersModel.isProcessedServerHasWriteAccess()) {
- return root.server.credentialsLogin + " · " + root.server.hostName
+ if (root.processedServer.isServerFromGatewayApi) {
+ if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
+ return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate")
+ } else {
+ return ApiServicesModel.getSelectedServiceData("serviceDescription")
+ }
+ } else if (root.processedServer.isServerFromTelegramApi) {
+ return root.processedServer.serverDescription
+ } else if (root.processedServer.hasWriteAccess) {
+ return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName
} else {
- return root.server.hostName
+ return root.processedServer.hostName
}
}
@@ -129,7 +142,7 @@ PageType {
Layout.fillWidth: true
headerText: qsTr("Server name")
- textFieldText: root.server.name
+ textFieldText: root.processedServer.name
textField.maximumLength: 30
checkEmptyText: true
}
@@ -146,9 +159,8 @@ PageType {
return
}
- if (serverName.textFieldText !== root.server.name) {
+ if (serverName.textFieldText !== root.processedServer.name) {
ServersModel.setProcessedServerData("name", serverName.textFieldText);
- root.server = proxyServersModel.get(0);
}
serverNameEditDrawer.closeTriggered()
}
@@ -238,6 +250,5 @@ PageType {
stackView: root.stackView
}
}
-
}
}
diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml
index 6ecfdc99..5e00eebd 100644
--- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml
+++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml
@@ -14,84 +14,83 @@ import "../Config"
PageType {
id: root
- FlickableType {
- id: fl
+ ColumnLayout {
+ id: header
+
anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ spacing: 0
+
+ BackButtonType {
+ id: backButton
+ Layout.topMargin: 20
+ }
+
+ HeaderType {
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.rightMargin: 16
+ Layout.leftMargin: 16
+ Layout.bottomMargin: 16
+
+ headerText: qsTr("VPN by Amnezia")
+ descriptionText: qsTr("Choose a VPN service that suits your needs.")
+ }
+ }
+
+ ListView {
+ id: servicesListView
+
+ anchors.top: header.bottom
+ anchors.right: parent.right
+ anchors.left: parent.left
anchors.bottom: parent.bottom
- contentHeight: content.height
+ anchors.topMargin: 16
+ spacing: 0
- ColumnLayout {
- id: content
+ property bool isFocusable: true
+ selectedIndex: 1
+ clip: true
+ model: ApiServicesModel
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.right: parent.right
+ ScrollBar.vertical: ScrollBar {}
- spacing: 0
+ delegate: Item {
+ implicitWidth: servicesListView.width
+ implicitHeight: delegateContent.implicitHeight
- BackButtonType {
- id: backButton
- Layout.topMargin: 20
- }
+ ColumnLayout {
+ id: delegateContent
- HeaderType {
- Layout.fillWidth: true
- Layout.topMargin: 8
- Layout.rightMargin: 16
- Layout.leftMargin: 16
- Layout.bottomMargin: 32
+ anchors.fill: parent
- headerText: qsTr("VPN by Amnezia")
- descriptionText: qsTr("Choose a VPN service that suits your needs.")
- }
+ CardWithIconsType {
+ id: card
- ListView {
- id: containers
- width: parent.width
- height: containers.contentItem.height
- spacing: 16
+ Layout.fillWidth: true
+ Layout.rightMargin: 16
+ Layout.leftMargin: 16
+ Layout.bottomMargin: 16
- property bool isFocusable: true
+ headerText: name
+ bodyText: cardDescription
+ footerText: price
- currentIndex: 1
- interactive: false
- model: ApiServicesModel
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
- delegate: Item {
- implicitWidth: containers.width
- implicitHeight: delegateContent.implicitHeight
+ enabled: isServiceAvailable
- ColumnLayout {
- id: delegateContent
-
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.right: parent.right
-
- CardWithIconsType {
- id: card
-
- Layout.fillWidth: true
- Layout.rightMargin: 16
- Layout.leftMargin: 16
-
- headerText: name
- bodyText: cardDescription
- footerText: price
-
- rightImageSource: "qrc:/images/controls/chevron-right.svg"
-
- onClicked: {
- if (isServiceAvailable) {
- ApiServicesModel.setServiceIndex(index)
- PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
- }
- }
-
- Keys.onEnterPressed: clicked()
- Keys.onReturnPressed: clicked()
+ onClicked: {
+ if (isServiceAvailable) {
+ ApiServicesModel.setServiceIndex(index)
+ PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
}
}
+
+ Keys.onEnterPressed: clicked()
+ Keys.onReturnPressed: clicked()
}
}
}
diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml
index 82fec030..2f17de84 100644
--- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml
+++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml
@@ -32,30 +32,6 @@ PageType {
property bool isFocusable: true
- Keys.onTabPressed: {
- FocusController.nextKeyTabItem()
- }
-
- Keys.onBacktabPressed: {
- FocusController.previousKeyTabItem()
- }
-
- Keys.onUpPressed: {
- FocusController.nextKeyUpItem()
- }
-
- Keys.onDownPressed: {
- FocusController.nextKeyDownItem()
- }
-
- Keys.onLeftPressed: {
- FocusController.nextKeyLeftItem()
- }
-
- Keys.onRightPressed: {
- FocusController.nextKeyRightItem()
- }
-
ScrollBar.vertical: ScrollBarType {}
model: variants
diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml
index a95fd78e..02a64bf6 100644
--- a/client/ui/qml/Pages2/PageShare.qml
+++ b/client/ui/qml/Pages2/PageShare.qml
@@ -90,7 +90,7 @@ PageType {
break
}
case PageShare.ConfigType.Xray: {
- ExportController.generateXrayConfig()
+ ExportController.generateXrayConfig(clientNameTextField.textFieldText)
shareConnectionDrawer.configCaption = qsTr("Save XRay config")
shareConnectionDrawer.configExtension = ".json"
shareConnectionDrawer.configFileName = "amnezia_for_xray"
diff --git a/deploy/data/linux/post_install.sh b/deploy/data/linux/post_install.sh
index b3345bac..324462d9 100755
--- a/deploy/data/linux/post_install.sh
+++ b/deploy/data/linux/post_install.sh
@@ -19,6 +19,11 @@ date > $LOG_FILE
echo "Script started" >> $LOG_FILE
sudo killall -9 $APP_NAME 2>> $LOG_FILE
+if command -v steamos-readonly &> /dev/null; then
+ sudo steamos-readonly disable >> $LOG_FILE
+ echo "steamos-readonly disabled" >> $LOG_FILE
+fi
+
if sudo systemctl is-active --quiet $APP_NAME; then
sudo systemctl stop $APP_NAME >> $LOG_FILE
sudo systemctl disable $APP_NAME >> $LOG_FILE
@@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE
echo "user desktop creation loop ended" >> $LOG_FILE
+if command -v steamos-readonly &> /dev/null; then
+ sudo steamos-readonly enable >> $LOG_FILE
+ echo "steamos-readonly enabled" >> $LOG_FILE
+fi
+
date >> $LOG_FILE
echo "Service status:" >> $LOG_FILE
sudo systemctl status $APP_NAME >> $LOG_FILE
diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh
index 5849a90e..98090d20 100755
--- a/deploy/data/linux/post_uninstall.sh
+++ b/deploy/data/linux/post_uninstall.sh
@@ -13,6 +13,11 @@ date >> $LOG_FILE
echo "Uninstall Script started" >> $LOG_FILE
sudo killall -9 $APP_NAME 2>> $LOG_FILE
+if command -v steamos-readonly &> /dev/null; then
+ sudo steamos-readonly disable >> $LOG_FILE
+ echo "steamos-readonly disabled" >> $LOG_FILE
+fi
+
ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do
sudo unlink $dir >> $LOG_FILE
done
@@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then
fi
+if command -v steamos-readonly &> /dev/null; then
+ sudo steamos-readonly enable >> $LOG_FILE
+ echo "steamos-readonly enabled" >> $LOG_FILE
+fi
+
date >> $LOG_FILE
echo "Service after uninstall status:" >> $LOG_FILE
sudo systemctl status $APP_NAME >> $LOG_FILE
diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png
deleted file mode 100644
index 6dedfa12..00000000
Binary files a/metadata/img-readme/apl.png and /dev/null differ
diff --git a/metadata/img-readme/download-alt.svg b/metadata/img-readme/download-alt.svg
new file mode 100644
index 00000000..f97c9c3d
--- /dev/null
+++ b/metadata/img-readme/download-alt.svg
@@ -0,0 +1,8 @@
+
diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg
new file mode 100644
index 00000000..386ae4fe
--- /dev/null
+++ b/metadata/img-readme/download-website-ru.svg
@@ -0,0 +1,8 @@
+
diff --git a/metadata/img-readme/download-website.svg b/metadata/img-readme/download-website.svg
new file mode 100644
index 00000000..d0cf8375
--- /dev/null
+++ b/metadata/img-readme/download-website.svg
@@ -0,0 +1,8 @@
+
diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png
deleted file mode 100644
index 0e6a8850..00000000
Binary files a/metadata/img-readme/download.png and /dev/null differ
diff --git a/metadata/img-readme/play.png b/metadata/img-readme/play.png
deleted file mode 100644
index 2fb316c8..00000000
Binary files a/metadata/img-readme/play.png and /dev/null differ