mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-03 14:07:39 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eda14f57d6 | |||
| de02be61f3 | |||
| a754a11913 | |||
| 4d25e3b6f6 | |||
| 1fac280497 | |||
| c886c5e6a7 | |||
| cd7f78b9ca | |||
| a587d3230f | |||
| 93e7b45136 | |||
| e024f71ce1 | |||
| 50d1be7b4a | |||
| 3ec6d8973b | |||
| 3ea47d31a9 | |||
| 30c8cc4548 | |||
| 98586d2dd9 |
@@ -238,13 +238,14 @@ jobs:
|
||||
IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }}
|
||||
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
|
||||
|
||||
- name: 'Upload unsigned .ipa to artifacts'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-unsigned-ipa
|
||||
path: |
|
||||
${{ github.workspace }}/build-ios/AmneziaVPN_unsigned.ipa
|
||||
retention-days: 7
|
||||
# - name: 'Upload appstore .ipa and dSYMs to artifacts'
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: app-store ipa & dsyms
|
||||
# path: |
|
||||
# ${{ github.workspace }}/AmneziaVPN-iOS.ipa
|
||||
# ${{ github.workspace }}/*.app.dSYM.zip
|
||||
# retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# tags:
|
||||
# - "**"
|
||||
# - **
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ deploy/build_32/*
|
||||
deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
|
||||
|
||||
# Qt-es
|
||||
|
||||
+15
-4
@@ -136,10 +136,21 @@ set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
|
||||
add_subdirectory(ios/networkextension)
|
||||
add_dependencies(${PROJECT} networkextension)
|
||||
|
||||
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
|
||||
set(OPENVPN_FRAMEWORK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios")
|
||||
set(OPENVPN_EMBEDDED_FRAMEWORKS
|
||||
"${OPENVPN_FRAMEWORK_DIR}/OpenVPNAdapter.framework"
|
||||
"${OPENVPN_FRAMEWORK_DIR}/OpenVPNClient.framework"
|
||||
"${OPENVPN_FRAMEWORK_DIR}/mbedTLS.framework"
|
||||
"${OPENVPN_FRAMEWORK_DIR}/LZ4.framework"
|
||||
)
|
||||
|
||||
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
|
||||
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
|
||||
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
|
||||
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}")
|
||||
|
||||
foreach(_framework ${OPENVPN_EMBEDDED_FRAMEWORKS})
|
||||
target_link_libraries(networkextension PRIVATE "${_framework}")
|
||||
endforeach()
|
||||
|
||||
set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
|
||||
set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON)
|
||||
set_property(TARGET networkextension PROPERTY XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}")
|
||||
|
||||
@@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials,
|
||||
return "";
|
||||
}
|
||||
|
||||
auto sanitizeStaticKey = [](const QString &key) {
|
||||
QStringList lines = key.split('\n');
|
||||
QStringList filtered;
|
||||
filtered.reserve(lines.size());
|
||||
for (const QString &line : lines) {
|
||||
const QString trimmed = line.trimmed();
|
||||
if (trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
filtered.append(line);
|
||||
}
|
||||
QString result = filtered.join('\n');
|
||||
if (!result.endsWith('\n')) {
|
||||
result.append('\n');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
config.replace("$OPENVPN_CA_CERT", connData.caCert);
|
||||
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
|
||||
config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
|
||||
|
||||
if (config.contains("$OPENVPN_TA_KEY")) {
|
||||
config.replace("$OPENVPN_TA_KEY", connData.taKey);
|
||||
config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
|
||||
} else {
|
||||
config.replace("<tls-auth>", "");
|
||||
config.replace("</tls-auth>", "");
|
||||
@@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
|
||||
if (!isApiConfig) {
|
||||
QRegularExpression regex("redirect-gateway.*");
|
||||
config.replace(regex, "");
|
||||
|
||||
|
||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||
if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
|
||||
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
|
||||
|
||||
@@ -64,6 +64,10 @@ namespace apiDefs
|
||||
constexpr QLatin1String id("id");
|
||||
constexpr QLatin1String orderId("order_id");
|
||||
constexpr QLatin1String migrationCode("migration_code");
|
||||
|
||||
constexpr QLatin1String transactionId("transaction_id");
|
||||
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
}
|
||||
|
||||
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
|
||||
@@ -82,7 +82,9 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
|
||||
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
{
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
@@ -90,21 +92,19 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
} else if (reply->error() == QNetworkReply::NoError) {
|
||||
} else if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << reply->error();
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << reply->error();
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << QString::fromUtf8(reply->readAll());
|
||||
qDebug() << reply->error();
|
||||
qDebug() << err;
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << replyErrorString;
|
||||
qDebug() << httpStatusCode;
|
||||
if (httpStatusCode == httpStatusCodeConflict) {
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
@@ -162,3 +162,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
|
||||
}
|
||||
|
||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QString vpnKeyText = "";
|
||||
|
||||
auto apiConfig = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||
auto authData = serverConfigObject.value(QLatin1String("auth_data")).toObject();
|
||||
|
||||
const QString name = serverConfigObject.value(apiDefs::key::name).toString();
|
||||
const QString description = serverConfigObject.value(apiDefs::key::description).toString();
|
||||
const double configVersion = serverConfigObject.value(apiDefs::key::configVersion).toDouble();
|
||||
|
||||
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
|
||||
const QString serviceProtocol = apiConfig.value(QLatin1String("service_protocol")).toString();
|
||||
const QString userCountryCode = apiConfig.value(QLatin1String("user_country_code")).toString();
|
||||
|
||||
const QString apiKey = authData.value(apiDefs::key::apiKey).toString();
|
||||
|
||||
QString vpnKeyStr = "{";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::name) + "\": \"" + name + "\", ";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::description) + "\": \"" + description + "\", ";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::configVersion) + "\": " + QString::number(static_cast<int>(configVersion)) + ", ";
|
||||
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::apiConfig) + "\": {";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::serviceType) + "\": \"" + serviceType + "\", ";
|
||||
vpnKeyStr += "\"service_protocol\": \"" + serviceProtocol + "\", ";
|
||||
vpnKeyStr += "\"user_country_code\": \"" + userCountryCode + "\"";
|
||||
vpnKeyStr += "}, ";
|
||||
|
||||
vpnKeyStr += "\"auth_data\": {";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::apiKey) + "\": \"" + apiKey + "\"";
|
||||
vpnKeyStr += "}";
|
||||
|
||||
vpnKeyStr += "}";
|
||||
|
||||
QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8();
|
||||
vpnKeyCompressed = qCompress(vpnKeyCompressed, 6);
|
||||
vpnKeyCompressed = vpnKeyCompressed.mid(4);
|
||||
|
||||
QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed;
|
||||
vpnKeyText = QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
|
||||
|
||||
return vpnKeyText;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,12 @@ namespace apiUtils
|
||||
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
||||
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
|
||||
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply);
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
|
||||
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
}
|
||||
|
||||
#endif // APIUTILS_H
|
||||
|
||||
@@ -99,6 +99,9 @@ void CoreController::initModels()
|
||||
|
||||
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
|
||||
|
||||
m_newsModel.reset(new NewsModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get());
|
||||
}
|
||||
|
||||
void CoreController::initControllers()
|
||||
@@ -153,6 +156,9 @@ void CoreController::initControllers()
|
||||
|
||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
||||
|
||||
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||
}
|
||||
|
||||
void CoreController::initAndroidController()
|
||||
@@ -316,6 +322,11 @@ void CoreController::initContainerModelUpdateHandler()
|
||||
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
|
||||
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
|
||||
&ContainersModel::updateModel);
|
||||
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
|
||||
if (m_serversModel->hasServersFromGatewayApi()) {
|
||||
m_apiNewsController->fetchNews();
|
||||
}
|
||||
});
|
||||
m_serversModel->resetModel();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ui/controllers/api/apiConfigsController.h"
|
||||
#include "ui/controllers/api/apiSettingsController.h"
|
||||
#include "ui/controllers/api/apiPremV1MigrationController.h"
|
||||
#include "ui/controllers/api/apiNewsController.h"
|
||||
#include "ui/controllers/appSplitTunnelingController.h"
|
||||
#include "ui/controllers/allowedDnsController.h"
|
||||
#include "ui/controllers/connectionController.h"
|
||||
@@ -47,6 +48,7 @@
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/sites_model.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/notificationhandler.h"
|
||||
@@ -118,6 +120,7 @@ private:
|
||||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
||||
QScopedPointer<ApiNewsController> m_apiNewsController;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
||||
@@ -125,6 +128,7 @@ private:
|
||||
QSharedPointer<LanguageModel> m_languageModel;
|
||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||
QSharedPointer<SitesModel> m_sitesModel;
|
||||
QSharedPointer<NewsModel> m_newsModel;
|
||||
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
||||
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||
|
||||
@@ -50,69 +50,6 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
|
||||
{
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
||||
|
||||
request.setUrl(QString(endpoint).arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
QString host = QUrl(request.url()).host();
|
||||
QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
QNetworkReply *reply;
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
|
||||
responseBody = reply->readAll();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
|
||||
auto requestFunction = [&request, &responseBody](const QString &url) {
|
||||
request.setUrl(url);
|
||||
return amnApp->networkManager()->get(request);
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply,
|
||||
const QList<QSslError> &nestedSslErrors) {
|
||||
responseBody = nestedReply->readAll();
|
||||
if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
reply = nestedReply;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
@@ -188,29 +125,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
wait.exec();
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
QString replyErrorString = reply->errorString();
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
|
||||
reply->deleteLater();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
|
||||
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
|
||||
request.setUrl(url);
|
||||
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt,
|
||||
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) {
|
||||
encryptedResponseBody = nestedReply->readAll();
|
||||
reply = nestedReply;
|
||||
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
|
||||
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &key, &iv,
|
||||
&salt, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
|
||||
encryptedResponseBody = reply->readAll();
|
||||
replyErrorString = reply->errorString();
|
||||
replyError = reply->error();
|
||||
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
|
||||
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -225,7 +170,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
||||
}
|
||||
}
|
||||
|
||||
QStringList GatewayController::getProxyUrls()
|
||||
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
@@ -235,15 +180,26 @@ QStringList GatewayController::getProxyUrls()
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
QStringList baseUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
if (!serviceType.isEmpty()) {
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||
}
|
||||
}
|
||||
for (const auto &baseUrl : baseUrls) {
|
||||
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
|
||||
}
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||
request.setUrl(proxyStorageUrl);
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
@@ -288,7 +244,10 @@ QStringList GatewayController::getProxyUrls()
|
||||
}
|
||||
return endpoints;
|
||||
} else {
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
||||
auto replyError = reply->error();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << replyError;
|
||||
qDebug() << httpStatusCode;
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
|
||||
reply->deleteLater();
|
||||
@@ -297,33 +256,33 @@ QStringList GatewayController::getProxyUrls()
|
||||
return {};
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
|
||||
bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt)
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << "timeout occurred";
|
||||
qDebug() << reply->error();
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
} else if (responseBody.contains("html")) {
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
|
||||
} else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << reply->error();
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << reply->error();
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
}
|
||||
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << reply->error();
|
||||
} else if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
} else if (checkEncryption) {
|
||||
try {
|
||||
@@ -337,35 +296,33 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
|
||||
return false;
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply,
|
||||
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
||||
{
|
||||
QStringList proxyUrls = getProxyUrls();
|
||||
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
||||
|
||||
QByteArray responseBody;
|
||||
|
||||
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, QNetworkReply *reply,
|
||||
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
reply->deleteLater(); // delete the previous reply
|
||||
reply = requestFunction(endpoint.arg(proxyUrl));
|
||||
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
|
||||
if (replyProcessingFunction(reply, sslErrors)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
auto result = replyProcessingFunction(reply, sslErrors);
|
||||
reply->deleteLater();
|
||||
return result;
|
||||
};
|
||||
|
||||
if (m_proxyUrl.isEmpty()) {
|
||||
@@ -399,13 +356,13 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
|
||||
}
|
||||
|
||||
if (!m_proxyUrl.isEmpty()) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, reply, requestFunction, replyProcessingFunction)) {
|
||||
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
if (bypassFunction(endpoint, proxyUrl, reply, requestFunction, replyProcessingFunction)) {
|
||||
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||
m_proxyUrl = proxyUrl;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ public:
|
||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
||||
|
||||
amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody);
|
||||
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||
|
||||
private:
|
||||
QStringList getProxyUrls();
|
||||
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
|
||||
const QByteArray &iv = "", const QByteArray &salt = "");
|
||||
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption,
|
||||
const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
|
||||
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
|
||||
int m_requestTimeoutMsecs;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4_34)">
|
||||
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4_34">
|
||||
<rect width="74" height="74" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 982 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Основа газеты -->
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||
<!-- Линии текста -->
|
||||
<line x1="7" y1="8" x2="17" y2="8"/>
|
||||
<line x1="7" y1="12" x2="17" y2="12"/>
|
||||
<line x1="7" y1="16" x2="13" y2="16"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 188 B |
@@ -2,7 +2,8 @@ import Foundation
|
||||
import os.log
|
||||
|
||||
struct Log {
|
||||
static let osLog = Logger()
|
||||
private static let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
|
||||
static let osLog = Logger(subsystem: subsystemIdentifier, category: "App")
|
||||
|
||||
private static let IsLoggingEnabledKey = "IsLoggingEnabled"
|
||||
static var isLoggingEnabled: Bool {
|
||||
@@ -77,9 +78,40 @@ struct Log {
|
||||
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
|
||||
NSLog("\(title) \(message)")
|
||||
|
||||
guard isLoggingEnabled else { return }
|
||||
switch type {
|
||||
case .debug:
|
||||
if title.isEmpty {
|
||||
osLog.debug("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.debug("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .info:
|
||||
if title.isEmpty {
|
||||
osLog.info("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.info("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .error:
|
||||
if title.isEmpty {
|
||||
osLog.error("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.error("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .fault:
|
||||
if title.isEmpty {
|
||||
osLog.fault("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.fault("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
default:
|
||||
if title.isEmpty {
|
||||
osLog.log("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.log("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
osLog.log(level: type, "\(title) \(message)")
|
||||
guard isLoggingEnabled else { return }
|
||||
|
||||
let date = Date()
|
||||
let level = Record.Level(from: type)
|
||||
|
||||
@@ -1,22 +1,76 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
private let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
|
||||
private let wireGuardSystemLogger = Logger(subsystem: subsystemIdentifier, category: "WireGuard")
|
||||
private let openVPNSystemLogger = Logger(subsystem: subsystemIdentifier, category: "OpenVPN")
|
||||
private let xraySystemLogger = Logger(subsystem: subsystemIdentifier, category: "Xray")
|
||||
private let networkExtensionLogger = Logger(subsystem: subsystemIdentifier, category: "NetworkExtension")
|
||||
|
||||
private func logToSystem(_ logger: Logger, type: OSLogType, prefix: String, title: String, message: String) {
|
||||
let combinedTitle: String
|
||||
if title.isEmpty {
|
||||
combinedTitle = prefix
|
||||
} else {
|
||||
combinedTitle = "\(prefix): \(title)"
|
||||
}
|
||||
|
||||
switch type {
|
||||
case .debug:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.debug("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.debug("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .info:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.info("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.info("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .error:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.error("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.error("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .fault:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.fault("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.fault("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
default:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.log("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.log("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
|
||||
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
|
||||
let stringMessage = String(describing: staticMessage)
|
||||
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: stringMessage)
|
||||
neLog(type, title: "WG: \(title)", message: stringMessage)
|
||||
}
|
||||
|
||||
public func wg_log(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: message)
|
||||
neLog(type, title: "WG: \(title)", message: message)
|
||||
}
|
||||
|
||||
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(openVPNSystemLogger, type: type, prefix: "OVPN", title: title, message: message)
|
||||
neLog(type, title: "OVPN: \(title)", message: message)
|
||||
}
|
||||
|
||||
public func xrayLog(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(xraySystemLogger, type: type, prefix: "XRAY", title: title, message: message)
|
||||
neLog(type, title: "XRAY: \(title)", message: message)
|
||||
}
|
||||
|
||||
public func neLog(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(networkExtensionLogger, type: type, prefix: "NE", title: title, message: message)
|
||||
Log.log(type, title: "NE: \(title)", message: message)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import OpenVPNAdapter
|
||||
import CryptoKit
|
||||
|
||||
struct OpenVPNConfig: Decodable {
|
||||
let config: String
|
||||
@@ -27,26 +28,83 @@ extension PacketTunnelProvider {
|
||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||
} catch {
|
||||
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
|
||||
|
||||
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
|
||||
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
|
||||
}
|
||||
|
||||
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func logOpenVPNError(_ error: NSError) {
|
||||
let fatalFlag = (error.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false
|
||||
var lines: [String] = []
|
||||
lines.append("domain=\(error.domain) code=\(error.code) fatal=\(fatalFlag)")
|
||||
|
||||
if let adapterMessage = error.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !adapterMessage.isEmpty {
|
||||
lines.append("message=\(adapterMessage)")
|
||||
}
|
||||
|
||||
let userInfoKeys = error.userInfo.keys.map { String(describing: $0) }.sorted()
|
||||
if !userInfoKeys.isEmpty {
|
||||
lines.append("userInfoKeys=[\(userInfoKeys.joined(separator: ","))]")
|
||||
}
|
||||
|
||||
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
|
||||
lines.append("underlying=\(underlying.domain)#\(underlying.code) fatal=\((underlying.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false)")
|
||||
if let underlyingMessage = underlying.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !underlyingMessage.isEmpty {
|
||||
lines.append("underlyingMessage=\(underlyingMessage)")
|
||||
} else if !underlying.localizedDescription.isEmpty {
|
||||
lines.append("underlyingLocalized=\(underlying.localizedDescription)")
|
||||
}
|
||||
} else if let underlying = error.userInfo[NSUnderlyingErrorKey] {
|
||||
lines.append("underlyingRaw=\(underlying)")
|
||||
}
|
||||
|
||||
let formatted = lines.joined(separator: "\n ")
|
||||
ovpnLog(.error, title: "Error", message: formatted)
|
||||
}
|
||||
|
||||
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
|
||||
withShadowSocks viaSS: Bool = false,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
ovpnLog(.info, message: "Setup and launch")
|
||||
|
||||
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
|
||||
var configString = String(decoding: ovpnConfiguration, as: UTF8.self)
|
||||
|
||||
let digest = SHA256.hash(data: ovpnConfiguration)
|
||||
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
||||
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
||||
|
||||
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
||||
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
||||
|
||||
let lines = configString.split(separator: "\n")
|
||||
let head = lines.prefix(10).joined(separator: "\n")
|
||||
let tail = lines.suffix(10).joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "ConfigHead", message: head)
|
||||
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
||||
|
||||
if let start = configString.range(of: "<tls-auth>"),
|
||||
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
|
||||
let keyBody = String(configString[start.upperBound..<end.lowerBound])
|
||||
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
|
||||
let sanitizedLines = keyBody
|
||||
.split(whereSeparator: { $0.isNewline })
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.filter { !$0.hasPrefix("#") }
|
||||
|
||||
let sanitizedKey = sanitizedLines.joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
|
||||
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
|
||||
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
|
||||
}
|
||||
|
||||
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
let sanitizedData = Data(normalizedConfig.utf8)
|
||||
|
||||
let configuration = OpenVPNConfiguration()
|
||||
configuration.fileContent = ovpnConfiguration
|
||||
if str.contains("cloak") {
|
||||
configuration.fileContent = sanitizedData
|
||||
if configString.contains("cloak") {
|
||||
configuration.setPTCloak()
|
||||
}
|
||||
|
||||
@@ -57,6 +115,8 @@ extension PacketTunnelProvider {
|
||||
evaluation = try ovpnAdapter?.apply(configuration: configuration)
|
||||
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
@@ -73,7 +133,7 @@ extension PacketTunnelProvider {
|
||||
startHandler = completionHandler
|
||||
ovpnAdapter?.connect(using: packetFlow)
|
||||
}
|
||||
|
||||
|
||||
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
guard let completionHandler = completionHandler else { return }
|
||||
let bytesin = ovpnAdapter?.transportStatistics.bytesIn
|
||||
@@ -169,20 +229,8 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||
}
|
||||
if splitTunnelType == 0 || splitTunnelType == nil {
|
||||
// Full tunnel: send all traffic via VPN
|
||||
if let ipv4Settings = networkSettings?.ipv4Settings {
|
||||
ipv4Settings.includedRoutes = [NEIPv4Route.default()]
|
||||
NSLog("[Route] Added default IPv4 route (0.0.0.0/0)")
|
||||
}
|
||||
|
||||
if let ipv6Settings = networkSettings?.ipv6Settings {
|
||||
let ipv6DefaultRoute = NEIPv6Route(destinationAddress: "::", networkPrefixLength: 0)
|
||||
ipv6Settings.includedRoutes = [ipv6DefaultRoute]
|
||||
NSLog("[Route] Added default IPv6 route (::/0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the network settings for the current tunneling session.
|
||||
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
||||
}
|
||||
@@ -220,8 +268,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
|
||||
// Handle errors thrown by the OpenVPN library
|
||||
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
|
||||
let nsError = error as NSError
|
||||
logOpenVPNError(nsError)
|
||||
|
||||
// Handle only fatal errors
|
||||
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
|
||||
guard let fatal = nsError.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
|
||||
fatal == true else { return }
|
||||
|
||||
if vpnReachability.isTracking {
|
||||
|
||||
@@ -27,6 +27,7 @@ const char* MessageKey::isOnDemand = "is-on-demand";
|
||||
const char* MessageKey::SplitTunnelType = "SplitTunnelType";
|
||||
const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
|
||||
|
||||
#if !MACOS_NE
|
||||
static UIViewController* getViewController() {
|
||||
UIApplication *application = [UIApplication sharedApplication];
|
||||
|
||||
@@ -70,6 +71,7 @@ static UIViewController* getViewController() {
|
||||
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
|
||||
switch (status) {
|
||||
@@ -160,39 +162,6 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
||||
m_rawConfig = configuration;
|
||||
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
||||
|
||||
if (proto == amnezia::Proto::OpenVpn) {
|
||||
QJsonObject ovpn = configuration["openvpn_config_data"].toObject();
|
||||
QString ovpnConfig = ovpn["config"].toString();
|
||||
QStringList unsupportedDirectives = {
|
||||
"resolv-retry",
|
||||
"persist-key",
|
||||
"persist-tun",
|
||||
"block-ipv6",
|
||||
"redirect-gateway"
|
||||
};
|
||||
|
||||
QStringList lines = ovpnConfig.split('\n');
|
||||
QStringList filteredLines;
|
||||
for (const QString &line : lines) {
|
||||
QString trimmedLine = line.trimmed();
|
||||
|
||||
bool shouldIgnore = false;
|
||||
for (const QString &bad : unsupportedDirectives) {
|
||||
if (trimmedLine.startsWith(bad)) {
|
||||
shouldIgnore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldIgnore) {
|
||||
filteredLines.append(line);
|
||||
}
|
||||
}
|
||||
ovpnConfig = filteredLines.join("\n");
|
||||
ovpn["config"] = ovpnConfig;
|
||||
m_rawConfig["openvpn_config_data"] = ovpn;
|
||||
}
|
||||
|
||||
QString tunnelName;
|
||||
if (configuration.value(config_key::description).toString().isEmpty()) {
|
||||
tunnelName = QString("%1 %2")
|
||||
@@ -316,6 +285,21 @@ void IosController::checkStatus()
|
||||
sendVpnExtensionMessage(message, [&](NSDictionary* response){
|
||||
uint64_t txBytes = [response[@"tx_bytes"] intValue];
|
||||
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
|
||||
|
||||
uint64_t last_handshake_time_sec = 0;
|
||||
#if !MACOS_NE
|
||||
if (response[@"last_handshake_time_sec"] && ![response[@"last_handshake_time_sec"] isKindOfClass:[NSNull class]]) {
|
||||
last_handshake_time_sec = [response[@"last_handshake_time_sec"] intValue];
|
||||
} else {
|
||||
qDebug() << "Key last_handshake_time_sec is missing or null";
|
||||
}
|
||||
|
||||
if (last_handshake_time_sec < 0) {
|
||||
disconnectVpn();
|
||||
qDebug() << "Invalid handshake time, disconnecting VPN.";
|
||||
}
|
||||
#endif
|
||||
|
||||
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
|
||||
m_rxBytes = rxBytes;
|
||||
m_txBytes = txBytes;
|
||||
@@ -574,6 +558,8 @@ bool IosController::setupWireGuard()
|
||||
|
||||
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
|
||||
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
|
||||
wgConfig.insert(config_key::cookieReplyPacketJunkSize, config[config_key::cookieReplyPacketJunkSize]);
|
||||
wgConfig.insert(config_key::transportPacketJunkSize, config[config_key::transportPacketJunkSize]);
|
||||
|
||||
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
|
||||
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
|
||||
@@ -672,11 +658,23 @@ bool IosController::setupAwg()
|
||||
|
||||
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
|
||||
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
|
||||
wgConfig.insert(config_key::cookieReplyPacketJunkSize, config[config_key::cookieReplyPacketJunkSize]);
|
||||
wgConfig.insert(config_key::transportPacketJunkSize, config[config_key::transportPacketJunkSize]);
|
||||
|
||||
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
|
||||
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
|
||||
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
|
||||
|
||||
wgConfig.insert(config_key::specialJunk1, config[config_key::specialJunk1]);
|
||||
wgConfig.insert(config_key::specialJunk2, config[config_key::specialJunk2]);
|
||||
wgConfig.insert(config_key::specialJunk3, config[config_key::specialJunk3]);
|
||||
wgConfig.insert(config_key::specialJunk4, config[config_key::specialJunk4]);
|
||||
wgConfig.insert(config_key::specialJunk5, config[config_key::specialJunk5]);
|
||||
wgConfig.insert(config_key::controlledJunk1, config[config_key::controlledJunk1]);
|
||||
wgConfig.insert(config_key::controlledJunk2, config[config_key::controlledJunk2]);
|
||||
wgConfig.insert(config_key::controlledJunk3, config[config_key::controlledJunk3]);
|
||||
wgConfig.insert(config_key::specialHandshakeTimeout, config[config_key::specialHandshakeTimeout]);
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
@@ -856,14 +854,14 @@ bool IosController::shareText(const QStringList& filesToSend) {
|
||||
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
|
||||
[sharingItems addObject:logFileUrl];
|
||||
}
|
||||
|
||||
#if !MACOS_NE
|
||||
UIViewController *qtController = getViewController();
|
||||
if (!qtController) return;
|
||||
|
||||
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
|
||||
|
||||
#endif
|
||||
__block bool isAccepted = false;
|
||||
|
||||
#if !MACOS_NE
|
||||
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
|
||||
isAccepted = completed;
|
||||
emit finished();
|
||||
@@ -875,15 +873,17 @@ bool IosController::shareText(const QStringList& filesToSend) {
|
||||
popController.sourceView = qtController.view;
|
||||
popController.sourceRect = CGRectMake(100, 100, 100, 100);
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
|
||||
|
||||
return isAccepted;
|
||||
}
|
||||
|
||||
QString IosController::openFile() {
|
||||
#if !MACOS_NE
|
||||
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
|
||||
|
||||
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
|
||||
@@ -893,9 +893,10 @@ QString IosController::openFile() {
|
||||
if (!qtController) return;
|
||||
|
||||
[qtController presentViewController:documentPicker animated:YES completion:nil];
|
||||
|
||||
__block QString filePath;
|
||||
|
||||
#endif
|
||||
__block QString filePath;
|
||||
#if !MACOS_NE
|
||||
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
|
||||
if (path) {
|
||||
filePath = QString::fromUtf8(path.UTF8String);
|
||||
@@ -904,11 +905,11 @@ QString IosController::openFile() {
|
||||
}
|
||||
emit finished();
|
||||
};
|
||||
|
||||
#endif
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ void XrayProtocol::stop()
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
|
||||
IpcClient::Interface()->disableKillSwitch();
|
||||
IpcClient::Interface()->StartRoutingIpv6();
|
||||
IpcClient::Interface()->restoreResolvers();
|
||||
#endif
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
m_xrayProcess.disconnect();
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
<file>images/controls/mail.svg</file>
|
||||
<file>images/controls/map-pin.svg</file>
|
||||
<file>images/controls/more-vertical.svg</file>
|
||||
<file>images/controls/news.svg</file>
|
||||
<file>images/controls/news-unread.svg</file>
|
||||
<file>images/controls/unread-dot.svg</file>
|
||||
<file>images/controls/plus.svg</file>
|
||||
<file>images/controls/qr-code.svg</file>
|
||||
<file>images/controls/radio-button-inner-circle-pressed.png</file>
|
||||
@@ -49,6 +52,7 @@
|
||||
<file>images/controls/server.svg</file>
|
||||
<file>images/controls/settings-2.svg</file>
|
||||
<file>images/controls/settings.svg</file>
|
||||
<file>images/controls/settings-news.svg</file>
|
||||
<file>images/controls/share-2.svg</file>
|
||||
<file>images/controls/split-tunneling.svg</file>
|
||||
<file>images/controls/tag.svg</file>
|
||||
@@ -212,6 +216,8 @@
|
||||
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsNewsNotifications.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
|
||||
|
||||
+12
-2
@@ -541,12 +541,12 @@ QString Settings::getGatewayEndpoint()
|
||||
|
||||
bool Settings::isDevGatewayEnv()
|
||||
{
|
||||
return m_isDevGatewayEnv;
|
||||
return value("Conf/devGatewayEnv", false).toBool();
|
||||
}
|
||||
|
||||
void Settings::toggleDevGatewayEnv(bool enabled)
|
||||
{
|
||||
m_isDevGatewayEnv = enabled;
|
||||
setValue("Conf/devGatewayEnv", enabled);
|
||||
}
|
||||
|
||||
bool Settings::isHomeAdLabelVisible()
|
||||
@@ -578,3 +578,13 @@ void Settings::setAllowedDnsServers(const QStringList &servers)
|
||||
{
|
||||
setValue("Conf/allowedDnsServers", servers);
|
||||
}
|
||||
|
||||
QStringList Settings::readNewsIds() const
|
||||
{
|
||||
return value("News/readIds").toStringList();
|
||||
}
|
||||
|
||||
void Settings::setReadNewsIds(const QStringList &ids)
|
||||
{
|
||||
setValue("News/readIds", ids);
|
||||
}
|
||||
|
||||
+3
-1
@@ -236,6 +236,9 @@ public:
|
||||
QStringList allowedDnsServers() const;
|
||||
void setAllowedDnsServers(const QStringList &servers);
|
||||
|
||||
QStringList readNewsIds() const;
|
||||
void setReadNewsIds(const QStringList &ids);
|
||||
|
||||
signals:
|
||||
void saveLogsChanged(bool enabled);
|
||||
void screenshotsEnabledChanged(bool enabled);
|
||||
@@ -251,7 +254,6 @@ private:
|
||||
mutable SecureQSettings m_settings;
|
||||
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isDevGatewayEnv = false;
|
||||
};
|
||||
|
||||
#endif // SETTINGS_H
|
||||
|
||||
@@ -94,12 +94,12 @@
|
||||
<translation>%1 успешно установлен.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="472"/>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="473"/>
|
||||
<source>API config reloaded</source>
|
||||
<translation>Конфигурация API перезагружена</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="476"/>
|
||||
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="477"/>
|
||||
<source>Successfully changed the country of connection to %1</source>
|
||||
<translation>Страна подключения изменена на %1</translation>
|
||||
</message>
|
||||
@@ -2079,52 +2079,40 @@ Thank you for staying with us!</source>
|
||||
<context>
|
||||
<name>PageSettingsApiSubscriptionKey</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="43"/>
|
||||
<source>Amnezia Premium
|
||||
subscription key</source>
|
||||
<translation>Amnezia Premium
|
||||
ключ подключения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="56"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
|
||||
<source>Copy key</source>
|
||||
<translation>Скопировать ключ</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="61"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="90"/>
|
||||
<source>Copied</source>
|
||||
<translation>Скопировано</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="77"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="106"/>
|
||||
<source>Save key as a file</source>
|
||||
<translation>Сохранить ключ как файл</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="84"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="113"/>
|
||||
<source>Save AmneziaVPN config</source>
|
||||
<translation>Сохранить конфигурацию AmneziaVPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="114"/>
|
||||
<source>Config files (*.vpn)</source>
|
||||
<translation>Файлы конфигов (*.vpn)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="110"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="139"/>
|
||||
<source>Show key text</source>
|
||||
<translation>Показать ключ</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="147"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="180"/>
|
||||
<source>To read the QR code in the Amnezia app, tap + in the main menu → 'QR code'</source>
|
||||
<translation>Для считывания QR-кода в приложении Amnezia выберите + в главном меню → 'QR-код'</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="176"/>
|
||||
<source>Amnezia Premium Subscription key</source>
|
||||
<translation>Ключ подключения Amnezia Premium</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PageSettingsApiSupport</name>
|
||||
@@ -2197,32 +2185,37 @@ subscription key</source>
|
||||
<translation>Режим</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="185"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="155"/>
|
||||
<source>Only "Apps from the list should not have access via VPN" mode is available on Windows</source>
|
||||
<translation>На Windows доступен только режим "Приложения из списка не должны работать через VPN"</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="199"/>
|
||||
<source>Remove </source>
|
||||
<translation>Удалить </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="186"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="200"/>
|
||||
<source>Continue</source>
|
||||
<translation>Продолжить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="187"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="201"/>
|
||||
<source>Cancel</source>
|
||||
<translation>Отменить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="228"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="242"/>
|
||||
<source>application name</source>
|
||||
<translation>название приложения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="238"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="252"/>
|
||||
<source>Open executable file</source>
|
||||
<translation>Открыть исполняемый файл</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="239"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="253"/>
|
||||
<source>Executable files (*.*)</source>
|
||||
<translation>Исполняемые файлы (*.*)</translation>
|
||||
</message>
|
||||
@@ -3013,19 +3006,19 @@ subscription key</source>
|
||||
<translation>Режим</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="207"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
|
||||
<source>Remove </source>
|
||||
<translation>Удалить </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="208"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="210"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="358"/>
|
||||
<source>Continue</source>
|
||||
<translation>Продолжить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="211"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="359"/>
|
||||
<source>Cancel</source>
|
||||
<translation>Отменить</translation>
|
||||
</message>
|
||||
@@ -3040,70 +3033,70 @@ subscription key</source>
|
||||
<translation>Невозможно изменить настройки раздельного туннелирования во время активного соединения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="257"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="259"/>
|
||||
<source>website or IP</source>
|
||||
<translation>веб-сайт или IP</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="302"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="304"/>
|
||||
<source>Additional options</source>
|
||||
<translation>Дополнительные настройки</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="309"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="311"/>
|
||||
<source>Import</source>
|
||||
<translation>Импорт</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="322"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="324"/>
|
||||
<source>Save site list</source>
|
||||
<translation>Сохранить список сайтов</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="329"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="331"/>
|
||||
<source>Save sites</source>
|
||||
<translation>Сохранить сайты</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="330"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="332"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="459"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="472"/>
|
||||
<source>Sites files (*.json)</source>
|
||||
<translation>Файлы сайтов (*.json)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="350"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="352"/>
|
||||
<source>Clear site list</source>
|
||||
<translation>Очистить список сайтов</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="354"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
|
||||
<source>Clear site list?</source>
|
||||
<translation>Очистить список сайтов?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
|
||||
<source>All sites will be removed from list.</source>
|
||||
<translation>Все сайты будут удалены из списка.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="419"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="421"/>
|
||||
<source>Import a list of sites</source>
|
||||
<translation>Импортировать список с сайтами</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="454"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/>
|
||||
<source>Replace site list</source>
|
||||
<translation>Заменить список с сайтами</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="458"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="471"/>
|
||||
<source>Open sites file</source>
|
||||
<translation>Открыть список с сайтами</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="467"/>
|
||||
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/>
|
||||
<source>Add imported sites to existing ones</source>
|
||||
<translation>Добавить импортированные сайты к существующим</translation>
|
||||
</message>
|
||||
@@ -3394,38 +3387,38 @@ subscription key</source>
|
||||
<context>
|
||||
<name>PageSetupWizardInstalling</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="59"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="63"/>
|
||||
<source>The server has already been added to the application</source>
|
||||
<translation>Сервер уже был добавлен в приложение</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="65"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="69"/>
|
||||
<source>Amnezia has detected that your server is currently </source>
|
||||
<translation>Amnezia обнаружила, что ваш сервер в настоящее время </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="66"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="70"/>
|
||||
<source>busy installing other software. Amnezia installation </source>
|
||||
<translation>занят установкой других протоколов или сервисов. Установка Amnezia </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="67"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
|
||||
<source>will pause until the server finishes installing other software</source>
|
||||
<translation>будет приостановлена до тех пор, пока сервер не завершит установку другого ПО</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="106"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="110"/>
|
||||
<source>Installing</source>
|
||||
<translation>Установка</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="151"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="155"/>
|
||||
<source>Cancel installation</source>
|
||||
<translation>Отменить установку</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="22"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="75"/>
|
||||
<source>Usually it takes no more than 5 minutes</source>
|
||||
<translation>Обычно это занимает не более 5 минут</translation>
|
||||
</message>
|
||||
@@ -3561,217 +3554,217 @@ subscription key</source>
|
||||
<context>
|
||||
<name>PageShare</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
|
||||
<source>OpenVPN native format</source>
|
||||
<translation>Оригинальный формат OpenVPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
|
||||
<source>WireGuard native format</source>
|
||||
<translation>Оригинальный формат WireGuard</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="260"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="255"/>
|
||||
<source>Connection</source>
|
||||
<translation>Соединение</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="328"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="329"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="323"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="324"/>
|
||||
<source>Server</source>
|
||||
<translation>Сервер</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="39"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="37"/>
|
||||
<source>Config revoked</source>
|
||||
<translation>Конфигурация отозвана</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="55"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="50"/>
|
||||
<source>Save AmneziaVPN config</source>
|
||||
<translation>Сохранить конфигурацию AmneziaVPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="62"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="57"/>
|
||||
<source>Save OpenVPN config</source>
|
||||
<translation>Сохранить конфигурацию OpenVPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="69"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="64"/>
|
||||
<source>Save WireGuard config</source>
|
||||
<translation>Сохранить конфигурацию WireGuard</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="76"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="71"/>
|
||||
<source>Save AmneziaWG config</source>
|
||||
<translation>Сохранить конфигурацию AmneziaWG</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="83"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="78"/>
|
||||
<source>Save Shadowsocks config</source>
|
||||
<translation>Сохранить конфигурацию Shadowsocks</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="90"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="85"/>
|
||||
<source>Save Cloak config</source>
|
||||
<translation>Сохранить конфигурацию Cloak</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="97"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="92"/>
|
||||
<source>Save XRay config</source>
|
||||
<translation>Сохранить конфигурацию XRay</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="106"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="101"/>
|
||||
<source>Connection to </source>
|
||||
<translation>Подключение к </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="107"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="102"/>
|
||||
<source>File with connection settings to </source>
|
||||
<translation>Файл с настройками подключения к </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="120"/>
|
||||
<source>For the AmneziaVPN app</source>
|
||||
<translation>Для приложения AmneziaVPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
|
||||
<source>AmneziaWG native format</source>
|
||||
<translation>Оригинальный формат AmneziaWG</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
|
||||
<source>Shadowsocks native format</source>
|
||||
<translation>Оригинальный формат Shadowsocks</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
|
||||
<source>Cloak native format</source>
|
||||
<translation>Оригинальный формат Cloak</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="155"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
|
||||
<source>XRay native format</source>
|
||||
<translation>Оригинальный формат XRay</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="183"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="178"/>
|
||||
<source>Share VPN Access</source>
|
||||
<translation>Поделиться VPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="217"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="212"/>
|
||||
<source>Share full access to the server and VPN</source>
|
||||
<translation>Поделиться полным доступом к серверу и VPN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="218"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="213"/>
|
||||
<source>Use for your own devices, or share with those you trust to manage the server.</source>
|
||||
<translation>Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="275"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="555"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="270"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="550"/>
|
||||
<source>Users</source>
|
||||
<translation>Пользователи</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="309"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="304"/>
|
||||
<source>User name</source>
|
||||
<translation>Имя пользователя</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="571"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="566"/>
|
||||
<source>Search</source>
|
||||
<translation>Поиск</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="694"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="691"/>
|
||||
<source>Creation date: %1</source>
|
||||
<translation>Дата создания: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="706"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="703"/>
|
||||
<source>Latest handshake: %1</source>
|
||||
<translation>Последнее рукопожатие: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="718"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="715"/>
|
||||
<source>Data received: %1</source>
|
||||
<translation>Получено данных: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="730"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="727"/>
|
||||
<source>Data sent: %1</source>
|
||||
<translation>Отправлено данных: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="740"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="737"/>
|
||||
<source>Allowed IPs: %1</source>
|
||||
<translation>Разрешенные подсети: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="755"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="752"/>
|
||||
<source>Rename</source>
|
||||
<translation>Переименовать</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="780"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="777"/>
|
||||
<source>Client name</source>
|
||||
<translation>Имя клиента</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="791"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="788"/>
|
||||
<source>Save</source>
|
||||
<translation>Сохранить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="825"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="824"/>
|
||||
<source>Revoke</source>
|
||||
<translation>Отозвать</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="827"/>
|
||||
<source>Revoke the config for a user - %1?</source>
|
||||
<translation>Отозвать конфигурацию для пользователя - %1?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
|
||||
<source>The user will no longer be able to connect to your server.</source>
|
||||
<translation>Пользователь больше не сможет подключаться к вашему серверу.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
|
||||
<source>Continue</source>
|
||||
<translation>Продолжить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="831"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
|
||||
<source>Cancel</source>
|
||||
<translation>Отменить</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="298"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="293"/>
|
||||
<source>Share VPN access without the ability to manage the server</source>
|
||||
<translation>Поделиться доступом к VPN без возможности управления сервером</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="389"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="390"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="384"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="385"/>
|
||||
<source>Protocol</source>
|
||||
<translation>Протокол</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="496"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="497"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="491"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="492"/>
|
||||
<source>Connection format</source>
|
||||
<translation>Формат подключения</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="225"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="537"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="220"/>
|
||||
<location filename="../ui/qml/Pages2/PageShare.qml" line="532"/>
|
||||
<source>Share</source>
|
||||
<translation>Поделиться</translation>
|
||||
</message>
|
||||
@@ -3810,7 +3803,7 @@ subscription key</source>
|
||||
<translation>Скопировано</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="319"/>
|
||||
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="323"/>
|
||||
<source>To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file"</source>
|
||||
<translation>Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код"</translation>
|
||||
</message>
|
||||
@@ -3841,22 +3834,22 @@ subscription key</source>
|
||||
<translation>Сервер</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="114"/>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
|
||||
<source>Accessing </source>
|
||||
<translation>Доступ </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="116"/>
|
||||
<source>File with accessing settings to </source>
|
||||
<translation>Файл с настройками доступа к </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="146"/>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="147"/>
|
||||
<source>Share</source>
|
||||
<translation>Поделиться</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="154"/>
|
||||
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="155"/>
|
||||
<source>Access error!</source>
|
||||
<translation>Ошибка доступа!</translation>
|
||||
</message>
|
||||
@@ -4957,12 +4950,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<context>
|
||||
<name>SettingsController</name>
|
||||
<message>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="242"/>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="250"/>
|
||||
<source>All settings have been reset to default values</source>
|
||||
<translation>Все настройки сброшены до значений по умолчанию</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="219"/>
|
||||
<location filename="../ui/controllers/settingsController.cpp" line="227"/>
|
||||
<source>Backup file is corrupted</source>
|
||||
<translation>Файл резервной копии поврежден</translation>
|
||||
</message>
|
||||
@@ -5051,7 +5044,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
|
||||
<context>
|
||||
<name>TextFieldWithHeaderType</name>
|
||||
<message>
|
||||
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="117"/>
|
||||
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="133"/>
|
||||
<source>The field can't be empty</source>
|
||||
<translation>Поле не может быть пустым</translation>
|
||||
</message>
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace
|
||||
|
||||
constexpr char subscription[] = "subscription";
|
||||
constexpr char endDate[] = "end_date";
|
||||
|
||||
constexpr char isConnectEvent[] = "is_connect_event";
|
||||
}
|
||||
|
||||
struct ProtocolData
|
||||
@@ -249,6 +251,23 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &s
|
||||
{
|
||||
}
|
||||
|
||||
bool ApiConfigsController::exportVpnKey(const QString &fileName)
|
||||
{
|
||||
if (fileName.isEmpty()) {
|
||||
emit errorOccurred(ErrorCode::PermissionsError);
|
||||
return false;
|
||||
}
|
||||
|
||||
prepareVpnKeyExport();
|
||||
if (m_vpnKey.isEmpty()) {
|
||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
||||
return false;
|
||||
}
|
||||
|
||||
SystemController::saveFile(fileName, m_vpnKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName)
|
||||
{
|
||||
if (fileName.isEmpty()) {
|
||||
@@ -330,6 +349,13 @@ void ApiConfigsController::prepareVpnKeyExport()
|
||||
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
|
||||
|
||||
auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString();
|
||||
if (vpnKey.isEmpty()) {
|
||||
vpnKey = apiUtils::getPremiumV2VpnKey(serverConfigObject);
|
||||
apiConfigObject.insert(apiDefs::key::vpnKey, vpnKey);
|
||||
serverConfigObject.insert(configKey::apiConfig, apiConfigObject);
|
||||
m_serversModel->editServer(serverConfigObject, m_serversModel->getProcessedServerIndex());
|
||||
}
|
||||
|
||||
m_vpnKey = vpnKey;
|
||||
|
||||
vpnKey.replace("vpn://", "");
|
||||
@@ -443,6 +469,10 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
|
||||
|
||||
if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) {
|
||||
apiPayload.insert(configKey::isConnectEvent, true);
|
||||
}
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||
|
||||
@@ -526,7 +556,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
|
||||
}
|
||||
}
|
||||
|
||||
bool ApiConfigsController::deactivateDevice()
|
||||
bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
|
||||
{
|
||||
auto serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
|
||||
@@ -537,8 +567,12 @@ bool ApiConfigsController::deactivateDevice()
|
||||
}
|
||||
|
||||
if (isSubscriptionExpired(apiConfigObject)) {
|
||||
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
|
||||
return false;
|
||||
if (isRemoveEvent) {
|
||||
return true;
|
||||
} else {
|
||||
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
|
||||
@@ -21,7 +21,7 @@ public:
|
||||
public slots:
|
||||
bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName);
|
||||
bool revokeNativeConfig(const QString &serverCountryCode);
|
||||
// bool exportVpnKey(const QString &fileName);
|
||||
bool exportVpnKey(const QString &fileName);
|
||||
void prepareVpnKeyExport();
|
||||
void copyVpnKeyToClipboard();
|
||||
|
||||
@@ -30,7 +30,7 @@ public slots:
|
||||
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig = false);
|
||||
bool updateServiceFromTelegram(const int serverIndex);
|
||||
bool deactivateDevice();
|
||||
bool deactivateDevice(const bool isRemoveEvent);
|
||||
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
|
||||
|
||||
bool isConfigValid();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "apiNewsController.h"
|
||||
|
||||
#include "core/api/apiUtils.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace configKey
|
||||
{
|
||||
constexpr char userCountryCode[] = "user_country_code";
|
||||
constexpr char serviceType[] = "service_type";
|
||||
}
|
||||
}
|
||||
|
||||
ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
|
||||
const QSharedPointer<ServersModel> &serversModel, QObject *parent)
|
||||
: QObject(parent), m_newsModel(newsModel), m_settings(settings), m_serversModel(serversModel)
|
||||
{
|
||||
}
|
||||
|
||||
void ApiNewsController::fetchNews()
|
||||
{
|
||||
if (m_serversModel.isNull()) {
|
||||
qWarning() << "ServersModel is null, skip fetchNews";
|
||||
return;
|
||||
}
|
||||
const auto stacks = m_serversModel->gatewayStacks();
|
||||
if (stacks.isEmpty()) {
|
||||
qDebug() << "No Gateway stacks, skip fetchNews";
|
||||
return;
|
||||
}
|
||||
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_settings->isStrictKillSwitchEnabled());
|
||||
QByteArray responseBody;
|
||||
QJsonObject payload;
|
||||
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
|
||||
|
||||
const QJsonObject stacksJson = stacks.toJson();
|
||||
if (stacksJson.contains(configKey::userCountryCode)) {
|
||||
payload.insert(configKey::userCountryCode, stacksJson.value(configKey::userCountryCode));
|
||||
}
|
||||
if (stacksJson.contains(configKey::serviceType)) {
|
||||
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
|
||||
}
|
||||
|
||||
ErrorCode errorCode = gatewayController.post(QString("%1v1/news"), payload, responseBody);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit errorOccurred(errorCode);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
|
||||
QJsonArray newsArray;
|
||||
if (doc.isArray()) {
|
||||
newsArray = doc.array();
|
||||
} else if (doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
if (obj.value("news").isArray()) {
|
||||
newsArray = obj.value("news").toArray();
|
||||
}
|
||||
}
|
||||
|
||||
m_newsModel->updateModel(newsArray);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef APINEWSCONTROLLER_H
|
||||
#define APINEWSCONTROLLER_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QObject>
|
||||
#include <QSharedPointer>
|
||||
#include <memory>
|
||||
|
||||
#include "core/api/apiDefs.h"
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "settings.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
#include "ui/models/servers_model.h"
|
||||
|
||||
class ApiNewsController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
|
||||
const QSharedPointer<ServersModel> &serversModel, QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void fetchNews();
|
||||
|
||||
signals:
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
|
||||
private:
|
||||
QSharedPointer<NewsModel> m_newsModel;
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
};
|
||||
|
||||
#endif // APINEWSCONTROLLER_H
|
||||
@@ -26,6 +26,8 @@ namespace PageLoader
|
||||
PageSettingsConnection,
|
||||
PageSettingsDns,
|
||||
PageSettingsApplication,
|
||||
PageSettingsNewsNotifications,
|
||||
PageSettingsNewsDetail,
|
||||
PageSettingsBackup,
|
||||
PageSettingsAbout,
|
||||
PageSettingsLogging,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "logger.h"
|
||||
#include "systemController.h"
|
||||
#include "ui/qautostart.h"
|
||||
#include "amnezia_application.h"
|
||||
#include "version.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
@@ -33,6 +34,9 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged);
|
||||
#endif
|
||||
|
||||
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
|
||||
toggleDevGatewayEnv(m_isDevModeEnabled);
|
||||
}
|
||||
|
||||
QString getPlatformName()
|
||||
@@ -139,6 +143,10 @@ void SettingsController::clearLogs()
|
||||
Logger::clearLogs(false);
|
||||
Logger::clearServiceLogs();
|
||||
#endif
|
||||
|
||||
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
|
||||
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
|
||||
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
|
||||
}
|
||||
|
||||
void SettingsController::backupAppConfig(const QString &fileName)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#include "ui/models/newsModel.h"
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QQmlEngine>
|
||||
#include <QStandardPaths>
|
||||
#include <algorithm>
|
||||
|
||||
NewsModel::NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent) : QAbstractListModel(parent), m_settings(settings)
|
||||
{
|
||||
loadReadIds();
|
||||
}
|
||||
|
||||
int NewsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_items.size();
|
||||
}
|
||||
|
||||
QVariant NewsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
|
||||
return QVariant();
|
||||
|
||||
const NewsItem &item = m_items.at(index.row());
|
||||
switch (role) {
|
||||
case IdRole: return item.id;
|
||||
case TitleRole: return item.title;
|
||||
case ContentRole: return item.content;
|
||||
case TimestampRole: return item.timestamp.toString(Qt::ISODate);
|
||||
case IsReadRole: return item.read;
|
||||
case IsProcessedRole: return index.row() == m_processedIndex;
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> NewsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[IdRole] = "id";
|
||||
roles[TitleRole] = "title";
|
||||
roles[ContentRole] = "content";
|
||||
roles[TimestampRole] = "timestamp";
|
||||
roles[IsReadRole] = "read";
|
||||
roles[IsProcessedRole] = "isProcessed";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void NewsModel::markAsRead(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_items.size())
|
||||
return;
|
||||
if (!m_items[index].read) {
|
||||
m_items[index].read = true;
|
||||
m_readIds.insert(m_items[index].id);
|
||||
saveReadIds();
|
||||
QModelIndex idx = createIndex(index, 0);
|
||||
emit dataChanged(idx, idx, { IsReadRole });
|
||||
emit hasUnreadChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int NewsModel::processedIndex() const
|
||||
{
|
||||
return m_processedIndex;
|
||||
}
|
||||
|
||||
void NewsModel::setProcessedIndex(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_items.size() || m_processedIndex == index)
|
||||
return;
|
||||
m_processedIndex = index;
|
||||
emit processedIndexChanged(index);
|
||||
}
|
||||
|
||||
void NewsModel::updateModel(const QJsonArray &serverItems)
|
||||
{
|
||||
QSet<QString> existingIds;
|
||||
for (const NewsItem &item : m_items) {
|
||||
existingIds.insert(item.id);
|
||||
}
|
||||
|
||||
QList<NewsItem> newItems;
|
||||
for (const QJsonValue &value : serverItems) {
|
||||
if (!value.isObject())
|
||||
continue;
|
||||
const QJsonObject obj = value.toObject();
|
||||
QString id = obj.value("id").toString();
|
||||
|
||||
if (!existingIds.contains(id)) {
|
||||
NewsItem item;
|
||||
item.id = id;
|
||||
item.title = obj.value("title").toString();
|
||||
item.content = obj.value("content").toString();
|
||||
item.timestamp = QDateTime::fromString(obj.value("timestamp").toString(), Qt::ISODate);
|
||||
item.read = m_readIds.contains(id);
|
||||
newItems.append(item);
|
||||
existingIds.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_items.append(newItems);
|
||||
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
|
||||
endResetModel();
|
||||
emit hasUnreadChanged();
|
||||
}
|
||||
|
||||
bool NewsModel::hasUnread() const
|
||||
{
|
||||
for (const NewsItem &item : m_items) {
|
||||
if (!item.read)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void NewsModel::loadReadIds()
|
||||
{
|
||||
QStringList ids = m_settings->readNewsIds();
|
||||
m_readIds = QSet<QString>(ids.begin(), ids.end());
|
||||
}
|
||||
|
||||
void NewsModel::saveReadIds() const
|
||||
{
|
||||
m_settings->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end()));
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef NEWSMODEL_H
|
||||
#define NEWSMODEL_H
|
||||
|
||||
#include "settings.h"
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <memory>
|
||||
|
||||
struct NewsItem
|
||||
{
|
||||
QString id;
|
||||
QString title;
|
||||
QString content;
|
||||
QDateTime timestamp;
|
||||
bool read;
|
||||
};
|
||||
|
||||
class NewsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
TitleRole,
|
||||
ContentRole,
|
||||
TimestampRole,
|
||||
IsReadRole,
|
||||
IsProcessedRole
|
||||
};
|
||||
explicit NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
|
||||
Q_INVOKABLE void markAsRead(int index);
|
||||
|
||||
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
|
||||
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
|
||||
int processedIndex() const;
|
||||
void setProcessedIndex(int index);
|
||||
|
||||
void updateModel(const QJsonArray &items);
|
||||
bool hasUnread() const;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
signals:
|
||||
void processedIndexChanged(int index);
|
||||
void hasUnreadChanged();
|
||||
|
||||
private:
|
||||
QVector<NewsItem> m_items;
|
||||
int m_processedIndex = -1;
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
QSet<QString> m_readIds;
|
||||
void loadReadIds();
|
||||
void saveReadIds() const;
|
||||
};
|
||||
|
||||
#endif // NEWSMODEL_H
|
||||
@@ -44,6 +44,8 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
|
||||
|
||||
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
|
||||
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
|
||||
|
||||
connect(this, &QAbstractItemModel::modelReset, this, &ServersModel::recomputeGatewayStacks);
|
||||
}
|
||||
|
||||
int ServersModel::rowCount(const QModelIndex &parent) const
|
||||
@@ -375,7 +377,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
|
||||
roles[NameRole] = "serverName";
|
||||
roles[NameRole] = "name";
|
||||
roles[ServerDescriptionRole] = "serverDescription";
|
||||
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
|
||||
@@ -756,6 +757,68 @@ bool ServersModel::isServerFromApi(const int serverIndex)
|
||||
return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool();
|
||||
}
|
||||
|
||||
bool ServersModel::hasServersFromGatewayApi()
|
||||
{
|
||||
return !m_gatewayStacks.isEmpty();
|
||||
}
|
||||
|
||||
bool ServersModel::GatewayStacks::operator==(const GatewayStacks &other) const
|
||||
{
|
||||
return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes;
|
||||
}
|
||||
|
||||
QJsonObject ServersModel::GatewayStacks::toJson() const
|
||||
{
|
||||
QJsonObject obj;
|
||||
if (!userCountryCodes.isEmpty()) {
|
||||
obj.insert(configKey::userCountryCode, QJsonArray::fromStringList(userCountryCodes.values()));
|
||||
}
|
||||
if (!serviceTypes.isEmpty()) {
|
||||
obj.insert(configKey::serviceType, QJsonArray::fromStringList(serviceTypes.values()));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
void ServersModel::recomputeGatewayStacks()
|
||||
{
|
||||
const bool wasEmpty = m_gatewayStacks.isEmpty();
|
||||
GatewayStacks computed;
|
||||
bool hasNewTags = false;
|
||||
|
||||
for (int i = 0; i < m_servers.count(); ++i) {
|
||||
if (data(i, IsServerFromGatewayApiRole).toBool()) {
|
||||
const QJsonObject server = m_servers.at(i).toObject();
|
||||
const QJsonObject apiConfig = server.value(configKey::apiConfig).toObject();
|
||||
|
||||
const QString userCountryCode = apiConfig.value(configKey::userCountryCode).toString();
|
||||
const QString serviceType = apiConfig.value(configKey::serviceType).toString();
|
||||
|
||||
if (!userCountryCode.isEmpty()) {
|
||||
if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) {
|
||||
hasNewTags = true;
|
||||
}
|
||||
computed.userCountryCodes.insert(userCountryCode);
|
||||
}
|
||||
|
||||
if (!serviceType.isEmpty()) {
|
||||
if (!m_gatewayStacks.serviceTypes.contains(serviceType)) {
|
||||
hasNewTags = true;
|
||||
}
|
||||
computed.serviceTypes.insert(serviceType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_gatewayStacks = std::move(computed);
|
||||
if (hasNewTags) {
|
||||
emit gatewayStacksExpanded();
|
||||
}
|
||||
|
||||
if (wasEmpty != m_gatewayStacks.isEmpty()) {
|
||||
emit hasServersFromGatewayApiChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool ServersModel::isApiKeyExpired(const int serverIndex)
|
||||
{
|
||||
auto serverConfig = m_servers.at(serverIndex).toObject();
|
||||
|
||||
@@ -10,6 +10,16 @@ class ServersModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct GatewayStacks
|
||||
{
|
||||
QSet<QString> userCountryCodes;
|
||||
QSet<QString> serviceTypes;
|
||||
|
||||
bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); }
|
||||
bool operator==(const GatewayStacks &other) const;
|
||||
QJsonObject toJson() const;
|
||||
};
|
||||
|
||||
enum Roles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
ServerDescriptionRole,
|
||||
@@ -52,6 +62,8 @@ public:
|
||||
|
||||
void resetModel();
|
||||
|
||||
GatewayStacks gatewayStacks() const { return m_gatewayStacks; }
|
||||
|
||||
Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged)
|
||||
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged)
|
||||
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
|
||||
@@ -62,6 +74,8 @@ public:
|
||||
defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
|
||||
|
||||
Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged)
|
||||
|
||||
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
|
||||
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
|
||||
|
||||
@@ -82,6 +96,8 @@ public slots:
|
||||
bool isDefaultServerHasWriteAccess();
|
||||
bool hasServerWithWriteAccess();
|
||||
|
||||
bool hasServersFromGatewayApi();
|
||||
|
||||
const int getServersCount();
|
||||
|
||||
void setProcessedServerIndex(const int index);
|
||||
@@ -147,6 +163,9 @@ signals:
|
||||
void updateApiCountryModel();
|
||||
void updateApiServicesModel();
|
||||
|
||||
void hasServersFromGatewayApiChanged();
|
||||
void gatewayStacksExpanded();
|
||||
|
||||
private:
|
||||
ServerCredentials serverCredentials(int index) const;
|
||||
|
||||
@@ -167,6 +186,9 @@ private:
|
||||
int m_processedServerIndex;
|
||||
|
||||
bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
|
||||
|
||||
GatewayStacks m_gatewayStacks;
|
||||
void recomputeGatewayStacks();
|
||||
};
|
||||
|
||||
#endif // SERVERSMODEL_H
|
||||
|
||||
@@ -109,6 +109,34 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: devGatewayButton
|
||||
objectName: "devGatewayButton"
|
||||
|
||||
property bool isDevGatewayEnabled: SettingsController.isDevGatewayEnv
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
implicitHeight: 36
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.mutedGray
|
||||
borderWidth: 0
|
||||
|
||||
visible: SettingsController.isDevModeEnabled && isDevGatewayEnabled
|
||||
text: qsTr("Dev gateway enabled")
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
|
||||
onClicked: {
|
||||
PageController.goToPage(PageEnum.PageDevMenu)
|
||||
}
|
||||
}
|
||||
|
||||
ConnectButton {
|
||||
id: connectButton
|
||||
objectName: "connectButton"
|
||||
|
||||
@@ -1,163 +1,181 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
model: settingsEntries
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: isVisible
|
||||
|
||||
text: title
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
leftImageSource: leftImagePath
|
||||
|
||||
clickedFunction: clickedHandler
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithButtonType {
|
||||
id: close
|
||||
|
||||
visible: GC.isDesktop()
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Close application")
|
||||
leftImageSource: "qrc:/images/controls/x-circle.svg"
|
||||
isLeftImageHoverEnabled: false
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.closeApplication()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: GC.isDesktop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> settingsEntries: [
|
||||
servers,
|
||||
connection,
|
||||
application,
|
||||
backup,
|
||||
about,
|
||||
devConsole
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: servers
|
||||
|
||||
property string title: qsTr("Servers")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/server.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsServersList)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: connection
|
||||
|
||||
property string title: qsTr("Connection")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/radio.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsConnection)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: application
|
||||
|
||||
property string title: qsTr("Application")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/app.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsApplication)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: backup
|
||||
|
||||
property string title: qsTr("Backup")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/save.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsBackup)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: about
|
||||
|
||||
property string title: qsTr("About AmneziaVPN")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsAbout)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: devConsole
|
||||
|
||||
property string title: qsTr("Dev console")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/bug.svg"
|
||||
property bool isVisible: SettingsController.isDevModeEnabled
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageDevMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
model: settingsEntries
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: isVisible
|
||||
|
||||
text: title
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
leftImageSource: leftImagePath
|
||||
|
||||
clickedFunction: clickedHandler
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithButtonType {
|
||||
id: close
|
||||
|
||||
visible: GC.isDesktop()
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Close application")
|
||||
leftImageSource: "qrc:/images/controls/x-circle.svg"
|
||||
isLeftImageHoverEnabled: false
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.closeApplication()
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: GC.isDesktop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> settingsEntries: [
|
||||
servers,
|
||||
connection,
|
||||
application,
|
||||
news,
|
||||
backup,
|
||||
about,
|
||||
devConsole
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: servers
|
||||
|
||||
property string title: qsTr("Servers")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/server.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsServersList)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: connection
|
||||
|
||||
property string title: qsTr("Connection")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/radio.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsConnection)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: application
|
||||
|
||||
property string title: qsTr("Application")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/app.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsApplication)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: news
|
||||
|
||||
property string title: qsTr("News & Notifications")
|
||||
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||
property bool isVisible: ServersModel.hasServersFromGatewayApi
|
||||
readonly property var clickedHandler: function() {
|
||||
if (!ServersModel.hasServersFromGatewayApi) {
|
||||
return;
|
||||
}
|
||||
PageController.showBusyIndicator(true)
|
||||
ApiNewsController.fetchNews();
|
||||
PageController.goToPage(PageEnum.PageSettingsNewsNotifications)
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: backup
|
||||
|
||||
property string title: qsTr("Backup")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/save.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsBackup)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: about
|
||||
|
||||
property string title: qsTr("About AmneziaVPN")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/amnezia.svg"
|
||||
property bool isVisible: true
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsAbout)
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: devConsole
|
||||
|
||||
property string title: qsTr("Dev console")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/bug.svg"
|
||||
property bool isVisible: SettingsController.isDevModeEnabled
|
||||
readonly property var clickedHandler: function() {
|
||||
PageController.goToPage(PageEnum.PageDevMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ PageType {
|
||||
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
if (ApiConfigsController.deactivateDevice()) {
|
||||
if (ApiConfigsController.deactivateDevice(false)) {
|
||||
ApiSettingsController.getAccountInfo(true)
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
@@ -396,7 +396,7 @@ PageType {
|
||||
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
if (ApiConfigsController.deactivateDevice()) {
|
||||
if (ApiConfigsController.deactivateDevice(true)) {
|
||||
InstallController.removeProcessedServer()
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
@@ -119,7 +119,7 @@ PageType {
|
||||
|
||||
if (fileName !== "") {
|
||||
PageController.showBusyIndicator(true)
|
||||
ExportController.exportConfig(fileName)
|
||||
ApiConfigsController.exportVpnKey(fileName)
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ PageType {
|
||||
textString: qsTr("Only \"Apps from the list should not have access via VPN\" mode is available on Windows")
|
||||
iconPath: "qrc:/images/controls/alert-circle.svg"
|
||||
|
||||
enabled: (Qt.platform.os === "windows") && root.pageEnabled
|
||||
visible: (Qt.platform.os === "windows") && root.pageEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
property var newsItem
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyNews
|
||||
sourceModel: NewsModel
|
||||
filters: [ ValueFilter { roleName: "isProcessed"; value: true } ]
|
||||
Component.onCompleted: root.newsItem = proxyNews.get(0)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NewsModel
|
||||
function onProcessedIndexChanged() {
|
||||
root.newsItem = proxyNews.get(0)
|
||||
}
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: fl
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: content.height
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: newsItem.title
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
text: newsItem.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
anchors.topMargin: 20
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
}
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("News & Notifications")
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: newsList
|
||||
width: parent.width
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 16
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
model: NewsModel
|
||||
|
||||
clip: true
|
||||
reuseItems: true
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: newsList.width
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
LabelWithButtonType {
|
||||
Layout.fillWidth: true
|
||||
leftImageSource: read ? "" : "qrc:/images/controls/unread-dot.svg"
|
||||
isSmallLeftImage: !read
|
||||
text: title
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
NewsModel.markAsRead(index)
|
||||
NewsModel.processedIndex = index
|
||||
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,7 +350,6 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Clear site list")
|
||||
rightImageSource: "qrc:/images/controls/trash.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
var headerText = qsTr("Clear site list?")
|
||||
|
||||
@@ -87,7 +87,7 @@ PageType {
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free"))
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -35,6 +35,7 @@ PageType {
|
||||
target: ImportController
|
||||
|
||||
function onImportErrorOccurred(error, goToPageHome) {
|
||||
PageController.showBusyIndicator(false)
|
||||
if (goToPageHome) {
|
||||
PageController.goToStartPage()
|
||||
} else {
|
||||
@@ -43,6 +44,7 @@ PageType {
|
||||
}
|
||||
|
||||
function onImportFinished() {
|
||||
PageController.showBusyIndicator(false)
|
||||
if (!ConnectionController.isConnected) {
|
||||
ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1);
|
||||
ServersModel.processedIndex = ServersModel.defaultIndex
|
||||
@@ -216,6 +218,7 @@ PageType {
|
||||
if (cloakingCheckBoxItem.checked) {
|
||||
ImportController.processNativeWireGuardConfig()
|
||||
}
|
||||
PageController.showBusyIndicator(true)
|
||||
ImportController.importConfig()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,13 @@ PageType {
|
||||
objectName: "settingsTabButton"
|
||||
|
||||
isSelected: tabBar.currentIndex === 2
|
||||
image: "qrc:/images/controls/settings.svg"
|
||||
image: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread) ? "qrc:/images/controls/settings-news.svg" : "qrc:/images/controls/settings.svg"
|
||||
Binding {
|
||||
target: settingsTabButton
|
||||
property: "defaultColor"
|
||||
value: "transparent"
|
||||
when: (ServersModel.hasServersFromGatewayApi && NewsModel.hasUnread)
|
||||
}
|
||||
clickedFunc: function () {
|
||||
tabBarStackView.goToTabBarPage(PageEnum.PageSettings)
|
||||
tabBar.currentIndex = 2
|
||||
|
||||
+57
-14
@@ -34,23 +34,66 @@ clang -v
|
||||
# Generate XCodeProj
|
||||
$QT_BIN_DIR/qt-cmake . -B $BUILD_DIR -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -DDEPLOY=ON
|
||||
|
||||
KEYCHAIN=amnezia.build.ios.keychain
|
||||
KEYCHAIN_FILE=$HOME/Library/Keychains/${KEYCHAIN}-db
|
||||
|
||||
cd $BUILD_DIR
|
||||
xcodebuild archive \
|
||||
-project AmneziaVPN.xcodeproj \
|
||||
-scheme AmneziaVPN \
|
||||
-configuration Release \
|
||||
-archivePath ./build/AmneziaVPN.xcarchive \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGN_IDENTITY="" \
|
||||
CODE_SIGNING_REQUIRED=NO
|
||||
# Setup keychain
|
||||
if [ "${IOS_SIGNING_CERT_BASE64+x}" ]; then
|
||||
echo "Import certificate"
|
||||
|
||||
mkdir -p Payload
|
||||
TRUST_CERT_CER=$BUILD_DIR/trust-cert.cer
|
||||
SIGNING_CERT_P12=$BUILD_DIR/signing-cert.p12
|
||||
|
||||
cp -R ./build/AmneziaVPN.xcarchive/Products/Applications/AmneziaVPN.app Payload/
|
||||
echo $IOS_TRUST_CERT_BASE64 | base64 --decode > $TRUST_CERT_CER
|
||||
echo $IOS_SIGNING_CERT_BASE64 | base64 --decode > $SIGNING_CERT_P12
|
||||
|
||||
zip -r AmneziaVPN_unsigned.ipa Payload
|
||||
shasum -a 256 $TRUST_CERT_CER
|
||||
shasum -a 256 $SIGNING_CERT_P12
|
||||
|
||||
rm -rf Payload
|
||||
KEYCHAIN_PASS=$IOS_SIGNING_CERT_PASSWORD
|
||||
|
||||
echo " Build setup completed successfully."
|
||||
security create-keychain -p $KEYCHAIN_PASS $KEYCHAIN || true
|
||||
security default-keychain -s $KEYCHAIN
|
||||
security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN
|
||||
|
||||
security default-keychain
|
||||
security list-keychains
|
||||
|
||||
security import $TRUST_CERT_CER -k $KEYCHAIN -P "" -T /usr/bin/codesign
|
||||
security import $SIGNING_CERT_P12 -k $KEYCHAIN -P $IOS_SIGNING_CERT_PASSWORD -T /usr/bin/codesign
|
||||
|
||||
security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k $KEYCHAIN_PASS $KEYCHAIN
|
||||
security find-identity -p codesigning
|
||||
security set-keychain-settings $KEYCHAIN_FILE
|
||||
security set-keychain-settings -t 3600 $KEYCHAIN_FILE
|
||||
security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN_FILE
|
||||
|
||||
# Copy provisioning prifiles
|
||||
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles/"
|
||||
|
||||
echo $IOS_APP_PROVISIONING_PROFILE | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision
|
||||
echo $IOS_NE_PROVISIONING_PROFILE | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision
|
||||
|
||||
shasum -a 256 ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision
|
||||
shasum -a 256 ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision
|
||||
|
||||
profile_uuid=`grep UUID -A1 -a ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision | grep -io "[-A-F0-9]\{36\}"`
|
||||
profile_ne_uuid=`grep UUID -A1 -a ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision | grep -io "[-A-F0-9]\{36\}"`
|
||||
|
||||
mv ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$profile_uuid.mobileprovision
|
||||
mv ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$profile_ne_uuid.mobileprovision
|
||||
else
|
||||
echo "Failed to import certificate, aborting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build project
|
||||
xcodebuild \
|
||||
"OTHER_CODE_SIGN_FLAGS=--keychain '$KEYCHAIN_FILE'" \
|
||||
-configuration Release \
|
||||
-scheme AmneziaVPN \
|
||||
-destination "generic/platform=iOS,name=Any iOS'" \
|
||||
-project $BUILD_DIR/AmneziaVPN.xcodeproj
|
||||
|
||||
# restore keychain
|
||||
security default-keychain -s login.keychain
|
||||
|
||||
@@ -36,5 +36,6 @@ class IpcInterface
|
||||
SLOT( bool enablePeerTraffic( const QJsonObject &configStr) );
|
||||
SLOT( bool enableKillSwitch( const QJsonObject &excludeAddr, int vpnAdapterIndex) );
|
||||
SLOT( bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) );
|
||||
SLOT( bool restoreResolvers() );
|
||||
};
|
||||
|
||||
|
||||
@@ -157,6 +157,10 @@ bool IpcServer::updateResolvers(const QString &ifname, const QList<QHostAddress>
|
||||
return Router::updateResolvers(ifname, resolvers);
|
||||
}
|
||||
|
||||
bool IpcServer::restoreResolvers() {
|
||||
return Router::restoreResolvers();
|
||||
}
|
||||
|
||||
void IpcServer::StartRoutingIpv6()
|
||||
{
|
||||
Router::StartRoutingIpv6();
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
virtual bool disableKillSwitch() override;
|
||||
virtual bool refreshKillSwitch( bool enabled ) override;
|
||||
virtual bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers) override;
|
||||
virtual bool restoreResolvers() override;
|
||||
|
||||
private:
|
||||
int m_localpid = 0;
|
||||
|
||||
@@ -99,6 +99,17 @@ bool Router::updateResolvers(const QString& ifname, const QList<QHostAddress>& r
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Router::restoreResolvers() {
|
||||
#ifdef Q_OS_LINUX
|
||||
return RouterLinux::Instance().restoreResolvers();
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
return RouterMac::Instance().restoreResolvers();
|
||||
#endif
|
||||
#ifdef Q_OS_WIN
|
||||
return RouterWin::Instance().restoreResolvers();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Router::StopRoutingIpv6()
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
static void StartRoutingIpv6();
|
||||
static void StopRoutingIpv6();
|
||||
static bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
|
||||
static bool restoreResolvers();
|
||||
};
|
||||
|
||||
#endif // ROUTER_H
|
||||
|
||||
@@ -279,6 +279,10 @@ bool RouterLinux::updateResolvers(const QString& ifname, const QList<QHostAddres
|
||||
return m_dnsUtil->updateResolvers(ifname, resolvers);
|
||||
}
|
||||
|
||||
bool RouterLinux::restoreResolvers() {
|
||||
return m_dnsUtil->restoreResolvers();
|
||||
}
|
||||
|
||||
void RouterLinux::StartRoutingIpv6()
|
||||
{
|
||||
QProcess process;
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
void StartRoutingIpv6();
|
||||
void StopRoutingIpv6();
|
||||
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
|
||||
bool restoreResolvers();
|
||||
public slots:
|
||||
|
||||
private:
|
||||
|
||||
@@ -158,6 +158,9 @@ bool RouterMac::updateResolvers(const QString& ifname, const QList<QHostAddress>
|
||||
return m_dnsUtil->updateResolvers(ifname, resolvers);
|
||||
}
|
||||
|
||||
bool RouterMac::restoreResolvers() {
|
||||
return m_dnsUtil->restoreResolvers();
|
||||
}
|
||||
|
||||
bool RouterMac::deleteTun(const QString &dev)
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ public:
|
||||
bool createTun(const QString &dev, const QString &subnet);
|
||||
bool deleteTun(const QString &dev);
|
||||
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
|
||||
bool restoreResolvers();
|
||||
|
||||
public slots:
|
||||
|
||||
|
||||
@@ -443,6 +443,9 @@ bool RouterWin::updateResolvers(const QString& ifname, const QList<QHostAddress>
|
||||
return m_dnsUtil->updateResolvers(ifname, resolvers);
|
||||
}
|
||||
|
||||
bool RouterWin::restoreResolvers() {
|
||||
return m_dnsUtil->restoreResolvers();
|
||||
}
|
||||
|
||||
void RouterWin::StopRoutingIpv6()
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
|
||||
void suspendWcmSvc(bool suspend);
|
||||
bool updateResolvers(const QString& ifname, const QList<QHostAddress>& resolvers);
|
||||
bool restoreResolvers();
|
||||
|
||||
private:
|
||||
RouterWin(RouterWin const &) = delete;
|
||||
|
||||
Reference in New Issue
Block a user