Files
amnezia-client/client/core/controllers/selfhosted/usersController.cpp
vkamn 847bb6923b refactor: refactor the application to the mvvm architecture (#2009)
* refactor: move business logic from servers model

* refactor: move containersModel initialization

* refactor: added protocol ui controller and removed settings class from protocols model

* refactor: moved cli management to separate controller

* refactor: moved app split to separate controller

* refactor: moved site split to separate controller

* refactor: moved allowed dns to separate controller

* refactor: moved language logic to separate ui controller

* refactor: removed Settings from devices model

* refactor: moved configs and services api logit to separate core controller

* refactor: added a layer with a repository between the storage and controllers

* refactor: use child parent system instead of smart pointers for controllers and models initialization

* refactor: moved install functions from server controller to install controller

* refactor: install controller refactoring

* chore: renamed exportController to exportUiController

* refactor: separate export controller

* refactor: removed VpnConfigurationsController

* chore: renamed ServerController to SshSession

* refactor: replaced ServerController to SshSession

* chore: moved qml controllers to separate folder

* chore: include fixes

* chore: moved utils from core root to core/utils

* chore: include fixes

* chore: rename core/utils files to camelCase foramt

* chore: include fixes

* chore: moved some utils to api and selfhosted folders

* chore: include fixes

* chore: remove unused file

* chore: moved serialization folder to core/utils

* chore: include fixes

* chore: moved some files from client root to core/utils

* chore: include fixes

* chore: moved ui utils to ui/utils folder

* chore: include fixes

* chore: move utils from root to ui/utils

* chore: include fixes

* chore: moved configurators to core/configurators

* chore: include fixes

* refactor: moved iap logic from ui controller to core

* refactor: moved remaining core logic from ApiConfigsController to SubscriptionController

* chore: rename apiNewsController to apiNewsUiController

* refactor: moved core logic from news ui controller to core

* chore: renamed apiConfigsController to subscriptionUiController

* chore: include fixes

* refactor: merge ApiSettingsController with SubscriptionUiController

* chore: moved ui selfhosted controllers to separate folder

* chore: include fixes

* chore: rename connectionController to connectiomUiController

* refactor: moved core logic from connectionUiController

* chore: rename settingsController to settingsUiController

* refactor: move core logic from settingsUiController

* refactor: moved core controller signal/slot connections to separate class

* fix: newsController fixes after refactoring

* chore: rename model to camelCase

* chore: include fixes

* chore: remove unused code

* chore: move selfhosted core to separate folder

* chore: include fixes

* chore: rename importController to importUiController

* refactor: move core logic from importUiController

* chore: minor fixes

* chore: remove prem v1 migration

* refactor: remove openvpn over cloak and openvpn over shadowsocks

* refactor: removed protocolsForContainer function

* refactor: add core models

* refactor: replace json with c++ structs for server config

* refactor: move getDnsPair to ServerConfigUtils

* feat: add admin selfhosted config export test

* feat: add multi import test

* refactor: use coreController for tests

* feat: add few simple tests

* chore: qrepos in all core controllers

* feat: add test for settings

* refactor: remove repo dependency from configurators

* chore: moved protocols to core folder

* chore: include fixes

* refactor: moved containersDefs, defs, apiDefs, protocolsDefs to different places

* chore: include fixes

* chore: build fixes

* chore: build fixes

* refactor: remove q repo and interface repo

* feat: add test for ui servers model and controller

* chore: renamed to camelCase

* chore: include fixes

* refactor: moved core logic from sites ui controller

* fix: fixed api config processing

* fix: fixed processed server index processing

* refactor: protocol models now use c++ structs instead of json configs

* refactor: servers model now use c++ struct instead of json config

* fix: fixed default server index processing

* fix: fix logs init

* fix: fix secure settings load keys

* chore: build fixes

* fix: fixed clear settings

* fix: fixed restore backup

* fix: sshSession usage

* fix: fixed export functions signatures

* fix: return missing part from buildContainerWorker

* fix: fixed server description on page home

* refactor: add container config helpers functions

* refactor: c++ structs instead of json

* chore: add dns protocol config struct

* refactor: move config utils functions to config structs

* feat: add test for selfhosted server setup

* refactor: separate resources.qrc

* fix: fixed server rename

* chore: return nameOverriddenByUser

* fix: build fixes

* fix: fixed models init

* refactor: cleanup models usage

* fix: fixed models init

* chore: cleanup connections and functions signatures

* chore: cleanup updateModel calls

* feat: added cache to servers repo

* chore: cleanup unused functions

* chore: ssxray processing

* chore: remove transportProtoWithDefault and portWithDefault functions

* chore: removed proto types any and l2tp

* refactor: moved some constants

* fix: fixed native configs export

* refactor: remove json from processConfigWith functions

* fix: fixed processed server index usage

* fix: qml warning fixes

* chore: merge fixes

* chore: update tests

* fix: fixed xray config processing

* fix: fixed split tunneling processing

* chore: rename sites controllers and model

* chore: rename fixes

* chore: minor fixes

* chore: remove ability to load backup from "file with connection settings" button

* fix: fixed api device revoke

* fix: remove full model update when renaming a user

* fix: fixed premium/free server rename

* fix: fixed selfhosted new server install

* fix: fixed updateContainer function

* fix: fixed revoke for external premium configs

* feat: add native configs qr processing

* chore: codestyle fixes

* fix: fixed admin config create

* chore: again remove ability to load backup from "file with connection settings" button

* chore: minor fixes

* fix: fixed variables initialization

* fix: fixed qml imports

* fix: minor fixes

* fix: fix vpnConnection function calls

* feat: add buckup error handling

* fix: fixed admin config revok

* fix: fixed selfhosted awg installation

* fix: ad visability

* feat: add empty check for primary dns

* chore: minor fixes
2026-04-30 14:53:03 +08:00

808 lines
31 KiB
C++

#include "usersController.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QDateTime>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
#include "logger.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/models/serverConfig.h"
#include "core/models/containerConfig.h"
using namespace amnezia;
namespace
{
Logger logger("UsersController");
}
UsersController::UsersController(SecureServersRepository* serversRepository, QObject *parent)
: QObject(parent),
m_serversRepository(serversRepository)
{
}
bool UsersController::isClientExists(const QString &clientId, const QJsonArray &clientsTable)
{
for (const QJsonValue &value : std::as_const(clientsTable)) {
if (value.isObject()) {
QJsonObject obj = value.toObject();
if (obj.contains(configKey::clientId) && obj[configKey::clientId].toString() == clientId) {
return true;
}
}
}
return false;
}
int UsersController::clientIndexById(const QString &clientId, const QJsonArray &clientsTable)
{
for (int i = 0; i < clientsTable.size(); ++i) {
if (clientsTable.at(i).isObject()) {
QJsonObject obj = clientsTable.at(i).toObject();
if (obj.contains(configKey::clientId) && obj[configKey::clientId].toString() == clientId) {
return i;
}
}
}
return -1;
}
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
{
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
for (auto &clientId : clientsTableObj.keys()) {
QJsonObject client;
client[configKey::clientId] = clientId;
QJsonObject userData;
userData[configKey::clientName] = clientsTableObj.value(clientId).toObject().value(configKey::clientName);
client[configKey::userData] = userData;
clientsTable.push_back(client);
}
}
ErrorCode UsersController::wgShow(const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, std::vector<WgShowData> &data)
{
if (container != DockerContainer::WireGuard && !ContainerUtils::isAwgContainer(container)) {
return ErrorCode::NoError;
}
ErrorCode error = ErrorCode::NoError;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
QString showBin = (container == DockerContainer::Awg2)
? QStringLiteral("awg")
: QStringLiteral("wg");
const QString command = QString("sudo docker exec -i $CONTAINER_NAME bash -c '%1 show all'").arg(showBin);
QString script = sshSession->replaceVars(command, amnezia::genBaseVars(credentials, container, QString(), QString()));
error = sshSession->runScript(credentials, script, cbReadStdOut);
if (error != ErrorCode::NoError) {
logger.error() << QString("Failed to execute %1 show command").arg(showBin);
return error;
}
if (stdOut.isEmpty()) {
return error;
}
const auto getStrValue = [](const auto str) { return str.mid(str.indexOf(":") + 1).trimmed(); };
const auto parts = stdOut.split('\n');
const auto peerList = parts.filter("peer:");
const auto latestHandshakeList = parts.filter("latest handshake:");
const auto transferredDataList = parts.filter("transfer:");
const auto allowedIpsList = parts.filter("allowed ips:");
if (allowedIpsList.isEmpty() || latestHandshakeList.isEmpty() || transferredDataList.isEmpty() || peerList.isEmpty()) {
return error;
}
const auto changeHandshakeFormat = [](QString &latestHandshake) {
const std::vector<std::pair<QString, QString>> replaceMap = { { " days", "d" }, { " hours", "h" }, { " minutes", "m" },
{ " seconds", "s" }, { " day", "d" }, { " hour", "h" },
{ " minute", "m" }, { " second", "s" } };
for (const auto &item : replaceMap) {
latestHandshake.replace(item.first, item.second);
}
};
for (int i = 0; i < peerList.size() && i < transferredDataList.size() && i < latestHandshakeList.size() && i < allowedIpsList.size(); ++i) {
const auto transferredData = getStrValue(transferredDataList[i]).split(",");
auto latestHandshake = getStrValue(latestHandshakeList[i]);
auto serverBytesReceived = transferredData.front().trimmed();
auto serverBytesSent = transferredData.back().trimmed();
auto allowedIps = getStrValue(allowedIpsList[i]);
changeHandshakeFormat(latestHandshake);
serverBytesReceived.chop(QStringLiteral(" received").length());
serverBytesSent.chop(QStringLiteral(" sent").length());
data.push_back({ getStrValue(peerList[i]), latestHandshake, serverBytesSent, serverBytesReceived, allowedIps });
}
return error;
}
ErrorCode UsersController::getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable)
{
ErrorCode error = ErrorCode::NoError;
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
const QString getOpenVpnClientsList = "sudo docker exec -i $CONTAINER_NAME bash -c 'ls /opt/amnezia/openvpn/pki/issued'";
QString script = sshSession->replaceVars(getOpenVpnClientsList, amnezia::genBaseVars(credentials, container, QString(), QString()));
error = sshSession->runScript(credentials, script, cbReadStdOut);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to retrieve the list of issued certificates on the server";
return error;
}
if (!stdOut.isEmpty()) {
QStringList certsIds = stdOut.split("\n", Qt::SkipEmptyParts);
certsIds.removeAll("AmneziaReq.crt");
for (auto &openvpnCertId : certsIds) {
openvpnCertId.replace(".crt", "");
if (!isClientExists(openvpnCertId, clientsTable)) {
QJsonObject client;
client[configKey::clientId] = openvpnCertId;
QJsonObject userData;
userData[configKey::clientName] = QString("Client %1").arg(count);
client[configKey::userData] = userData;
clientsTable.push_back(client);
count++;
}
}
}
return error;
}
ErrorCode UsersController::getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable)
{
ErrorCode error = ErrorCode::NoError;
QString configPath;
if (container == DockerContainer::Awg) {
configPath = QString::fromLatin1(protocols::awg::serverLegacyConfigPath);
} else if (container == DockerContainer::Awg2) {
configPath = QString::fromLatin1(protocols::awg::serverConfigPath);
} else {
configPath = QString::fromLatin1(protocols::wireguard::serverConfigPath);
}
const QString wireguardConfigString = sshSession->getTextFileFromContainer(container, credentials, configPath, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the wg conf file from the server";
return error;
}
auto configLines = wireguardConfigString.split("\n", Qt::SkipEmptyParts);
QStringList wireguardKeys;
for (const auto &line : configLines) {
auto configPair = line.split(" = ", Qt::SkipEmptyParts);
if (configPair.front() == "PublicKey") {
wireguardKeys.push_back(configPair.back());
}
}
for (auto &wireguardKey : wireguardKeys) {
if (!isClientExists(wireguardKey, clientsTable)) {
QJsonObject client;
client[configKey::clientId] = wireguardKey;
QJsonObject userData;
userData[configKey::clientName] = QString("Client %1").arg(count);
client[configKey::userData] = userData;
clientsTable.push_back(client);
count++;
}
}
return error;
}
ErrorCode UsersController::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable)
{
ErrorCode error = ErrorCode::NoError;
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
const QString configString = sshSession->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(protocols::xray::inbounds) || serverConfig.object()[protocols::xray::inbounds].toArray().isEmpty()) {
logger.error() << "Invalid xray server config structure";
return ErrorCode::InternalError;
}
const QJsonObject inbound = serverConfig.object()[protocols::xray::inbounds].toArray()[0].toObject();
if (!inbound.contains(protocols::xray::settings)) {
logger.error() << "Missing settings in xray inbound config";
return ErrorCode::InternalError;
}
const QJsonObject settings = inbound[protocols::xray::settings].toObject();
if (!settings.contains(protocols::xray::clients)) {
logger.error() << "Missing clients in xray settings config";
return ErrorCode::InternalError;
}
const QJsonArray clients = settings[protocols::xray::clients].toArray();
for (const auto &clientValue : clients) {
const QJsonObject clientObj = clientValue.toObject();
if (!clientObj.contains(protocols::xray::id)) {
logger.error() << "Missing id in xray client config";
continue;
}
QString clientId = clientObj[protocols::xray::id].toString();
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
xrayDefaultUuid.replace("\n", "");
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
QJsonObject client;
client[configKey::clientId] = clientId;
QJsonObject userData;
userData[configKey::clientName] = QString("Client %1").arg(count);
client[configKey::userData] = userData;
clientsTable.push_back(client);
count++;
}
}
return error;
}
ErrorCode UsersController::updateClients(int serverIndex, const DockerContainer container)
{
ErrorCode error = ErrorCode::NoError;
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable");
if (container == DockerContainer::OpenVpn) {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(DockerContainer::OpenVpn));
} else {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(container));
}
const QByteArray clientsTableString = sshSession.getTextFileFromContainer(container, credentials, clientsTableFile, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the clientsTable file from the server";
emit clientsUpdated(QJsonArray());
return error;
}
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
if (m_clientsTable.isEmpty()) {
migration(clientsTableString, m_clientsTable);
int count = 0;
if (container == DockerContainer::OpenVpn) {
error = getOpenVpnClients(container, credentials, &sshSession, count, m_clientsTable);
} else if (container == DockerContainer::WireGuard || ContainerUtils::isAwgContainer(container)) {
error = getWireGuardClients(container, credentials, &sshSession, count, m_clientsTable);
} else if (container == DockerContainer::Xray) {
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
}
if (error != ErrorCode::NoError) {
emit clientsUpdated(QJsonArray());
return error;
}
const QByteArray newClientsTableString = QJsonDocument(m_clientsTable).toJson();
if (clientsTableString != newClientsTableString) {
error = sshSession.uploadTextFileToContainer(container, credentials, newClientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file to the server";
}
}
}
std::vector<WgShowData> data;
wgShow(container, credentials, &sshSession, data);
for (const auto &client : data) {
int i = 0;
for (const auto &it : std::as_const(m_clientsTable)) {
if (it.isObject()) {
QJsonObject obj = it.toObject();
if (obj.contains(configKey::clientId) && obj[configKey::clientId].toString() == client.clientId) {
QJsonObject userData = obj[configKey::userData].toObject();
if (!client.latestHandshake.isEmpty()) {
userData[configKey::latestHandshake] = client.latestHandshake;
}
if (!client.dataReceived.isEmpty()) {
userData[configKey::dataReceived] = client.dataReceived;
}
if (!client.dataSent.isEmpty()) {
userData[configKey::dataSent] = client.dataSent;
}
if (!client.allowedIps.isEmpty()) {
userData[configKey::allowedIps] = client.allowedIps;
}
obj[configKey::userData] = userData;
m_clientsTable.replace(i, obj);
break;
}
}
++i;
}
}
emit clientsUpdated(m_clientsTable);
return error;
}
ErrorCode UsersController::appendClient(int serverIndex, const QString &clientId, const QString &clientName, const DockerContainer container)
{
ErrorCode error = ErrorCode::NoError;
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
error = updateClients(serverIndex, container);
if (error != ErrorCode::NoError) {
return error;
}
int existingIndex = clientIndexById(clientId, m_clientsTable);
if (existingIndex >= 0) {
return renameClient(serverIndex, existingIndex, clientName, container, true);
}
QJsonObject client;
client[configKey::clientId] = clientId;
QJsonObject userData;
userData[configKey::clientName] = clientName;
userData[configKey::creationDate] = QDateTime::currentDateTime().toString();
client[configKey::userData] = userData;
m_clientsTable.push_back(client);
const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson();
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable");
if (container == DockerContainer::OpenVpn) {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(DockerContainer::OpenVpn));
} else {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(container));
}
error = sshSession.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
emit clientAdded(client);
emit clientsUpdated(m_clientsTable);
return error;
}
ErrorCode UsersController::renameClient(int serverIndex, const int row, const QString &clientName,
const DockerContainer container, bool addTimeStamp)
{
if (row < 0 || row >= m_clientsTable.size()) {
return ErrorCode::InternalError;
}
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
auto client = m_clientsTable.at(row).toObject();
auto userData = client[configKey::userData].toObject();
userData[configKey::clientName] = clientName;
if (addTimeStamp) {
userData[configKey::creationDate] = QDateTime::currentDateTime().toString();
}
client[configKey::userData] = userData;
m_clientsTable.replace(row, client);
const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson();
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable");
if (container == DockerContainer::OpenVpn) {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(DockerContainer::OpenVpn));
} else {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(container));
}
ErrorCode error = sshSession.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
if (addTimeStamp) {
emit clientsUpdated(m_clientsTable);
} else {
emit clientRenamed(row, clientName);
}
return error;
}
ErrorCode UsersController::revokeOpenVpn(const int row, const DockerContainer container, const ServerCredentials &credentials,
const int serverIndex, SshSession* sshSession, QJsonArray &clientsTable)
{
if (row < 0 || row >= clientsTable.size()) {
return ErrorCode::InternalError;
}
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
const QString getOpenVpnCertData = QString("sudo docker exec -i $CONTAINER_NAME bash -c '"
"cd /opt/amnezia/openvpn ;\\"
"easyrsa revoke %1 ;\\"
"easyrsa gen-crl ;\\"
"chmod 666 pki/crl.pem ;\\"
"cp pki/crl.pem .'")
.arg(clientId);
const QString script = sshSession->replaceVars(getOpenVpnCertData, amnezia::genBaseVars(credentials, container, QString(), QString()));
ErrorCode error = sshSession->runScript(credentials, script);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to revoke the certificate";
return error;
}
clientsTable.removeAt(row);
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable");
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(DockerContainer::OpenVpn));
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
return ErrorCode::NoError;
}
ErrorCode UsersController::revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, QJsonArray &clientsTable)
{
if (row < 0 || row >= clientsTable.size()) {
return ErrorCode::InternalError;
}
ErrorCode error = ErrorCode::NoError;
QString configPath;
if (container == DockerContainer::Awg) {
configPath = QString::fromLatin1(protocols::awg::serverLegacyConfigPath);
} else if (container == DockerContainer::Awg2) {
configPath = QString::fromLatin1(protocols::awg::serverConfigPath);
} else {
configPath = QString::fromLatin1(protocols::wireguard::serverConfigPath);
}
const QString wireguardConfigString = sshSession->getTextFileFromContainer(container, credentials, configPath, error);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to get the wg conf file from the server";
return error;
}
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
auto configSections = wireguardConfigString.split("[", Qt::SkipEmptyParts);
for (auto &section : configSections) {
if (section.contains(clientId)) {
configSections.removeOne(section);
break;
}
}
QString newWireGuardConfig = configSections.join("[");
newWireGuardConfig.insert(0, "[");
error = sshSession->uploadTextFileToContainer(container, credentials, newWireGuardConfig, configPath);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the wg conf file to the server";
return error;
}
clientsTable.removeAt(row);
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable");
if (container == DockerContainer::OpenVpn) {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(DockerContainer::OpenVpn));
} else {
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(container));
}
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
bool isAwg2 = (container == DockerContainer::Awg2);
QString command = isAwg2 ? QStringLiteral("awg") : QStringLiteral("wg");
QString iface = isAwg2 ? QStringLiteral("awg0") : QStringLiteral("wg0");
QString script = QString(
"sudo docker exec -i $CONTAINER_NAME bash -c '%1 syncconf %2 <(%1-quick strip %3)'"
).arg(command, iface, configPath);
error = sshSession->runScript(
credentials,
sshSession->replaceVars(script, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
logger.error() << QString("Failed to execute command '%1 syncconf %2' on the server").arg(command, iface);
return error;
}
return ErrorCode::NoError;
}
ErrorCode UsersController::revokeXray(const int row,
const DockerContainer container,
const ServerCredentials &credentials,
SshSession* sshSession, QJsonArray &clientsTable)
{
if (row < 0 || row >= clientsTable.size()) {
return ErrorCode::InternalError;
}
ErrorCode error = ErrorCode::NoError;
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
const QString configString = sshSession->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;
}
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
QJsonObject configObj = serverConfig.object();
if (!configObj.contains(protocols::xray::inbounds)) {
logger.error() << "Missing inbounds in xray config";
return ErrorCode::InternalError;
}
QJsonArray inbounds = configObj[protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Empty inbounds array in xray config";
return ErrorCode::InternalError;
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(protocols::xray::settings)) {
logger.error() << "Missing settings in xray inbound config";
return ErrorCode::InternalError;
}
QJsonObject settings = inbound[protocols::xray::settings].toObject();
if (!settings.contains(protocols::xray::clients)) {
logger.error() << "Missing clients in xray settings";
return ErrorCode::InternalError;
}
QJsonArray clients = settings[protocols::xray::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(protocols::xray::id) && clientObj[protocols::xray::id].toString() == clientId) {
clients.removeAt(i);
break;
}
}
settings[protocols::xray::clients] = clients;
inbound[protocols::xray::settings] = settings;
inbounds[0] = inbound;
configObj[protocols::xray::inbounds] = inbounds;
error = sshSession->uploadTextFileToContainer(
container,
credentials,
QJsonDocument(configObj).toJson(),
serverConfigPath
);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload updated xray config";
return error;
}
clientsTable.removeAt(row);
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
.arg(ContainerUtils::containerTypeToString(container));
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to upload the clientsTable file";
}
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = sshSession->runScript(
credentials,
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
logger.error() << "Failed to restart xray container";
return error;
}
return error;
}
ErrorCode UsersController::revokeClient(int serverIndex, const int index, const DockerContainer container)
{
if (index < 0 || index >= m_clientsTable.size()) {
return ErrorCode::InternalError;
}
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
QString clientId = m_clientsTable.at(index).toObject().value(configKey::clientId).toString();
ErrorCode errorCode = ErrorCode::NoError;
switch(container)
{
case DockerContainer::OpenVpn: {
errorCode = revokeOpenVpn(index, container, credentials, serverIndex, &sshSession, m_clientsTable);
break;
}
case DockerContainer::WireGuard:
case DockerContainer::Awg:
case DockerContainer::Awg2: {
errorCode = revokeWireGuard(index, container, credentials, &sshSession, m_clientsTable);
break;
}
case DockerContainer::Xray: {
errorCode = revokeXray(index, container, credentials, &sshSession, m_clientsTable);
break;
}
default: {
logger.error() << "Internal error: received unexpected container type";
return ErrorCode::InternalError;
}
}
if (errorCode == ErrorCode::NoError) {
ServerConfig serverConfig = m_serversRepository->server(serverIndex);
ContainerConfig containerCfg = m_serversRepository->containerConfig(serverIndex, container);
QString containerClientId = containerCfg.protocolConfig.clientId();
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
emit adminConfigRevoked(serverIndex, container);
}
emit clientRevoked(index);
emit clientsUpdated(m_clientsTable);
}
return errorCode;
}
ErrorCode UsersController::revokeClient(int serverIndex, const ContainerConfig &containerConfig, const DockerContainer container)
{
SshSession sshSession;
ServerCredentials credentials = m_serversRepository->serverCredentials(serverIndex);
ErrorCode errorCode = ErrorCode::NoError;
errorCode = updateClients(serverIndex, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
Proto protocol = containerConfig.getProtocolType();
switch(container)
{
case DockerContainer::OpenVpn:
case DockerContainer::WireGuard:
case DockerContainer::Awg:
case DockerContainer::Awg2:
case DockerContainer::Xray: {
protocol = ContainerUtils::defaultProtocol(container);
break;
}
default: {
logger.error() << "Internal error: received unexpected container type";
return ErrorCode::InternalError;
}
}
QString clientId = containerConfig.protocolConfig.clientId();
int row = clientIndexById(clientId, m_clientsTable);
if (row < 0) {
return errorCode;
}
switch (container)
{
case DockerContainer::OpenVpn: {
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, &sshSession, m_clientsTable);
break;
}
case DockerContainer::WireGuard:
case DockerContainer::Awg:
case DockerContainer::Awg2: {
errorCode = revokeWireGuard(row, container, credentials, &sshSession, m_clientsTable);
break;
}
case DockerContainer::Xray: {
errorCode = revokeXray(row, container, credentials, &sshSession, m_clientsTable);
break;
}
default:
logger.error() << "Internal error: received unexpected container type";
return ErrorCode::InternalError;
}
if (errorCode == ErrorCode::NoError) {
emit adminConfigRevoked(serverIndex, container);
emit clientRevoked(row);
emit clientsUpdated(m_clientsTable);
}
return errorCode;
}