mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
feat: add xray subscription link support
This commit is contained in:
@@ -11,8 +11,13 @@
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QRegularExpressionMatchIterator>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
#include "core/utils/protocolEnum.h"
|
||||
@@ -372,6 +377,206 @@ 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_appSettingsRepository->nextAvailableServerName());
|
||||
serverConfig["xray_subscription_config"] = configStrings;
|
||||
serverConfig["xray_subscription_config_name"] = configNames;
|
||||
serverConfig["xray_subscription_config_current"] = 0;
|
||||
|
||||
result.config = serverConfig;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ImportController::ImportResult ImportController::editServerConfigWithData(QString data, int serverIndex, const QJsonObject& uiConfig)
|
||||
{
|
||||
ImportResult result = extractConfigFromData(data);
|
||||
|
||||
if (result.errorCode != ErrorCode::NoError)
|
||||
return result;
|
||||
|
||||
const ServerConfig currentServerConfig = m_serversRepository->server(serverIndex);
|
||||
|
||||
QJsonObject editedConfig = result.config;
|
||||
const QJsonObject currentConfig = currentServerConfig.toJson();
|
||||
|
||||
for (auto it = uiConfig.begin(); it != uiConfig.end(); ++it) {
|
||||
editedConfig.insert(it.key(), it.value());
|
||||
}
|
||||
|
||||
if (currentConfig.contains(configKey::description)) {
|
||||
editedConfig.insert(configKey::description, currentConfig.value(configKey::description));
|
||||
}
|
||||
|
||||
if (currentConfig.contains("xray_subscription_config")) {
|
||||
editedConfig.insert("xray_subscription_config", currentConfig.value("xray_subscription_config"));
|
||||
}
|
||||
|
||||
if (currentConfig.contains("xray_subscription_config_name")) {
|
||||
editedConfig.insert("xray_subscription_config_name", currentConfig.value("xray_subscription_config_name"));
|
||||
}
|
||||
|
||||
if (currentConfig.contains("xray_subscription_config_current")) {
|
||||
editedConfig.insert("xray_subscription_config_current", currentConfig.value("xray_subscription_config_current"));
|
||||
}
|
||||
|
||||
const ServerConfig finalServerConfig = ServerConfig::fromJson(editedConfig);
|
||||
|
||||
m_serversRepository->editServer(serverIndex, finalServerConfig);
|
||||
|
||||
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(QString data, int serverIndex, const QJsonObject &uiConfig);
|
||||
bool isValidBase64(const QByteArray &input);
|
||||
|
||||
void importConfig(const QJsonObject &config);
|
||||
QJsonObject processNativeWireGuardConfig(const QJsonObject &config);
|
||||
|
||||
|
||||
@@ -58,6 +58,31 @@ void ServersController::clearCachedProfile(int serverIndex, DockerContainer cont
|
||||
m_serversRepository->clearLastConnectionConfig(serverIndex, container);
|
||||
}
|
||||
|
||||
void ServersController::setCurrentConfigIndex(const int index)
|
||||
{
|
||||
m_serversRepository->setCurrentConfigIndex(index);
|
||||
}
|
||||
|
||||
int ServersController::getCurrentConfigIndex() const
|
||||
{
|
||||
return m_serversRepository->getCurrentConfigIndex();
|
||||
}
|
||||
|
||||
QString ServersController::getConfigString(const int index) const
|
||||
{
|
||||
return m_serversRepository->getConfigString(index);
|
||||
}
|
||||
|
||||
QString ServersController::getConfigName(const int index) const
|
||||
{
|
||||
return m_serversRepository->getConfigName(index);
|
||||
}
|
||||
|
||||
QJsonArray ServersController::getConfigNames() const
|
||||
{
|
||||
return m_serversRepository->getConfigNames();
|
||||
}
|
||||
|
||||
QJsonArray ServersController::getServersArray() const
|
||||
{
|
||||
QJsonArray result;
|
||||
|
||||
@@ -64,6 +64,13 @@ public:
|
||||
// Cache management
|
||||
void clearCachedProfile(int serverIndex, DockerContainer container);
|
||||
|
||||
// XRay subscription config getters/setters
|
||||
void setCurrentConfigIndex(int index);
|
||||
int getCurrentConfigIndex() const;
|
||||
QString getConfigString(const int index) const;
|
||||
QString getConfigName(const int index) const;
|
||||
QJsonArray getConfigNames() const;
|
||||
|
||||
// Getters
|
||||
QJsonArray getServersArray() const;
|
||||
QVector<ServerConfig> getServers() const;
|
||||
|
||||
@@ -58,6 +58,18 @@ QJsonObject NativeServerConfig::toJson() const
|
||||
if (!dns2.isEmpty()) {
|
||||
obj[configKey::dns2] = dns2;
|
||||
}
|
||||
|
||||
if (configString) {
|
||||
obj[QLatin1String("xray_subscription_config")] = configString.value();
|
||||
}
|
||||
|
||||
if (configName) {
|
||||
obj[QLatin1String("xray_subscription_config_name")] = configName.value();
|
||||
}
|
||||
|
||||
if (currentConfig) {
|
||||
obj[QLatin1String("xray_subscription_config_current")] = currentConfig.value();
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -85,7 +97,16 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
|
||||
|
||||
config.dns1 = json.value(configKey::dns1).toString();
|
||||
config.dns2 = json.value(configKey::dns2).toString();
|
||||
|
||||
|
||||
if (json.contains(QLatin1String("xray_subscription_config")))
|
||||
config.configString = json.value(QLatin1String("xray_subscription_config")).toArray();
|
||||
|
||||
if (json.contains(QLatin1String("xray_subscription_config_name")))
|
||||
config.configName = json.value(QLatin1String("xray_subscription_config_name")).toArray();
|
||||
|
||||
if (json.contains(QLatin1String("xray_subscription_config_current")))
|
||||
config.currentConfig = json.value(QLatin1String("xray_subscription_config_current")).toInt();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#define NATIVESERVERCONFIG_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QMap>
|
||||
#include <optional>
|
||||
|
||||
#include "core/utils/containerEnum.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
@@ -21,6 +23,10 @@ struct NativeServerConfig {
|
||||
DockerContainer defaultContainer;
|
||||
QString dns1;
|
||||
QString dns2;
|
||||
|
||||
std::optional<QJsonArray> configString;
|
||||
std::optional<QJsonArray> configName;
|
||||
std::optional<int> currentConfig;
|
||||
|
||||
bool hasContainers() const;
|
||||
ContainerConfig containerConfig(DockerContainer container) const;
|
||||
|
||||
@@ -120,6 +120,10 @@ bool ServerConfig::isApiConfig() const
|
||||
return isApiV1() || isApiV2();
|
||||
}
|
||||
|
||||
bool ServerConfig::isXRayConfig() const {
|
||||
return isNative() && std::get<NativeServerConfig>(data).configString.has_value();
|
||||
}
|
||||
|
||||
QJsonObject ServerConfig::toJson() const
|
||||
{
|
||||
return std::visit([](const auto& v) { return v.toJson(); }, data);
|
||||
@@ -150,7 +154,7 @@ ServerConfig ServerConfig::fromJson(const QJsonObject& json)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasThirdPartyConfig) {
|
||||
return ServerConfig{NativeServerConfig::fromJson(json)};
|
||||
} else {
|
||||
@@ -186,7 +190,7 @@ ServerConfig ServerConfig::fromJson(const QJsonObject& json)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasThirdPartyConfig) {
|
||||
return ServerConfig{NativeServerConfig::fromJson(json)};
|
||||
} else {
|
||||
|
||||
@@ -57,6 +57,7 @@ struct ServerConfig {
|
||||
bool isApiV1() const;
|
||||
bool isApiV2() const;
|
||||
bool isApiConfig() const;
|
||||
bool isXRayConfig() const;
|
||||
|
||||
template<typename T>
|
||||
T* as() {
|
||||
|
||||
@@ -176,6 +176,73 @@ void SecureServersRepository::clearLastConnectionConfig(int serverIndex, DockerC
|
||||
setContainerConfig(serverIndex, container, containerCfg);
|
||||
}
|
||||
|
||||
void SecureServersRepository::setCurrentConfigIndex(const int index)
|
||||
{
|
||||
ServerConfig serverConfig = server(m_defaultServerIndex);
|
||||
NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
|
||||
|
||||
xrayConfig->currentConfig = index;
|
||||
editServer(m_defaultServerIndex, serverConfig);
|
||||
}
|
||||
|
||||
int SecureServersRepository::getCurrentConfigIndex() const
|
||||
{
|
||||
const ServerConfig serverConfig = server(m_defaultServerIndex);
|
||||
if (!serverConfig.isXRayConfig())
|
||||
return int();
|
||||
|
||||
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
|
||||
if (!xrayConfig->currentConfig.has_value())
|
||||
return int();
|
||||
|
||||
return xrayConfig->currentConfig.value();
|
||||
}
|
||||
|
||||
QString SecureServersRepository::getConfigString(const int index) const
|
||||
{
|
||||
const ServerConfig serverConfig = server(m_defaultServerIndex);
|
||||
if (!serverConfig.isXRayConfig())
|
||||
return QString();
|
||||
|
||||
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
|
||||
if (!xrayConfig->configString.has_value())
|
||||
return QString();
|
||||
|
||||
if (index < 0 || index >= xrayConfig->configString.value().size())
|
||||
return QString();
|
||||
|
||||
return xrayConfig->configString.value().at(index).toString();
|
||||
}
|
||||
|
||||
QString SecureServersRepository::getConfigName(const int index) const
|
||||
{
|
||||
const ServerConfig serverConfig = server(m_defaultServerIndex);
|
||||
if (!serverConfig.isXRayConfig())
|
||||
return QString();
|
||||
|
||||
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
|
||||
if (!xrayConfig->configName.has_value())
|
||||
return QString();
|
||||
|
||||
if (index < 0 || index >= xrayConfig->configName.value().size())
|
||||
return QString();
|
||||
|
||||
return xrayConfig->configName.value().at(index).toString();
|
||||
}
|
||||
|
||||
QJsonArray SecureServersRepository::getConfigNames() const
|
||||
{
|
||||
const ServerConfig serverConfig = server(m_defaultServerIndex);
|
||||
if (!serverConfig.isXRayConfig())
|
||||
return QJsonArray();
|
||||
|
||||
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
|
||||
if (!xrayConfig->configName.has_value())
|
||||
return QJsonArray();
|
||||
|
||||
return xrayConfig->configName.value();
|
||||
}
|
||||
|
||||
ServerCredentials SecureServersRepository::serverCredentials(int index) const
|
||||
{
|
||||
ServerConfig config = server(index);
|
||||
|
||||
@@ -35,6 +35,12 @@ public:
|
||||
void setContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config);
|
||||
void clearLastConnectionConfig(int serverIndex, DockerContainer container);
|
||||
|
||||
void setCurrentConfigIndex(int index);
|
||||
int getCurrentConfigIndex() const;
|
||||
QString getConfigString(const int index) const;
|
||||
QString getConfigName(const int index) const;
|
||||
QJsonArray getConfigNames() const;
|
||||
|
||||
ServerCredentials serverCredentials(int index) const;
|
||||
bool hasServerWithVpnKey(const QString &vpnKey) const;
|
||||
bool hasServerWithCrc(quint16 crc) const;
|
||||
|
||||
Reference in New Issue
Block a user