Skip to content

Commit

Permalink
feature/xray user management (#972)
Browse files Browse the repository at this point in the history
* feature: implement client management functionality for Xray

---------

Co-authored-by: aiamnezia <[email protected]>
Co-authored-by: vladimir.kuznetsov <[email protected]>
  • Loading branch information
3 people authored Dec 10, 2024
1 parent 2db9971 commit d06924c
Show file tree
Hide file tree
Showing 7 changed files with 485 additions and 36 deletions.
147 changes: 139 additions & 8 deletions client/configurators/xray_configurator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,169 @@
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include "logger.h"

#include "containers/containers_defs.h"
#include "core/controllers/serverController.h"
#include "core/scripts_registry.h"

namespace {
Logger logger("XrayConfigurator");
}

XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent)
: ConfiguratorBase(settings, serverController, parent)
{
}

QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode)
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
// Generate new UUID for client
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);

// Get current server config
QString currentConfig = m_serverController->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);

if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return "";
}

// Parse current config as JSON
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
errorCode = ErrorCode::InternalError;
return "";
}

QJsonObject serverConfig = doc.object();

// Validate server config structure
if (!serverConfig.contains("inbounds")) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
}

QJsonArray inbounds = serverConfig["inbounds"].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
}

QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains("settings")) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
}

QJsonObject settings = inbound["settings"].toObject();
if (!settings.contains("clients")) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
}

QJsonArray clients = settings["clients"].toArray();

// Create configuration for new client
QJsonObject clientConfig {
{"id", clientId},
{"flow", "xtls-rprx-vision"}
};

clients.append(clientConfig);

// Update config
settings["clients"] = clients;
inbound["settings"] = settings;
inbounds[0] = inbound;
serverConfig["inbounds"] = inbounds;

// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_serverController->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
}

// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_serverController->runScript(
credentials,
m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container))
);

if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
}

return clientId;
}

QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
// Get client ID from prepareServerConfig
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
errorCode = ErrorCode::InternalError;
return "";
}

QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));

if (config.isEmpty()) {
logger.error() << "Failed to get config template";
errorCode = ErrorCode::InternalError;
return "";
}

QString xrayPublicKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
errorCode = ErrorCode::InternalError;
return "";
}
xrayPublicKey.replace("\n", "");

QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode);
xrayUuid.replace("\n", "");


QString xrayShortId =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
errorCode = ErrorCode::InternalError;
return "";
}
xrayShortId.replace("\n", "");

if (errorCode != ErrorCode::NoError) {
// Validate all required variables are present
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
logger.error() << "Config template missing required variables:"
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
errorCode = ErrorCode::InternalError;
return "";
}

config.replace("$XRAY_CLIENT_ID", xrayUuid);
config.replace("$XRAY_CLIENT_ID", xrayClientId);
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
config.replace("$XRAY_SHORT_ID", xrayShortId);

Expand Down
4 changes: 4 additions & 0 deletions client/configurators/xray_configurator.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class XrayConfigurator : public ConfiguratorBase

QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode);

private:
QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode &errorCode);
};

#endif // XRAY_CONFIGURATOR_H
9 changes: 4 additions & 5 deletions client/ui/controllers/exportController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container

jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();

if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) {
auto clientId = jsonNativeConfig.value(config_key::clientId).toString();
errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController);
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) {
errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController);
}
return errorCode;
}
Expand Down Expand Up @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig()
emit exportConfigChanged();
}

void ExportController::generateXrayConfig()
void ExportController::generateXrayConfig(const QString &clientName)
{
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig);
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig);
if (errorCode) {
emit exportErrorOccurred(errorCode);
return;
Expand Down
2 changes: 1 addition & 1 deletion client/ui/controllers/exportController.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public slots:
void generateAwgConfig(const QString &clientName);
void generateShadowSocksConfig();
void generateCloakConfig();
void generateXrayConfig();
void generateXrayConfig(const QString &clientName);

QString getConfig();
QString getNativeConfigString();
Expand Down
Loading

0 comments on commit d06924c

Please sign in to comment.