Compare commits

...

7 Commits

Author SHA1 Message Date
MrMirDan 470c736571 update: changes after merging with dev 2026-06-16 18:44:15 +03:00
MrMirDan de5205aa6f Merge branch 'dev' into feat/xray-subscription-link 2026-06-16 17:14:05 +03:00
MrMirDan 6d121776a3 fix: config reload 2026-05-20 12:08:51 +03:00
MrMirDan 940b3a2f2b fix: fixes after merge with dev 2026-05-18 17:38:45 +03:00
MrMirDan 34a63e7d11 Merge branch 'dev' into feat/xray-subscription-link 2026-05-18 11:58:58 +03:00
MrMirDan 73e0c9b92e update: separate XRaySubscriptionConfig from NativeServerConfig and fixed some bugs 2026-05-16 12:22:26 +03:00
vkamn 476f16d027 feat: add xray subscription link support 2026-05-15 16:32:47 +08:00
31 changed files with 1061 additions and 13 deletions
@@ -11,6 +11,9 @@
#include <QRegularExpressionMatch>
#include <QRegularExpressionMatchIterator>
#include <QUrl>
#include <QUrlQuery>
#include <QEventLoop>
#include <QTimer>
#include <algorithm>
#include "core/utils/containerEnum.h"
@@ -377,6 +380,209 @@ int ImportController::qrChunksTotal() const
return m_totalQrCodeChunksCount;
}
ImportController::ImportResult ImportController::importLink(const QUrl &url)
{
ImportResult result;
if (!url.isValid()) {
qWarning() << "Invalid URL:" << url;
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QNetworkAccessManager manager;
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
bool timedOut = false;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(&timer, &QTimer::timeout, &loop, [&]() {
timedOut = true;
reply->abort();
loop.quit();
});
timer.start(10000);
loop.exec();
if (timedOut) {
qWarning() << "Request timed out";
reply->deleteLater();
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Network error:" << reply->errorString();
reply->deleteLater();
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QByteArray data = reply->readAll();
reply->deleteLater();
if (data.isEmpty()) {
qWarning() << "Empty response";
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QByteArray decoded;
QString text;
if (isValidBase64(data)) {
decoded = QByteArray::fromBase64(data);
text = QString::fromUtf8(decoded).trimmed();
} else {
data.replace('\r', "");
text = QString::fromUtf8(data).trimmed();
}
if (text.isEmpty()) {
qWarning() << "Decoded text is empty";
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QStringList configs = text.split('\n', Qt::SkipEmptyParts);
QJsonArray configStrings;
QJsonArray configNames;
for (const QString &cfg : configs) {
bool supported = true;
if (!(cfg.startsWith("vless://") || cfg.startsWith("vmess://") || cfg.startsWith("trojan://")
|| cfg.startsWith("ss://") || cfg.startsWith("ssd://"))) {
supported = false;
qWarning() << "Unknown protocol:" << cfg.left(20);
continue;
}
QUrl url(cfg);
QUrlQuery query(url);
QString name = QUrl::fromPercentEncoding(url.fragment().toUtf8());
name.isEmpty() ? name = "Unnamed" : name = "[" + name.replace(" /", "]");
if (!name.contains("v2ray") && supported) {
configStrings.append(cfg);
configNames.append(name);
} else {
qWarning() << "Config unsupported";
}
}
if (configStrings.isEmpty()) {
qWarning() << "No valid configs found";
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QString firstConfig = configStrings.first().toString();
result = extractConfigFromData(firstConfig);
QJsonObject serverConfig;
for (auto it = result.config.begin(); it != result.config.end(); ++it) {
serverConfig.insert(it.key(), it.value());
}
serverConfig.insert(configKey::description, m_serversRepository->nextAvailableServerName());
serverConfig[configKey::xraySubscriptionLink] = url.toString();
serverConfig[configKey::xraySubscriptionConfig] = configStrings;
serverConfig[configKey::xraySubscriptionConfigName] = configNames;
serverConfig[configKey::xraySubscriptionConfigCurrent] = 0;
result.config = serverConfig;
return result;
}
ImportController::ImportResult ImportController::editServerConfigWithData(const QString &serverId, QString data, const QJsonObject& uiConfig)
{
ImportResult result = extractConfigFromData(data);
if (result.errorCode != ErrorCode::NoError)
return result;
QJsonObject editedConfig = result.config;
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
if (!cfg.has_value()) {
result.errorCode = ErrorCode::ImportInvalidConfigError;
break;
}
QJsonObject currentConfig = cfg->toJson();
for (auto it = uiConfig.begin(); it != uiConfig.end(); ++it) {
editedConfig.insert(it.key(), it.value());
}
editedConfig.insert(configKey::description, currentConfig.value(configKey::description));
editedConfig.insert(configKey::xraySubscriptionLink, currentConfig.value(configKey::xraySubscriptionLink));
editedConfig.insert(configKey::xraySubscriptionConfig, currentConfig.value(configKey::xraySubscriptionConfig));
editedConfig.insert(configKey::xraySubscriptionConfigName, currentConfig.value(configKey::xraySubscriptionConfigName));
editedConfig.insert(configKey::xraySubscriptionConfigCurrent, currentConfig.value(configKey::xraySubscriptionConfigCurrent));
m_serversRepository->editServer(serverId, editedConfig, kind);
break;
}
case serverConfigUtils::ConfigType::Invalid:
default: result.errorCode = ErrorCode::ImportInvalidConfigError;
}
result.config = editedConfig;
return result;
}
bool ImportController::isValidBase64(const QByteArray &input)
{
QByteArray data = input;
data = data.trimmed();
if (data.isEmpty())
return false;
static QRegularExpression base64Regex("^[A-Za-z0-9+/=_\\r\\n-]+$");
if (!base64Regex.match(QString::fromLatin1(data)).hasMatch())
return false;
data.replace("\r", "");
data.replace("\n", "");
if (data.size() % 4 != 0)
return false;
QByteArray decoded = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding);
if (decoded.isEmpty())
decoded = QByteArray::fromBase64(data);
return !decoded.isEmpty();
}
void ImportController::importConfig(const QJsonObject &config)
{
ServerCredentials credentials;
@@ -63,6 +63,10 @@ public:
int qrChunksReceived() const;
int qrChunksTotal() const;
ImportResult importLink(const QUrl &url);
ImportResult editServerConfigWithData(const QString &serverId, QString data, const QJsonObject &uiConfig);
bool isValidBase64(const QByteArray &input);
void importConfig(const QJsonObject &config);
QJsonObject processNativeWireGuardConfig(const QJsonObject &config);
+126 -7
View File
@@ -64,6 +64,13 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
@@ -118,6 +125,13 @@ void ServersController::setDefaultContainer(const QString &serverId, DockerConta
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
if (!cfg.has_value()) return;
cfg->defaultContainer = container;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
@@ -135,6 +149,88 @@ void ServersController::setDefaultContainer(const QString &serverId, DockerConta
}
}
QString ServersController::getSubLink(const QString &serverId) const
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->subLink : QString();
}
case serverConfigUtils::ConfigType::Invalid:
default: return QString();
}
}
QString ServersController::getConfigString(const QString &serverId, const int index) const
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->configString.at(index).toString() : QString();
}
case serverConfigUtils::ConfigType::Invalid:
default: return QString();
}
}
QString ServersController::getConfigName(const QString &serverId, const int index) const
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->configName.at(index).toString() : QString();
}
case serverConfigUtils::ConfigType::Invalid:
default: return QString();
}
}
QJsonArray ServersController::getConfigNames(const QString &serverId) const
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->configName : QJsonArray();
}
case serverConfigUtils::ConfigType::Invalid:
default: return QJsonArray();
}
}
int ServersController::getCurrentConfigIndex(const QString &serverId) const
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->currentConfig : int();
}
case serverConfigUtils::ConfigType::Invalid:
default: return int();
}
}
void ServersController::setCurrentConfigIndex(const QString &serverId, const int index)
{
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::XRaySubscription: {
auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
if (!cfg.has_value())
return;
cfg->currentConfig = index;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return;
}
case serverConfigUtils::ConfigType::Invalid:
default: return;
}
}
QVector<ServerDescription> ServersController::buildServerDescriptions(bool isAmneziaDnsEnabled) const
{
QVector<ServerDescription> out;
@@ -170,6 +266,14 @@ QVector<ServerDescription> ServersController::buildServerDescriptions(bool isAmn
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::XRaySubscription: {
const auto cfg = m_serversRepository->xraySubscriptionConfig(id);
if (!cfg) {
continue;
}
d = buildServerDescription(*cfg, isAmneziaDnsEnabled);
break;
}
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium: {
@@ -205,30 +309,33 @@ QMap<DockerContainer, ContainerConfig> ServersController::getServerContainersMap
switch (m_serversRepository->serverKind(serverId)) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig> {};
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig> {};
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig> {};
}
case serverConfigUtils::ConfigType::XRaySubscription: {
const auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig> {};
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig> {};
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2: {
const auto cfg = m_serversRepository->legacyApiConfig(serverId);
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig>{};
return cfg.has_value() ? cfg->containers : QMap<DockerContainer, ContainerConfig> {};
}
case serverConfigUtils::ConfigType::Invalid:
default:
return {};
default: return {};
}
}
@@ -247,6 +354,10 @@ DockerContainer ServersController::getDefaultContainer(const QString &serverId)
const auto cfg = m_serversRepository->nativeConfig(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::XRaySubscription: {
const auto cfg = m_serversRepository->xraySubscriptionConfig(serverId);
return cfg.has_value() ? cfg->defaultContainer : DockerContainer::None;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
@@ -326,6 +437,14 @@ QString ServersController::notificationDisplayName(const QString &serverId) cons
}
break;
}
case Kind::XRaySubscription: {
if (const auto cfg = m_serversRepository->xraySubscriptionConfig(serverId)) {
if (!cfg->displayName.isEmpty()) {
return cfg->displayName;
}
}
break;
}
case Kind::AmneziaPremiumV2:
case Kind::AmneziaFreeV3:
case Kind::ExternalPremium: {
@@ -43,6 +43,14 @@ public:
// Container management
void setDefaultContainer(const QString &serverId, DockerContainer container);
// XRay subscription config getters/setters
QString getSubLink(const QString &serverId) const;
QString getConfigString(const QString &serverId, const int index) const;
QString getConfigName(const QString &serverId, const int index) const;
QJsonArray getConfigNames(const QString &serverId) const;
int getCurrentConfigIndex(const QString &serverId) const;
void setCurrentConfigIndex(const QString &serverId, int index);
// Getters
QVector<ServerDescription> buildServerDescriptions(bool isAmneziaDnsEnabled) const;
int getDefaultServerIndex() const;
@@ -5,7 +5,6 @@
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
@@ -0,0 +1,117 @@
#include "xraySubscriptionConfig.h"
#include <QJsonArray>
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
namespace amnezia
{
using namespace ContainerEnumNS;
bool XRaySubscriptionConfig::hasContainers() const
{
return !containers.isEmpty();
}
ContainerConfig XRaySubscriptionConfig::containerConfig(DockerContainer container) const
{
if (!containers.contains(container)) {
return ContainerConfig {};
}
return containers.value(container);
}
QJsonObject XRaySubscriptionConfig::toJson() const
{
QJsonObject obj;
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
QJsonArray containersArray;
for (auto it = containers.begin(); it != containers.end(); ++it) {
QJsonObject containerObj = it.value().toJson();
containersArray.append(containerObj);
}
if (!containersArray.isEmpty()) {
obj[configKey::containers] = containersArray;
}
if (defaultContainer != DockerContainer::None) {
obj[configKey::defaultContainer] = ContainerUtils::containerToString(defaultContainer);
}
if (!dns1.isEmpty()) {
obj[configKey::dns1] = dns1;
}
if (!dns2.isEmpty()) {
obj[configKey::dns2] = dns2;
}
if (!subLink.isEmpty()) {
obj[configKey::xraySubscriptionLink] = subLink;
}
if (!configString.isEmpty()) {
obj[configKey::xraySubscriptionConfig] = configString;
}
if (!configName.isEmpty()) {
obj[configKey::xraySubscriptionConfigName] = configName;
}
if (currentConfig > -1) {
obj[configKey::xraySubscriptionConfigCurrent] = currentConfig;
}
return obj;
}
XRaySubscriptionConfig XRaySubscriptionConfig::fromJson(const QJsonObject &json)
{
XRaySubscriptionConfig config;
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();
for (const QJsonValue &val : containersArray) {
QJsonObject containerObj = val.toObject();
ContainerConfig containerConfig = ContainerConfig::fromJson(containerObj);
QString containerStr = containerObj.value(configKey::container).toString();
DockerContainer container = ContainerUtils::containerFromString(containerStr);
config.containers.insert(container, containerConfig);
}
QString defaultContainerStr = json.value(configKey::defaultContainer).toString();
config.defaultContainer = ContainerUtils::containerFromString(defaultContainerStr);
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
if (config.displayName.isEmpty()) {
config.displayName = config.description.isEmpty() ? config.hostName : config.description;
}
config.subLink = json.value(configKey::xraySubscriptionLink).toString();
config.configString = json.value(configKey::xraySubscriptionConfig).toArray();
config.configName = json.value(configKey::xraySubscriptionConfigName).toArray();
config.currentConfig = json.value(configKey::xraySubscriptionConfigCurrent).toInt();
return config;
}
} // namespace amnezia
@@ -0,0 +1,42 @@
#ifndef XRAYSUBSCRIPTIONCONFIG_H
#define XRAYSUBSCRIPTIONCONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QMap>
#include <optional>
#include "core/models/containerConfig.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
namespace amnezia
{
using namespace ContainerEnumNS;
struct XRaySubscriptionConfig
{
QString description;
QString displayName;
QString hostName;
QMap<DockerContainer, ContainerConfig> containers;
DockerContainer defaultContainer;
QString dns1;
QString dns2;
QString subLink;
QJsonArray configString;
QJsonArray configName;
int currentConfig;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
QJsonObject toJson() const;
static XRaySubscriptionConfig fromJson(const QJsonObject &json);
};
} // namespace amnezia
#endif // XRAYSUBSCRIPTIONCONFIG_H
+14
View File
@@ -130,6 +130,20 @@ ServerDescription buildServerDescription(const NativeServerConfig &server, bool
return row;
}
ServerDescription buildServerDescription(const XRaySubscriptionConfig &server, bool isAmneziaDnsEnabled)
{
ServerDescription row = buildBaseDescription(server);
row.hasWriteAccess = false;
row.isXRaySubscription = true;
row.serverName = server.displayName;
row.baseDescription = getBaseDescription(server.containers, isAmneziaDnsEnabled, row.hasWriteAccess, row.primaryDnsIsAmnezia);
row.expandedServerDescription = row.baseDescription + server.configName.at(server.currentConfig).toString();
row.collapsedServerDescription = row.baseDescription + server.configName.at(server.currentConfig).toString();
return row;
}
ServerDescription buildServerDescription(const LegacyApiServerConfig &server, bool /*isAmneziaDnsEnabled*/)
{
ServerDescription row = buildBaseDescription(server);
+4
View File
@@ -9,6 +9,7 @@
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/selfhosted/selfHostedUserServerConfig.h"
#include "core/models/selfhosted/nativeServerConfig.h"
#include "core/models/selfhosted/xraySubscriptionConfig.h"
#include "core/models/api/legacyApiServerConfig.h"
#include "core/models/api/apiV2ServerConfig.h"
@@ -32,6 +33,8 @@ struct ServerDescription
DockerContainer defaultContainer = DockerContainer::None;
bool hasInstalledVpnContainers = false;
bool isXRaySubscription = false;
bool isApiV1 = false;
bool isApiV2 = false;
bool isServerFromGatewayApi = false;
@@ -56,6 +59,7 @@ struct ServerDescription
ServerDescription buildServerDescription(const SelfHostedAdminServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const SelfHostedUserServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const NativeServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const XRaySubscriptionConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const LegacyApiServerConfig &server, bool isAmneziaDnsEnabled);
ServerDescription buildServerDescription(const ApiV2ServerConfig &server, bool isAmneziaDnsEnabled);
@@ -348,6 +348,19 @@ std::optional<NativeServerConfig> SecureServersRepository::nativeConfig(const QS
return NativeServerConfig::fromJson(strippedJson);
}
std::optional<XRaySubscriptionConfig> SecureServersRepository::xraySubscriptionConfig(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
if (it == m_serverJsonById.constEnd()) {
return std::nullopt;
}
const QJsonObject strippedJson = withoutStorageServerId(it.value());
if (serverConfigUtils::configTypeFromJson(strippedJson) != serverConfigUtils::ConfigType::XRaySubscription) {
return std::nullopt;
}
return XRaySubscriptionConfig::fromJson(strippedJson);
}
std::optional<ApiV2ServerConfig> SecureServersRepository::apiV2Config(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
@@ -11,6 +11,7 @@
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/selfhosted/selfHostedUserServerConfig.h"
#include "core/models/selfhosted/nativeServerConfig.h"
#include "core/models/selfhosted/xraySubscriptionConfig.h"
#include "core/models/api/apiV2ServerConfig.h"
#include "core/models/api/legacyApiServerConfig.h"
#include "core/models/containerConfig.h"
@@ -34,6 +35,7 @@ public:
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
std::optional<XRaySubscriptionConfig> xraySubscriptionConfig(const QString &serverId) const;
std::optional<ApiV2ServerConfig> apiV2Config(const QString &serverId) const;
std::optional<LegacyApiServerConfig> legacyApiConfig(const QString &serverId) const;
+5
View File
@@ -33,6 +33,11 @@ namespace amnezia
constexpr QLatin1String protocol("protocol");
constexpr QLatin1String protocols("protocols");
constexpr QLatin1String xraySubscriptionLink("xray_subscription_link");
constexpr QLatin1String xraySubscriptionConfig("xray_subscription_config");
constexpr QLatin1String xraySubscriptionConfigName("xray_subscription_config_name");
constexpr QLatin1String xraySubscriptionConfigCurrent("xray_subscription_config_current");
constexpr QLatin1String remote("remote");
constexpr QLatin1String transportProto("transport_proto");
constexpr QLatin1String cipher("cipher");
+10
View File
@@ -29,6 +29,12 @@ bool hasThirdPartyConfig(const QJsonObject &json)
return false;
}
bool hasXrayConfigs(const QJsonObject &json)
{
const QJsonArray configsArray = json.value(amnezia::configKey::xraySubscriptionConfig).toArray();
return !configsArray.isEmpty();
}
} // namespace
namespace serverConfigUtils
@@ -93,6 +99,10 @@ ConfigType configTypeFromJson(const QJsonObject &serverConfigObject)
break;
}
if (hasXrayConfigs(serverConfigObject)) {
return ConfigType::XRaySubscription;
}
if (hasThirdPartyConfig(serverConfigObject)) {
return ConfigType::Native;
}
+1
View File
@@ -17,6 +17,7 @@ enum ConfigType {
SelfHostedAdmin = 8,
SelfHostedUser,
Native,
XRaySubscription,
Invalid
};
+4
View File
@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 3.75V14.25" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.25 9L9 14.25L3.75 9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 14.25V3.75" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.75 9L9 3.75L14.25 9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

+2
View File
@@ -9,8 +9,10 @@
<file>controls/amnezia.svg</file>
<file>controls/app.svg</file>
<file>controls/archive-restore.svg</file>
<file>controls/arrow-down.svg</file>
<file>controls/arrow-left.svg</file>
<file>controls/arrow-right.svg</file>
<file>controls/arrow-up.svg</file>
<file>controls/bug.svg</file>
<file>controls/check.svg</file>
<file>controls/chevron-down.svg</file>
@@ -31,6 +31,41 @@ ImportUiController::ImportUiController(ImportController* importController, QObje
connect(m_importController, &ImportController::restoreAppConfig, this, &ImportUiController::restoreAppConfig);
}
bool ImportUiController::importLink(const QUrl &url)
{
auto result = m_importController->importLink(url);
if (result.errorCode != ErrorCode::NoError) {
emit importErrorOccurred(result.errorCode, false);
return false;
}
m_config = result.config;
m_configFileName = result.configFileName;
m_maliciousWarningText = result.maliciousWarningText;
m_isNativeWireGuardConfig = result.isNativeWireGuardConfig;
return true;
}
bool ImportUiController::editServerConfigWithData(const QString &serverId, QString data)
{
auto result = m_importController->editServerConfigWithData(serverId, data, m_config);
if (result.errorCode != ErrorCode::NoError) {
emit importErrorOccurred(result.errorCode, false);
return false;
}
m_config = result.config;
m_configFileName = result.configFileName;
m_maliciousWarningText = result.maliciousWarningText;
m_isNativeWireGuardConfig = result.isNativeWireGuardConfig;
return true;
}
bool ImportUiController::extractConfigFromFile(const QString &fileName)
{
QString data;
@@ -20,6 +20,8 @@ public:
public slots:
void importConfig();
void clearConfigFileName();
bool importLink(const QUrl &url);
bool editServerConfigWithData(const QString &serverId, QString data);
bool extractConfigFromFile(const QString &fileName);
bool extractConfigFromData(QString data);
bool extractConfigFromQr(const QByteArray &data);
@@ -44,6 +44,8 @@ namespace PageLoader
PageSettingsApiNativeConfigs,
PageSettingsApiDevices,
PageSettingsApiSubscriptionKey,
PageSettingsXRayAvailableConfigs,
PageSettingsXRayServerInfo,
PageSettingsKillSwitchExceptions,
PageServiceSftpSettings,
@@ -276,6 +276,17 @@ bool ServersUiController::hasServerWithWriteAccess() const
return false;
}
bool ServersUiController::isDefaultServerContainXRayConfigs() const
{
const QString defaultServerId = m_serversController->getDefaultServerId();
for (const auto &description : m_orderedServerDescriptions) {
if (description.serverId == defaultServerId) {
return description.isXRaySubscription;
}
}
return false;
}
QString ServersUiController::serverName(const QString &serverId) const
{
return serverDescriptionById(serverId).serverName;
@@ -332,6 +343,11 @@ bool ServersUiController::isServerSubscriptionExpiringSoon(const QString &server
return serverDescriptionById(serverId).isSubscriptionExpiringSoon;
}
bool ServersUiController::isServerContainXRayConfigs(const QString &serverId) const
{
return serverDescriptionById(serverId).isXRaySubscription;
}
int ServersUiController::getProcessedContainerIndex() const
{
return m_processedContainerIndex;
@@ -470,6 +486,36 @@ int ServersUiController::getServersCount() const
return m_orderedServerDescriptions.size();
}
QString ServersUiController::getSubLink() const
{
return m_serversController->getSubLink(m_processedServerId);
}
QString ServersUiController::getConfigString(const int index) const
{
return m_serversController->getConfigString(m_processedServerId, index);
}
QString ServersUiController::getConfigName(const int index) const
{
return m_serversController->getConfigName(m_processedServerId, index);
}
QJsonArray ServersUiController::getConfigNames() const
{
return m_serversController->getConfigNames(m_processedServerId);
}
int ServersUiController::getCurrentConfigIndex() const
{
return m_serversController->getCurrentConfigIndex(m_processedServerId);
}
void ServersUiController::setCurrentConfigIndex(const int index)
{
m_serversController->setCurrentConfigIndex(m_processedServerId, index);
}
void ServersUiController::updateContainersModel()
{
if (m_processedServerId.isEmpty()) {
+14 -1
View File
@@ -28,6 +28,8 @@ class ServersUiController : public QObject
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIdChanged)
Q_PROPERTY(bool isDefaultServerContainXRayConfigs READ isDefaultServerContainXRayConfigs NOTIFY defaultServerIdChanged)
Q_PROPERTY(QString processedServerId READ getProcessedServerId WRITE setProcessedServerId NOTIFY processedServerIdChanged)
Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIdChanged)
@@ -70,6 +72,8 @@ public slots:
QString getDefaultServerDescriptionExpanded() const;
bool isDefaultServerDefaultContainerHasSplitTunneling() const;
bool isDefaultServerFromApi() const;
bool isDefaultServerContainXRayConfigs() const;
bool hasServerWithWriteAccess() const;
QString serverName(const QString &serverId) const;
@@ -83,6 +87,8 @@ public slots:
bool isServerRenewalAvailable(const QString &serverId) const;
bool isServerSubscriptionExpired(const QString &serverId) const;
bool isServerSubscriptionExpiringSoon(const QString &serverId) const;
bool isServerContainXRayConfigs(const QString &serverId) const;
QString getProcessedServerId() const;
void setProcessedServerId(const QString &serverId);
@@ -99,7 +105,14 @@ public slots:
bool isAdVisible() const;
QString adHeader() const;
QString adDescription() const;
QString getSubLink() const;
QString getConfigString(const int index) const;
QString getConfigName(const int index) const;
QJsonArray getConfigNames() const;
int getCurrentConfigIndex() const;
void setCurrentConfigIndex(int index);
QString getServerId(int index) const;
int getServerIndexById(const QString &serverId) const;
int getServersCount() const;
+4
View File
@@ -81,6 +81,8 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
return row.isSubscriptionExpired;
case IsSubscriptionExpiringSoonRole:
return row.isSubscriptionExpiringSoon;
case IsXRayConfigSelectionAvailableRole:
return row.isXRaySubscription;
}
return QVariant();
@@ -141,6 +143,8 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[IsServerFromGatewayApiRole] = "isServerFromGatewayApi";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[IsXRayConfigSelectionAvailableRole] = "isXRayConfigSelectionAvailable";
return roles;
}
+2
View File
@@ -30,6 +30,8 @@ public:
IsServerFromGatewayApiRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
IsXRayConfigSelectionAvailableRole
};
ServersModel(QObject *parent = nullptr);
@@ -121,6 +121,8 @@ ListViewType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
} else if (ServersUiController.isServerContainXRayConfigs(ServersUiController.processedServerId)) {
PageController.goToPage(PageEnum.PageSettingsXRayAvailableConfigs)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}
+5 -3
View File
@@ -311,11 +311,11 @@ PageType {
objectName: "rowLayoutLabel"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: 8
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : (ServersUiController.isDefaultServerFromApi || ServersUiController.isDefaultServerContainXRayConfigs) ? 61 : 16
spacing: 0
BasicButtonType {
enabled: (ServersUiController.defaultServerImagePathCollapsed !== "") && drawer.isCollapsedStateActive
enabled: (ServersUiController.defaultServerImagePathCollapsed !== "" || ServersUiController.isDefaultServerContainXRayConfigs) && drawer.isCollapsedStateActive
hoverEnabled: enabled
implicitHeight: 36
@@ -359,6 +359,8 @@ PageType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
} else if (ServersUiController.isServerContainXRayConfigs(ServersUiController.processedServerId)) {
PageController.goToPage(PageEnum.PageSettingsXRayAvailableConfigs)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}
@@ -379,7 +381,7 @@ PageType {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: 8
visible: !ServersUiController.isDefaultServerFromApi
visible: !ServersUiController.isDefaultServerFromApi && !ServersUiController.isDefaultServerContainXRayConfigs
DropDownType {
id: containersDropDown
@@ -97,6 +97,8 @@ PageType {
}
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
} else if (ServersModel.getProcessedServerData("isXRayConfigSelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsXRayServerInfo)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}
@@ -0,0 +1,180 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property var processedServer
Connections {
target: PageController
function onGoToPageSettingsServerServices() {
tabBar.setCurrentIndex(root.pageSettingsServerServices)
}
}
Connections {
target: ServersUiController
function onProcessedServerIdChanged() {
root.processedServer = proxyServersModel.get(0)
}
}
Connections {
target: ServersModel
function onModelReset() {
root.processedServer = proxyServersModel.get(0)
}
}
SortFilterProxyModel {
id: proxyServersModel
objectName: "proxyServersModel"
sourceModel: ServersModel
filters: [
ValueFilter {
roleName: "serverId"
value: ServersUiController.processedServerId
}
]
Component.onCompleted: {
root.processedServer = proxyServersModel.get(0)
}
}
ListViewType {
id: menuContent
anchors.fill: parent
model: xrayConfigs
currentIndex: 0
ButtonGroup {
id: containersRadioButtonGroup
}
header: ColumnLayout {
width: menuContent.width
spacing: 4
BackButtonType {
id: backButton
objectName: "backButton"
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 10
actionButtonImage: "qrc:/images/controls/settings.svg"
headerText: root.processedServer.name
actionButtonFunction: function() {
PageController.goToPage(PageEnum.PageSettingsXRayServerInfo)
}
}
}
delegate: ColumnLayout {
id: content
width: menuContent.width
height: content.implicitHeight
RowLayout {
VerticalRadioButton {
id: containerRadioButton
Layout.fillWidth: true
Layout.leftMargin: 16
text: model.title
ButtonGroup.group: containersRadioButtonGroup
imageSource: "qrc:/images/controls/download.svg"
checked: index === ServersUiController.getCurrentConfigIndex()
checkable: !ConnectionController.isConnected
onClicked: {
if (ConnectionController.isConnectionInProgress) {
PageController.showNotificationMessage(qsTr("Unable change config while trying to make an active connection"))
return
}
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Unable change config while there is an active connection"))
return
}
if (index !== ServersUiController.getCurrentConfigIndex()) {
PageController.showBusyIndicator(true)
ServersUiController.setCurrentConfigIndex(index)
ImportController.editServerConfigWithData(ServersUiController.getProcessedServerId(), ServersUiController.getConfigString(index))
PageController.showBusyIndicator(false)
}
}
Keys.onEnterPressed: {
if (checkable) {
checked = true
}
containerRadioButton.clicked()
}
Keys.onReturnPressed: {
if (checkable) {
checked = true
}
containerRadioButton.clicked()
}
}
}
DividerType {
Layout.fillWidth: true
}
}
}
ListModel {
id: xrayConfigs
}
Component.onCompleted: {
xrayConfigs.clear()
const names = ServersUiController.getConfigNames()
for (let i = 0; i < names.length; ++i) {
xrayConfigs.append({ title: names[i] })
}
}
}
@@ -0,0 +1,191 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property var processedServer
Connections {
target: PageController
function onGoToPageSettingsServerServices() {
tabBar.setCurrentIndex(root.pageSettingsServerServices)
}
}
Connections {
target: ServersUiController
function onProcessedServerIdChanged() {
root.processedServer = proxyServersModel.get(0)
}
}
Connections {
target: ServersModel
function onModelReset() {
root.processedServer = proxyServersModel.get(0)
}
}
SortFilterProxyModel {
id: proxyServersModel
objectName: "proxyServersModel"
sourceModel: ServersModel
filters: [
ValueFilter {
roleName: "serverId"
value: ServersUiController.processedServerId
}
]
Component.onCompleted: {
root.processedServer = proxyServersModel.get(0)
}
}
ListViewType {
id: listView
anchors.fill: parent
model: 1
header: ColumnLayout {
width: listView.width
spacing: 4
BackButtonType {
id: backButton
objectName: "backButton"
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 10
actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer.name
actionButtonFunction: function() {
serverNameEditDrawer.openTriggered()
}
}
}
footer: ColumnLayout {
id: footer
width: listView.width
spacing: 0
BasicButtonType {
id: resetButton
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 24
Layout.bottomMargin: 16
Layout.leftMargin: 8
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Reload config")
clickedFunc: function() {
var headerText = qsTr("Reload config?")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot reload config during active connection"))
} else {
PageController.showBusyIndicator(true)
if (!ImportController.importLink(ServersUiController.getSubLink()) &&
!ImportController.editServerConfigWithData(ServersUiController.getProcessedServerId(), ServersUiController.getConfigString(ServersUiController.getCurrentConfigIndex()))) {
PageController.showNotificationMessage(qsTr("Error during config reload"))
}
PageController.showBusyIndicator(false)
}
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
BasicButtonType {
id: removeButton
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 16
Layout.leftMargin: 8
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Remove from application")
clickedFunc: function() {
var headerText = qsTr("Remove from application?")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else {
PageController.showBusyIndicator(true)
InstallController.removeServer(ServersUiController.getProcessedServerId())
PageController.showBusyIndicator(false)
}
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}
}
RenameServerDrawer {
id: serverNameEditDrawer
anchors.fill: parent
expandedHeight: parent.height * 0.35
serverNameText: root.processedServer.name
}
}
@@ -187,8 +187,19 @@ PageType {
text: qsTr("Continue")
function isValidUrl(text) {
try {
var u = new URL(text)
return u.protocol === "http:" || u.protocol === "https:"
} catch(e) {
return false
}
}
clickedFunc: function() {
if (ImportController.extractConfigFromData(textKey.textField.text)) {
if (isValidUrl(textKey.textField.text) && ImportController.importLink(textKey.textField.text)) {
PageController.goToPage(PageEnum.PageSetupWizardViewConfig)
}else if (ImportController.extractConfigFromData(textKey.textField.text)) {
PageController.goToPage(PageEnum.PageSetupWizardViewConfig)
}
}
+2
View File
@@ -113,6 +113,8 @@
<file>Pages2/PageSettingsServerServices.qml</file>
<file>Pages2/PageSettingsServersList.qml</file>
<file>Pages2/PageSettingsSplitTunneling.qml</file>
<file>Pages2/PageSettingsXRayAvailableConfigs.qml</file>
<file>Pages2/PageSettingsXRayServerInfo.qml</file>
<file>Pages2/PageSettingsNewsNotifications.qml</file>
<file>Pages2/PageSettingsNewsDetail.qml</file>
<file>Pages2/PageProtocolAwgClientSettings.qml</file>