From 1533270e4e875d52ce0989f10c43c144a32cf306 Mon Sep 17 00:00:00 2001 From: albexk Date: Sat, 2 Nov 2024 00:54:24 +0300 Subject: [PATCH 01/23] Fix connection check for AWG/WG --- .../vpn/protocol/wireguard/Wireguard.kt | 85 +++++++++++-------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt index e93834f4f..80cab96d3 100644 --- a/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt +++ b/client/android/wireguard/src/main/kotlin/org/amnezia/vpn/protocol/wireguard/Wireguard.kt @@ -1,11 +1,12 @@ package org.amnezia.vpn.protocol.wireguard import android.net.VpnService.Builder -import java.io.IOException -import java.util.Locale +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext +import kotlinx.coroutines.launch import org.amnezia.awg.GoBackend import org.amnezia.vpn.protocol.Protocol import org.amnezia.vpn.protocol.ProtocolState.CONNECTED @@ -27,6 +28,8 @@ open class Wireguard : Protocol() { private var tunnelHandle: Int = -1 protected open val ifName: String = "amn0" + private lateinit var scope: CoroutineScope + private var statusJob: Job? = null override val statistics: Statistics get() { @@ -49,46 +52,17 @@ open class Wireguard : Protocol() { override fun internalInit() { if (!isInitialized) loadSharedLibrary(context, "wg-go") + if (this::scope.isInitialized) { + scope.cancel() + } + scope = CoroutineScope(Dispatchers.IO) } override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) { val wireguardConfig = parseConfig(config) - val startTime = System.currentTimeMillis() start(wireguardConfig, vpnBuilder, protect) - waitForConnection(startTime) - state.value = CONNECTED } - private suspend fun waitForConnection(startTime: Long) { - Log.d(TAG, "Waiting for connection") - withContext(Dispatchers.IO) { - val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0) - try { - delay(1000) - var log = getLogcat(time) - Log.v(TAG, "First waiting log: $log") - // check that there is a connection log, - // to avoid infinite connection - if (!log.contains("Attaching to interface")) { - Log.w(TAG, "Logs do not contain a connection log") - return@withContext - } - while (!log.contains("Received handshake response")) { - delay(1000) - log = getLogcat(time) - } - } catch (e: IOException) { - Log.e(TAG, "Failed to get logcat: $e") - } - } - } - - private fun getLogcat(time: String): String = - ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time) - .redirectErrorStream(true) - .start() - .inputStream.reader().readText() - protected open fun parseConfig(config: JSONObject): WireguardConfig { val configData = config.getJSONObject("wireguard_config_data") return WireguardConfig.build { @@ -178,6 +152,43 @@ open class Wireguard : Protocol() { tunnelHandle = -1 throw VpnStartException("Protect VPN interface: permission not granted or revoked") } + launchStatusJob() + } + + private fun launchStatusJob() { + Log.d(TAG, "Launch status job") + statusJob = scope.launch { + while (true) { + val lastHandshake = getLastHandshake() + Log.v(TAG, "lastHandshake=$lastHandshake") + if (lastHandshake == 0L) { + delay(1000) + continue + } + if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED + else if (lastHandshake == -1L) state.value = DISCONNECTED + statusJob = null + break + } + } + } + + private fun getLastHandshake(): Long { + if (tunnelHandle == -1) { + Log.e(TAG, "Trying to get config of a non-existent tunnel") + return -1 + } + val config = GoBackend.awgGetConfig(tunnelHandle) + if (config == null) { + Log.e(TAG, "Failed to get tunnel config") + return -2 + } + val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong() + if (lastHandshake == null) { + Log.e(TAG, "Failed to get last_handshake_time_sec") + return -2 + } + return lastHandshake } override fun stopVpn() { @@ -185,6 +196,8 @@ open class Wireguard : Protocol() { Log.w(TAG, "Tunnel already down") return } + statusJob?.cancel() + statusJob = null val handleToClose = tunnelHandle tunnelHandle = -1 GoBackend.awgTurnOff(handleToClose) From 576e2226fe2742d14b326d9609f55a19dd02dcd1 Mon Sep 17 00:00:00 2001 From: albexk Date: Sun, 3 Nov 2024 16:11:23 +0300 Subject: [PATCH 02/23] fix(android): add CHANGE_NETWORK_STATE permission for all Android versions --- client/android/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 179def863..9e44e0221 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -20,7 +20,7 @@ - + From 31867993ce5a77732b95c5e1717c6d53395f59b5 Mon Sep 17 00:00:00 2001 From: Nethius Date: Wed, 6 Nov 2024 09:57:39 +0400 Subject: [PATCH 03/23] chore: minor fixes (#1235) --- client/ui/controllers/installController.cpp | 1 - client/ui/qml/Controls2/BusyIndicatorType.qml | 2 +- client/ui/qml/Controls2/CardType.qml | 2 +- client/ui/qml/Controls2/DrawerType2.qml | 2 +- client/ui/qml/Controls2/PopupType.qml | 2 +- client/ui/qml/Controls2/TopCloseButtonType.qml | 2 +- client/ui/qml/Modules/Style/AmneziaStyle.qml | 4 ++++ client/ui/qml/Pages2/PageHome.qml | 4 ++-- client/ui/qml/Pages2/PageSettingsApiServerInfo.qml | 8 ++++---- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 306e7f38a..ae0804cb7 100755 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -848,7 +848,6 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::authData, authData); - newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc)); m_serversModel->editServer(newServerConfig, serverIndex); if (reloadServiceConfig) { diff --git a/client/ui/qml/Controls2/BusyIndicatorType.qml b/client/ui/qml/Controls2/BusyIndicatorType.qml index 55af280f6..480f25c1a 100644 --- a/client/ui/qml/Controls2/BusyIndicatorType.qml +++ b/client/ui/qml/Controls2/BusyIndicatorType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Controls2/CardType.qml b/client/ui/qml/Controls2/CardType.qml index 50f84dbf5..f584a8fcd 100644 --- a/client/ui/qml/Controls2/CardType.qml +++ b/client/ui/qml/Controls2/CardType.qml @@ -19,7 +19,7 @@ RadioButton { property string textColor: AmneziaStyle.color.midnightBlack - property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3) + property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot property string selectedBorderColor: AmneziaStyle.color.goldenApricot property string defaultBodredColor: AmneziaStyle.color.transparent property int borderWidth: 0 diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index 6647bc88b..c4b584c1e 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -92,7 +92,7 @@ Item { id: background anchors.fill: parent - color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack Behavior on color { PropertyAnimation { duration: 200 } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index bd4aa4fbc..7a6a770e0 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -24,7 +24,7 @@ Popup { Overlay.modal: Rectangle { visible: root.closeButtonVisible - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } onOpened: { diff --git a/client/ui/qml/Controls2/TopCloseButtonType.qml b/client/ui/qml/Controls2/TopCloseButtonType.qml index 1bd7fef6b..3a652da6c 100644 --- a/client/ui/qml/Controls2/TopCloseButtonType.qml +++ b/client/ui/qml/Controls2/TopCloseButtonType.qml @@ -14,7 +14,7 @@ Popup { visible: false Overlay.modal: Rectangle { - color: Qt.rgba(14/255, 14/255, 17/255, 0.8) + color: AmneziaStyle.color.translucentMidnightBlack } background: Rectangle { diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index c0038246d..1abfbe3ae 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -22,5 +22,9 @@ QtObject { readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12) readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08) readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05) + readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8) + readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3) + readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8) + readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) } } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8074337a9..5689e4d47 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -316,8 +316,8 @@ PageType { rootButtonImageColor: AmneziaStyle.color.midnightBlack rootButtonBackgroundColor: AmneziaStyle.color.paleGray - rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8) - rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65) + rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray + rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray rootButtonHoveredBorderColor: AmneziaStyle.color.transparent rootButtonDefaultBorderColor: AmneziaStyle.color.transparent rootButtonTextTopMargin: 8 diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index f23e36d9e..2d6c1d9be 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -132,8 +132,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Reload API config") @@ -172,8 +172,8 @@ PageType { implicitHeight: 32 defaultColor: "transparent" - hoveredColor: Qt.rgba(1, 1, 1, 0.08) - pressedColor: Qt.rgba(1, 1, 1, 0.12) + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite textColor: AmneziaStyle.color.vibrantRed text: qsTr("Remove from application") From 23806e1defcf827cec2f7727f432329eb5c0399c Mon Sep 17 00:00:00 2001 From: albexk Date: Fri, 8 Nov 2024 11:22:16 +0300 Subject: [PATCH 04/23] chore: bump version to 4.8.2.4 (#1240) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b5e64e324..cb6956316 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.2.3 +project(${PROJECT} VERSION 4.8.2.4 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 2069) +set(APP_ANDROID_VERSION_CODE 2071) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") From aa871bd1c99a693cd88e8cd635bc7d1bf69f75db Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 12 Nov 2024 10:22:34 +0400 Subject: [PATCH 05/23] feature: added country selection on home page drawer (#1215) --- .../qml/Components/ShareConnectionDrawer.qml | 6 +-- client/ui/qml/Controls2/BasicButtonType.qml | 22 ++++++---- .../qml/Controls2/TextFieldWithHeaderType.qml | 2 +- client/ui/qml/Pages2/PageHome.qml | 41 ++++++++++++++----- client/ui/qml/Pages2/PageShare.qml | 2 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 2 +- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 3235ad0a0..d2bf28ab8 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -84,7 +84,7 @@ DrawerType2 { Layout.topMargin: 16 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" KeyNavigation.tab: copyConfigTextButton @@ -120,7 +120,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } Keys.onEnterPressed: { copyConfigTextButton.clicked() } @@ -143,7 +143,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy config string") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" KeyNavigation.tab: showSettingsButton } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 5c599013e..828c32bcd 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -22,9 +22,10 @@ Button { property int borderWidth: 0 property int borderFocusedWidth: 1 - property string imageSource + property string leftImageSource property string rightImageSource - property string leftImageColor: textColor + property string leftImageColor + property bool changeLeftImageSize: true property bool squareLeftSide: false @@ -127,18 +128,23 @@ Button { anchors.centerIn: parent Image { - Layout.preferredHeight: 20 - Layout.preferredWidth: 20 - - source: root.imageSource - visible: root.imageSource === "" ? false : true + id: leftImage + source: root.leftImageSource + visible: root.leftImageSource === "" ? false : true layer { - enabled: true + enabled: leftImageColor !== "" ? true : false effect: ColorOverlay { color: leftImageColor } } + + Component.onCompleted: { + if (root.changeLeftImageSize) { + leftImage.Layout.preferredHeight = 20 + leftImage.Layout.preferredWidth = 20 + } + } } ButtonTextType { diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 4ec0976bc..365faa94c 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -183,7 +183,7 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText - imageSource: root.buttonImageSource + leftImageSource: root.buttonImageSource anchors.top: content.top anchors.bottom: content.bottom diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 5689e4d47..8422a10f2 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -98,7 +98,6 @@ PageType { pressedColor: AmneziaStyle.color.sheerWhite disabledColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.mutedGray - leftImageColor: AmneziaStyle.color.transparent borderWidth: 0 buttonTextLabel.lineHeight: 20 @@ -110,7 +109,7 @@ PageType { text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") - imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() @@ -166,6 +165,7 @@ PageType { anchors.left: parent.left anchors.right: parent.right + spacing: 0 Component.onCompleted: { drawer.collapsedHeight = collapsed.implicitHeight @@ -267,18 +267,39 @@ PageType { RowLayout { Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44 + Layout.topMargin: 8 + Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16 spacing: 0 - Image { - Layout.rightMargin: 8 - visible: source !== "" - source: ServersModel.defaultServerImagePathCollapsed - } + BasicButtonType { + enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsed + hoverEnabled: enabled + + implicitHeight: 36 + + leftPadding: 16 + rightPadding: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.transparent + textColor: AmneziaStyle.color.mutedGray + + buttonTextLabel.lineHeight: 16 + buttonTextLabel.font.pixelSize: 13 + buttonTextLabel.font.weight: 400 - LabelTextType { - id: collapsedServerMenuDescription text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded + leftImageSource: ServersModel.defaultServerImagePathCollapsed + changeLeftImageSize: false + + rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : "" + + onClicked: { + ServersModel.processedIndex = ServersModel.defaultIndex + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 617b1091a..995fa3e76 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -573,7 +573,7 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 2a5652309..404ba563f 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -135,7 +135,7 @@ PageType { Layout.topMargin: 40 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" Keys.onTabPressed: lastItemTabClicked(focusItem) From 8547de82ea940cfb3220bc6c013c9ed4eb66dd39 Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 14 Nov 2024 07:58:04 +0400 Subject: [PATCH 06/23] bump xcode-version for macos build (#1249) --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 64a4986d1..0ce8d5768 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -256,7 +256,7 @@ jobs: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.3.1' + xcode-version: '15.4.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v3 From e0b091b47424b87ac980c9e7e2bcd80bd6e0e476 Mon Sep 17 00:00:00 2001 From: Aftershock669 <58403826+Aftershock669@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:51:46 +0300 Subject: [PATCH 07/23] Update readme (#1267) --- README.md | 19 ++++++++++--------- metadata/img-readme/apl.png | Bin 14495 -> 0 bytes metadata/img-readme/download-alt.svg | 8 ++++++++ metadata/img-readme/download-website.svg | 8 ++++++++ metadata/img-readme/download.png | Bin 3451 -> 0 bytes metadata/img-readme/play.png | Bin 15126 -> 0 bytes 6 files changed, 26 insertions(+), 9 deletions(-) delete mode 100644 metadata/img-readme/apl.png create mode 100644 metadata/img-readme/download-alt.svg create mode 100644 metadata/img-readme/download-website.svg delete mode 100644 metadata/img-readme/download.png delete mode 100644 metadata/img-readme/play.png diff --git a/README.md b/README.md index eed800f5d..27a29edf1 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,21 @@ [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) -Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. -![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png) +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -
+### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) - - - +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). -[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads) + + [All releases](https://github.com/amnezia-vpn/amnezia-client/releases) -
+
@@ -33,7 +33,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation - [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit - [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) - [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png deleted file mode 100644 index 6dedfa12ea1d6605b1419bb2c079d64dac02080f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14495 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 diff --git a/metadata/img-readme/download-alt.svg b/metadata/img-readme/download-alt.svg new file mode 100644 index 000000000..f97c9c3df --- /dev/null +++ b/metadata/img-readme/download-alt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website.svg b/metadata/img-readme/download-website.svg new file mode 100644 index 000000000..d0cf83753 --- /dev/null +++ b/metadata/img-readme/download-website.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png deleted file mode 100644 index 0e6a885004aba6dd61392155d4edbe021d4a304d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3451 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 D@^Fny diff --git a/metadata/img-readme/play.png b/metadata/img-readme/play.png deleted file mode 100644 index 2fb316c8d0edd9b8767f460417bf7cbe469c682a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15126 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 b?l Date: Tue, 26 Nov 2024 11:41:17 +0700 Subject: [PATCH 08/23] bugfix: added scroll on page with services list (#1262) * added scroll on page with services list * fixed margins on PageSetupWizardApiServicesList --- client/ui/qml/Controls2/CardWithIconsType.qml | 1 + .../Pages2/PageSetupWizardApiServicesList.qml | 117 +++++++++--------- .../Pages2/PageSetupWizardConfigSource.qml | 1 - 3 files changed, 59 insertions(+), 60 deletions(-) diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index fea651167..18a29b872 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -145,6 +145,7 @@ Button { cursorShape: Qt.PointingHandCursor hoverEnabled: true + enabled: root.enabled onEntered: { backgroundRect.color = root.hoveredColor diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index 85a50393c..f726cd49a 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -16,83 +16,82 @@ PageType { defaultActiveFocusItem: focusItem - FlickableType { - id: fl + ColumnLayout { + id: header + anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.height + anchors.left: parent.left + anchors.right: parent.right - ColumnLayout { - id: content + spacing: 0 - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Item { + id: focusItem + KeyNavigation.tab: backButton + } - spacing: 0 + BackButtonType { + id: backButton + Layout.topMargin: 20 +// KeyNavigation.tab: fileButton.rightButton + } - Item { - id: focusItem - KeyNavigation.tab: backButton - } + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - BackButtonType { - id: backButton - Layout.topMargin: 20 -// KeyNavigation.tab: fileButton.rightButton - } + headerText: qsTr("VPN by Amnezia") + descriptionText: qsTr("Choose a VPN service that suits your needs.") + } + } - HeaderType { - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 32 + ListView { + id: servicesListView + anchors.top: header.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 16 + spacing: 0 - headerText: qsTr("VPN by Amnezia") - descriptionText: qsTr("Choose a VPN service that suits your needs.") - } + currentIndex: 1 + clip: true + model: ApiServicesModel - ListView { - id: containers - width: parent.width - height: containers.contentItem.height - spacing: 16 + ScrollBar.vertical: ScrollBar {} - currentIndex: 1 - interactive: false - model: ApiServicesModel + delegate: Item { + implicitWidth: servicesListView.width + implicitHeight: delegateContent.implicitHeight - delegate: Item { - implicitWidth: containers.width - implicitHeight: delegateContent.implicitHeight + ColumnLayout { + id: delegateContent - ColumnLayout { - id: delegateContent + anchors.fill: parent - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + CardWithIconsType { + id: card - CardWithIconsType { - id: card + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 + headerText: name + bodyText: cardDescription + footerText: price - headerText: name - bodyText: cardDescription - footerText: price + rightImageSource: "qrc:/images/controls/chevron-right.svg" - rightImageSource: "qrc:/images/controls/chevron-right.svg" + enabled: isServiceAvailable - onClicked: { - if (isServiceAvailable) { - ApiServicesModel.setServiceIndex(index) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) - } - } + onClicked: { + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 7c031997f..f973c89cf 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -47,7 +47,6 @@ PageType { KeyNavigation.tab: textKey.textField } - HeaderType { property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() From 1d721ffb9abb4c0c5a123f33edb5c066a6eee40b Mon Sep 17 00:00:00 2001 From: Anton Sosnin Date: Wed, 27 Nov 2024 04:55:23 +0200 Subject: [PATCH 09/23] SteamDeck/OS installation fix (#1270) --- deploy/data/linux/post_install.sh | 10 ++++++++++ deploy/data/linux/post_uninstall.sh | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/deploy/data/linux/post_install.sh b/deploy/data/linux/post_install.sh index b3345bac8..324462d91 100755 --- a/deploy/data/linux/post_install.sh +++ b/deploy/data/linux/post_install.sh @@ -19,6 +19,11 @@ date > $LOG_FILE echo "Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + if sudo systemctl is-active --quiet $APP_NAME; then sudo systemctl stop $APP_NAME >> $LOG_FILE sudo systemctl disable $APP_NAME >> $LOG_FILE @@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE echo "user desktop creation loop ended" >> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh index 5849a90e4..98090d20a 100755 --- a/deploy/data/linux/post_uninstall.sh +++ b/deploy/data/linux/post_uninstall.sh @@ -13,6 +13,11 @@ date >> $LOG_FILE echo "Uninstall Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do sudo unlink $dir >> $LOG_FILE done @@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then fi +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service after uninstall status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE From 9d96b1cd13ac1e6a06a0f413ceca9d187f839c71 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 29 Nov 2024 22:10:35 +0000 Subject: [PATCH 10/23] Update Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27a29edf1..8b4539075 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
-XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 - +XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns ## Acknowledgments This project is tested with BrowserStack. From 4efaf20a1ca5e88c137b6116eeba4a6765f48804 Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 2 Dec 2024 10:46:20 +0300 Subject: [PATCH 11/23] chore: fix deploy workflow (#1280) --- .github/workflows/deploy.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ce8d5768..a51c19b27 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -217,7 +217,11 @@ jobs: export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin" export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos" export PATH=$PATH:~/go/bin - sh deploy/build_ios.sh + sh deploy/build_ios.sh | \ + sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d' env: IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }} IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }} From 5dc16c06f158660ee011ea5cdb46005c1121d5a2 Mon Sep 17 00:00:00 2001 From: Nethius Date: Tue, 3 Dec 2024 08:47:33 +0300 Subject: [PATCH 12/23] chore: increased the api request timeout (#1276) --- client/core/controllers/apiController.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 75a3f93cb..c50165e7e 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -50,6 +50,8 @@ namespace constexpr char authData[] = "auth_data"; } + const int requestTimeoutMsecs = 12 * 1000; // 12 secs + ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { if (!sslErrors.empty()) { @@ -177,7 +179,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle QStringList ApiController::getProxyUrls() { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; @@ -280,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c if (serverConfig.value(config_key::configVersion).toInt()) { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); @@ -336,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); @@ -390,7 +392,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint)); From 1c1e74d06f6e6738a1976d2c3af7bf9b6f1e2286 Mon Sep 17 00:00:00 2001 From: Pokamest Nikak Date: Fri, 6 Dec 2024 12:40:04 +0000 Subject: [PATCH 13/23] ru readme --- README_RU.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 README_RU.md diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 000000000..8b4539075 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,191 @@ +# Amnezia VPN +## _The best client for self-hosted VPN_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. + +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) + +### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) + +> [!TIP] +> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). + + + + +[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) + +
+ + + +## Features + +- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. +- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. +- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). +- Windows, MacOS, Linux, Android, iOS releases. +- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). + +## Links + +- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium + +## Tech + +AmneziaVPN uses several open-source projects to work: + +- [OpenSSL](https://www.openssl.org/) +- [OpenVPN](https://openvpn.net/) +- [Shadowsocks](https://shadowsocks.org/) +- [Qt](https://www.qt.io/) +- [LibSsh](https://libssh.org) - forked from Qt Creator +- and more... + +## Checking out the source code + +Make sure to pull all submodules after checking out the repo. + +```bash +git submodule update --init --recursive +``` + +## Development + +Want to contribute? Welcome! + +### Help with translations + +Download the most actual translation files. + +Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. +Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". + +Unzip this file. +Each *.ts file contains strings for one corresponding language. + +Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. +You can do it via a web-interface or any other method you're familiar with. + +### Building sources and deployment + +Check deploy folder for build scripts. + +### How to build an iOS app from source code on MacOS + +1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. + +2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: + - MacOS + - iOS + - Qt 5 Compatibility Module + - Qt Shader Tools + - Additional Libraries: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + +3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) + +4. You also need to install go >= v1.16. If you don't have it installed already, +download go from the [official website](https://golang.org/dl/) or use Homebrew. +The latest version is recommended. Install gomobile +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Build the project +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment + + +If you get `gomobile: command not found` make sure to set PATH to the location +of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Open the XCode project. You can then run /test/archive/ship the app. + +If the build fails with the following error +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with +key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +if the above error persists on your M1 Mac, then most probably you need to install arch based CMake +``` +arch -arm64 brew install cmake +``` + +Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that +require them. In this case, simply restart the build. + +## How to build the Android app + +_Tested on Mac OS_ + +The Android app has the following requirements: +* JDK 11 +* Android platform SDK 33 +* CMake 3.25.0 + +After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. + +- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. +- Set path to JDK 11 +- Set path to Android SDK (`$ANDROID_HOME`) + +In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  +Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  +Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. + +That's it! You should be ready to compile the project from QT Creator! + +### Development flow + +After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. +If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! + +You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. + +## License + +GPL v3.0 + +## Donate + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
+USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
+USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
+XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns +## Acknowledgments + +This project is tested with BrowserStack. +We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. From ea910ba30054d5a88a9bfc62f35b04e1ece2986f Mon Sep 17 00:00:00 2001 From: KsZnak Date: Fri, 6 Dec 2024 22:15:01 +0200 Subject: [PATCH 14/23] Update README_RU.md --- README_RU.md | 181 +++++++++------------------------------------------ 1 file changed, 30 insertions(+), 151 deletions(-) diff --git a/README_RU.md b/README_RU.md index 8b4539075..6ebdb97f7 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,182 +1,60 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ +## _Лучший клиент для создания VPN на собственном сервере_ -[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) -[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) - -[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. +[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting) +### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting) > [!TIP] -> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org). +> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). -[All releases](https://github.com/amnezia-vpn/amnezia-client/releases) +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
-## Features +## Особенности -- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN. -- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols. -- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. -- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop). -- Windows, MacOS, Linux, Android, iOS releases. -- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). +- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN. +- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2. +- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay. +- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них. +- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS. +- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved). -## Links +## Ссылки -- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org) -- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation +- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org) +- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация - [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit -- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) -- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi) -- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar) -- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian) -- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium +- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский) +- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси) +- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма) +- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский) +- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\) -## Tech +## Технологии -AmneziaVPN uses several open-source projects to work: +AmneziaVPN использует несколько проектов с открытым исходным кодом: - [OpenSSL](https://www.openssl.org/) - [OpenVPN](https://openvpn.net/) - [Shadowsocks](https://shadowsocks.org/) - [Qt](https://www.qt.io/) -- [LibSsh](https://libssh.org) - forked from Qt Creator -- and more... - -## Checking out the source code - -Make sure to pull all submodules after checking out the repo. - -```bash -git submodule update --init --recursive -``` - -## Development - -Want to contribute? Welcome! - -### Help with translations - -Download the most actual translation files. - -Go to ["Actions" tab](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), click on the first line. -Then scroll down to the "Artifacts" section and download "AmneziaVPN_translations". - -Unzip this file. -Each *.ts file contains strings for one corresponding language. - -Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder. -You can do it via a web-interface or any other method you're familiar with. - -### Building sources and deployment - -Check deploy folder for build scripts. - -### How to build an iOS app from source code on MacOS - -1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. - -2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: - - MacOS - - iOS - - Qt 5 Compatibility Module - - Qt Shader Tools - - Additional Libraries: - - Qt Image Formats - - Qt Multimedia - - Qt Remote Objects - -3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/) - -4. You also need to install go >= v1.16. If you don't have it installed already, -download go from the [official website](https://golang.org/dl/) or use Homebrew. -The latest version is recommended. Install gomobile -```bash -export PATH=$PATH:~/go/bin -go install golang.org/x/mobile/cmd/gomobile@latest -gomobile init -``` +- [LibSsh](https://libssh.org) +- и другие... -5. Build the project -```bash -export QT_BIN_DIR="/Qt//ios/bin" -export QT_MACOS_ROOT_DIR="/Qt//macos" -export QT_IOS_BIN=$QT_BIN_DIR -export PATH=$PATH:~/go/bin -mkdir build-ios -$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -``` -Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment - - -If you get `gomobile: command not found` make sure to set PATH to the location -of the bin folder where gomobile was installed. Usually, it's in `GOPATH`. -```bash -export PATH=$(PATH):/path/to/GOPATH/bin -``` - -6. Open the XCode project. You can then run /test/archive/ship the app. - -If the build fails with the following error -``` -make: *** -[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] -Error 1 -``` -Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with -key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. - -if the above error persists on your M1 Mac, then most probably you need to install arch based CMake -``` -arch -arm64 brew install cmake -``` - -Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that -require them. In this case, simply restart the build. - -## How to build the Android app - -_Tested on Mac OS_ - -The Android app has the following requirements: -* JDK 11 -* Android platform SDK 33 -* CMake 3.25.0 - -After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. - -- Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. -- Set path to JDK 11 -- Set path to Android SDK (`$ANDROID_HOME`) - -In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!  -Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at ` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.  -Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`. - -That's it! You should be ready to compile the project from QT Creator! - -### Development flow - -After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt__Clang_-`. -If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt__Clang_-/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes! - -You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`/client/android-build/.`) and you should be good to go. - -## License +## Лицензия GPL v3.0 -## Donate +## Донаты Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) @@ -185,7 +63,8 @@ USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns -## Acknowledgments -This project is tested with BrowserStack. -We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. From 569d63ef0f750f9938dfdda2fc69c9559a7be4c8 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 7 Dec 2024 15:53:40 +0200 Subject: [PATCH 15/23] Add files via upload --- metadata/img-readme/download-website-ru.svg | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 metadata/img-readme/download-website-ru.svg diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 000000000..386ae4fe5 --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + From d67201ede9dc2f39b525ec55e04ac0225e551462 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:34:18 +0200 Subject: [PATCH 16/23] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b4539075..8f8878081 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # Amnezia VPN -## _The best client for self-hosted VPN_ + +### _The best client for self-hosted VPN_ + [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) +### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md) + + [Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) From c5aa070bf4cd7ff1de931dc22a887aea3104ae92 Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sun, 8 Dec 2024 05:49:26 +0200 Subject: [PATCH 17/23] Update README_RU.md --- README_RU.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README_RU.md b/README_RU.md index 6ebdb97f7..fe9dd2860 100644 --- a/README_RU.md +++ b/README_RU.md @@ -1,6 +1,11 @@ # Amnezia VPN -## _Лучший клиент для создания VPN на собственном сервере_ +### _Лучший клиент для создания VPN на собственном сервере_ + +[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) + +### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский [AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере. [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) @@ -10,8 +15,8 @@ > [!TIP] > Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org). - - + + [Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) From 6ea6ab1bd983fd2be880e9e14a9184bda9b79349 Mon Sep 17 00:00:00 2001 From: Nethius Date: Sun, 8 Dec 2024 08:14:22 +0300 Subject: [PATCH 18/23] chore: added clang-format config files (#1293) --- .clang-format | 39 +++++++++++++++++++++++++++++++++++++++ .clang-format-ignore | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-format-ignore diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..5c459fd2f --- /dev/null +++ b/.clang-format @@ -0,0 +1,39 @@ +BasedOnStyle: WebKit +AccessModifierOffset: '-4' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'true' +AllowShortEnumsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: None +AlwaysBreakTemplateDeclarations: 'No' +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakConstructorInitializers: BeforeColon +ColumnLimit: '120' +CommentPragmas: '"^!|^:"' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '8' +IndentPPDirectives: BeforeHash +NamespaceIndentation: All +PenaltyExcessCharacter: '10' +PointerAlignment: Right +SortIncludes: 'true' +SpaceAfterTemplateKeyword: 'false' +Standard: Auto diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 000000000..4019357f0 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,20 @@ +/client/3rd +/client/3rd-prebuild +/client/android +/client/cmake +/client/core/serialization +/client/daemon +/client/fonts +/client/images +/client/ios +/client/mozilla +/client/platforms/dummy +/client/platforms/linux +/client/platforms/macos +/client/platforms/windows +/client/server_scripts +/client/translations +/deploy +/docs +/metadata +/service/src From 2db99715b1fc5a7ef1f8b800c72d6e5b3422ce1f Mon Sep 17 00:00:00 2001 From: Nethius Date: Mon, 9 Dec 2024 09:32:49 +0300 Subject: [PATCH 19/23] feature: added subscription expiration date for premium v2 (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: added subscription expiration date for premium v2 * feature: added a check for the presence of the “services” field in the response body of the getServicesList() function * feature: added prohibition to change location when connection is active * bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend --- client/core/controllers/apiController.cpp | 7 + client/core/defs.h | 1 + client/core/errorstrings.cpp | 3 +- .../ui/controllers/connectionController.cpp | 2 +- client/ui/models/apiServicesModel.cpp | 112 ++++++--- client/ui/models/apiServicesModel.h | 40 +++- client/ui/models/servers_model.cpp | 33 ++- client/ui/models/servers_model.h | 5 + .../Pages2/PageSettingsApiLanguageList.qml | 6 + .../qml/Pages2/PageSettingsApiServerInfo.qml | 7 +- .../ui/qml/Pages2/PageSettingsServerInfo.qml | 219 +++++++++--------- 11 files changed, 280 insertions(+), 155 deletions(-) diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index c50165e7e..6562632a9 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -379,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); + + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + return ErrorCode::ApiServicesMissingError; + } + } + return errorCode; } diff --git a/client/core/defs.h b/client/core/defs.h index d00d347b1..c0db2e127 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 495346063..70f433c62 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -63,7 +63,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; - + case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f8516f6e3..f9491d4e6 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -55,7 +55,7 @@ void ConnectionController::openConnection() && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by end_date event"; + qDebug() << "attempt to update api config by expires_at event"; if (configVersion == ApiConfigSources::Telegram) { emit updateApiConfigFromTelegram(); } else { diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 2a87bde3f..81a10f873 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -27,6 +27,9 @@ namespace constexpr char storeEndpoint[] = "store_endpoint"; constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; } namespace serviceType @@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject service = m_services.at(index.row()).toObject(); - QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); - auto serviceType = service.value(configKey::serviceType).toString(); + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; switch (role) { case NameRole: { - return serviceInfo.value(configKey::name).toString(); + return apiServiceData.serviceInfo.name; } case CardDescriptionRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); + auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); } else if (serviceType == serviceType::amneziaFree){ QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { description += tr("

Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); } return description; @@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case IsServiceAvailableRole: { if (serviceType == serviceType::amneziaFree) { - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (isServiceAvailable) { return false; } } return true; } case SpeedRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); - return tr("%1 MBit/s").arg(speed); + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); } - case WorkPeriodRole: { - auto timelimit = serviceInfo.value(configKey::timelimit).toString(); - if (timelimit == "0") { + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { return ""; } - return tr("%1 days").arg(timelimit); + return tr("%1 days").arg(timeLimit); } case RegionRole: { - return serviceInfo.value(configKey::region).toString(); + return apiServiceData.serviceInfo.region; } case FeaturesRole: { if (serviceType == serviceType::amneziaPremium) { @@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } } case PriceRole: { - auto price = serviceInfo.value(configKey::price).toString(); + auto price = apiServiceData.serviceInfo.price; if (price == "free") { return tr("Free"); } return tr("%1 $/month").arg(price); } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } } return QVariant(); @@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data) { beginResetModel(); + m_services.clear(); + m_countryCode = data.value(configKey::userCountryCode).toString(); - m_services = data.value(configKey::services).toArray(); - if (m_services.isEmpty()) { - QJsonObject service; - service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo)); - service.insert(configKey::serviceType, data.value(configKey::serviceType)); + auto services = data.value(configKey::services).toArray(); - m_services.push_back(service); + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + m_services.push_back(getApiServicesData(service.toObject())); + } } endResetModel(); @@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index) QJsonObject ApiServicesModel::getSelectedServiceInfo() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceInfo).toObject(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; } QString ApiServicesModel::getSelectedServiceType() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceType).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.type; } QString ApiServicesModel::getSelectedServiceProtocol() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceProtocol).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; } QString ApiServicesModel::getSelectedServiceName() { - auto modelIndex = index(m_selectedServiceIndex, 0); - return data(modelIndex, ApiServicesModel::Roles::NameRole).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; } QJsonArray ApiServicesModel::getSelectedServiceCountries() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::availableCountries).toArray(); + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; } QString ApiServicesModel::getCountryCode() @@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode() QString ApiServicesModel::getStoreEndpoint() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::storeEndpoint).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; } QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) @@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; - roles[WorkPeriodRole] = "workPeriod"; + roles[TimeLimitRole] = "timeLimit"; roles[RegionRole] = "region"; roles[FeaturesRole] = "features"; roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; return roles; } + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString(); + + if (serviceInfo.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 499189401..c96a49abc 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -3,6 +3,7 @@ #include #include +#include class ApiServicesModel : public QAbstractListModel { @@ -15,10 +16,11 @@ class ApiServicesModel : public QAbstractListModel ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ public slots: QHash roleNames() const override; private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + QString m_countryCode; - QJsonArray m_services; + QVector m_services; int m_selectedServiceIndex; }; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index c87499a73..b72b10c31 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -22,7 +22,7 @@ namespace constexpr char serviceProtocol[] = "service_protocol"; constexpr char publicKeyInfo[] = "public_key"; - constexpr char endDate[] = "end_date"; + constexpr char expiresAt[] = "expires_at"; } } @@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); - const QString endDate = publicKeyInfo.value(configKey::endDate).toString(); - if (endDate.isEmpty()) { - publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); serverConfig.insert(configKey::apiConfig, apiConfig); editServer(serverConfig, serverIndex); @@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) return false; } - auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC(); - if (endDateDateTime < QDateTime::currentDateTimeUtc()) { + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 0f18ea301..78bc22cc0 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ class ServersModel : public QAbstractListModel int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -115,6 +116,7 @@ public slots: QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); @@ -127,6 +129,9 @@ public slots: signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 120313cd1..600db85d1 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -54,8 +54,14 @@ PageType { imageSource: "qrc:/images/controls/download.svg" checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + if (index !== ApiCountryModel.currentIndex) { PageController.showBusyIndicator(true) var prevIndex = ApiCountryModel.currentIndex diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 2d6c1d9be..167e56e5a 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -56,12 +56,15 @@ PageType { } LabelWithImageType { + property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable") + Layout.fillWidth: true Layout.margins: 16 imageSource: "qrc:/images/controls/history.svg" - leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period") + rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate") + : ApiServicesModel.getSelectedServiceData("workPeriod") visible: rightText !== "" } diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 95ae5c8a6..ffcfb4411 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -25,6 +25,8 @@ PageType { property int pageSettingsApiServerInfo: 3 property int pageSettingsApiLanguageList: 4 + property var processedServer + defaultActiveFocusItem: focusItem Connections { @@ -35,8 +37,18 @@ PageType { } } + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) + } + } + SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -44,147 +56,139 @@ PageType { value: true } ] + + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } Item { id: focusItem - KeyNavigation.tab: header + //KeyNavigation.tab: header } ColumnLayout { anchors.fill: parent - spacing: 16 - - Repeater { - id: header - model: proxyServersModel - - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() - } + spacing: 4 - delegate: ColumnLayout { + BackButtonType { + id: backButton - property alias focusItem: backButton + Layout.topMargin: 20 + KeyNavigation.tab: headerContent.actionButton - id: content + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } - Layout.topMargin: 20 + HeaderType { + id: headerContent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton + actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" + : "qrc:/images/controls/edit-3.svg" - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } + headerText: root.processedServer.name + descriptionText: { + if (root.processedServer.isServerFromGatewayApi) { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate") + } else { + return ApiServicesModel.getSelectedServiceData("serviceDescription") } + } else if (root.processedServer.isServerFromTelegramApi) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName } + } - HeaderType { - id: headerContent - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) { - return serverDescription - } else if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar + KeyNavigation.tab: tabBar - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.open() } + } + } - DrawerType2 { - id: serverNameEditDrawer + DrawerType2 { + id: serverNameEditDrawer - parent: root + parent: root - anchors.fill: parent - expandedHeight: root.height * 0.35 + anchors.fill: parent + expandedHeight: root.height * 0.35 - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } + onClosed: { + if (!GC.isMobile()) { + headerContent.actionButton.forceActiveFocus() + } + } - expandedContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Connections { - target: serverNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - serverName.textField.forceActiveFocus() - } - } + expandedContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Connections { + target: serverNameEditDrawer + enabled: !GC.isMobile() + function onOpened() { + serverName.textField.forceActiveFocus() + } + } - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } - TextFieldWithHeaderType { - id: serverName + TextFieldWithHeaderType { + id: serverName - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true + Layout.fillWidth: true + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true - KeyNavigation.tab: saveButton - } + KeyNavigation.tab: saveButton + } - BasicButtonType { - id: saveButton + BasicButtonType { + id: saveButton - Layout.fillWidth: true + Layout.fillWidth: true - text: qsTr("Save") - KeyNavigation.tab: focusItem1 + text: qsTr("Save") + KeyNavigation.tab: focusItem1 - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return + } - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } + if (serverName.textFieldText !== root.processedServer.name) { + ServersModel.setProcessedServerData("name", serverName.textFieldText); } + serverNameEditDrawer.close() } } } @@ -257,8 +261,7 @@ PageType { StackLayout { id: nestedStackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + Layout.fillWidth: true currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? From d06924c59dd8684c28b6257efe5d1a11db34b19b Mon Sep 17 00:00:00 2001 From: Cyril Anisimov Date: Tue, 10 Dec 2024 03:17:16 +0100 Subject: [PATCH 20/23] feature/xray user management (#972) * feature: implement client management functionality for Xray --------- Co-authored-by: aiamnezia Co-authored-by: vladimir.kuznetsov --- client/configurators/xray_configurator.cpp | 147 ++++++++- client/configurators/xray_configurator.h | 4 + client/ui/controllers/exportController.cpp | 9 +- client/ui/controllers/exportController.h | 2 +- client/ui/models/clientManagementModel.cpp | 351 +++++++++++++++++++-- client/ui/models/clientManagementModel.h | 6 + client/ui/qml/Pages2/PageShare.qml | 2 +- 7 files changed, 485 insertions(+), 36 deletions(-) diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c5..514aa8211 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -3,38 +3,169 @@ #include #include #include +#include +#include "logger.h" #include "containers/containers_defs.h" #include "core/controllers/serverController.h" #include "core/scripts_registry.h" +namespace { +Logger logger("XrayConfigurator"); +} + XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent) : ConfiguratorBase(settings, serverController, parent) { } -QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode &errorCode) +QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) +{ + // Generate new UUID for client + QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Get current server config + QString currentConfig = m_serverController->getTextFileFromContainer( + container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to get server config file"; + return ""; + } + + // Parse current config as JSON + QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + logger.error() << "Failed to parse server config JSON"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject serverConfig = doc.object(); + + // Validate server config structure + if (!serverConfig.contains("inbounds")) { + logger.error() << "Server config missing 'inbounds' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray inbounds = serverConfig["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Server config has empty 'inbounds' array"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Inbound missing 'settings' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Settings missing 'clients' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray clients = settings["clients"].toArray(); + + // Create configuration for new client + QJsonObject clientConfig { + {"id", clientId}, + {"flow", "xtls-rprx-vision"} + }; + + clients.append(clientConfig); + + // Update config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + serverConfig["inbounds"] = inbounds; + + // Save updated config to server + QString updatedConfig = QJsonDocument(serverConfig).toJson(); + errorCode = m_serverController->uploadTextFileToContainer( + container, + credentials, + updatedConfig, + amnezia::protocols::xray::serverConfigPath, + libssh::ScpOverwriteMode::ScpOverwriteExisting + ); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to upload updated config"; + return ""; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + errorCode = m_serverController->runScript( + credentials, + m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container)) + ); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to restart container"; + return ""; + } + + return clientId; +} + +QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) { + // Get client ID from prepareServerConfig + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode); + if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { + logger.error() << "Failed to prepare server config"; + errorCode = ErrorCode::InternalError; + return ""; + } + QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), m_serverController->genVarsForScript(credentials, container, containerConfig)); + + if (config.isEmpty()) { + logger.error() << "Failed to get config template"; + errorCode = ErrorCode::InternalError; + return ""; + } QString xrayPublicKey = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + errorCode = ErrorCode::InternalError; + return ""; + } xrayPublicKey.replace("\n", ""); - - QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode); - xrayUuid.replace("\n", ""); - + QString xrayShortId = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } xrayShortId.replace("\n", ""); - if (errorCode != ErrorCode::NoError) { + // Validate all required variables are present + if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { + logger.error() << "Config template missing required variables:" + << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") + << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") + << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); + errorCode = ErrorCode::InternalError; return ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + config.replace("$XRAY_CLIENT_ID", xrayClientId); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_SHORT_ID", xrayShortId); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 2acfdf71d..8ed4e7752 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ class XrayConfigurator : public ConfiguratorBase QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2690b5b1e..8681406e9 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index b031ea393..a2c9fcfaf 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -28,7 +28,7 @@ public slots: void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7445d60fb..f07eae712 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + + return appendClient(clientId, clientName, container, credentials, serverController); } ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, @@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + ErrorCode error = ErrorCode::NoError; + + // Get server config + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + // Get client ID to remove + auto client = m_clientsTable.at(row).toObject(); + QString clientId = client.value(configKey::clientId).toString(); + + // Remove client from server config + QJsonObject configObj = serverConfig.object(); + if (!configObj.contains("inbounds")) { + logger.error() << "Missing inbounds in xray config"; + return ErrorCode::InternalError; + } + + QJsonArray inbounds = configObj["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Empty inbounds array in xray config"; + return ErrorCode::InternalError; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings"; + return ErrorCode::InternalError; + } + + QJsonArray clients = settings["clients"].toArray(); + if (clients.isEmpty()) { + logger.error() << "Empty clients array in xray config"; + return ErrorCode::InternalError; + } + + for (int i = 0; i < clients.size(); ++i) { + QJsonObject clientObj = clients[i].toObject(); + if (clientObj.contains("id") && clientObj["id"].toString() == clientId) { + clients.removeAt(i); + break; + } + } + + // Update server config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + configObj["inbounds"] = inbounds; + + // Upload updated config + error = serverController->uploadTextFileToContainer( + container, + credentials, + QJsonDocument(configObj).toJson(), + serverConfigPath + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload updated xray config"; + return error; + } + + // Remove from local table + beginRemoveRows(QModelIndex(), row, row); + m_clientsTable.removeAt(row); + endRemoveRows(); + + // Update clients table file on server + const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson(); + QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable") + .arg(ContainerProps::containerTypeToString(container)); + + error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to upload the clientsTable file"; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + error = serverController->runScript( + credentials, + serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container)) + ); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to restart xray container"; + return error; + } + + return error; +} + QHash ClientManagementModel::roleNames() const { QHash roles; @@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const roles[DataSentRole] = "dataSent"; roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 60132abe2..989120a9e 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -40,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -64,11 +66,15 @@ public slots: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 995fa3e76..d6ce78488 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -92,7 +92,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray" From 367789bda28f33a11a72d61b222453c5b502299e Mon Sep 17 00:00:00 2001 From: KsZnak Date: Sat, 14 Dec 2024 14:29:33 +0200 Subject: [PATCH 21/23] Update README_RU.md (#1300) * Update README_RU.md --- README_RU.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/README_RU.md b/README_RU.md index fe9dd2860..59518f4b8 100644 --- a/README_RU.md +++ b/README_RU.md @@ -55,6 +55,112 @@ AmneziaVPN использует несколько проектов с откр - [LibSsh](https://libssh.org) - и другие... +## Проверка исходного кода +После клонирования репозитория обязательно загрузите все подмодули. + +```bash +git submodule update --init --recursive +``` + + +## Разработка +Хотите внести свой вклад? Добро пожаловать! + +### Помощь с переводами + +Загрузите самые актуальные файлы перевода. + +Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations". + +Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка. + +Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом. + +### Сборка исходного кода и деплой +Проверьте папку deploy для скриптов сборки. + +### Как собрать iOS-приложение из исходного кода на MacOS +1. Убедитесь, что у вас установлен XCode версии 14 или выше. +2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули: +- MacOS +- iOS +- Модуль совместимости с Qt 5 +- Qt Shader Tools +- Дополнительные библиотеки: + - Qt Image Formats + - Qt Multimedia + - Qt Remote Objects + + +3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь. +4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile: + +```bash +export PATH=$PATH:~/go/bin +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init +``` + +5. Соберите проект: +```bash +export QT_BIN_DIR="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//macos" +export QT_IOS_BIN=$QT_BIN_DIR +export PATH=$PATH:~/go/bin +mkdir build-ios +$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR +``` +Замените и на ваши значения. + +Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile: +```bash +export PATH=$(PATH):/path/to/GOPATH/bin +``` + +6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение. + +Если сборка завершится с ошибкой: +``` +make: *** +[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] +Error 1 +``` +Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. + +Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM: +``` +arch -arm64 brew install cmake +``` + + При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку. + + +## Как собрать Android-приложение +Сборка тестировалась на MacOS. Требования: +- JDK 11 +- Android SDK 33 +- CMake 3.25.0 + +Установите QT, QT Creator и Android Studio. +Настройте QT Creator: + +- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`. +- Укажите путь к JDK 11. +- Укажите путь к Android SDK (`$ANDROID_HOME`) + +Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере! + +Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake. + +Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK. + +### Разработка Android-компонентов + +После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt__Clang_-`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt__Clang_-/client/android-build` в качестве корневой. +Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения. +Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`/client/android-build/.`). + + ## Лицензия GPL v3.0 From 48f6cf904e7f41c002be3a6daf3b53535191456c Mon Sep 17 00:00:00 2001 From: Nethius Date: Thu, 19 Dec 2024 10:36:20 +0300 Subject: [PATCH 22/23] chore/minor UI fixes (#1308) * chore: corrected the translation error * bugfix: fixed basic button left iamge color --- client/translations/amneziavpn_ru_RU.ts | 2 +- client/ui/qml/Controls2/BasicButtonType.qml | 2 +- client/ui/qml/Pages2/PageHome.qml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 2fb21259e..c0d855b25 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -2679,7 +2679,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Where to get connection data, step-by-step instructions for buying a VPS - Где взять данные для подключения, пошаговые инстуркции по покупке VPS + Где взять данные для подключения, пошаговые инструкции по покупке VPS diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 828c32bcd..ef66e0e22 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -24,7 +24,7 @@ Button { property string leftImageSource property string rightImageSource - property string leftImageColor + property string leftImageColor: textColor property bool changeLeftImageSize: true property bool squareLeftSide: false diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 8422a10f2..e51125758 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -110,6 +110,7 @@ PageType { text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled") leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" + leftImageColor: "" rightImageSource: "qrc:/images/controls/chevron-down.svg" Keys.onEnterPressed: splitTunnelingButton.clicked() From b88ab8e432fbd2ab5002856cd83ef57181a6d7fa Mon Sep 17 00:00:00 2001 From: albexk Date: Mon, 23 Dec 2024 04:27:09 +0300 Subject: [PATCH 23/23] fix(build): fix aqtinstall (#1312) --- .github/workflows/deploy.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a51c19b27..35e740b0d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -335,7 +335,8 @@ jobs: arch: 'linux_gcc_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86_64 Qt' uses: jurplel/install-qt-action@v4 @@ -346,7 +347,8 @@ jobs: arch: 'android_x86_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86 Qt' uses: jurplel/install-qt-action@v4 @@ -357,7 +359,8 @@ jobs: arch: 'android_x86' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_armv7 Qt' uses: jurplel/install-qt-action@v4 @@ -368,7 +371,8 @@ jobs: arch: 'android_armv7' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_arm64_v8a Qt' uses: jurplel/install-qt-action@v4 @@ -379,7 +383,8 @@ jobs: arch: 'android_arm64_v8a' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Grant execute permission for qt-cmake' shell: bash