Compare commits
85 Commits
fix/clicka
...
test-win-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad85424ccb | ||
|
|
3fc9edd346 | ||
|
|
1a1f75d873 | ||
|
|
df02e0bf78 | ||
|
|
264d77463d | ||
|
|
0a37ffd5e3 | ||
|
|
1343d10aa7 | ||
|
|
6f96ebd8bf | ||
|
|
cb531dacb3 | ||
|
|
dfd0b4d0e5 | ||
|
|
f978f55e7f | ||
|
|
dc85a99e08 | ||
|
|
ef06fcb4f4 | ||
|
|
ffe2314d47 | ||
|
|
4e970322d0 | ||
|
|
8dee0d27cf | ||
|
|
c520f9a2a4 | ||
|
|
003c3a23c4 | ||
|
|
1754a82f67 | ||
|
|
3384008277 | ||
|
|
af22115706 | ||
|
|
4b114fd3b6 | ||
|
|
9d531f5d74 | ||
|
|
a5564148f5 | ||
|
|
196f7778fc | ||
|
|
de20add857 | ||
|
|
c59216b58a | ||
|
|
acf7fa261a | ||
|
|
c3eddc92bd | ||
|
|
18c74f4b02 | ||
|
|
3f90ee915d | ||
|
|
401ad0db0e | ||
|
|
5945133d30 | ||
|
|
ff4fbde0b0 | ||
|
|
74ae4f3e67 | ||
|
|
ae4b33d042 | ||
|
|
53fa280037 | ||
|
|
8ecde90bc7 | ||
|
|
34a583f272 | ||
|
|
5de4b8eeb8 | ||
|
|
aae420e469 | ||
|
|
0612f70c06 | ||
|
|
d0c82efa1c | ||
|
|
cf8492240e | ||
|
|
2bceb9f7ba | ||
|
|
760f935965 | ||
|
|
eeeb2805c5 | ||
|
|
9a592d67ad | ||
|
|
ea6618b2f6 | ||
|
|
7b092e73ad | ||
|
|
b2e25c42c7 | ||
|
|
c8dd38ac31 | ||
|
|
563ee4703f | ||
|
|
beceed81de | ||
|
|
3bf96253db | ||
|
|
da2d0ec203 | ||
|
|
008b858203 | ||
|
|
130fc8277d | ||
|
|
468d3357b8 | ||
|
|
f1271da527 | ||
|
|
249a7c7ca3 | ||
|
|
0094d0ebc4 | ||
|
|
834b504dff | ||
|
|
a516d0e757 | ||
|
|
afdfbdbc59 | ||
|
|
ef712b7054 | ||
|
|
c22f9ff08a | ||
|
|
04fb1825d5 | ||
|
|
4f8f873682 | ||
|
|
9fe75c6120 | ||
|
|
bb7e8f46cb | ||
|
|
5db0c281ee | ||
|
|
aac9bfcea6 | ||
|
|
e6ee9085a2 | ||
|
|
d62ade58a5 | ||
|
|
d8020878d5 | ||
|
|
b027fff103 | ||
|
|
a0c06048cd | ||
|
|
53746f2f66 | ||
|
|
2649dba4ad | ||
|
|
6a1e3c07b1 | ||
|
|
a365eff76f | ||
|
|
8f9acd9367 | ||
|
|
9be13ea465 | ||
|
|
9faabe9e7d |
7
.github/workflows/deploy.yml
vendored
@@ -65,6 +65,13 @@ jobs:
|
||||
path: deploy/AppDir
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload translations artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN_translations
|
||||
path: client/translations
|
||||
retention-days: 7
|
||||
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-Windows:
|
||||
|
||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
|
||||
project(${PROJECT} VERSION 4.5.3.0
|
||||
project(${PROJECT} VERSION 4.6.1.0
|
||||
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 52)
|
||||
set(APP_ANDROID_VERSION_CODE 56)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
64
README.md
@@ -6,20 +6,42 @@
|
||||
|
||||
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.6.0.3/AmneziaVPN_4.6.0.3_x64.exe"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/win.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.6.0.3/AmneziaVPN_4.6.0.3.dmg"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/mac.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.6.0.3/AmneziaVPN_Linux_4.6.0.3.tar.zip"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/lin.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/tag/4.6.0.3"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/andr.png" width="150" style="max-width: 100%;"></a>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="https://play.google.com/store/search?q=amnezia+vpn&c=apps"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/play.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://apps.apple.com/us/app/amneziavpn/id1600529900"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/apl.png" width="150" style="max-width: 100%;"></a>
|
||||
|
||||
|
||||
[All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Very easy to use - enter your IP address, SSH login, and password, and Amnezia will automatically install VPN docker containers to your server and connect to the VPN.
|
||||
- OpenVPN, ShadowSocks, WireGuard, and IKEv2 protocols support.
|
||||
- OpenVPN, Shadowsocks, WireGuard, and IKEv2 protocols support.
|
||||
- Masking VPN with OpenVPN over Cloak plugin
|
||||
- Split tunneling support - add any sites to the client to enable VPN only for them (only for desktops)
|
||||
- Windows, MacOS, Linux, Android, iOS releases.
|
||||
|
||||
## Links
|
||||
|
||||
[https://amnezia.org](https://amnezia.org) - project website
|
||||
[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](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [https://amnezia.org](https://amnezia.org) - project website
|
||||
- [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)
|
||||
|
||||
## Tech
|
||||
|
||||
@@ -27,7 +49,7 @@ AmneziaVPN uses several open-source projects to work:
|
||||
|
||||
- [OpenSSL](https://www.openssl.org/)
|
||||
- [OpenVPN](https://openvpn.net/)
|
||||
- [ShadowSocks](https://shadowsocks.org/)
|
||||
- [Shadowsocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [LibSsh](https://libssh.org) - forked from Qt Creator
|
||||
- and more...
|
||||
@@ -44,6 +66,19 @@ git submodule update --init --recursive
|
||||
|
||||
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.
|
||||
@@ -52,7 +87,7 @@ Check deploy folder for build scripts.
|
||||
|
||||
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.1. 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:
|
||||
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
|
||||
@@ -119,9 +154,11 @@ The Android app has the following requirements:
|
||||
* 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)
|
||||
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 <path>` 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.
|
||||
@@ -142,10 +179,11 @@ GPL v3.0
|
||||
|
||||
## Donate
|
||||
|
||||
Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u
|
||||
Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn)
|
||||
|
||||
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
|
||||
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
|
||||
payeer.com: P2561305
|
||||
ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
||||
2
client/3rd/OpenVPNAdapter
vendored
2
client/3rd/QJsonStruct/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.user
|
||||
build/
|
||||
19
client/3rd/QJsonStruct/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
project(QJsonStruct LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
option(BUILD_TESTING ON)
|
||||
|
||||
include(QJsonStruct.cmake)
|
||||
find_package(Qt5 COMPONENTS Core REQUIRED)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
include(CTest)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
21
client/3rd/QJsonStruct/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Qv2ray Workgroup
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
190
client/3rd/QJsonStruct/QJsonIO.hpp
Normal file
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QList>
|
||||
#include <tuple>
|
||||
|
||||
enum class QJsonIOPathType
|
||||
{
|
||||
JSONIO_MODE_ARRAY,
|
||||
JSONIO_MODE_OBJECT
|
||||
};
|
||||
|
||||
typedef QPair<QString, QJsonIOPathType> QJsonIONodeType;
|
||||
|
||||
struct QJsonIOPath : QList<QJsonIONodeType>
|
||||
{
|
||||
template<typename type1, typename type2, typename... types>
|
||||
QJsonIOPath(const type1 t1, const type2 t2, const types... ts)
|
||||
{
|
||||
AppendPath(t1);
|
||||
AppendPath(t2);
|
||||
(AppendPath(ts), ...);
|
||||
}
|
||||
|
||||
void AppendPath(size_t index)
|
||||
{
|
||||
append({ QString::number(index), QJsonIOPathType::JSONIO_MODE_ARRAY });
|
||||
}
|
||||
|
||||
void AppendPath(const QString &key)
|
||||
{
|
||||
append({ key, QJsonIOPathType::JSONIO_MODE_OBJECT });
|
||||
}
|
||||
|
||||
template<typename t>
|
||||
QJsonIOPath &operator<<(const t &str)
|
||||
{
|
||||
AppendPath(str);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<typename t>
|
||||
QJsonIOPath &operator+=(const t &val)
|
||||
{
|
||||
AppendPath(val);
|
||||
return *this;
|
||||
}
|
||||
|
||||
QJsonIOPath &operator<<(const QJsonIOPath &other)
|
||||
{
|
||||
for (const auto &x : other)
|
||||
this->append(x);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<typename t>
|
||||
QJsonIOPath &operator<<(const t &val) const
|
||||
{
|
||||
auto _new = *this;
|
||||
return _new << val;
|
||||
}
|
||||
|
||||
template<typename t>
|
||||
QJsonIOPath operator+(const t &val) const
|
||||
{
|
||||
auto _new = *this;
|
||||
return _new << val;
|
||||
}
|
||||
|
||||
QJsonIOPath operator+(const QJsonIOPath &other) const
|
||||
{
|
||||
auto _new = *this;
|
||||
for (const auto &x : other)
|
||||
_new.append(x);
|
||||
return _new;
|
||||
}
|
||||
};
|
||||
|
||||
class QJsonIO
|
||||
{
|
||||
public:
|
||||
const static inline QJsonValue Null = QJsonValue::Null;
|
||||
const static inline QJsonValue Undefined = QJsonValue::Undefined;
|
||||
|
||||
template<typename current_key_type, typename... t_other_types>
|
||||
static QJsonValue GetValue(const QJsonValue &parent, const current_key_type ¤t, const t_other_types &...other)
|
||||
{
|
||||
if constexpr (sizeof...(t_other_types) == 0)
|
||||
if constexpr (std::is_integral_v<current_key_type>)
|
||||
return parent.toArray()[current];
|
||||
else
|
||||
return parent.toObject()[current];
|
||||
else if constexpr (std::is_integral_v<current_key_type>)
|
||||
return GetValue(parent.toArray()[current], other...);
|
||||
else
|
||||
return GetValue(parent.toObject()[current], other...);
|
||||
}
|
||||
|
||||
template<typename... key_types_t>
|
||||
static QJsonValue GetValue(QJsonValue value, const std::tuple<key_types_t...> &keys, const QJsonValue &defaultValue = Undefined)
|
||||
{
|
||||
std::apply([&](auto &&...args) { ((value = value[args]), ...); }, keys);
|
||||
return value.isUndefined() ? defaultValue : value;
|
||||
}
|
||||
|
||||
template<typename parent_type, typename t_value_type, typename current_key_type, typename... t_other_key_types>
|
||||
static void SetValue(parent_type &parent, const t_value_type &val, const current_key_type ¤t, const t_other_key_types &...other)
|
||||
{
|
||||
// If current parent is an array, increase its size to fit the "key"
|
||||
if constexpr (std::is_integral_v<current_key_type>)
|
||||
for (auto i = parent.size(); i <= current; i++)
|
||||
parent.insert(i, {});
|
||||
|
||||
// If the t_other_key_types has nothing....
|
||||
// Means we have reached the end of recursion.
|
||||
if constexpr (sizeof...(t_other_key_types) == 0)
|
||||
parent[current] = val;
|
||||
else if constexpr (std::is_integral_v<typename std::tuple_element_t<0, std::tuple<t_other_key_types...>>>)
|
||||
{
|
||||
// Means we still have many keys
|
||||
// So this element is an array.
|
||||
auto _array = parent[current].toArray();
|
||||
SetValue(_array, val, other...);
|
||||
parent[current] = _array;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto _object = parent[current].toObject();
|
||||
SetValue(_object, val, other...);
|
||||
parent[current] = _object;
|
||||
}
|
||||
}
|
||||
|
||||
static QJsonValue GetValue(const QJsonValue &parent, const QJsonIOPath &path, const QJsonValue &defaultValue = QJsonIO::Undefined)
|
||||
{
|
||||
QJsonValue val = parent;
|
||||
for (const auto &[k, t] : path)
|
||||
{
|
||||
if (t == QJsonIOPathType::JSONIO_MODE_ARRAY)
|
||||
val = val.toArray()[k.toInt()];
|
||||
else
|
||||
val = val.toObject()[k];
|
||||
}
|
||||
return val.isUndefined() ? defaultValue : val;
|
||||
}
|
||||
|
||||
template<typename parent_type, typename value_type>
|
||||
static void SetValue(parent_type &parent, const value_type &t, const QJsonIOPath &path)
|
||||
{
|
||||
QList<std::tuple<QString, QJsonIOPathType, QJsonValue>> _stack;
|
||||
QJsonValue lastNode = parent;
|
||||
for (const auto &[key, type] : path)
|
||||
{
|
||||
_stack.prepend({ key, type, lastNode });
|
||||
if (type == QJsonIOPathType::JSONIO_MODE_ARRAY)
|
||||
lastNode = lastNode.toArray().at(key.toInt());
|
||||
else
|
||||
lastNode = lastNode.toObject()[key];
|
||||
}
|
||||
|
||||
lastNode = t;
|
||||
|
||||
for (const auto &[key, type, node] : _stack)
|
||||
{
|
||||
if (type == QJsonIOPathType::JSONIO_MODE_ARRAY)
|
||||
{
|
||||
const auto index = key.toInt();
|
||||
auto nodeArray = node.toArray();
|
||||
for (auto i = nodeArray.size(); i <= index; i++)
|
||||
nodeArray.insert(i, {});
|
||||
nodeArray[index] = lastNode;
|
||||
lastNode = nodeArray;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto nodeObject = node.toObject();
|
||||
nodeObject[key] = lastNode;
|
||||
lastNode = nodeObject;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<parent_type, QJsonObject>)
|
||||
parent = lastNode.toObject();
|
||||
else if constexpr (std::is_same_v<parent_type, QJsonArray>)
|
||||
parent = lastNode.toArray();
|
||||
else
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
};
|
||||
5
client/3rd/QJsonStruct/QJsonStruct.cmake
Normal file
@@ -0,0 +1,5 @@
|
||||
include_directories(${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
set(QJSONSTRUCT_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/QJsonStruct.hpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/QJsonIO.hpp)
|
||||
215
client/3rd/QJsonStruct/QJsonStruct.hpp
Normal file
@@ -0,0 +1,215 @@
|
||||
#pragma once
|
||||
#include "macroexpansion.hpp"
|
||||
|
||||
#ifndef _X
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#endif
|
||||
|
||||
/// macro to define an operator==
|
||||
#define ___JSONSTRUCT_DEFAULT_COMPARE_IMPL(x) (this->x == ___another___instance__.x) &&
|
||||
#define JSONSTRUCT_COMPARE(CLASS, ...) \
|
||||
bool operator==(const CLASS &___another___instance__) const \
|
||||
{ \
|
||||
return FOR_EACH(___JSONSTRUCT_DEFAULT_COMPARE_IMPL, __VA_ARGS__) true; \
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
// Load JSON IMPL
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC_IMPL(name) name::loadJson(___json_object_);
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_A_FUNC(name) ___DESERIALIZE_FROM_JSON_CONVERT_F_FUNC(name)
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC(...) FOREACH_CALL_FUNC_3(___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC_IMPL, __VA_ARGS__)
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_F_FUNC(name) \
|
||||
if (___json_object_.toObject().contains(#name)) \
|
||||
{ \
|
||||
JsonStructHelper::Deserialize(this->name, ___json_object_.toObject()[#name]); \
|
||||
} \
|
||||
else \
|
||||
{ \
|
||||
this->name = ___qjsonstruct_default_check.name; \
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
// To JSON IMPL
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_F_FUNC(name) \
|
||||
if (!(___qjsonstruct_default_check.name == this->name)) \
|
||||
{ \
|
||||
___json_object_.insert(#name, JsonStructHelper::Serialize(name)); \
|
||||
}
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_A_FUNC(name) ___json_object_.insert(#name, JsonStructHelper::Serialize(name));
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_B_FUNC_IMPL(name) JsonStructHelper::MergeJson(___json_object_, name::toJson());
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_B_FUNC(...) FOREACH_CALL_FUNC_3(___SERIALIZE_TO_JSON_CONVERT_B_FUNC_IMPL, __VA_ARGS__)
|
||||
|
||||
// ============================================================================================
|
||||
// Load JSON Wrapper
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_A(...) FOREACH_CALL_FUNC_2(___DESERIALIZE_FROM_JSON_CONVERT_A_FUNC, __VA_ARGS__)
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_F(...) FOREACH_CALL_FUNC_2(___DESERIALIZE_FROM_JSON_CONVERT_F_FUNC, __VA_ARGS__)
|
||||
#define ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_B(...) FOREACH_CALL_FUNC_2(___DESERIALIZE_FROM_JSON_CONVERT_B_FUNC, __VA_ARGS__)
|
||||
#define ___DESERIALIZE_FROM_JSON_EXTRACT_B_F(name_option) ___DESERIALIZE_FROM_JSON_CONVERT_FUNC_DECL_##name_option
|
||||
|
||||
// ============================================================================================
|
||||
// To JSON Wrapper
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_A(...) FOREACH_CALL_FUNC_2(___SERIALIZE_TO_JSON_CONVERT_A_FUNC, __VA_ARGS__)
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_F(...) FOREACH_CALL_FUNC_2(___SERIALIZE_TO_JSON_CONVERT_F_FUNC, __VA_ARGS__)
|
||||
#define ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_B(...) FOREACH_CALL_FUNC_2(___SERIALIZE_TO_JSON_CONVERT_B_FUNC, __VA_ARGS__)
|
||||
#define ___SERIALIZE_TO_JSON_EXTRACT_B_F(name_option) ___SERIALIZE_TO_JSON_CONVERT_FUNC_DECL_##name_option
|
||||
|
||||
// ============================================================================================
|
||||
#define JSONSTRUCT_REGISTER_NOCOPYMOVE(___class_type_, ...) \
|
||||
void loadJson(const QJsonValue &___json_object_) \
|
||||
{ \
|
||||
___class_type_ ___qjsonstruct_default_check; \
|
||||
FOREACH_CALL_FUNC(___DESERIALIZE_FROM_JSON_EXTRACT_B_F, __VA_ARGS__); \
|
||||
} \
|
||||
[[nodiscard]] const QJsonObject toJson() const \
|
||||
{ \
|
||||
___class_type_ ___qjsonstruct_default_check; \
|
||||
QJsonObject ___json_object_; \
|
||||
FOREACH_CALL_FUNC(___SERIALIZE_TO_JSON_EXTRACT_B_F, __VA_ARGS__); \
|
||||
return ___json_object_; \
|
||||
}
|
||||
|
||||
#define JSONSTRUCT_REGISTER(___class_type_, ...) \
|
||||
JSONSTRUCT_REGISTER_NOCOPYMOVE(___class_type_, __VA_ARGS__); \
|
||||
[[nodiscard]] static auto fromJson(const QJsonValue &___json_object_) \
|
||||
{ \
|
||||
___class_type_ _t; \
|
||||
_t.loadJson(___json_object_); \
|
||||
return _t; \
|
||||
}
|
||||
|
||||
#define ___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(type, convert_func) \
|
||||
static void Deserialize(type &t, const QJsonValue &d) \
|
||||
{ \
|
||||
t = d.convert_func; \
|
||||
}
|
||||
|
||||
class JsonStructHelper
|
||||
{
|
||||
public:
|
||||
static void MergeJson(QJsonObject &mergeTo, const QJsonObject &mergeIn)
|
||||
{
|
||||
for (const auto &key : mergeIn.keys())
|
||||
mergeTo[key] = mergeIn.value(key);
|
||||
}
|
||||
//
|
||||
template<typename T>
|
||||
static void Deserialize(T &t, const QJsonValue &d)
|
||||
{
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
t = (T) d.toInt();
|
||||
else if constexpr (std::is_same_v<T, QJsonObject>)
|
||||
t = d.toObject();
|
||||
else if constexpr (std::is_same_v<T, QJsonArray>)
|
||||
t = d.toArray();
|
||||
else
|
||||
t.loadJson(d);
|
||||
}
|
||||
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(QString, toString());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(QChar, toVariant().toChar());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(std::string, toString().toStdString());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(std::wstring, toString().toStdWString());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(bool, toBool());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(double, toDouble());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(float, toVariant().toFloat());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(int, toInt());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(long, toVariant().toLongLong());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(long long, toVariant().toLongLong());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(unsigned int, toVariant().toUInt());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(unsigned long, toVariant().toULongLong());
|
||||
___DECL_JSON_STRUCT_LOAD_SIMPLE_TYPE_FUNC(unsigned long long, toVariant().toULongLong());
|
||||
|
||||
template<typename T>
|
||||
static void Deserialize(QList<T> &t, const QJsonValue &d)
|
||||
{
|
||||
t.clear();
|
||||
for (const auto &val : d.toArray())
|
||||
{
|
||||
T data;
|
||||
Deserialize(data, val);
|
||||
t.push_back(data);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TKey, typename TValue>
|
||||
static void Deserialize(QMap<TKey, TValue> &t, const QJsonValue &d)
|
||||
{
|
||||
t.clear();
|
||||
const auto &jsonObject = d.toObject();
|
||||
TKey keyVal;
|
||||
TValue valueVal;
|
||||
for (const auto &key : jsonObject.keys())
|
||||
{
|
||||
Deserialize(keyVal, key);
|
||||
Deserialize(valueVal, jsonObject.value(key));
|
||||
t.insert(keyVal, valueVal);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Store Json Data ===========================
|
||||
|
||||
template<typename T>
|
||||
static QJsonValue Serialize(const T &t)
|
||||
{
|
||||
if constexpr (std::is_enum_v<T>)
|
||||
return (int) t;
|
||||
else if constexpr (std::is_same_v<T, QJsonObject> || std::is_same_v<T, QJsonArray>)
|
||||
return t;
|
||||
else
|
||||
return t.toJson();
|
||||
}
|
||||
|
||||
#define pure_func(x) (x)
|
||||
#define ___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(type) \
|
||||
static QJsonValue Serialize(const type &t) \
|
||||
{ \
|
||||
return QJsonValue(t); \
|
||||
}
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(int);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(bool);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(QJsonArray);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(QJsonObject);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(QString);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(long long);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(float);
|
||||
___DECL_JSON_STRUCT_STORE_SIMPLE_TYPE_FUNC(double);
|
||||
|
||||
#define ___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(type, func) \
|
||||
static QJsonValue Serialize(const type &t) \
|
||||
{ \
|
||||
return QJsonValue::fromVariant(func); \
|
||||
}
|
||||
|
||||
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(std::string, QString::fromStdString(t))
|
||||
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(std::wstring, QString::fromStdWString(t))
|
||||
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(long, QVariant::fromValue<long>(t))
|
||||
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(unsigned int, QVariant::fromValue<unsigned int>(t))
|
||||
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(unsigned long, QVariant::fromValue<unsigned long>(t))
|
||||
___DECL_JSON_STRUCT_STORE_VARIANT_TYPE_FUNC(unsigned long long, QVariant::fromValue<unsigned long long>(t))
|
||||
|
||||
template<typename TValue>
|
||||
static QJsonValue Serialize(const QMap<QString, TValue> &t)
|
||||
{
|
||||
QJsonObject mapObject;
|
||||
for (const auto &key : t.keys())
|
||||
{
|
||||
auto valueVal = Serialize(t.value(key));
|
||||
mapObject.insert(key, valueVal);
|
||||
}
|
||||
return mapObject;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static QJsonValue Serialize(const QList<T> &t)
|
||||
{
|
||||
QJsonArray listObject;
|
||||
for (const auto &item : t)
|
||||
{
|
||||
listObject.push_back(Serialize(item));
|
||||
}
|
||||
return listObject;
|
||||
}
|
||||
};
|
||||
74
client/3rd/QJsonStruct/macroexpansion.hpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2)
|
||||
#define CONCATENATE2(arg1, arg2) arg1##arg2
|
||||
#define CONCATENATE(x, y) x##y
|
||||
|
||||
#define EXPAND(...) __VA_ARGS__
|
||||
#define FOR_EACH_1(what, x, ...) what(x)
|
||||
#define FOR_EACH_2(what, x, ...) what(x) EXPAND(FOR_EACH_1(what, __VA_ARGS__))
|
||||
#define FOR_EACH_3(what, x, ...) what(x) EXPAND(FOR_EACH_2(what, __VA_ARGS__))
|
||||
#define FOR_EACH_4(what, x, ...) what(x) EXPAND(FOR_EACH_3(what, __VA_ARGS__))
|
||||
#define FOR_EACH_5(what, x, ...) what(x) EXPAND(FOR_EACH_4(what, __VA_ARGS__))
|
||||
#define FOR_EACH_6(what, x, ...) what(x) EXPAND(FOR_EACH_5(what, __VA_ARGS__))
|
||||
#define FOR_EACH_7(what, x, ...) what(x) EXPAND(FOR_EACH_6(what, __VA_ARGS__))
|
||||
#define FOR_EACH_8(what, x, ...) what(x) EXPAND(FOR_EACH_7(what, __VA_ARGS__))
|
||||
#define FOR_EACH_9(what, x, ...) what(x) EXPAND(FOR_EACH_8(what, __VA_ARGS__))
|
||||
#define FOR_EACH_10(what, x, ...) what(x) EXPAND(FOR_EACH_9(what, __VA_ARGS__))
|
||||
#define FOR_EACH_11(what, x, ...) what(x) EXPAND(FOR_EACH_10(what, __VA_ARGS__))
|
||||
#define FOR_EACH_12(what, x, ...) what(x) EXPAND(FOR_EACH_11(what, __VA_ARGS__))
|
||||
#define FOR_EACH_13(what, x, ...) what(x) EXPAND(FOR_EACH_12(what, __VA_ARGS__))
|
||||
#define FOR_EACH_14(what, x, ...) what(x) EXPAND(FOR_EACH_13(what, __VA_ARGS__))
|
||||
#define FOR_EACH_15(what, x, ...) what(x) EXPAND(FOR_EACH_14(what, __VA_ARGS__))
|
||||
#define FOR_EACH_16(what, x, ...) what(x) EXPAND(FOR_EACH_15(what, __VA_ARGS__))
|
||||
|
||||
#define FOR_EACH_NARG(...) FOR_EACH_NARG_(__VA_ARGS__, FOR_EACH_RSEQ_N())
|
||||
#define FOR_EACH_NARG_(...) EXPAND(FOR_EACH_ARG_N(__VA_ARGS__))
|
||||
#define FOR_EACH_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, N, ...) N
|
||||
#define FOR_EACH_RSEQ_N() 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
|
||||
|
||||
#define FOR_EACH_(N, what, ...) EXPAND(CONCATENATE(FOR_EACH_, N)(what, __VA_ARGS__))
|
||||
#define FOR_EACH(what, ...) FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
|
||||
#define FOREACH_CALL_FUNC(func, ...) FOR_EACH(func, __VA_ARGS__)
|
||||
|
||||
// Bad hack ==========================================================================================================================
|
||||
#define _2X_FOR_EACH_1(what, x, ...) what(x)
|
||||
#define _2X_FOR_EACH_2(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_1(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_3(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_2(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_4(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_3(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_5(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_4(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_6(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_5(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_7(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_6(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_8(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_7(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_9(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_8(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_10(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_9(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_11(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_10(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_12(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_11(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_13(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_12(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_14(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_13(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_15(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_14(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_16(what, x, ...) what(x) EXPAND(_2X_FOR_EACH_15(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH_(N, what, ...) EXPAND(CONCATENATE(_2X_FOR_EACH_, N)(what, __VA_ARGS__))
|
||||
#define _2X_FOR_EACH(what, ...) _2X_FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
|
||||
#define FOREACH_CALL_FUNC_2(func, ...) _2X_FOR_EACH(func, __VA_ARGS__)
|
||||
|
||||
// Bad hack ==========================================================================================================================
|
||||
#define _3X_FOR_EACH_1(what, x, ...) what(x)
|
||||
#define _3X_FOR_EACH_2(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_1(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_3(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_2(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_4(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_3(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_5(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_4(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_6(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_5(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_7(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_6(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_8(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_7(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_9(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_8(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_10(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_9(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_11(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_10(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_12(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_11(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_13(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_12(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_14(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_13(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_15(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_14(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_16(what, x, ...) what(x) EXPAND(_3X_FOR_EACH_15(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH_(N, what, ...) EXPAND(CONCATENATE(_3X_FOR_EACH_, N)(what, __VA_ARGS__))
|
||||
#define _3X_FOR_EACH(what, ...) _3X_FOR_EACH_(FOR_EACH_NARG(__VA_ARGS__), what, __VA_ARGS__)
|
||||
#define FOREACH_CALL_FUNC_3(func, ...) _3X_FOR_EACH(func, __VA_ARGS__)
|
||||
16
client/3rd/QJsonStruct/test/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
function(QJSONSTRUCT_ADD_TEST TEST_NAME TEST_SOURCE)
|
||||
add_executable(${TEST_NAME} ${TEST_SOURCE} catch.hpp ${QJSONSTRUCT_SOURCES})
|
||||
target_include_directories(${TEST_NAME}
|
||||
PRIVATE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||
)
|
||||
target_link_libraries(
|
||||
${TEST_NAME}
|
||||
PRIVATE
|
||||
Qt::Core
|
||||
)
|
||||
add_test(NAME QJSONSTRUCT_TEST_${TEST_NAME} COMMAND $<TARGET_FILE:${TEST_NAME}> -s)
|
||||
endfunction()
|
||||
|
||||
QJSONSTRUCT_ADD_TEST(serialization serialize/main.cpp)
|
||||
#QJSONSTRUCT_ADD_TEST(serialize_strings serialize/strings.cpp)
|
||||
45
client/3rd/QJsonStruct/test/TestIO.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include "QJsonStruct.hpp"
|
||||
#ifndef _X
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#endif
|
||||
|
||||
struct BaseStruct
|
||||
{
|
||||
QString baseStr;
|
||||
int o;
|
||||
JSONSTRUCT_REGISTER(BaseStruct, F(baseStr, o))
|
||||
};
|
||||
|
||||
struct BaseStruct2
|
||||
{
|
||||
QString baseStr2;
|
||||
int o2;
|
||||
JSONSTRUCT_REGISTER(BaseStruct, F(baseStr2, o2))
|
||||
};
|
||||
struct TestInnerStruct
|
||||
: BaseStruct
|
||||
, BaseStruct2
|
||||
{
|
||||
QJsonObject jobj;
|
||||
QJsonArray jarray;
|
||||
QString str;
|
||||
JSONSTRUCT_REGISTER(TestInnerStruct, B(BaseStruct, BaseStruct2), F(str, jobj, jarray))
|
||||
};
|
||||
|
||||
struct JsonIOTest
|
||||
{
|
||||
QString str;
|
||||
QList<int> listOfNumber;
|
||||
QList<bool> listOfBool;
|
||||
QList<QString> listOfString;
|
||||
QList<QList<QString>> listOfListOfString;
|
||||
|
||||
QMap<QString, QString> map;
|
||||
TestInnerStruct inner;
|
||||
|
||||
JSONSTRUCT_REGISTER(JsonIOTest, F(str, listOfNumber, listOfBool, listOfString, listOfListOfString, map, inner));
|
||||
JsonIOTest(){};
|
||||
};
|
||||
19
client/3rd/QJsonStruct/test/TestOut.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "QJsonStruct.hpp"
|
||||
|
||||
struct SubData
|
||||
{
|
||||
QString subString;
|
||||
JSONSTRUCT_REGISTER_TOJSON(subString)
|
||||
};
|
||||
|
||||
struct ToJsonOnlyData
|
||||
{
|
||||
QString x;
|
||||
int y;
|
||||
int z;
|
||||
QList<int> ints;
|
||||
SubData sub;
|
||||
QMap<QString, SubData> subs;
|
||||
JSONSTRUCT_REGISTER_TOJSON(x, y, z, sub, ints, subs)
|
||||
};
|
||||
17698
client/3rd/QJsonStruct/test/catch.hpp
Normal file
70
client/3rd/QJsonStruct/test/main.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "QJsonIO.hpp"
|
||||
#include "QJsonStruct.hpp"
|
||||
#include "TestIO.hpp"
|
||||
#include "TestOut.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
Q_UNUSED(argc)
|
||||
Q_UNUSED(argv)
|
||||
|
||||
{
|
||||
ToJsonOnlyData data;
|
||||
data.x = "1string";
|
||||
data.y = 2;
|
||||
data.ints << 0;
|
||||
data.ints << 100;
|
||||
data.ints << 900;
|
||||
data.sub.subString = "subs";
|
||||
data.subs["subs-1"] = { "subs1-data" };
|
||||
data.subs["subs-2"] = { "subs2-data" };
|
||||
data.subs["subs-3"] = { "subs3-data" };
|
||||
data.z = 3;
|
||||
auto x = data.toJson();
|
||||
std::cout << QJsonDocument(x).toJson().toStdString() << std::endl;
|
||||
}
|
||||
//
|
||||
{
|
||||
auto f = JsonIOTest::fromJson( //
|
||||
QJsonObject{
|
||||
{ "inner", QJsonObject{ { "str", "innerString" }, //
|
||||
{ "jobj", QJsonObject{ { "key", "value" } } }, //
|
||||
{ "jarray", QJsonArray{ "array0", "array1", "array2" } }, //
|
||||
{ "baseStr", "baseInnerString" } } }, //
|
||||
{ "str", "data1" }, //
|
||||
{ "map", QJsonObject{ { "mapStr", "mapData" } } }, //
|
||||
{ "listOfString", QJsonArray{ "1", "2", "3", "4", "5" } }, //
|
||||
{ "listOfNumber", QJsonArray{ 1, 2, 3, 4, 5 } }, //
|
||||
{ "listOfBool", QJsonArray{ true, false, false, true, true } }, //
|
||||
{ "listOfListOfString", QJsonArray{ QJsonArray{ "1" }, //
|
||||
QJsonArray{ "1", "2" }, //
|
||||
QJsonArray{ "1", "2", "3" }, //
|
||||
QJsonArray{ "1", "2", "3", "4" }, //
|
||||
QJsonArray{ "1", "2", "3", "4", "5" } } }, //
|
||||
});
|
||||
auto x = f.toJson();
|
||||
std::cout << QJsonDocument(x).toJson().toStdString() << std::endl;
|
||||
}
|
||||
{
|
||||
QJsonObject obj{
|
||||
{ "inner", QJsonObject{ { "str", "innerString" }, { "baseStr", "baseInnerString" } } }, //
|
||||
{ "str", "data1" }, //
|
||||
{ "map", QJsonObject{ { "mapStr", "mapData" } } }, //
|
||||
{ "listOfString", QJsonArray{ "1", "2", "3", "4", "5" } }, //
|
||||
{ "listOfNumber", QJsonArray{ 1, 2, 3, 4, 5 } }, //
|
||||
{ "listOfBool", QJsonArray{ true, false, false, true, true } }, //
|
||||
{ "listOfListOfString", QJsonArray{ QJsonArray{ "1" }, //
|
||||
QJsonArray{ "1", "2" }, //
|
||||
QJsonArray{ "1", "2", "3" }, //
|
||||
QJsonArray{ "1", "2", "3", "4" }, //
|
||||
QJsonArray{ "1", "2", "3", "4", "5" } } }, //
|
||||
};
|
||||
auto y = QJsonIO::GetValue(obj, std::tuple{ "listOfListOfString", 2 });
|
||||
y.toObject();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
181
client/3rd/QJsonStruct/test/serialize/main.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "QJsonStruct.hpp"
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch.hpp"
|
||||
|
||||
const static inline auto INT_TEST_MAX = std::numeric_limits<int>::max() - 1;
|
||||
const static inline auto INT_TEST_MIN = -(std::numeric_limits<int>::min() + 1);
|
||||
|
||||
#define SINGLE_ELEMENT_CLASS_DECL(CLASS, TYPE, field, defaultvalue, existance) \
|
||||
class CLASS \
|
||||
{ \
|
||||
public: \
|
||||
TYPE field = defaultvalue; \
|
||||
JSONSTRUCT_REGISTER(CLASS, existance(field)); \
|
||||
};
|
||||
|
||||
// SINGLE_ELEMENT_REQUIRE( CLASS_NAME , TYPE , FIELD , DEFAULT_VALUE , SET VALUE , CHECK VALUE )
|
||||
#define SINGLE_ELEMENT_REQUIRE(CLASS, TYPE, field, defaultvalue, value, checkvalue, existance) \
|
||||
SINGLE_ELEMENT_CLASS_DECL(CLASS, TYPE, field, defaultvalue, existance); \
|
||||
CLASS CLASS##_class; \
|
||||
CLASS##_class.field = value; \
|
||||
REQUIRE(CLASS##_class.toJson()[#field] == checkvalue);
|
||||
|
||||
using namespace std;
|
||||
SCENARIO("Test Serialization", "[Serialize]")
|
||||
{
|
||||
GIVEN("Single Element")
|
||||
{
|
||||
const static QList<QString> defaultList{ "entry 1", "entry 2" };
|
||||
const static QMap<QString, QString> defaultMap{ { "key1", "value1" }, { "key2", "value2" } };
|
||||
typedef QMap<QString, QString> QStringQStringMap;
|
||||
|
||||
WHEN("Serialize a single element")
|
||||
{
|
||||
const static QStringQStringMap setValueMap{ { "newkey1", "newvalue1" } };
|
||||
const static QJsonObject setValueJson{ { "newkey1", QJsonValue{ "newvalue1" } } };
|
||||
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_Empty, QString, a, "empty", "", "", F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest, QString, a, "empty", "Some QString", "Some QString", F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_WithQoutes, QString, a, "empty", "\"", "\"", F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_zint, int, a, -10, 0, 0, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_nint, int, a, -10, 1, 1, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_pint, int, a, -10, -1, -1, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_pmint, int, a, -1, INT_TEST_MAX, INT_TEST_MAX, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_zmint, int, a, -1, INT_TEST_MIN, INT_TEST_MIN, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_zuint, uint, a, -10, 0, 0, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QStringTest_puint, uint, a, -10, 1, 1, F);
|
||||
SINGLE_ELEMENT_REQUIRE(BoolTest_True, bool, a, false, true, true, F);
|
||||
SINGLE_ELEMENT_REQUIRE(BoolTest_False, bool, a, true, false, false, F);
|
||||
SINGLE_ELEMENT_REQUIRE(StdStringTest, string, a, "def", "std::string _test", "std::string _test", F);
|
||||
SINGLE_ELEMENT_REQUIRE(QListTest, QList<QString>, a, defaultList, { "newEntry" }, QJsonArray{ "newEntry" }, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QMapTest, QStringQStringMap, a, defaultMap, {}, QJsonObject{}, F);
|
||||
SINGLE_ELEMENT_REQUIRE(QMapValueTest, QStringQStringMap, a, defaultMap, setValueMap, setValueJson, F);
|
||||
}
|
||||
|
||||
WHEN("Serialize a default value")
|
||||
{
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultQString, QString, a, "defaultvalue", "defaultvalue", QJsonValue::Undefined, F);
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultInteger, int, a, 12345, 12345, QJsonValue::Undefined, F);
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultList, QList<QString>, a, defaultList, defaultList, QJsonValue::Undefined, F);
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultMap, QStringQStringMap, a, defaultMap, defaultMap, QJsonValue::Undefined, F);
|
||||
}
|
||||
|
||||
WHEN("Serialize a force existance default value")
|
||||
{
|
||||
const static QJsonArray defaultListJson{ "entry 1", "entry 2" };
|
||||
const static QJsonObject defaultMapJson{ { "key1", "value1" }, { "key2", "value2" } };
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultQString, QString, a, "defaultvalue", "defaultvalue", "defaultvalue", A);
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultInteger, int, a, 12345, 12345, 12345, A);
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultList, QList<QString>, a, defaultList, defaultList, defaultListJson, A);
|
||||
SINGLE_ELEMENT_REQUIRE(DefaultMap, QStringQStringMap, a, defaultMap, defaultMap, defaultMapJson, A);
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Multiple Simple Elements")
|
||||
{
|
||||
WHEN("Can Omit Default Value")
|
||||
{
|
||||
class MultipleNonDefaultElementTestClass
|
||||
{
|
||||
public:
|
||||
QString astring;
|
||||
int integer = 0;
|
||||
double adouble = 0.0;
|
||||
QList<QString> myList;
|
||||
JSONSTRUCT_REGISTER(MultipleNonDefaultElementTestClass, F(astring, integer, adouble, myList))
|
||||
};
|
||||
MultipleNonDefaultElementTestClass instance;
|
||||
const auto json = instance.toJson();
|
||||
REQUIRE(json["astring"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["integer"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["adouble"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["myList"] == QJsonValue::Undefined);
|
||||
}
|
||||
|
||||
WHEN("Forcing Existance")
|
||||
{
|
||||
class MultipleNonDefaultExistanceElementTestClass
|
||||
{
|
||||
public:
|
||||
QString astring;
|
||||
int integer = 0;
|
||||
double adouble = 0.0;
|
||||
QList<QString> myList;
|
||||
JSONSTRUCT_REGISTER(MultipleNonDefaultExistanceElementTestClass, A(astring, integer, adouble, myList))
|
||||
};
|
||||
MultipleNonDefaultExistanceElementTestClass instance;
|
||||
const auto json = instance.toJson();
|
||||
REQUIRE(json["astring"] == "");
|
||||
REQUIRE(json["integer"] == 0);
|
||||
REQUIRE(json["adouble"] == 0.0);
|
||||
REQUIRE(json["myList"] == QJsonArray{});
|
||||
}
|
||||
}
|
||||
|
||||
GIVEN("Nested Elements")
|
||||
{
|
||||
WHEN("Can Omit Default Value")
|
||||
{
|
||||
class Parent
|
||||
{
|
||||
class NestedChild
|
||||
{
|
||||
class NestedChild2
|
||||
{
|
||||
public:
|
||||
int childChildInt = 13579;
|
||||
JSONSTRUCT_COMPARE(NestedChild2, childChildInt)
|
||||
JSONSTRUCT_REGISTER(NestedChild2, F(childChildInt))
|
||||
};
|
||||
|
||||
public:
|
||||
int childInt = 54321;
|
||||
QString childQString = "A QString";
|
||||
NestedChild2 anotherChild;
|
||||
JSONSTRUCT_COMPARE(NestedChild, childInt, childQString, anotherChild)
|
||||
JSONSTRUCT_REGISTER(NestedChild, F(childInt, childQString, anotherChild))
|
||||
};
|
||||
|
||||
public:
|
||||
int parentInt = 12345;
|
||||
NestedChild child;
|
||||
JSONSTRUCT_REGISTER(Parent, F(parentInt, child))
|
||||
};
|
||||
|
||||
WHEN("Omitted whole child element")
|
||||
{
|
||||
Parent parent;
|
||||
const auto json = parent.toJson();
|
||||
REQUIRE(json["parentInt"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["child"] == QJsonValue::Undefined);
|
||||
}
|
||||
|
||||
WHEN("Omitted one element in the child")
|
||||
{
|
||||
const auto childJson = QJsonObject{ { "childInt", 1314 } };
|
||||
Parent parent;
|
||||
parent.child.childInt = 1314;
|
||||
const auto json = parent.toJson();
|
||||
REQUIRE(json["parentInt"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["child"] == childJson);
|
||||
REQUIRE(json["child"]["childInt"] == 1314);
|
||||
REQUIRE(json["child"]["childQString"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["child"]["child"]["anotherChild"] == QJsonValue::Undefined);
|
||||
}
|
||||
|
||||
WHEN("Omitted one element in the child child")
|
||||
{
|
||||
Parent parent;
|
||||
parent.child.childInt = 1314;
|
||||
parent.child.anotherChild.childChildInt = 97531;
|
||||
const auto json = parent.toJson();
|
||||
REQUIRE(json["parentInt"] == QJsonValue::Undefined);
|
||||
REQUIRE(json["child"]["childInt"] == 1314);
|
||||
REQUIRE(json["child"]["childQString"] == QJsonValue::Undefined);
|
||||
const QJsonObject childChild{ { "childChildInt", 97531 } };
|
||||
REQUIRE(json["child"]["anotherChild"] == childChild);
|
||||
REQUIRE(json["child"]["anotherChild"]["childChildInt"] == 97531);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
client/3rd/amneziawg-apple
vendored
@@ -134,6 +134,8 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/sshclient.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/serialization.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -176,6 +178,14 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/networkUtilities.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/outbound.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/inbound.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/ss.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/ssd.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vless.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/trojan.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
|
||||
)
|
||||
|
||||
# Mozilla sources
|
||||
|
||||
@@ -157,16 +157,19 @@ void AmneziaApplication::init()
|
||||
connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated);
|
||||
#endif
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
m_engine->load(url);
|
||||
m_systemController->setQmlRoot(m_engine->rootObjects().value(0));
|
||||
|
||||
bool enabled = m_settings->isSaveLogs();
|
||||
#ifndef Q_OS_ANDROID
|
||||
if (m_settings->isSaveLogs()) {
|
||||
if (enabled) {
|
||||
if (!Logger::init()) {
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Logger::setServiceLogsEnabled(enabled);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (m_parser.isSet("a"))
|
||||
@@ -351,6 +354,9 @@ void AmneziaApplication::initModels()
|
||||
m_sftpConfigModel.reset(new SftpConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("SftpConfigModel", m_sftpConfigModel.get());
|
||||
|
||||
m_socks5ConfigModel.reset(new Socks5ProxyConfigModel(this));
|
||||
m_engine->rootContext()->setContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel.get());
|
||||
|
||||
m_clientManagementModel.reset(new ClientManagementModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ClientManagementModel", m_clientManagementModel.get());
|
||||
connect(m_clientManagementModel.get(), &ClientManagementModel::adminConfigRevoked, m_serversModel.get(),
|
||||
@@ -363,10 +369,16 @@ void AmneziaApplication::initControllers()
|
||||
new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get());
|
||||
|
||||
connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](const QString &errorMessage) {
|
||||
connect(m_connectionController.get(), qOverload<const QString &>(&ConnectionController::connectionErrorOccurred), this, [this](const QString &errorMessage) {
|
||||
emit m_pageController->showErrorMessage(errorMessage);
|
||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||
});
|
||||
|
||||
connect(m_connectionController.get(), qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred), this, [this](ErrorCode errorCode) {
|
||||
emit m_pageController->showErrorMessage(errorCode);
|
||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||
});
|
||||
|
||||
connect(m_connectionController.get(), &ConnectionController::connectButtonClicked, m_connectionController.get(),
|
||||
&ConnectionController::toggleConnection, Qt::QueuedConnection);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "ui/models/protocols_model.h"
|
||||
#include "ui/models/servers_model.h"
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/sites_model.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
#include "ui/models/appSplitTunnelingModel.h"
|
||||
@@ -114,6 +115,7 @@ private:
|
||||
#endif
|
||||
|
||||
QScopedPointer<SftpConfigModel> m_sftpConfigModel;
|
||||
QScopedPointer<Socks5ProxyConfigModel> m_socks5ConfigModel;
|
||||
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QThread m_vpnConnectionThread;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<!-- for TV -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<!-- The following comment will be replaced upon deployment with default features based on the dependencies
|
||||
of the application. Remove the comment if you do not require these default features. -->
|
||||
@@ -31,9 +34,11 @@
|
||||
android:label="-- %%INSERT_APP_NAME%% --"
|
||||
android:icon="@mipmap/icon"
|
||||
android:roundIcon="@mipmap/icon_round"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:theme="@style/NoActionBar"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:hasFragileUserData="false"
|
||||
tools:targetApi="s">
|
||||
|
||||
<activity
|
||||
@@ -47,6 +52,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
@@ -136,8 +142,34 @@
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".AmneziaVpnService"
|
||||
android:process=":amneziaVpnService"
|
||||
android:name=".AwgService"
|
||||
android:process=":amneziaAwgService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false"
|
||||
tools:ignore="ForegroundServicePermission">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".OpenVpnService"
|
||||
android:process=":amneziaOpenVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false"
|
||||
tools:ignore="ForegroundServicePermission">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".XrayService"
|
||||
android:process=":amneziaXrayService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false"
|
||||
|
||||
@@ -3,6 +3,7 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
id("property-delegate")
|
||||
}
|
||||
|
||||
@@ -68,6 +69,12 @@ android {
|
||||
}
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
|
||||
create("fdroid") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = null
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
@@ -98,7 +105,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
|
||||
implementation(project(":qt"))
|
||||
implementation(project(":utils"))
|
||||
implementation(project(":protocolApi"))
|
||||
@@ -106,9 +112,11 @@ dependencies {
|
||||
implementation(project(":awg"))
|
||||
implementation(project(":openvpn"))
|
||||
implementation(project(":cloak"))
|
||||
implementation(project(":xray"))
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.kotlinx.serialization.protobuf)
|
||||
implementation(libs.bundles.androidx.camera)
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
|
||||
@@ -8,6 +8,7 @@ androidx-camera = "1.3.0"
|
||||
androidx-security-crypto = "1.1.0-alpha06"
|
||||
androidx-datastore = "1.1.0-beta01"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
kotlinx-serialization = "1.6.3"
|
||||
google-mlkit = "17.2.0"
|
||||
|
||||
[libraries]
|
||||
@@ -21,6 +22,7 @@ androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
|
||||
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
|
||||
|
||||
[bundles]
|
||||
@@ -35,3 +37,4 @@ androidx-camera = [
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package org.amnezia.vpn.protocol.openvpn
|
||||
|
||||
import android.content.Context
|
||||
import android.net.VpnService.Builder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openvpn.ovpn3.ClientAPI_Config
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
@@ -37,7 +35,6 @@ import org.json.JSONObject
|
||||
|
||||
open class OpenVpn : Protocol() {
|
||||
|
||||
private lateinit var context: Context
|
||||
private var openVpnClient: OpenVpnClient? = null
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
@@ -53,10 +50,11 @@ open class OpenVpn : Protocol() {
|
||||
return Statistics.EMPTY_STATISTICS
|
||||
}
|
||||
|
||||
override fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
|
||||
super.initialize(context, state, onError)
|
||||
loadSharedLibrary(context, "ovpn3")
|
||||
this.context = context
|
||||
override fun internalInit() {
|
||||
if (!isInitialized) loadSharedLibrary(context, "ovpn3")
|
||||
if (this::scope.isInitialized) {
|
||||
scope.cancel()
|
||||
}
|
||||
scope = CoroutineScope(Dispatchers.IO)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,21 @@ private const val SPLIT_TUNNEL_EXCLUDE = 2
|
||||
abstract class Protocol {
|
||||
|
||||
abstract val statistics: Statistics
|
||||
protected lateinit var context: Context
|
||||
protected lateinit var state: MutableStateFlow<ProtocolState>
|
||||
protected lateinit var onError: (String) -> Unit
|
||||
protected var isInitialized: Boolean = false
|
||||
|
||||
open fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
|
||||
fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
|
||||
this.context = context
|
||||
this.state = state
|
||||
this.onError = onError
|
||||
internalInit()
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
protected abstract fun internalInit()
|
||||
|
||||
abstract fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean)
|
||||
|
||||
abstract fun stopVpn()
|
||||
|
||||
@@ -21,5 +21,5 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar", "*.aar"))))
|
||||
implementation(fileTree(mapOf("dir" to "../libs", "include" to listOf("*.jar"))))
|
||||
}
|
||||
|
||||
5
client/android/res/mipmap-anydpi-v26/ic_banner.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.7 KiB |
BIN
client/android/res/mipmap-xhdpi/ic_banner.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
client/android/res/mipmap-xhdpi/ic_banner_foreground.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 30 KiB |
4
client/android/res/values/ic_banner_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#1E1E1F</color>
|
||||
</resources>
|
||||
@@ -36,6 +36,8 @@ include(":wireguard")
|
||||
include(":awg")
|
||||
include(":openvpn")
|
||||
include(":cloak")
|
||||
include(":xray")
|
||||
include(":xray:libXray")
|
||||
|
||||
// get values from gradle or local properties
|
||||
val androidBuildToolsVersion: String by gradleProperties
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -34,6 +35,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -43,6 +45,8 @@ import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.Prefs
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.qtproject.qt.android.bindings.QtActivity
|
||||
|
||||
private const val TAG = "AmneziaActivity"
|
||||
@@ -59,6 +63,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private val qtInitialized = CompletableDeferred<Unit>()
|
||||
private var vpnProto: VpnProto? = null
|
||||
private var isWaitingStatus = true
|
||||
private var isServiceConnected = false
|
||||
private var isInBoundState = false
|
||||
@@ -141,6 +146,7 @@ class AmneziaActivity : QtActivity() {
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
|
||||
doUnbindService()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
doBindService()
|
||||
}
|
||||
}
|
||||
@@ -153,15 +159,20 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "Create Amnezia activity: $intent")
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
val proto = mainScope.async(Dispatchers.IO) {
|
||||
VpnStateStore.getVpnState().vpnProto
|
||||
}
|
||||
vpnServiceMessenger = IpcMessenger(
|
||||
"VpnService",
|
||||
onDeadObjectException = {
|
||||
doUnbindService()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
doBindService()
|
||||
}
|
||||
)
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
}
|
||||
|
||||
private fun registerBroadcastReceivers() {
|
||||
@@ -209,13 +220,21 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.d(TAG, "Start Amnezia activity")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
doBindService()
|
||||
vpnProto?.let { proto ->
|
||||
if (AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
|
||||
doBindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.d(TAG, "Stop Amnezia activity")
|
||||
doUnbindService()
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@@ -269,10 +288,12 @@ class AmneziaActivity : QtActivity() {
|
||||
@MainThread
|
||||
private fun doBindService() {
|
||||
Log.d(TAG, "Bind service")
|
||||
Intent(this, AmneziaVpnService::class.java).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
|
||||
vpnProto?.let { proto ->
|
||||
Intent(this, proto.serviceClass).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
|
||||
}
|
||||
isInBoundState = true
|
||||
}
|
||||
isInBoundState = true
|
||||
}
|
||||
|
||||
@MainThread
|
||||
@@ -280,7 +301,6 @@ class AmneziaActivity : QtActivity() {
|
||||
if (isInBoundState) {
|
||||
Log.d(TAG, "Unbind service")
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
isServiceConnected = false
|
||||
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
|
||||
vpnServiceMessenger.reset()
|
||||
@@ -365,13 +385,32 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
@MainThread
|
||||
private fun startVpn(vpnConfig: String) {
|
||||
if (isServiceConnected) {
|
||||
connectToVpn(vpnConfig)
|
||||
} else {
|
||||
getVpnProto(vpnConfig)?.let { proto ->
|
||||
Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
|
||||
if (isServiceConnected) {
|
||||
if (proto.serviceClass == vpnProto?.serviceClass) {
|
||||
vpnProto = proto
|
||||
connectToVpn(vpnConfig)
|
||||
return
|
||||
}
|
||||
doUnbindService()
|
||||
}
|
||||
vpnProto = proto
|
||||
isWaitingStatus = false
|
||||
startVpnService(vpnConfig)
|
||||
startVpnService(vpnConfig, proto)
|
||||
doBindService()
|
||||
}
|
||||
} ?: QtAndroidController.onServiceError()
|
||||
}
|
||||
|
||||
private fun getVpnProto(vpnConfig: String): VpnProto? = try {
|
||||
require(vpnConfig.isNotBlank()) { "Blank VPN config" }
|
||||
VpnProto.get(JSONObject(vpnConfig).getString("protocol"))
|
||||
} catch (e: JSONException) {
|
||||
Log.e(TAG, "Invalid VPN config json format: ${e.message}")
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "Protocol not found: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
private fun connectToVpn(vpnConfig: String) {
|
||||
@@ -383,15 +422,15 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpnService(vpnConfig: String) {
|
||||
Log.d(TAG, "Start VPN service")
|
||||
Intent(this, AmneziaVpnService::class.java).apply {
|
||||
private fun startVpnService(vpnConfig: String, proto: VpnProto) {
|
||||
Log.d(TAG, "Start VPN service: $proto")
|
||||
Intent(this, proto.serviceClass).apply {
|
||||
putExtra(MSG_VPN_CONFIG, vpnConfig)
|
||||
}.also {
|
||||
try {
|
||||
ContextCompat.startForegroundService(this, it)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
|
||||
Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
|
||||
QtAndroidController.onServiceError()
|
||||
}
|
||||
}
|
||||
@@ -507,7 +546,7 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}.also {
|
||||
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
onAny = {
|
||||
val uri = it?.data?.toString() ?: ""
|
||||
Log.d(TAG, "Open file: $uri")
|
||||
mainScope.launch {
|
||||
@@ -521,8 +560,12 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
|
||||
@Suppress("unused")
|
||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
@Suppress("unused")
|
||||
fun startQrCodeReader() {
|
||||
Log.v(TAG, "Start camera")
|
||||
|
||||
@@ -39,6 +39,9 @@ class AmneziaTileService : TileService() {
|
||||
|
||||
@Volatile
|
||||
private var isServiceConnected = false
|
||||
|
||||
@Volatile
|
||||
private var vpnProto: VpnProto? = null
|
||||
private var isInBoundState = false
|
||||
@Volatile
|
||||
private var isVpnConfigExists = false
|
||||
@@ -94,16 +97,21 @@ class AmneziaTileService : TileService() {
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Log.d(TAG, "Start listening")
|
||||
if (AmneziaVpnService.isRunning(applicationContext)) {
|
||||
Log.d(TAG, "Vpn service is running")
|
||||
doBindService()
|
||||
} else {
|
||||
Log.d(TAG, "Vpn service is not running")
|
||||
isServiceConnected = false
|
||||
updateVpnState(DISCONNECTED)
|
||||
scope.launch {
|
||||
Log.d(TAG, "Start listening")
|
||||
vpnProto = VpnStateStore.getVpnState().vpnProto
|
||||
vpnProto.also { proto ->
|
||||
if (proto != null && AmneziaVpnService.isRunning(applicationContext, proto.processName)) {
|
||||
Log.d(TAG, "Vpn service is running")
|
||||
doBindService()
|
||||
} else {
|
||||
Log.d(TAG, "Vpn service is not running")
|
||||
isServiceConnected = false
|
||||
updateVpnState(DISCONNECTED)
|
||||
}
|
||||
}
|
||||
vpnStateListeningJob = launchVpnStateListening()
|
||||
}
|
||||
vpnStateListeningJob = launchVpnStateListening()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
@@ -124,7 +132,7 @@ class AmneziaTileService : TileService() {
|
||||
}
|
||||
|
||||
private fun onClickInternal() {
|
||||
if (isVpnConfigExists) {
|
||||
if (isVpnConfigExists && vpnProto != null) {
|
||||
Log.d(TAG, "Change VPN state")
|
||||
if (qsTile.state == Tile.STATE_INACTIVE) {
|
||||
Log.d(TAG, "Start VPN")
|
||||
@@ -147,10 +155,12 @@ class AmneziaTileService : TileService() {
|
||||
|
||||
private fun doBindService() {
|
||||
Log.d(TAG, "Bind service")
|
||||
Intent(this, AmneziaVpnService::class.java).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
|
||||
vpnProto?.let { proto ->
|
||||
Intent(this, proto.serviceClass).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
|
||||
}
|
||||
isInBoundState = true
|
||||
}
|
||||
isInBoundState = true
|
||||
}
|
||||
|
||||
private fun doUnbindService() {
|
||||
@@ -180,6 +190,7 @@ class AmneziaTileService : TileService() {
|
||||
if (VpnService.prepare(applicationContext) != null) {
|
||||
Intent(this, VpnRequestActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
putExtra(EXTRA_PROTOCOL, vpnProto)
|
||||
}.also {
|
||||
startActivityAndCollapseCompat(it)
|
||||
}
|
||||
@@ -189,14 +200,16 @@ class AmneziaTileService : TileService() {
|
||||
}
|
||||
|
||||
private fun startVpnService() {
|
||||
try {
|
||||
ContextCompat.startForegroundService(
|
||||
applicationContext,
|
||||
Intent(this, AmneziaVpnService::class.java)
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
|
||||
}
|
||||
vpnProto?.let { proto ->
|
||||
try {
|
||||
ContextCompat.startForegroundService(
|
||||
applicationContext,
|
||||
Intent(this, proto.serviceClass)
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Failed to start ${proto.serviceClass.simpleName}: $e")
|
||||
}
|
||||
} ?: Log.e(TAG, "Failed to start vpn service: vpnProto is null")
|
||||
}
|
||||
|
||||
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
|
||||
@@ -220,11 +233,8 @@ class AmneziaTileService : TileService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVpnState(state: ProtocolState) {
|
||||
scope.launch {
|
||||
VpnStateStore.store { it.copy(protocolState = state) }
|
||||
}
|
||||
}
|
||||
private fun updateVpnState(state: ProtocolState) =
|
||||
scope.launch { VpnStateStore.store { it.copy(protocolState = state) } }
|
||||
|
||||
private fun launchVpnStateListening() =
|
||||
scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
|
||||
@@ -232,9 +242,10 @@ class AmneziaTileService : TileService() {
|
||||
private fun updateTile(vpnState: VpnState) {
|
||||
Log.d(TAG, "Update tile: $vpnState")
|
||||
isVpnConfigExists = vpnState.serverName != null
|
||||
vpnProto = vpnState.vpnProto
|
||||
val tile = qsTile ?: return
|
||||
tile.apply {
|
||||
label = vpnState.serverName ?: DEFAULT_TILE_LABEL
|
||||
label = (vpnState.serverName ?: DEFAULT_TILE_LABEL) + (vpnProto?.let { " ${it.label}" } ?: "")
|
||||
when (val protocolState = vpnState.protocolState) {
|
||||
CONNECTED -> {
|
||||
state = Tile.STATE_ACTIVE
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
|
||||
import android.app.NotificationManager
|
||||
@@ -39,7 +40,6 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
import org.amnezia.vpn.protocol.LoadLibraryException
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
@@ -48,11 +48,7 @@ import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
|
||||
import org.amnezia.vpn.protocol.VpnException
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
import org.amnezia.vpn.protocol.awg.Awg
|
||||
import org.amnezia.vpn.protocol.cloak.Cloak
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpn
|
||||
import org.amnezia.vpn.protocol.putStatus
|
||||
import org.amnezia.vpn.protocol.wireguard.Wireguard
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.Prefs
|
||||
import org.amnezia.vpn.util.net.NetworkState
|
||||
@@ -63,6 +59,7 @@ import org.json.JSONObject
|
||||
private const val TAG = "AmneziaVpnService"
|
||||
|
||||
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect"
|
||||
const val ACTION_CONNECT = "org.amnezia.vpn.action.connect"
|
||||
|
||||
const val MSG_VPN_CONFIG = "VPN_CONFIG"
|
||||
const val MSG_ERROR = "ERROR"
|
||||
@@ -73,19 +70,18 @@ const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
|
||||
private const val PREFS_CONFIG_KEY = "LAST_CONF"
|
||||
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
|
||||
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
|
||||
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
|
||||
// private const val STATISTICS_SENDING_TIMEOUT = 1000L
|
||||
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
|
||||
private const val DISCONNECT_TIMEOUT = 5000L
|
||||
private const val STOP_SERVICE_TIMEOUT = 5000L
|
||||
|
||||
class AmneziaVpnService : VpnService() {
|
||||
@SuppressLint("Registered")
|
||||
open class AmneziaVpnService : VpnService() {
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private lateinit var connectionScope: CoroutineScope
|
||||
private var isServiceBound = false
|
||||
private var protocol: Protocol? = null
|
||||
private val protocolCache = mutableMapOf<String, Protocol>()
|
||||
private var vpnProto: VpnProto? = null
|
||||
private var protocolState = MutableStateFlow(UNKNOWN)
|
||||
private var serverName: String? = null
|
||||
private var serverIndex: Int = -1
|
||||
@@ -105,7 +101,7 @@ class AmneziaVpnService : VpnService() {
|
||||
// private var statisticsSendingJob: Job? = null
|
||||
private lateinit var networkState: NetworkState
|
||||
private lateinit var trafficStats: TrafficStats
|
||||
private var disconnectReceiver: BroadcastReceiver? = null
|
||||
private var controlReceiver: BroadcastReceiver? = null
|
||||
private var notificationStateReceiver: BroadcastReceiver? = null
|
||||
private var screenOnReceiver: BroadcastReceiver? = null
|
||||
private var screenOffReceiver: BroadcastReceiver? = null
|
||||
@@ -116,7 +112,6 @@ class AmneziaVpnService : VpnService() {
|
||||
|
||||
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
|
||||
protocolState.value = DISCONNECTED
|
||||
protocol = null
|
||||
when (e) {
|
||||
is IllegalArgumentException,
|
||||
is VpnStartException,
|
||||
@@ -227,7 +222,8 @@ class AmneziaVpnService : VpnService() {
|
||||
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
|
||||
}
|
||||
ServiceCompat.startForeground(
|
||||
this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value),
|
||||
this, NOTIFICATION_ID,
|
||||
serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
|
||||
foregroundServiceTypeCompat
|
||||
)
|
||||
return START_REDELIVER_INTENT
|
||||
@@ -292,9 +288,17 @@ class AmneziaVpnService : VpnService() {
|
||||
|
||||
private fun registerBroadcastReceivers() {
|
||||
Log.d(TAG, "Register broadcast receivers")
|
||||
disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) {
|
||||
Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT")
|
||||
disconnect()
|
||||
controlReceiver = registerBroadcastReceiver(
|
||||
arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
) {
|
||||
it?.action?.let { action ->
|
||||
Log.d(TAG, "Broadcast request received: $action")
|
||||
when (action) {
|
||||
ACTION_CONNECT -> connect()
|
||||
ACTION_DISCONNECT -> disconnect()
|
||||
else -> Log.w(TAG, "Unknown action received: $action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -340,10 +344,10 @@ class AmneziaVpnService : VpnService() {
|
||||
|
||||
private fun unregisterBroadcastReceivers() {
|
||||
Log.d(TAG, "Unregister broadcast receivers")
|
||||
unregisterBroadcastReceiver(disconnectReceiver)
|
||||
unregisterBroadcastReceiver(controlReceiver)
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
unregisterScreenStateBroadcastReceivers()
|
||||
disconnectReceiver = null
|
||||
controlReceiver = null
|
||||
notificationStateReceiver = null
|
||||
}
|
||||
|
||||
@@ -356,7 +360,7 @@ class AmneziaVpnService : VpnService() {
|
||||
protocolState.drop(1).collect { protocolState ->
|
||||
Log.d(TAG, "Protocol state changed: $protocolState")
|
||||
|
||||
serviceNotification.updateNotification(serverName, protocolState)
|
||||
serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState)
|
||||
|
||||
clientMessengers.send {
|
||||
ServiceEvent.STATUS_CHANGED.packToMessage {
|
||||
@@ -364,7 +368,7 @@ class AmneziaVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
|
||||
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex, vpnProto) }
|
||||
|
||||
when (protocolState) {
|
||||
CONNECTED -> {
|
||||
@@ -421,7 +425,7 @@ class AmneziaVpnService : VpnService() {
|
||||
@MainThread
|
||||
private fun enableNotification() {
|
||||
registerScreenStateBroadcastReceivers()
|
||||
serviceNotification.updateNotification(serverName, protocolState.value)
|
||||
serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState.value)
|
||||
launchTrafficStatsUpdate()
|
||||
}
|
||||
|
||||
@@ -484,8 +488,6 @@ class AmneziaVpnService : VpnService() {
|
||||
|
||||
Log.d(TAG, "Start VPN connection")
|
||||
|
||||
protocolState.value = CONNECTING
|
||||
|
||||
val config = parseConfigToJson(vpnConfig)
|
||||
saveServerData(config)
|
||||
if (config == null) {
|
||||
@@ -494,6 +496,16 @@ class AmneziaVpnService : VpnService() {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
vpnProto = VpnProto.get(config.getString("protocol"))
|
||||
} catch (e: Exception) {
|
||||
onError("Invalid VPN config: ${e.message}")
|
||||
protocolState.value = DISCONNECTED
|
||||
return
|
||||
}
|
||||
|
||||
protocolState.value = CONNECTING
|
||||
|
||||
if (!checkPermission()) {
|
||||
protocolState.value = DISCONNECTED
|
||||
return
|
||||
@@ -503,8 +515,10 @@ class AmneziaVpnService : VpnService() {
|
||||
disconnectionJob?.join()
|
||||
disconnectionJob = null
|
||||
|
||||
protocol = getProtocol(config.getString("protocol"))
|
||||
protocol?.startVpn(config, Builder(), ::protect)
|
||||
vpnProto?.protocol?.let { protocol ->
|
||||
protocol.initialize(applicationContext, protocolState, ::onError)
|
||||
protocol.startVpn(config, Builder(), ::protect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,8 +534,8 @@ class AmneziaVpnService : VpnService() {
|
||||
connectionJob?.join()
|
||||
connectionJob = null
|
||||
|
||||
protocol?.stopVpn()
|
||||
protocol = null
|
||||
vpnProto?.protocol?.stopVpn()
|
||||
|
||||
try {
|
||||
withTimeout(DISCONNECT_TIMEOUT) {
|
||||
// waiting for disconnect state
|
||||
@@ -543,22 +557,10 @@ class AmneziaVpnService : VpnService() {
|
||||
protocolState.value = RECONNECTING
|
||||
|
||||
connectionJob = connectionScope.launch {
|
||||
protocol?.reconnectVpn(Builder())
|
||||
vpnProto?.protocol?.reconnectVpn(Builder())
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun getProtocol(protocolName: String): Protocol =
|
||||
protocolCache[protocolName]
|
||||
?: when (protocolName) {
|
||||
"wireguard" -> Wireguard()
|
||||
"awg" -> Awg()
|
||||
"openvpn" -> OpenVpn()
|
||||
"cloak" -> Cloak()
|
||||
else -> throw IllegalArgumentException("Protocol '$protocolName' not found")
|
||||
}.apply { initialize(applicationContext, protocolState, ::onError) }
|
||||
.also { protocolCache[protocolName] = it }
|
||||
|
||||
/**
|
||||
* Utils methods
|
||||
*/
|
||||
@@ -603,6 +605,7 @@ class AmneziaVpnService : VpnService() {
|
||||
if (prepare(applicationContext) != null) {
|
||||
Intent(this, VpnRequestActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
putExtra(EXTRA_PROTOCOL, vpnProto)
|
||||
}.also {
|
||||
startActivity(it)
|
||||
}
|
||||
@@ -612,9 +615,9 @@ class AmneziaVpnService : VpnService() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isRunning(context: Context): Boolean =
|
||||
fun isRunning(context: Context, processName: String): Boolean =
|
||||
context.getSystemService<ActivityManager>()!!.runningAppProcesses.any {
|
||||
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
|
||||
it.processName == processName && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
client/android/src/org/amnezia/vpn/AwgService.kt
Normal file
@@ -0,0 +1,3 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
class AwgService : AmneziaVpnService()
|
||||
@@ -140,7 +140,7 @@ class CameraActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
Log.e(TAG, "Processing QR-code image failed: ${it.message}")
|
||||
Log.e(TAG, "Processing QR code image failed: ${it.message}")
|
||||
}.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
|
||||
3
client/android/src/org/amnezia/vpn/OpenVpnService.kt
Normal file
@@ -0,0 +1,3 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
class OpenVpnService : AmneziaVpnService()
|
||||
@@ -59,14 +59,14 @@ class ServiceNotification(private val context: Context) {
|
||||
formatSpeedString(rxString, txString)
|
||||
}
|
||||
|
||||
fun buildNotification(serverName: String?, state: ProtocolState): Notification {
|
||||
fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification {
|
||||
val speedString = if (state == CONNECTED) zeroSpeed else null
|
||||
|
||||
Log.d(TAG, "Build notification: $serverName, $state")
|
||||
|
||||
return notificationBuilder
|
||||
.setSmallIcon(R.drawable.ic_amnezia_round)
|
||||
.setContentTitle(serverName ?: "AmneziaVPN")
|
||||
.setContentTitle((serverName ?: "AmneziaVPN") + (protocol?.let { " $it" } ?: ""))
|
||||
.setContentText(context.getString(state))
|
||||
.setSubText(speedString)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
@@ -96,10 +96,10 @@ class ServiceNotification(private val context: Context) {
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun updateNotification(serverName: String?, state: ProtocolState) {
|
||||
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
|
||||
if (context.isNotificationPermissionGranted()) {
|
||||
Log.d(TAG, "Update notification: $serverName, $state")
|
||||
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, state))
|
||||
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class ServiceNotification(private val context: Context) {
|
||||
context,
|
||||
DISCONNECT_REQUEST_CODE,
|
||||
Intent(ACTION_DISCONNECT).apply {
|
||||
setPackage("org.amnezia.vpn")
|
||||
setPackage(context.packageName)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
@@ -135,10 +135,12 @@ class ServiceNotification(private val context: Context) {
|
||||
DISCONNECTED -> {
|
||||
Action(
|
||||
0, context.getString(R.string.connect),
|
||||
createServicePendingIntent(
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
CONNECT_REQUEST_CODE,
|
||||
Intent(context, AmneziaVpnService::class.java),
|
||||
Intent(ACTION_CONNECT).apply {
|
||||
setPackage(context.packageName)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
@@ -148,13 +150,6 @@ class ServiceNotification(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val createServicePendingIntent: (Context, Int, Intent, Int) -> PendingIntent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PendingIntent::getForegroundService
|
||||
} else {
|
||||
PendingIntent::getService
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createNotificationChannel(context: Context) {
|
||||
with(NotificationManagerCompat.from(context)) {
|
||||
|
||||
75
client/android/src/org/amnezia/vpn/VpnProto.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.awg.Awg
|
||||
import org.amnezia.vpn.protocol.cloak.Cloak
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpn
|
||||
import org.amnezia.vpn.protocol.wireguard.Wireguard
|
||||
import org.amnezia.vpn.protocol.xray.Xray
|
||||
|
||||
enum class VpnProto(
|
||||
val label: String,
|
||||
val processName: String,
|
||||
val serviceClass: Class<out AmneziaVpnService>
|
||||
) {
|
||||
WIREGUARD(
|
||||
"WireGuard",
|
||||
"org.amnezia.vpn:amneziaAwgService",
|
||||
AwgService::class.java
|
||||
) {
|
||||
override fun createProtocol(): Protocol = Wireguard()
|
||||
},
|
||||
|
||||
AWG(
|
||||
"AmneziaWG",
|
||||
"org.amnezia.vpn:amneziaAwgService",
|
||||
AwgService::class.java
|
||||
) {
|
||||
override fun createProtocol(): Protocol = Awg()
|
||||
},
|
||||
|
||||
OPENVPN(
|
||||
"OpenVPN",
|
||||
"org.amnezia.vpn:amneziaOpenVpnService",
|
||||
OpenVpnService::class.java
|
||||
) {
|
||||
override fun createProtocol(): Protocol = OpenVpn()
|
||||
},
|
||||
|
||||
CLOAK(
|
||||
"Cloak",
|
||||
"org.amnezia.vpn:amneziaOpenVpnService",
|
||||
OpenVpnService::class.java
|
||||
) {
|
||||
override fun createProtocol(): Protocol = Cloak()
|
||||
},
|
||||
|
||||
XRAY(
|
||||
"XRay",
|
||||
"org.amnezia.vpn:amneziaXrayService",
|
||||
XrayService::class.java
|
||||
) {
|
||||
override fun createProtocol(): Protocol = Xray.instance
|
||||
},
|
||||
|
||||
SSXRAY(
|
||||
"SSXRay",
|
||||
"org.amnezia.vpn:amneziaXrayService",
|
||||
XrayService::class.java
|
||||
) {
|
||||
override fun createProtocol(): Protocol = Xray.instance
|
||||
};
|
||||
|
||||
private var _protocol: Protocol? = null
|
||||
val protocol: Protocol
|
||||
get() {
|
||||
if (_protocol == null) _protocol = createProtocol()
|
||||
return _protocol ?: throw AssertionError("Set to null by another thread")
|
||||
}
|
||||
|
||||
protected abstract fun createProtocol(): Protocol
|
||||
|
||||
companion object {
|
||||
fun get(protocolName: String): VpnProto = VpnProto.valueOf(protocolName.uppercase())
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
@@ -18,9 +19,11 @@ import androidx.core.content.getSystemService
|
||||
import org.amnezia.vpn.util.Log
|
||||
|
||||
private const val TAG = "VpnRequestActivity"
|
||||
const val EXTRA_PROTOCOL = "PROTOCOL"
|
||||
|
||||
class VpnRequestActivity : ComponentActivity() {
|
||||
|
||||
private var vpnProto: VpnProto? = null
|
||||
private var userPresentReceiver: BroadcastReceiver? = null
|
||||
private val requestLauncher =
|
||||
registerForActivityResult(StartActivityForResult(), ::checkRequestResult)
|
||||
@@ -28,6 +31,12 @@ class VpnRequestActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "Start request activity")
|
||||
vpnProto = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.extras?.getSerializable(EXTRA_PROTOCOL, VpnProto::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.extras?.getSerializable(EXTRA_PROTOCOL) as VpnProto
|
||||
}
|
||||
val requestIntent = VpnService.prepare(applicationContext)
|
||||
if (requestIntent != null) {
|
||||
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
|
||||
@@ -66,10 +75,18 @@ class VpnRequestActivity : ComponentActivity() {
|
||||
|
||||
private fun onPermissionGranted() {
|
||||
Toast.makeText(this, resources.getString(R.string.vpnGranted), Toast.LENGTH_LONG).show()
|
||||
Intent(applicationContext, AmneziaVpnService::class.java).apply {
|
||||
putExtra(AFTER_PERMISSION_CHECK, true)
|
||||
}.also {
|
||||
ContextCompat.startForegroundService(this, it)
|
||||
vpnProto?.let { proto ->
|
||||
Intent(applicationContext, proto.serviceClass).apply {
|
||||
putExtra(AFTER_PERMISSION_CHECK, true)
|
||||
}.also {
|
||||
ContextCompat.startForegroundService(this, it)
|
||||
}
|
||||
} ?: run {
|
||||
Intent(this, AmneziaActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}.also {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Application
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.MultiProcessDataStoreFactory
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.Serializable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.util.Log
|
||||
@@ -21,13 +24,14 @@ import org.amnezia.vpn.util.Log
|
||||
private const val TAG = "VpnState"
|
||||
private const val STORE_FILE_NAME = "vpnState"
|
||||
|
||||
@Serializable
|
||||
data class VpnState(
|
||||
val protocolState: ProtocolState,
|
||||
val serverName: String? = null,
|
||||
val serverIndex: Int = -1
|
||||
) : Serializable {
|
||||
val serverIndex: Int = -1,
|
||||
val vpnProto: VpnProto? = null
|
||||
) {
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = -1760654961004181606
|
||||
val defaultState: VpnState = VpnState(DISCONNECTED)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +41,11 @@ object VpnStateStore {
|
||||
|
||||
private val dataStore = MultiProcessDataStoreFactory.create(
|
||||
serializer = VpnStateSerializer(),
|
||||
produceFile = { app.dataStoreFile(STORE_FILE_NAME) }
|
||||
produceFile = { app.dataStoreFile(STORE_FILE_NAME) },
|
||||
corruptionHandler = ReplaceFileCorruptionHandler { e ->
|
||||
Log.e(TAG, "VpnState DataStore corrupted: $e")
|
||||
VpnState.defaultState
|
||||
}
|
||||
)
|
||||
|
||||
fun init(app: Application) {
|
||||
@@ -45,36 +53,36 @@ object VpnStateStore {
|
||||
this.app = app
|
||||
}
|
||||
|
||||
fun dataFlow(): Flow<VpnState> = dataStore.data
|
||||
fun dataFlow(): Flow<VpnState> = dataStore.data.catch { e ->
|
||||
Log.e(TAG, "Failed to read VpnState from store: ${e.message}")
|
||||
emit(VpnState.defaultState)
|
||||
}
|
||||
|
||||
suspend fun getVpnState(): VpnState = dataFlow().firstOrNull() ?: VpnState.defaultState
|
||||
|
||||
suspend fun store(f: (vpnState: VpnState) -> VpnState) {
|
||||
try {
|
||||
dataStore.updateData(f)
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to store VpnState: $e")
|
||||
Log.w(TAG, "Remove DataStore file")
|
||||
app.dataStoreFile(STORE_FILE_NAME).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private class VpnStateSerializer : Serializer<VpnState> {
|
||||
override val defaultValue: VpnState = VpnState.defaultState
|
||||
|
||||
override suspend fun readFrom(input: InputStream): VpnState {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bios = ByteArrayInputStream(input.readBytes())
|
||||
ObjectInputStream(bios).use {
|
||||
it.readObject() as VpnState
|
||||
}
|
||||
}
|
||||
override suspend fun readFrom(input: InputStream): VpnState = try {
|
||||
ProtoBuf.decodeFromByteArray<VpnState>(input.readBytes())
|
||||
} catch (e: SerializationException) {
|
||||
Log.e(TAG, "Failed to deserialize data: $e")
|
||||
throw CorruptionException("Failed to deserialize data", e)
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: VpnState, output: OutputStream) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val baos = ByteArrayOutputStream()
|
||||
ObjectOutputStream(baos).use {
|
||||
it.writeObject(t)
|
||||
}
|
||||
output.write(baos.toByteArray())
|
||||
}
|
||||
}
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun writeTo(t: VpnState, output: OutputStream) =
|
||||
output.write(ProtoBuf.encodeToByteArray(t))
|
||||
}
|
||||
|
||||
3
client/android/src/org/amnezia/vpn/XrayService.kt
Normal file
@@ -0,0 +1,3 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
class XrayService : AmneziaVpnService()
|
||||
@@ -88,16 +88,24 @@ class NetworkState(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Failed to bind network listener: $e")
|
||||
// Android 11 bug: https://issuetracker.google.com/issues/175055271
|
||||
if (e.message?.startsWith("Package android does not belong to") == true) {
|
||||
delay(1000)
|
||||
val numberAttempts = 3
|
||||
var attemptCount = 0
|
||||
while(true) {
|
||||
try {
|
||||
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
|
||||
} else {
|
||||
throw e
|
||||
break
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Failed to bind network listener: $e")
|
||||
// Android 11 bug: https://issuetracker.google.com/issues/175055271
|
||||
if (e.message?.startsWith("Package android does not belong to") == true) {
|
||||
if (++attemptCount > numberAttempts) {
|
||||
throw e
|
||||
}
|
||||
delay(1000)
|
||||
continue
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package org.amnezia.vpn.protocol.wireguard
|
||||
|
||||
import android.content.Context
|
||||
import android.net.VpnService.Builder
|
||||
import java.util.TreeMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.amnezia.awg.GoBackend
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
@@ -78,9 +75,8 @@ open class Wireguard : Protocol() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun initialize(context: Context, state: MutableStateFlow<ProtocolState>, onError: (String) -> Unit) {
|
||||
super.initialize(context, state, onError)
|
||||
loadSharedLibrary(context, "wg-go")
|
||||
override fun internalInit() {
|
||||
if (!isInitialized) loadSharedLibrary(context, "wg-go")
|
||||
}
|
||||
|
||||
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
|
||||
19
client/android/xray/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.protocol.xray"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
compileOnly(project(":protocolApi"))
|
||||
implementation(project(":xray:libXray"))
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
}
|
||||
6
client/android/xray/libXray/build.gradle.kts
Normal file
@@ -0,0 +1,6 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
configurations {
|
||||
maybeCreate("default")
|
||||
}
|
||||
artifacts.add("default", file("libxray.aar"))
|
||||
239
client/android/xray/src/main/kotlin/Xray.kt
Normal file
@@ -0,0 +1,239 @@
|
||||
package org.amnezia.vpn.protocol.xray
|
||||
|
||||
import android.content.Context
|
||||
import android.net.VpnService.Builder
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import go.Seq
|
||||
import org.amnezia.vpn.protocol.BadConfigException
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
import org.amnezia.vpn.protocol.xray.libXray.DialerController
|
||||
import org.amnezia.vpn.protocol.xray.libXray.LibXray
|
||||
import org.amnezia.vpn.protocol.xray.libXray.Logger
|
||||
import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
import org.amnezia.vpn.util.net.parseInetAddress
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Config example:
|
||||
* {
|
||||
* "appSplitTunnelType": 0,
|
||||
* "config_version": 0,
|
||||
* "description": "Server 1",
|
||||
* "dns1": "1.1.1.1",
|
||||
* "dns2": "1.0.0.1",
|
||||
* "hostName": "100.100.100.0",
|
||||
* "protocol": "xray",
|
||||
* "splitTunnelApps": [],
|
||||
* "splitTunnelSites": [],
|
||||
* "splitTunnelType": 0,
|
||||
* "xray_config_data": {
|
||||
* "inbounds": [
|
||||
* {
|
||||
* "listen": "127.0.0.1",
|
||||
* "port": 8080,
|
||||
* "protocol": "socks",
|
||||
* "settings": {
|
||||
* "udp": true
|
||||
* }
|
||||
* }
|
||||
* ],
|
||||
* "log": {
|
||||
* "loglevel": "error"
|
||||
* },
|
||||
* "outbounds": [
|
||||
* {
|
||||
* "protocol": "vless",
|
||||
* "settings": {
|
||||
* "vnext": [
|
||||
* {
|
||||
* "address": "100.100.100.0",
|
||||
* "port": 443,
|
||||
* "users": [
|
||||
* {
|
||||
* "encryption": "none",
|
||||
* "flow": "xtls-rprx-vision",
|
||||
* "id": "id"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* "streamSettings": {
|
||||
* "network": "tcp",
|
||||
* "realitySettings": {
|
||||
* "fingerprint": "chrome",
|
||||
* "publicKey": "publicKey",
|
||||
* "serverName": "google.com",
|
||||
* "shortId": "id",
|
||||
* "spiderX": ""
|
||||
* },
|
||||
* "security": "reality"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
private const val TAG = "Xray"
|
||||
private const val LIBXRAY_TAG = "libXray"
|
||||
|
||||
class Xray : Protocol() {
|
||||
|
||||
private var isRunning: Boolean = false
|
||||
override val statistics: Statistics = Statistics.EMPTY_STATISTICS
|
||||
|
||||
override fun internalInit() {
|
||||
Seq.setContext(context)
|
||||
if (!isInitialized) {
|
||||
LibXray.initLogger(object : Logger {
|
||||
override fun warning(s: String) = Log.w(LIBXRAY_TAG, s)
|
||||
|
||||
override fun error(s: String) = Log.e(LIBXRAY_TAG, s)
|
||||
|
||||
override fun write(msg: ByteArray): Long {
|
||||
Log.w(LIBXRAY_TAG, String(msg))
|
||||
return msg.size.toLong()
|
||||
}
|
||||
}).isNotNullOrBlank { err ->
|
||||
Log.w(TAG, "Failed to initialize logger: $err")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
if (isRunning) {
|
||||
Log.w(TAG, "XRay already running")
|
||||
return
|
||||
}
|
||||
|
||||
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
||||
?: config.optJSONObject("ssxray_config_data")
|
||||
?: throw BadConfigException("config_data not found")
|
||||
val xrayConfig = parseConfig(config, xrayJsonConfig)
|
||||
|
||||
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
||||
.put("loglevel", "warning")
|
||||
.put("access", "none") // disable access log
|
||||
|
||||
start(xrayConfig, xrayJsonConfig.toString(), vpnBuilder, protect)
|
||||
state.value = CONNECTED
|
||||
isRunning = true
|
||||
}
|
||||
|
||||
private fun parseConfig(config: JSONObject, xrayJsonConfig: JSONObject): XrayConfig {
|
||||
return XrayConfig.build {
|
||||
addAddress(XrayConfig.DEFAULT_IPV4_ADDRESS)
|
||||
|
||||
config.optString("dns1").let {
|
||||
if (it.isNotBlank()) addDnsServer(parseInetAddress(it))
|
||||
}
|
||||
|
||||
config.optString("dns2").let {
|
||||
if (it.isNotBlank()) addDnsServer(parseInetAddress(it))
|
||||
}
|
||||
|
||||
addRoute(InetNetwork("0.0.0.0", 0))
|
||||
addRoute(InetNetwork("2000::0", 3))
|
||||
config.getString("hostName").let {
|
||||
excludeRoute(InetNetwork(it, 32))
|
||||
}
|
||||
|
||||
config.optString("mtu").let {
|
||||
if (it.isNotBlank()) setMtu(it.toInt())
|
||||
}
|
||||
|
||||
val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
|
||||
socksConfig.getInt("port").let { setSocksPort(it) }
|
||||
|
||||
configSplitTunneling(config)
|
||||
configAppSplitTunneling(config)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start(config: XrayConfig, configJson: String, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
buildVpnInterface(config, vpnBuilder)
|
||||
|
||||
DialerController { protect(it.toInt()) }.also {
|
||||
LibXray.registerDialerController(it).isNotNullOrBlank { err ->
|
||||
throw VpnStartException("Failed to register dialer controller: $err")
|
||||
}
|
||||
LibXray.registerListenerController(it).isNotNullOrBlank { err ->
|
||||
throw VpnStartException("Failed to register listener controller: $err")
|
||||
}
|
||||
}
|
||||
|
||||
vpnBuilder.establish().use { tunFd ->
|
||||
if (tunFd == null) {
|
||||
throw VpnStartException("Create VPN interface: permission not granted or revoked")
|
||||
}
|
||||
Log.d(TAG, "Run tun2Socks")
|
||||
runTun2Socks(config, tunFd.detachFd())
|
||||
|
||||
Log.d(TAG, "Run XRay")
|
||||
Log.i(TAG, "xray ${LibXray.xrayVersion()}")
|
||||
val assetsPath = context.getDir("assets", Context.MODE_PRIVATE).absolutePath
|
||||
LibXray.initXray(assetsPath)
|
||||
val geoDir = File(assetsPath, "geo").absolutePath
|
||||
val configPath = File(context.cacheDir, "config.json")
|
||||
Log.d(TAG, "xray.location.asset: $geoDir")
|
||||
Log.d(TAG, "config: $configPath")
|
||||
try {
|
||||
configPath.writeText(configJson)
|
||||
} catch (e: IOException) {
|
||||
LibXray.stopTun2Socks()
|
||||
throw VpnStartException("Failed to write xray config: ${e.message}")
|
||||
}
|
||||
LibXray.runXray(geoDir, configPath.absolutePath, config.maxMemory).isNotNullOrBlank { err ->
|
||||
LibXray.stopTun2Socks()
|
||||
throw VpnStartException("Failed to start xray: $err")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopVpn() {
|
||||
LibXray.stopXray().isNotNullOrBlank { err ->
|
||||
Log.e(TAG, "Failed to stop XRay: $err")
|
||||
}
|
||||
LibXray.stopTun2Socks().isNotNullOrBlank { err ->
|
||||
Log.e(TAG, "Failed to stop tun2Socks: $err")
|
||||
}
|
||||
|
||||
isRunning = false
|
||||
state.value = DISCONNECTED
|
||||
}
|
||||
|
||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
||||
state.value = CONNECTED
|
||||
}
|
||||
|
||||
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
||||
val tun2SocksConfig = Tun2SocksConfig().apply {
|
||||
mtu = config.mtu.toLong()
|
||||
proxy = "socks5://127.0.0.1:${config.socksPort}"
|
||||
device = "fd://$fd"
|
||||
logLevel = "warning"
|
||||
}
|
||||
LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err ->
|
||||
throw VpnStartException("Failed to start tun2socks: $err")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: Xray by lazy { Xray() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.isNotNullOrBlank(block: (String) -> Unit) {
|
||||
if (!this.isNullOrBlank()) {
|
||||
block(this)
|
||||
}
|
||||
}
|
||||
42
client/android/xray/src/main/kotlin/XrayConfig.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package org.amnezia.vpn.protocol.xray
|
||||
|
||||
import org.amnezia.vpn.protocol.ProtocolConfig
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
|
||||
private const val XRAY_DEFAULT_MTU = 1500
|
||||
private const val XRAY_DEFAULT_MAX_MEMORY: Long = 50 shl 20 // 50 MB
|
||||
|
||||
class XrayConfig protected constructor(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val socksPort: Int,
|
||||
val maxMemory: Long,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
builder,
|
||||
builder.socksPort,
|
||||
builder.maxMemory
|
||||
)
|
||||
|
||||
class Builder : ProtocolConfig.Builder(false) {
|
||||
internal var socksPort: Int = 0
|
||||
private set
|
||||
|
||||
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
||||
private set
|
||||
|
||||
override var mtu: Int = XRAY_DEFAULT_MTU
|
||||
|
||||
fun setSocksPort(port: Int) = apply { socksPort = port }
|
||||
|
||||
fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
|
||||
|
||||
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal val DEFAULT_IPV4_ADDRESS: InetNetwork = InetNetwork("10.0.42.2", 30)
|
||||
|
||||
inline fun build(block: Builder.() -> Unit): XrayConfig = Builder().apply(block).build()
|
||||
}
|
||||
}
|
||||
@@ -52,3 +52,6 @@ foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
|
||||
)
|
||||
endforeach()
|
||||
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
|
||||
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
|
||||
@@ -119,18 +119,21 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
|
||||
|
||||
if (!m_settings->isSitesSplitTunnelingEnabled()) {
|
||||
config.append("\nredirect-gateway def1 ipv6 bypass-dhcp\n");
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
// Prevent ipv6 leak
|
||||
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
|
||||
#endif
|
||||
config.append("block-ipv6\n");
|
||||
} else if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
|
||||
|
||||
// no redirect-gateway
|
||||
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
||||
#ifndef Q_OS_ANDROID
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n");
|
||||
#endif
|
||||
// Prevent ipv6 leak
|
||||
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
|
||||
#endif
|
||||
config.append("block-ipv6\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +63,14 @@ QVector<amnezia::Proto> ContainerProps::protocolsForContainer(amnezia::DockerCon
|
||||
|
||||
case DockerContainer::Xray: return { Proto::Xray };
|
||||
|
||||
case DockerContainer::SSXray: return { Proto::SSXray };
|
||||
|
||||
case DockerContainer::Dns: return { Proto::Dns };
|
||||
|
||||
case DockerContainer::Sftp: return { Proto::Sftp };
|
||||
|
||||
case DockerContainer::Socks5Proxy: return { Proto::Socks5Proxy };
|
||||
|
||||
default: return { defaultProtocol(container) };
|
||||
}
|
||||
}
|
||||
@@ -86,16 +90,18 @@ QMap<DockerContainer, QString> ContainerProps::containerHumanNames()
|
||||
{
|
||||
return { { DockerContainer::None, "Not installed" },
|
||||
{ DockerContainer::OpenVpn, "OpenVPN" },
|
||||
{ DockerContainer::ShadowSocks, "ShadowSocks" },
|
||||
{ DockerContainer::ShadowSocks, "OpenVPN over SS" },
|
||||
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
|
||||
{ DockerContainer::WireGuard, "WireGuard" },
|
||||
{ DockerContainer::Awg, "AmneziaWG" },
|
||||
{ DockerContainer::Xray, "XRay" },
|
||||
{ DockerContainer::Ipsec, QObject::tr("IPsec") },
|
||||
{ DockerContainer::SSXray, "Shadowsocks"},
|
||||
|
||||
{ DockerContainer::TorWebSite, QObject::tr("Website in Tor network") },
|
||||
{ DockerContainer::Dns, QObject::tr("Amnezia DNS") },
|
||||
{ DockerContainer::Sftp, QObject::tr("Sftp file sharing service") } };
|
||||
{ DockerContainer::Dns, QObject::tr("AmneziaDNS") },
|
||||
{ DockerContainer::Sftp, QObject::tr("SFTP file sharing service") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") } };
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
|
||||
@@ -104,7 +110,7 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
|
||||
QObject::tr("OpenVPN is the most popular VPN protocol, with flexible configuration options. It uses its "
|
||||
"own security protocol with SSL/TLS for key exchange.") },
|
||||
{ DockerContainer::ShadowSocks,
|
||||
QObject::tr("ShadowSocks - masks VPN traffic, making it similar to normal web traffic, but it "
|
||||
QObject::tr("Shadowsocks - masks VPN traffic, making it similar to normal web traffic, but it "
|
||||
"may be recognized by analysis systems in some highly censored regions.") },
|
||||
{ DockerContainer::Cloak,
|
||||
QObject::tr("OpenVPN over Cloak - OpenVPN with VPN masquerading as web traffic and protection against "
|
||||
@@ -121,14 +127,16 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
|
||||
QObject::tr("XRay with REALITY - Suitable for countries with the highest level of internet censorship. "
|
||||
"Traffic masking as web traffic at the TLS level, and protection against detection by active probing methods.") },
|
||||
{ DockerContainer::Ipsec,
|
||||
QObject::tr("IKEv2 - Modern stable protocol, a bit faster than others, restores connection after "
|
||||
QObject::tr("IKEv2/IPsec - Modern stable protocol, a bit faster than others, restores connection after "
|
||||
"signal loss. It has native support on the latest versions of Android and iOS.") },
|
||||
|
||||
{ DockerContainer::TorWebSite, QObject::tr("Deploy a WordPress site on the Tor network in two clicks.") },
|
||||
{ DockerContainer::Dns,
|
||||
QObject::tr("Replace the current DNS server with your own. This will increase your privacy level.") },
|
||||
{ DockerContainer::Sftp,
|
||||
QObject::tr("Create a file vault on your server to securely store and transfer files.") } };
|
||||
QObject::tr("Create a file vault on your server to securely store and transfer files.") },
|
||||
{ DockerContainer::Socks5Proxy,
|
||||
QObject::tr("") } };
|
||||
}
|
||||
|
||||
QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
|
||||
@@ -156,7 +164,6 @@ QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
|
||||
"However, certain traffic analysis systems might still detect a Shadowsocks connection. "
|
||||
"Due to limited support in Amnezia, it's recommended to use AmneziaWG protocol.\n\n"
|
||||
"* Available in the AmneziaVPN only on desktop platforms\n"
|
||||
"* Normal power consumption on mobile devices\n\n"
|
||||
"* Configurable encryption protocol\n"
|
||||
"* Detectable by some DPI systems\n"
|
||||
"* Works over TCP network protocol.") },
|
||||
@@ -237,7 +244,8 @@ QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
|
||||
QObject::tr("After installation, Amnezia will create a\n\n file storage on your server. "
|
||||
"You will be able to access it using\n FileZilla or other SFTP clients, "
|
||||
"as well as mount the disk on your device to access\n it directly from your device.\n\n"
|
||||
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") }
|
||||
"For more detailed information, you can\n find it in the support section under \"Create SFTP file storage.\" ") },
|
||||
{ DockerContainer::Socks5Proxy, QObject::tr("SOCKS5 proxy server") }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,10 +265,12 @@ Proto ContainerProps::defaultProtocol(DockerContainer c)
|
||||
case DockerContainer::Awg: return Proto::Awg;
|
||||
case DockerContainer::Xray: return Proto::Xray;
|
||||
case DockerContainer::Ipsec: return Proto::Ikev2;
|
||||
case DockerContainer::SSXray: return Proto::SSXray;
|
||||
|
||||
case DockerContainer::TorWebSite: return Proto::TorWebSite;
|
||||
case DockerContainer::Dns: return Proto::Dns;
|
||||
case DockerContainer::Sftp: return Proto::Sftp;
|
||||
case DockerContainer::Socks5Proxy: return Proto::Socks5Proxy;
|
||||
default: return Proto::Any;
|
||||
}
|
||||
}
|
||||
@@ -275,8 +285,9 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Cloak:
|
||||
return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::Cloak: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
// case DockerContainer::ShadowSocks: return true;
|
||||
default: return false;
|
||||
}
|
||||
@@ -294,6 +305,8 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::ShadowSocks: return false;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Cloak: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
@@ -363,6 +376,7 @@ bool ContainerProps::isShareable(DockerContainer container)
|
||||
case DockerContainer::TorWebSite: return false;
|
||||
case DockerContainer::Dns: return false;
|
||||
case DockerContainer::Sftp: return false;
|
||||
case DockerContainer::Socks5Proxy: return false;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -376,3 +390,18 @@ QJsonObject ContainerProps::getProtocolConfigFromContainer(const Proto protocol,
|
||||
|
||||
return QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
|
||||
}
|
||||
|
||||
int ContainerProps::installPageOrder(DockerContainer container)
|
||||
{
|
||||
switch (container) {
|
||||
case DockerContainer::OpenVpn: return 4;
|
||||
case DockerContainer::Cloak: return 5;
|
||||
case DockerContainer::ShadowSocks: return 6;
|
||||
case DockerContainer::WireGuard: return 2;
|
||||
case DockerContainer::Awg: return 1;
|
||||
case DockerContainer::Xray: return 3;
|
||||
case DockerContainer::Ipsec: return 7;
|
||||
case DockerContainer::SSXray: return 8;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ namespace amnezia
|
||||
ShadowSocks,
|
||||
Ipsec,
|
||||
Xray,
|
||||
SSXray,
|
||||
|
||||
// non-vpn
|
||||
TorWebSite,
|
||||
Dns,
|
||||
Sftp
|
||||
Sftp,
|
||||
Socks5Proxy
|
||||
};
|
||||
Q_ENUM_NS(DockerContainer)
|
||||
} // namespace ContainerEnumNS
|
||||
@@ -70,6 +72,8 @@ namespace amnezia
|
||||
static bool isShareable(amnezia::DockerContainer container);
|
||||
|
||||
static QJsonObject getProtocolConfigFromContainer(const amnezia::Proto protocol, const QJsonObject &containerConfig);
|
||||
|
||||
static int installPageOrder(amnezia::DockerContainer container);
|
||||
};
|
||||
|
||||
static void declareQmlContainerEnum()
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "amnezia_application.h"
|
||||
#include "configurators/wireguard_configurator.h"
|
||||
#include "version.h"
|
||||
#include "core/errorstrings.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -41,6 +40,28 @@ void ApiController::processApiConfig(const QString &protocol, const ApiControlle
|
||||
return;
|
||||
} else if (protocol == configKey::awg) {
|
||||
config.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey);
|
||||
auto serverConfig = QJsonDocument::fromJson(config.toUtf8()).object();
|
||||
auto containers = serverConfig.value(config_key::containers).toArray();
|
||||
if (containers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto container = containers.at(0).toObject();
|
||||
QString containerName = ContainerProps::containerTypeToString(DockerContainer::Awg);
|
||||
auto containerConfig = container.value(containerName).toObject();
|
||||
auto protocolConfig = QJsonDocument::fromJson(containerConfig.value(config_key::last_config).toString().toUtf8()).object();
|
||||
containerConfig[config_key::junkPacketCount] = protocolConfig.value(config_key::junkPacketCount);
|
||||
containerConfig[config_key::junkPacketMinSize] = protocolConfig.value(config_key::junkPacketMinSize);
|
||||
containerConfig[config_key::junkPacketMaxSize] = protocolConfig.value(config_key::junkPacketMaxSize);
|
||||
containerConfig[config_key::initPacketJunkSize] = protocolConfig.value(config_key::initPacketJunkSize);
|
||||
containerConfig[config_key::responsePacketJunkSize] = protocolConfig.value(config_key::responsePacketJunkSize);
|
||||
containerConfig[config_key::initPacketMagicHeader] = protocolConfig.value(config_key::initPacketMagicHeader);
|
||||
containerConfig[config_key::responsePacketMagicHeader] = protocolConfig.value(config_key::responsePacketMagicHeader);
|
||||
containerConfig[config_key::underloadPacketMagicHeader] = protocolConfig.value(config_key::underloadPacketMagicHeader);
|
||||
containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader);
|
||||
container[containerName] = containerConfig;
|
||||
containers.replace(0, container);
|
||||
serverConfig[config_key::containers] = containers;
|
||||
config = QString(QJsonDocument(serverConfig).toJson());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -110,7 +131,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
|
||||
QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
|
||||
if (ba.isEmpty()) {
|
||||
emit errorOccurred(errorString(ErrorCode::ApiConfigEmptyError));
|
||||
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,14 +156,14 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
|
||||
} else {
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
emit errorOccurred(errorString(ErrorCode::ApiConfigTimeoutError));
|
||||
emit errorOccurred(ErrorCode::ApiConfigTimeoutError);
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
qDebug() << QString::fromUtf8(reply->readAll());
|
||||
qDebug() << reply->error();
|
||||
qDebug() << err;
|
||||
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
emit errorOccurred(errorString(ErrorCode::ApiConfigDownloadError));
|
||||
emit errorOccurred(ErrorCode::ApiConfigDownloadError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +174,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
|
||||
[this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; });
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList<QSslError> &errors) {
|
||||
qDebug().noquote() << errors;
|
||||
emit errorOccurred(errorString(ErrorCode::ApiConfigSslError));
|
||||
emit errorOccurred(ErrorCode::ApiConfigSslError);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public slots:
|
||||
void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig);
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString &errorMessage);
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
void configUpdated(const bool updateConfig, const QJsonObject &config, const int serverIndex);
|
||||
|
||||
private:
|
||||
|
||||
@@ -106,7 +106,7 @@ ErrorCode ServerController::runContainerScript(const ServerCredentials &credenti
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
QString runner = QString("sudo docker exec -i $CONTAINER_NAME bash %1 ").arg(fileName);
|
||||
QString runner = QString("sudo docker exec -i $CONTAINER_NAME %2 %1 ").arg(fileName, (container == DockerContainer::Socks5Proxy ? "sh" : "bash"));
|
||||
e = runScript(credentials, replaceVars(runner, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
|
||||
|
||||
QString remover = QString("sudo docker exec -i $CONTAINER_NAME rm %1 ").arg(fileName);
|
||||
@@ -376,6 +376,10 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
|
||||
return true;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Socks5Proxy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -416,11 +420,16 @@ ErrorCode ServerController::prepareHostWorker(const ServerCredentials &credentia
|
||||
|
||||
ErrorCode ServerController::buildContainerWorker(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &config)
|
||||
{
|
||||
ErrorCode e = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),
|
||||
amnezia::server::getDockerfileFolder(container) + "/Dockerfile");
|
||||
QString dockerFilePath = amnezia::server::getDockerfileFolder(container) + "/Dockerfile";
|
||||
QString scriptString = QString("sudo rm %1").arg(dockerFilePath);
|
||||
ErrorCode errorCode = runScript(credentials, replaceVars(scriptString, genVarsForScript(credentials, container)));
|
||||
if (errorCode)
|
||||
return errorCode;
|
||||
|
||||
if (e)
|
||||
return e;
|
||||
errorCode = uploadFileToHost(credentials, amnezia::scriptData(ProtocolScriptType::dockerfile, container).toUtf8(),dockerFilePath);
|
||||
|
||||
if (errorCode)
|
||||
return errorCode;
|
||||
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
@@ -428,13 +437,13 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
e = runScript(credentials,
|
||||
errorCode = runScript(credentials,
|
||||
replaceVars(amnezia::scriptData(SharedScriptType::build_container), genVarsForScript(credentials, container, config)),
|
||||
cbReadStdOut);
|
||||
if (e)
|
||||
return e;
|
||||
if (errorCode)
|
||||
return errorCode;
|
||||
|
||||
return e;
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode ServerController::runContainerWorker(const ServerCredentials &credentials, DockerContainer container, QJsonObject &config)
|
||||
@@ -511,6 +520,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
|
||||
const QJsonObject &amneziaWireguarConfig = config.value(ProtocolProps::protoToString(Proto::Awg)).toObject();
|
||||
const QJsonObject &xrayConfig = config.value(ProtocolProps::protoToString(Proto::Xray)).toObject();
|
||||
const QJsonObject &sftpConfig = config.value(ProtocolProps::protoToString(Proto::Sftp)).toObject();
|
||||
const QJsonObject &socks5ProxyConfig = config.value(ProtocolProps::protoToString(Proto::Socks5Proxy)).toObject();
|
||||
|
||||
Vars vars;
|
||||
|
||||
@@ -608,6 +618,14 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
|
||||
vars.append({ { "$UNDERLOAD_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::underloadPacketMagicHeader).toString() } });
|
||||
vars.append({ { "$TRANSPORT_PACKET_MAGIC_HEADER", amneziaWireguarConfig.value(config_key::transportPacketMagicHeader).toString() } });
|
||||
|
||||
// Socks5 proxy vars
|
||||
vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } });
|
||||
auto username = socks5ProxyConfig.value(config_key:: userName).toString();
|
||||
auto password = socks5ProxyConfig.value(config_key::password).toString();
|
||||
QString socks5user = (!username.isEmpty() && !password.isEmpty()) ? QString("users %1:CL:%2").arg(username, password) : "";
|
||||
vars.append({ { "$SOCKS5_USER", socks5user } });
|
||||
vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } });
|
||||
|
||||
QString serverIp = NetworkUtilities::getIPAddress(credentials.hostName);
|
||||
if (!serverIp.isEmpty()) {
|
||||
vars.append({ { "$SERVER_IP_ADDRESS", serverIp } });
|
||||
@@ -686,6 +704,30 @@ ErrorCode ServerController::isServerPortBusy(const ServerCredentials &credential
|
||||
for (auto &port : fixedPorts) {
|
||||
script = script.append("|:%1").arg(port);
|
||||
}
|
||||
|
||||
if (transportProto == "tcpandudp") {
|
||||
QString tcpProtoScript = script;
|
||||
QString udpProtoScript = script;
|
||||
tcpProtoScript.append("' | grep -i tcp");
|
||||
udpProtoScript.append("' | grep -i udp");
|
||||
tcpProtoScript.append(" | grep LISTEN");
|
||||
|
||||
ErrorCode errorCode = runScript(credentials, replaceVars(tcpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
errorCode = runScript(credentials, replaceVars(udpProtoScript, genVarsForScript(credentials, container)), cbReadStdOut, cbReadStdErr);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (!stdOut.isEmpty()) {
|
||||
return ErrorCode::ServerPortAlreadyAllocatedError;
|
||||
}
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
script = script.append("' | grep -i %1").arg(transportProto);
|
||||
|
||||
if (transportProto == "tcp") {
|
||||
|
||||
@@ -24,6 +24,7 @@ QScopedPointer<ConfiguratorBase> VpnConfigurationsController::createConfigurator
|
||||
case Proto::Awg: return QScopedPointer<ConfiguratorBase>(new AwgConfigurator(m_settings, m_serverController));
|
||||
case Proto::Ikev2: return QScopedPointer<ConfiguratorBase>(new Ikev2Configurator(m_settings, m_serverController));
|
||||
case Proto::Xray: return QScopedPointer<ConfiguratorBase>(new XrayConfigurator(m_settings, m_serverController));
|
||||
case Proto::SSXray: return QScopedPointer<ConfiguratorBase>(new XrayConfigurator(m_settings, m_serverController));
|
||||
default: return QScopedPointer<ConfiguratorBase>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ namespace amnezia
|
||||
}
|
||||
};
|
||||
|
||||
enum ErrorCode {
|
||||
namespace error_code_ns
|
||||
{
|
||||
Q_NAMESPACE
|
||||
// TODO: change to enum class
|
||||
enum ErrorCode {
|
||||
// General error codes
|
||||
NoError = 0,
|
||||
UnknownError = 100,
|
||||
@@ -75,7 +79,7 @@ namespace amnezia
|
||||
AmneziaServiceConnectionFailed = 603,
|
||||
ExecutableMissing = 604,
|
||||
XrayExecutableMissing = 605,
|
||||
Tun2SockExecutableMissing = 606,
|
||||
Tun2SockExecutableMissing = 606,
|
||||
|
||||
// VPN errors
|
||||
OpenVpnAdaptersInUseError = 700,
|
||||
@@ -110,7 +114,11 @@ namespace amnezia
|
||||
UnspecifiedError = 1203,
|
||||
FatalError = 1204,
|
||||
AbortError = 1205
|
||||
};
|
||||
};
|
||||
Q_ENUM_NS(ErrorCode)
|
||||
}
|
||||
|
||||
using ErrorCode = error_code_ns::ErrorCode;
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
|
||||
@@ -8,68 +8,68 @@ QString errorString(ErrorCode code) {
|
||||
switch (code) {
|
||||
|
||||
// General error codes
|
||||
case(NoError): errorMessage = QObject::tr("No error"); break;
|
||||
case(UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
|
||||
case(NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
|
||||
case(AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break;
|
||||
case(ErrorCode::NoError): errorMessage = QObject::tr("No error"); break;
|
||||
case(ErrorCode::UnknownError): errorMessage = QObject::tr("Unknown error"); break;
|
||||
case(ErrorCode::NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
|
||||
case(ErrorCode::AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break;
|
||||
|
||||
// Server errors
|
||||
case(ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
|
||||
case(ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
|
||||
case(ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
|
||||
case(ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
|
||||
case(ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
|
||||
case(ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
|
||||
case(ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
|
||||
case(ErrorCode::ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
|
||||
case(ErrorCode::ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
|
||||
case(ErrorCode::ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
|
||||
case(ErrorCode::ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
|
||||
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
|
||||
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
|
||||
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
|
||||
|
||||
// Libssh errors
|
||||
case(SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
|
||||
case(SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
|
||||
case(SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
|
||||
case(SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
|
||||
case(SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
|
||||
case(SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
|
||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||
case(ErrorCode::SshInterruptedError): errorMessage = QObject::tr("SSH request was interrupted"); break;
|
||||
case(ErrorCode::SshInternalError): errorMessage = QObject::tr("SSH internal error"); break;
|
||||
case(ErrorCode::SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
|
||||
case(ErrorCode::SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
|
||||
case(ErrorCode::SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
|
||||
|
||||
// Ssh scp errors
|
||||
case(SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
|
||||
case(ErrorCode::SshScpFailureError): errorMessage = QObject::tr("SCP error: Generic failure"); break;
|
||||
|
||||
// Local errors
|
||||
case (OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
|
||||
case (OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
|
||||
case (ErrorCode::OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
|
||||
case (ErrorCode::OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
|
||||
|
||||
// Distro errors
|
||||
case (OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
|
||||
case (ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
|
||||
case (CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
|
||||
case (AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
|
||||
case (OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
|
||||
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
|
||||
case (ErrorCode::ShadowSocksExecutableMissing): errorMessage = QObject::tr("Shadowsocks (ss-local) executable missing"); break;
|
||||
case (ErrorCode::CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
|
||||
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
|
||||
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
|
||||
|
||||
// VPN errors
|
||||
case (OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
|
||||
case (OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
|
||||
case (AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
|
||||
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
|
||||
case (ErrorCode::OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
|
||||
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
|
||||
|
||||
case (ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
|
||||
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
|
||||
|
||||
// Android errors
|
||||
case (AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
|
||||
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
|
||||
|
||||
// Api errors
|
||||
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
|
||||
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
|
||||
case (ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break;
|
||||
case (ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
|
||||
case (ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
|
||||
|
||||
case (ErrorCode::ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
|
||||
case (ErrorCode::ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
|
||||
case (ErrorCode::ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break;
|
||||
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
|
||||
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
|
||||
|
||||
// QFile errors
|
||||
case(OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
case(ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
|
||||
case(PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
|
||||
case(UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
|
||||
case(FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
|
||||
case(AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
|
||||
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;
|
||||
case(ErrorCode::PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
|
||||
case(ErrorCode::UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
|
||||
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
|
||||
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
|
||||
|
||||
case(InternalError):
|
||||
case(ErrorCode::InternalError):
|
||||
default:
|
||||
errorMessage = QObject::tr("Internal error"); break;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
||||
case DockerContainer::TorWebSite: return QLatin1String("website_tor");
|
||||
case DockerContainer::Dns: return QLatin1String("dns");
|
||||
case DockerContainer::Sftp: return QLatin1String("sftp");
|
||||
case DockerContainer::Socks5Proxy: return QLatin1String("socks5_proxy");
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
|
||||
38
client/core/serialization/inbound.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "transfer.h"
|
||||
#include "serialization.h"
|
||||
|
||||
namespace amnezia::serialization::inbounds
|
||||
{
|
||||
|
||||
//"inbounds": [
|
||||
// {
|
||||
// "listen": "127.0.0.1",
|
||||
// "port": 10808,
|
||||
// "protocol": "socks",
|
||||
// "settings": {
|
||||
// "udp": true
|
||||
// }
|
||||
// }
|
||||
//],
|
||||
|
||||
const static QString listen = "127.0.0.1";
|
||||
const static int port = 10808;
|
||||
const static QString protocol = "socks";
|
||||
|
||||
QJsonObject GenerateInboundEntry()
|
||||
{
|
||||
QJsonObject root;
|
||||
QJsonIO::SetValue(root, listen, "listen");
|
||||
QJsonIO::SetValue(root, port, "port");
|
||||
QJsonIO::SetValue(root, protocol, "protocol");
|
||||
QJsonIO::SetValue(root, true, "settings", "udp");
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
} // namespace amnezia::serialization::inbounds
|
||||
|
||||
122
client/core/serialization/outbound.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "transfer.h"
|
||||
#include "serialization.h"
|
||||
|
||||
namespace amnezia::serialization::outbounds
|
||||
{
|
||||
QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect)
|
||||
{
|
||||
QJsonObject root;
|
||||
JADD(domainStrategy, redirect)
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateBlackHoleOUT(bool useHTTP)
|
||||
{
|
||||
QJsonObject root;
|
||||
QJsonObject resp;
|
||||
resp.insert("type", useHTTP ? "http" : "none");
|
||||
root.insert("response", resp);
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password)
|
||||
{
|
||||
QJsonObject root;
|
||||
JADD(address, port, method, password)
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateShadowSocksOUT(const QList<ShadowSocksServerObject> &_servers)
|
||||
{
|
||||
QJsonObject root;
|
||||
QJsonArray x;
|
||||
|
||||
for (const auto &server : _servers)
|
||||
{
|
||||
x.append(GenerateShadowSocksServerOUT(server.address, server.port, server.method, server.password));
|
||||
}
|
||||
|
||||
root.insert("servers", x);
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateHTTPSOCKSOut(const QString &addr, int port, bool useAuth, const QString &username, const QString &password)
|
||||
{
|
||||
QJsonObject root;
|
||||
QJsonIO::SetValue(root, addr, "servers", 0, "address");
|
||||
QJsonIO::SetValue(root, port, "servers", 0, "port");
|
||||
if (useAuth)
|
||||
{
|
||||
QJsonIO::SetValue(root, username, "servers", 0, "users", 0, "user");
|
||||
QJsonIO::SetValue(root, password, "servers", 0, "users", 0, "pass");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateOutboundEntry(const QString &tag, const QString &protocol, const QJsonObject &settings, const QJsonObject &streamSettings,
|
||||
const QJsonObject &mux, const QString &sendThrough)
|
||||
{
|
||||
QJsonObject root;
|
||||
JADD(sendThrough, protocol, settings, tag, streamSettings, mux)
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateTrojanOUT(const QList<TrojanObject> &_servers)
|
||||
{
|
||||
QJsonObject root;
|
||||
QJsonArray x;
|
||||
|
||||
for (const auto &server : _servers)
|
||||
{
|
||||
x.append(GenerateTrojanServerOUT(server.address, server.port, server.password));
|
||||
}
|
||||
|
||||
root.insert("servers", x);
|
||||
return root;
|
||||
}
|
||||
|
||||
QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password)
|
||||
{
|
||||
QJsonObject root;
|
||||
JADD(address, port, password)
|
||||
return root;
|
||||
}
|
||||
|
||||
} // namespace amnezia::serialization::outbounds
|
||||
|
||||
66
client/core/serialization/serialization.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef SERIALIZATION_H
|
||||
#define SERIALIZATION_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include "transfer.h"
|
||||
|
||||
namespace amnezia::serialization
|
||||
{
|
||||
namespace vmess
|
||||
{
|
||||
QJsonObject Deserialize(const QString &vmess, QString *alias, QString *errMessage);
|
||||
const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias);
|
||||
} // namespace vmess
|
||||
|
||||
namespace vmess_new
|
||||
{
|
||||
QJsonObject Deserialize(const QString &vmess, QString *alias, QString *errMessage);
|
||||
const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias);
|
||||
} // namespace vmess_new
|
||||
|
||||
namespace vless
|
||||
{
|
||||
QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage);
|
||||
} // namespace vless
|
||||
|
||||
namespace ss
|
||||
{
|
||||
QJsonObject Deserialize(const QString &ss, QString *alias, QString *errMessage);
|
||||
const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool isSip002);
|
||||
} // namespace ss
|
||||
|
||||
namespace ssd
|
||||
{
|
||||
QList<std::pair<QString, QJsonObject>> Deserialize(const QString &uri, QString *groupName, QStringList *logList);
|
||||
} // namespace ssd
|
||||
|
||||
namespace trojan
|
||||
{
|
||||
QJsonObject Deserialize(const QString &trojan, QString *alias, QString *errMessage);
|
||||
const QString Serialize(const TrojanObject &server, const QString &alias);
|
||||
} // namespace trojan
|
||||
|
||||
namespace outbounds
|
||||
{
|
||||
QJsonObject GenerateFreedomOUT(const QString &domainStrategy, const QString &redirect);
|
||||
QJsonObject GenerateBlackHoleOUT(bool useHTTP);
|
||||
QJsonObject GenerateShadowSocksOUT(const QList<ShadowSocksServerObject> &servers);
|
||||
QJsonObject GenerateShadowSocksServerOUT(const QString &address, int port, const QString &method, const QString &password);
|
||||
QJsonObject GenerateHTTPSOCKSOut(const QString &address, int port, bool useAuth, const QString &username, const QString &password);
|
||||
QJsonObject GenerateTrojanOUT(const QList<TrojanObject> &servers);
|
||||
QJsonObject GenerateTrojanServerOUT(const QString &address, int port, const QString &password);
|
||||
QJsonObject GenerateOutboundEntry(const QString &tag, //
|
||||
const QString &protocol, //
|
||||
const QJsonObject &settings, //
|
||||
const QJsonObject &streamSettings, //
|
||||
const QJsonObject &mux = {}, //
|
||||
const QString &sendThrough = "0.0.0.0");
|
||||
} // namespace outbounds
|
||||
|
||||
namespace inbounds
|
||||
{
|
||||
QJsonObject GenerateInboundEntry();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // SERIALIZATION_H
|
||||
142
client/core/serialization/ss.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
#include "utilities.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#define OUTBOUND_TAG_PROXY "PROXY"
|
||||
#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__)
|
||||
|
||||
namespace amnezia::serialization::ss
|
||||
{
|
||||
QJsonObject Deserialize(const QString &ssUri, QString *alias, QString *errMessage)
|
||||
{
|
||||
ShadowSocksServerObject server;
|
||||
QString d_name;
|
||||
|
||||
// auto ssUri = _ssUri.toStdString();
|
||||
if (ssUri.length() < 5)
|
||||
{
|
||||
*errMessage = QObject::tr("SS URI is too short");
|
||||
}
|
||||
|
||||
auto uri = ssUri.mid(5);
|
||||
auto hashPos = uri.lastIndexOf("#");
|
||||
|
||||
if (hashPos >= 0)
|
||||
{
|
||||
// Get the name/remark
|
||||
d_name = uri.mid(uri.lastIndexOf("#") + 1);
|
||||
uri.truncate(hashPos);
|
||||
}
|
||||
|
||||
auto atPos = uri.indexOf('@');
|
||||
|
||||
if (atPos < 0)
|
||||
{
|
||||
// Old URI scheme
|
||||
QString decoded = QByteArray::fromBase64(uri.toUtf8(), QByteArray::Base64Option::OmitTrailingEquals);
|
||||
auto colonPos = decoded.indexOf(':');
|
||||
|
||||
if (colonPos < 0)
|
||||
{
|
||||
*errMessage = QObject::tr("Can't find the colon separator between method and password");
|
||||
}
|
||||
|
||||
server.method = decoded.left(colonPos);
|
||||
decoded.remove(0, colonPos + 1);
|
||||
atPos = decoded.lastIndexOf('@');
|
||||
|
||||
if (atPos < 0)
|
||||
{
|
||||
*errMessage = QObject::tr("Can't find the at separator between password and hostname");
|
||||
}
|
||||
|
||||
server.password = decoded.mid(0, atPos);
|
||||
decoded.remove(0, atPos + 1);
|
||||
colonPos = decoded.lastIndexOf(':');
|
||||
|
||||
if (colonPos < 0)
|
||||
{
|
||||
*errMessage = QObject::tr("Can't find the colon separator between hostname and port");
|
||||
}
|
||||
|
||||
server.address = decoded.mid(0, colonPos);
|
||||
server.port = decoded.mid(colonPos + 1).toInt();
|
||||
}
|
||||
else
|
||||
{
|
||||
// SIP002 URI scheme
|
||||
auto x = QUrl::fromUserInput(uri);
|
||||
server.address = x.host();
|
||||
server.port = x.port();
|
||||
const auto userInfo = Utils::SafeBase64Decode(x.userName());
|
||||
const auto userInfoSp = userInfo.indexOf(':');
|
||||
|
||||
if (userInfoSp < 0)
|
||||
{
|
||||
*errMessage = QObject::tr("Can't find the colon separator between method and password");
|
||||
return QJsonObject{};
|
||||
}
|
||||
|
||||
const auto method = userInfo.mid(0, userInfoSp);
|
||||
server.method = method;
|
||||
server.password = userInfo.mid(userInfoSp + 1);
|
||||
}
|
||||
|
||||
d_name = QUrl::fromPercentEncoding(d_name.toUtf8());
|
||||
QJsonObject root;
|
||||
QJsonArray outbounds;
|
||||
outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ server }), {}));
|
||||
JADD(outbounds)
|
||||
QJsonObject inbound = inbounds::GenerateInboundEntry();
|
||||
root["inbounds"] = QJsonArray{ inbound };
|
||||
*alias = alias->isEmpty() ? d_name : *alias + "_" + d_name;
|
||||
return root;
|
||||
}
|
||||
|
||||
const QString Serialize(const ShadowSocksServerObject &server, const QString &alias, bool)
|
||||
{
|
||||
QUrl url;
|
||||
const auto plainUserInfo = server.method + ":" + server.password;
|
||||
const auto userinfo = plainUserInfo.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
url.setUserInfo(userinfo);
|
||||
url.setScheme("ss");
|
||||
url.setHost(server.address);
|
||||
url.setPort(server.port);
|
||||
url.setFragment(alias);
|
||||
return url.toString(QUrl::ComponentFormattingOption::FullyEncoded);
|
||||
}
|
||||
} // namespace amnezia::serialization::ss
|
||||
|
||||
244
client/core/serialization/ssd.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A Naive SSD Decoder for Qv2ray
|
||||
*
|
||||
* @author DuckSoft <realducksoft@gmail.com>
|
||||
* @copyright Licensed under GPLv3.
|
||||
*/
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
#include "utilities.h"
|
||||
#include "serialization.h"
|
||||
|
||||
const inline QString QV2RAY_SSD_DEFAULT_NAME_PATTERN = "%1 - %2 (rate %3)";
|
||||
#define OUTBOUND_TAG_PROXY "PROXY"
|
||||
|
||||
namespace amnezia::serialization::ssd
|
||||
{
|
||||
// These below are super strict checking schemes, but necessary.
|
||||
#define MUST_EXIST(fieldName) \
|
||||
if (!obj.contains((fieldName)) || obj[(fieldName)].isUndefined() || obj[(fieldName)].isNull()) \
|
||||
{ \
|
||||
*logList << QObject::tr("Invalid ssd link: json: field %1 must exist").arg(fieldName); \
|
||||
return {}; \
|
||||
}
|
||||
#define MUST_PORT(fieldName) \
|
||||
MUST_EXIST(fieldName); \
|
||||
if (int value = obj[(fieldName)].toInt(-1); value < 0 || value > 65535) \
|
||||
{ \
|
||||
*logList << QObject::tr("Invalid ssd link: json: field %1 must be valid port number"); \
|
||||
return {}; \
|
||||
}
|
||||
#define MUST_STRING(fieldName) \
|
||||
MUST_EXIST(fieldName); \
|
||||
if (!obj[(fieldName)].isString()) \
|
||||
{ \
|
||||
*logList << QObject::tr("Invalid ssd link: json: field %1 must be of type 'string'").arg(fieldName); \
|
||||
return {}; \
|
||||
}
|
||||
#define MUST_ARRAY(fieldName) \
|
||||
MUST_EXIST(fieldName); \
|
||||
if (!obj[(fieldName)].isArray()) \
|
||||
{ \
|
||||
*logList << QObject::tr("Invalid ssd link: json: field %1 must be an array").arg(fieldName); \
|
||||
return {}; \
|
||||
}
|
||||
|
||||
#define SERVER_SHOULD_BE_OBJECT(server) \
|
||||
if (!server.isObject()) \
|
||||
{ \
|
||||
*logList << QObject::tr("Skipping invalid ssd server: server must be an object"); \
|
||||
continue; \
|
||||
}
|
||||
#define SHOULD_EXIST(fieldName) \
|
||||
if (serverObject[(fieldName)].isUndefined()) \
|
||||
{ \
|
||||
*logList << QObject::tr("Skipping invalid ssd server: missing required field %1").arg(fieldName); \
|
||||
continue; \
|
||||
}
|
||||
#define SHOULD_STRING(fieldName) \
|
||||
SHOULD_EXIST(fieldName); \
|
||||
if (!serverObject[(fieldName)].isString()) \
|
||||
{ \
|
||||
*logList << QObject::tr("Skipping invalid ssd server: field %1 should be of type 'string'").arg(fieldName); \
|
||||
continue; \
|
||||
}
|
||||
|
||||
QList<std::pair<QString, QJsonObject>> Deserialize(const QString &uri, QString *groupName, QStringList *logList)
|
||||
{
|
||||
// ssd links should begin with "ssd://"
|
||||
if (!uri.startsWith("ssd://"))
|
||||
{
|
||||
*logList << QObject::tr("Invalid ssd link: should begin with ssd://");
|
||||
return {};
|
||||
}
|
||||
|
||||
// decode base64
|
||||
const auto ssdURIBody = uri.mid(6, uri.length() - 6); //(&uri, 6, uri.length() - 6);
|
||||
const auto decodedJSON = Utils::SafeBase64Decode(ssdURIBody).toUtf8();
|
||||
|
||||
if (decodedJSON.length() == 0)
|
||||
{
|
||||
*logList << QObject::tr("Invalid ssd link: base64 parse failed");
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto decodeError = Utils::VerifyJsonString(decodedJSON);
|
||||
if (!decodeError.isEmpty())
|
||||
{
|
||||
*logList << QObject::tr("Invalid ssd link: json parse failed");
|
||||
return {};
|
||||
}
|
||||
|
||||
// casting to object
|
||||
const auto obj = Utils::JsonFromString(decodedJSON);
|
||||
|
||||
// obj.airport
|
||||
MUST_STRING("airport");
|
||||
*groupName = obj["airport"].toString();
|
||||
|
||||
// obj.port
|
||||
MUST_PORT("port");
|
||||
const int port = obj["port"].toInt();
|
||||
|
||||
// obj.encryption
|
||||
MUST_STRING("encryption");
|
||||
const auto encryption = obj["encryption"].toString();
|
||||
|
||||
// check: rc4-md5 is not supported by v2ray-core
|
||||
// TODO: more checks, including all algorithms
|
||||
if (encryption.toLower() == "rc4-md5")
|
||||
{
|
||||
*logList << QObject::tr("Invalid ssd link: rc4-md5 encryption is not supported by v2ray-core");
|
||||
return {};
|
||||
}
|
||||
|
||||
// obj.password
|
||||
MUST_STRING("password");
|
||||
const auto password = obj["password"].toString();
|
||||
// obj.servers
|
||||
MUST_ARRAY("servers");
|
||||
//
|
||||
QList<std::pair<QString, QJsonObject>> serverList;
|
||||
//
|
||||
|
||||
// iterate through the servers
|
||||
for (const auto &server : obj["servers"].toArray())
|
||||
{
|
||||
SERVER_SHOULD_BE_OBJECT(server);
|
||||
const auto serverObject = server.toObject();
|
||||
ShadowSocksServerObject ssObject;
|
||||
|
||||
// encryption
|
||||
ssObject.method = encryption;
|
||||
|
||||
// password
|
||||
ssObject.password = password;
|
||||
|
||||
// address :-> "server"
|
||||
SHOULD_STRING("server");
|
||||
const auto serverAddress = serverObject["server"].toString();
|
||||
ssObject.address = serverAddress;
|
||||
|
||||
// port selection:
|
||||
// normal: use global settings
|
||||
// overriding: use current config
|
||||
if (serverObject["port"].isUndefined())
|
||||
{
|
||||
ssObject.port = port;
|
||||
}
|
||||
else if (auto currPort = serverObject["port"].toInt(-1); (currPort >= 0 && currPort <= 65535))
|
||||
{
|
||||
ssObject.port = currPort;
|
||||
}
|
||||
else
|
||||
{
|
||||
ssObject.port = port;
|
||||
}
|
||||
|
||||
// name decision:
|
||||
// untitled: using server:port as name
|
||||
// entitled: using given name
|
||||
QString nodeName;
|
||||
if (serverObject["remarks"].isUndefined())
|
||||
{
|
||||
nodeName = QString("%1:%2").arg(ssObject.address).arg(ssObject.port);
|
||||
}
|
||||
else if (serverObject["remarks"].isString())
|
||||
{
|
||||
nodeName = serverObject["remarks"].toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
nodeName = QString("%1:%2").arg(ssObject.address).arg(ssObject.port);
|
||||
}
|
||||
|
||||
// ratio decision:
|
||||
// unspecified: ratio = 1
|
||||
// specified: use given value
|
||||
double ratio = 1.0;
|
||||
if (auto currRatio = serverObject["ratio"].toDouble(-1.0); currRatio != -1.0)
|
||||
{
|
||||
ratio = currRatio;
|
||||
}
|
||||
// else if (!serverObject["ratio"].isUndefined())
|
||||
// {
|
||||
// //*logList << QObject::tr("Invalid ratio encountered. using fallback value.");
|
||||
// }
|
||||
|
||||
// format the total name of the node.
|
||||
const auto finalName = QV2RAY_SSD_DEFAULT_NAME_PATTERN.arg(*groupName, nodeName).arg(ratio);
|
||||
// appending to the total list
|
||||
QJsonObject root;
|
||||
QJsonArray outbounds;
|
||||
QJsonObject inbound = inbounds::GenerateInboundEntry();
|
||||
outbounds.append(outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "shadowsocks", outbounds::GenerateShadowSocksOUT({ ssObject }), {}));
|
||||
root["outbounds"] = outbounds;
|
||||
root["inbounds"] = QJsonArray{ inbound };
|
||||
serverList.append({ finalName, root });
|
||||
}
|
||||
|
||||
// returns the current result
|
||||
return serverList;
|
||||
}
|
||||
#undef MUST_EXIST
|
||||
#undef MUST_PORT
|
||||
#undef MUST_ARRAY
|
||||
#undef MUST_STRING
|
||||
#undef SERVER_SHOULD_BE_OBJECT
|
||||
#undef SHOULD_EXIST
|
||||
#undef SHOULD_STRING
|
||||
} // namespace amnezia::serialization::ssd
|
||||
|
||||
313
client/core/serialization/transfer.h
Normal file
@@ -0,0 +1,313 @@
|
||||
#ifndef TRANSFER_H
|
||||
#define TRANSFER_H
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
|
||||
#define JADDEx(field) root.insert(#field, field);
|
||||
#define JADD(...) FOR_EACH(JADDEx, __VA_ARGS__)
|
||||
|
||||
constexpr auto VMESS_USER_ALTERID_DEFAULT = 0;
|
||||
|
||||
namespace amnezia::serialization {
|
||||
|
||||
struct ShadowSocksServerObject
|
||||
{
|
||||
QString address;
|
||||
QString method;
|
||||
QString password;
|
||||
int port;
|
||||
JSONSTRUCT_COMPARE(ShadowSocksServerObject, address, method, password)
|
||||
JSONSTRUCT_REGISTER(ShadowSocksServerObject, F(address, port, method, password))
|
||||
};
|
||||
|
||||
|
||||
struct VMessServerObject
|
||||
{
|
||||
struct UserObject
|
||||
{
|
||||
QString id;
|
||||
int alterId = VMESS_USER_ALTERID_DEFAULT;
|
||||
QString security = "auto";
|
||||
int level = 0;
|
||||
JSONSTRUCT_COMPARE(UserObject, id, alterId, security, level)
|
||||
JSONSTRUCT_REGISTER(UserObject, F(id, alterId, security, level))
|
||||
};
|
||||
|
||||
QString address;
|
||||
int port;
|
||||
QList<UserObject> users;
|
||||
JSONSTRUCT_COMPARE(VMessServerObject, address, port, users)
|
||||
JSONSTRUCT_REGISTER(VMessServerObject, F(address, port, users))
|
||||
};
|
||||
|
||||
|
||||
namespace transfer
|
||||
{
|
||||
|
||||
struct HTTPRequestObject
|
||||
{
|
||||
QString version = "1.1";
|
||||
QString method = "GET";
|
||||
QList<QString> path = { "/" };
|
||||
QMap<QString, QList<QString>> headers;
|
||||
HTTPRequestObject()
|
||||
{
|
||||
headers = {
|
||||
{ "Host", { "www.baidu.com", "www.bing.com" } },
|
||||
{ "User-Agent",
|
||||
{ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46" } },
|
||||
{ "Accept-Encoding", { "gzip, deflate" } },
|
||||
{ "Connection", { "keep-alive" } },
|
||||
{ "Pragma", { "no-cache" } }
|
||||
};
|
||||
}
|
||||
JSONSTRUCT_COMPARE(HTTPRequestObject, version, method, path, headers)
|
||||
JSONSTRUCT_REGISTER(HTTPRequestObject, F(version, method, path, headers))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct HTTPResponseObject
|
||||
{
|
||||
QString version = "1.1";
|
||||
QString status = "200";
|
||||
QString reason = "OK";
|
||||
QMap<QString, QList<QString>> headers;
|
||||
HTTPResponseObject()
|
||||
{
|
||||
headers = { { "Content-Type", { "application/octet-stream", "video/mpeg" } }, //
|
||||
{ "Transfer-Encoding", { "chunked" } }, //
|
||||
{ "Connection", { "keep-alive" } }, //
|
||||
{ "Pragma", { "no-cache" } } };
|
||||
}
|
||||
JSONSTRUCT_COMPARE(HTTPResponseObject, version, status, reason, headers)
|
||||
JSONSTRUCT_REGISTER(HTTPResponseObject, F(version, status, reason, headers))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct TCPHeader_Internal
|
||||
{
|
||||
QString type = "none";
|
||||
HTTPRequestObject request;
|
||||
HTTPResponseObject response;
|
||||
JSONSTRUCT_COMPARE(TCPHeader_Internal, type, request, response)
|
||||
JSONSTRUCT_REGISTER(TCPHeader_Internal, A(type), F(request, response))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct ObfsHeaderObject
|
||||
{
|
||||
QString type = "none";
|
||||
JSONSTRUCT_COMPARE(ObfsHeaderObject, type)
|
||||
JSONSTRUCT_REGISTER(ObfsHeaderObject, F(type))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct TCPObject
|
||||
{
|
||||
TCPHeader_Internal header;
|
||||
JSONSTRUCT_COMPARE(TCPObject, header)
|
||||
JSONSTRUCT_REGISTER(TCPObject, F(header))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct KCPObject
|
||||
{
|
||||
int mtu = 1350;
|
||||
int tti = 50;
|
||||
int uplinkCapacity = 5;
|
||||
int downlinkCapacity = 20;
|
||||
bool congestion = false;
|
||||
int readBufferSize = 2;
|
||||
int writeBufferSize = 2;
|
||||
QString seed;
|
||||
ObfsHeaderObject header;
|
||||
KCPObject(){};
|
||||
JSONSTRUCT_COMPARE(KCPObject, mtu, tti, uplinkCapacity, downlinkCapacity, congestion, readBufferSize, writeBufferSize, seed, header)
|
||||
JSONSTRUCT_REGISTER(KCPObject, F(mtu, tti, uplinkCapacity, downlinkCapacity, congestion, readBufferSize, writeBufferSize, header, seed))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct WebSocketObject
|
||||
{
|
||||
QString path = "/";
|
||||
QMap<QString, QString> headers;
|
||||
int maxEarlyData = 0;
|
||||
bool useBrowserForwarding = false;
|
||||
QString earlyDataHeaderName;
|
||||
JSONSTRUCT_COMPARE(WebSocketObject, path, headers, maxEarlyData, useBrowserForwarding, earlyDataHeaderName)
|
||||
JSONSTRUCT_REGISTER(WebSocketObject, F(path, headers, maxEarlyData, useBrowserForwarding, earlyDataHeaderName))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct HttpObject
|
||||
{
|
||||
QList<QString> host;
|
||||
QString path = "/";
|
||||
QString method = "PUT";
|
||||
QMap<QString, QList<QString>> headers;
|
||||
JSONSTRUCT_COMPARE(HttpObject, host, path, method, headers)
|
||||
JSONSTRUCT_REGISTER(HttpObject, F(host, path, method, headers))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct DomainSocketObject
|
||||
{
|
||||
QString path = "/";
|
||||
JSONSTRUCT_COMPARE(DomainSocketObject, path)
|
||||
JSONSTRUCT_REGISTER(DomainSocketObject, F(path))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct QuicObject
|
||||
{
|
||||
QString security = "none";
|
||||
QString key;
|
||||
ObfsHeaderObject header;
|
||||
JSONSTRUCT_COMPARE(QuicObject, security, key, header)
|
||||
JSONSTRUCT_REGISTER(QuicObject, F(security, key, header))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct gRPCObject
|
||||
{
|
||||
QString serviceName;
|
||||
bool multiMode = false;
|
||||
JSONSTRUCT_COMPARE(gRPCObject, serviceName, multiMode)
|
||||
JSONSTRUCT_REGISTER(gRPCObject, F(serviceName, multiMode))
|
||||
};
|
||||
|
||||
//
|
||||
//
|
||||
struct SockoptObject
|
||||
{
|
||||
int mark = 0;
|
||||
bool tcpFastOpen = false;
|
||||
QString tproxy = "off";
|
||||
int tcpKeepAliveInterval = 0;
|
||||
JSONSTRUCT_COMPARE(SockoptObject, mark, tcpFastOpen, tproxy, tcpKeepAliveInterval)
|
||||
JSONSTRUCT_REGISTER(SockoptObject, F(mark, tcpFastOpen, tproxy, tcpKeepAliveInterval))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct CertificateObject
|
||||
{
|
||||
QString usage = "encipherment";
|
||||
QString certificateFile;
|
||||
QString keyFile;
|
||||
QList<QString> certificate;
|
||||
QList<QString> key;
|
||||
JSONSTRUCT_COMPARE(CertificateObject, usage, certificateFile, keyFile, certificate, key)
|
||||
JSONSTRUCT_REGISTER(CertificateObject, F(usage, certificateFile, keyFile, certificate, key))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct TLSObject
|
||||
{
|
||||
QString serverName;
|
||||
bool allowInsecure = false;
|
||||
bool enableSessionResumption = false;
|
||||
bool disableSystemRoot = false;
|
||||
QList<QString> alpn;
|
||||
QList<QString> pinnedPeerCertificateChainSha256;
|
||||
QList<CertificateObject> certificates;
|
||||
JSONSTRUCT_COMPARE(TLSObject, serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn,
|
||||
pinnedPeerCertificateChainSha256, certificates)
|
||||
JSONSTRUCT_REGISTER(TLSObject, F(serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn,
|
||||
pinnedPeerCertificateChainSha256, certificates))
|
||||
};
|
||||
//
|
||||
//
|
||||
struct XTLSObject
|
||||
{
|
||||
QString serverName;
|
||||
bool allowInsecure = false;
|
||||
bool enableSessionResumption = false;
|
||||
bool disableSystemRoot = false;
|
||||
QList<QString> alpn;
|
||||
QList<CertificateObject> certificates;
|
||||
JSONSTRUCT_COMPARE(XTLSObject, serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, certificates)
|
||||
JSONSTRUCT_REGISTER(XTLSObject, F(serverName, allowInsecure, enableSessionResumption, disableSystemRoot, alpn, certificates))
|
||||
};
|
||||
} // namespace transfer
|
||||
|
||||
//
|
||||
//
|
||||
struct TrojanObject
|
||||
{
|
||||
quint16 port;
|
||||
QString address;
|
||||
QString password;
|
||||
QString sni;
|
||||
bool ignoreCertificate = false;
|
||||
bool ignoreHostname = false;
|
||||
bool reuseSession = false;
|
||||
bool sessionTicket = false;
|
||||
bool reusePort = false;
|
||||
bool tcpFastOpen = false;
|
||||
|
||||
#define _X(name) json[#name] = name
|
||||
QJsonObject toJson() const
|
||||
{
|
||||
QJsonObject json;
|
||||
_X(port);
|
||||
_X(address);
|
||||
_X(password);
|
||||
_X(sni);
|
||||
_X(ignoreCertificate);
|
||||
_X(ignoreHostname);
|
||||
_X(reuseSession);
|
||||
_X(reusePort);
|
||||
_X(sessionTicket);
|
||||
_X(tcpFastOpen);
|
||||
return json;
|
||||
};
|
||||
#undef _X
|
||||
|
||||
#define _X(name, type) name = root[#name].to##type()
|
||||
void loadJson(const QJsonObject &root)
|
||||
{
|
||||
_X(port, Int);
|
||||
_X(address, String);
|
||||
_X(password, String);
|
||||
_X(sni, String);
|
||||
_X(ignoreHostname, Bool);
|
||||
_X(ignoreCertificate, Bool);
|
||||
_X(reuseSession, Bool);
|
||||
_X(reusePort, Bool);
|
||||
_X(sessionTicket, Bool);
|
||||
_X(tcpFastOpen, Bool);
|
||||
}
|
||||
#undef _X
|
||||
|
||||
[[nodiscard]] static TrojanObject fromJson(const QJsonObject &root)
|
||||
{
|
||||
TrojanObject o;
|
||||
o.loadJson(root);
|
||||
return o;
|
||||
}
|
||||
};
|
||||
|
||||
struct StreamSettingsObject
|
||||
{
|
||||
QString network = "tcp";
|
||||
QString security = "none";
|
||||
transfer::SockoptObject sockopt;
|
||||
transfer::TLSObject tlsSettings;
|
||||
transfer::XTLSObject xtlsSettings;
|
||||
transfer::TCPObject tcpSettings;
|
||||
transfer::KCPObject kcpSettings;
|
||||
transfer::WebSocketObject wsSettings;
|
||||
transfer::HttpObject httpSettings;
|
||||
transfer::DomainSocketObject dsSettings;
|
||||
transfer::QuicObject quicSettings;
|
||||
transfer::gRPCObject grpcSettings;
|
||||
JSONSTRUCT_COMPARE(StreamSettingsObject, network, security, sockopt, //
|
||||
tcpSettings, tlsSettings, xtlsSettings, kcpSettings, wsSettings, httpSettings, dsSettings, quicSettings, grpcSettings)
|
||||
JSONSTRUCT_REGISTER(StreamSettingsObject, F(network, security, sockopt),
|
||||
F(tcpSettings, tlsSettings, xtlsSettings, kcpSettings, wsSettings, httpSettings, dsSettings, quicSettings, grpcSettings))
|
||||
};
|
||||
|
||||
}
|
||||
#endif //TRANSFER_H
|
||||
271
client/core/serialization/trojan.cpp
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include <QUrlQuery>
|
||||
#include "serialization.h"
|
||||
|
||||
#define OUTBOUND_TAG_PROXY "PROXY"
|
||||
|
||||
namespace amnezia::serialization::trojan
|
||||
{
|
||||
|
||||
const QString Serialize(const TrojanObject &object, const QString &alias)
|
||||
{
|
||||
|
||||
QUrlQuery query;
|
||||
if (object.ignoreHostname)
|
||||
query.addQueryItem("allowInsecureHostname", "1");
|
||||
if (object.ignoreCertificate)
|
||||
query.addQueryItem("allowInsecureCertificate", "1");
|
||||
if (object.sessionTicket)
|
||||
query.addQueryItem("sessionTicket", "1");
|
||||
if (object.ignoreCertificate || object.ignoreHostname)
|
||||
query.addQueryItem("allowInsecure", "1");
|
||||
if (object.tcpFastOpen)
|
||||
query.addQueryItem("tfo", "1");
|
||||
|
||||
if (!object.sni.isEmpty())
|
||||
query.addQueryItem("sni", object.sni);
|
||||
|
||||
QUrl link;
|
||||
if (!object.password.isEmpty())
|
||||
link.setUserName(object.password, QUrl::DecodedMode);
|
||||
link.setPort(object.port);
|
||||
link.setHost(object.address);
|
||||
link.setFragment(alias);
|
||||
link.setQuery(query);
|
||||
link.setScheme("trojan");
|
||||
|
||||
return link.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
QJsonObject Deserialize(const QString &trojanUri, QString *alias, QString *errMessage)
|
||||
{
|
||||
const QString prefix = "trojan://";
|
||||
if (!trojanUri.startsWith(prefix))
|
||||
{
|
||||
*errMessage = ("Invalid Trojan URI");
|
||||
return {};
|
||||
}
|
||||
//
|
||||
const auto trueList = QStringList{ "true", "1", "yes", "y" };
|
||||
const QUrl trojanUrl(trojanUri.trimmed());
|
||||
const QUrlQuery query(trojanUrl.query());
|
||||
*alias = trojanUrl.fragment(QUrl::FullyDecoded);
|
||||
|
||||
auto getQueryValue = [&](const QString &key) {
|
||||
return query.queryItemValue(key, QUrl::FullyDecoded);
|
||||
};
|
||||
//
|
||||
TrojanObject result;
|
||||
result.address = trojanUrl.host();
|
||||
result.password = QUrl::fromPercentEncoding(trojanUrl.userInfo().toUtf8());
|
||||
result.port = trojanUrl.port();
|
||||
// process sni (and also "peer")
|
||||
if (query.hasQueryItem("sni"))
|
||||
{
|
||||
result.sni = getQueryValue("sni");
|
||||
}
|
||||
else if (query.hasQueryItem("peer"))
|
||||
{
|
||||
// This is evil and may be removed in a future version.
|
||||
qWarning() << "use of 'peer' in trojan url is deprecated";
|
||||
result.sni = getQueryValue("peer");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use the hostname
|
||||
result.sni = result.address;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
result.tcpFastOpen = trueList.contains(getQueryValue("tfo").toLower());
|
||||
result.sessionTicket = trueList.contains(getQueryValue("sessionTicket").toLower());
|
||||
//
|
||||
bool allowAllInsecure = trueList.contains(getQueryValue("allowInsecure").toLower());
|
||||
result.ignoreHostname = allowAllInsecure || trueList.contains(getQueryValue("allowInsecureHostname").toLower());
|
||||
result.ignoreCertificate = allowAllInsecure || trueList.contains(getQueryValue("allowInsecureCertificate").toLower());
|
||||
|
||||
QJsonObject stream;
|
||||
// handle type
|
||||
const auto hasType = query.hasQueryItem("type");
|
||||
const auto type = hasType ? query.queryItemValue("type") : "tcp";
|
||||
if (type != "tcp")
|
||||
QJsonIO::SetValue(stream, type, "network");
|
||||
|
||||
|
||||
// type-wise settings
|
||||
if (type == "kcp")
|
||||
{
|
||||
const auto hasSeed = query.hasQueryItem("seed");
|
||||
if (hasSeed)
|
||||
QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" });
|
||||
|
||||
const auto hasHeaderType = query.hasQueryItem("headerType");
|
||||
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
|
||||
if (headerType != "none")
|
||||
QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" });
|
||||
}
|
||||
else if (type == "http")
|
||||
{
|
||||
const auto hasPath = query.hasQueryItem("path");
|
||||
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
|
||||
if (path != "/")
|
||||
QJsonIO::SetValue(stream, path, { "httpSettings", "path" });
|
||||
|
||||
const auto hasHost = query.hasQueryItem("host");
|
||||
if (hasHost)
|
||||
{
|
||||
const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(","));
|
||||
QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" });
|
||||
}
|
||||
}
|
||||
else if (type == "ws")
|
||||
{
|
||||
const auto hasPath = query.hasQueryItem("path");
|
||||
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
|
||||
if (path != "/")
|
||||
QJsonIO::SetValue(stream, path, { "wsSettings", "path" });
|
||||
|
||||
const auto hasHost = query.hasQueryItem("host");
|
||||
if (hasHost)
|
||||
{
|
||||
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" });
|
||||
}
|
||||
}
|
||||
else if (type == "quic")
|
||||
{
|
||||
const auto hasQuicSecurity = query.hasQueryItem("quicSecurity");
|
||||
if (hasQuicSecurity)
|
||||
{
|
||||
const auto quicSecurity = query.queryItemValue("quicSecurity");
|
||||
QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" });
|
||||
|
||||
if (quicSecurity != "none")
|
||||
{
|
||||
const auto key = query.queryItemValue("key");
|
||||
QJsonIO::SetValue(stream, key, { "quicSettings", "key" });
|
||||
}
|
||||
|
||||
const auto hasHeaderType = query.hasQueryItem("headerType");
|
||||
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
|
||||
if (headerType != "none")
|
||||
QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" });
|
||||
}
|
||||
}
|
||||
else if (type == "grpc")
|
||||
{
|
||||
const auto hasServiceName = query.hasQueryItem("serviceName");
|
||||
if (hasServiceName)
|
||||
{
|
||||
const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8());
|
||||
QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" });
|
||||
}
|
||||
|
||||
const auto hasMode = query.hasQueryItem("mode");
|
||||
if (hasMode)
|
||||
{
|
||||
const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi";
|
||||
QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" });
|
||||
}
|
||||
}
|
||||
|
||||
// tls-wise settings
|
||||
const auto hasSecurity = query.hasQueryItem("security");
|
||||
const auto security = hasSecurity ? query.queryItemValue("security") : "none";
|
||||
const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" );
|
||||
if (security != "none")
|
||||
{
|
||||
QJsonIO::SetValue(stream, security, "security");
|
||||
}
|
||||
// sni
|
||||
const auto hasSNI = query.hasQueryItem("sni");
|
||||
if (hasSNI)
|
||||
{
|
||||
const auto sni = query.queryItemValue("sni");
|
||||
QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" });
|
||||
}
|
||||
// alpn
|
||||
const auto hasALPN = query.hasQueryItem("alpn");
|
||||
if (hasALPN)
|
||||
{
|
||||
const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8());
|
||||
QStringList aplnElems = alpnRaw.split(",");
|
||||
// h2 protocol is not supported by xray
|
||||
aplnElems.removeAll("h2");
|
||||
if (!aplnElems.isEmpty()) {
|
||||
const auto alpnArray = QJsonArray::fromStringList(aplnElems);
|
||||
QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" });
|
||||
}
|
||||
}
|
||||
|
||||
if (security == "reality")
|
||||
{
|
||||
if (query.hasQueryItem("fp"))
|
||||
{
|
||||
const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8());
|
||||
QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" });
|
||||
}
|
||||
if (query.hasQueryItem("pbk"))
|
||||
{
|
||||
const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8());
|
||||
QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" });
|
||||
}
|
||||
if (query.hasQueryItem("spiderX"))
|
||||
{
|
||||
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8());
|
||||
QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" });
|
||||
}
|
||||
if (query.hasQueryItem("sid"))
|
||||
{
|
||||
const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8());
|
||||
QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" });
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
QJsonArray outbounds;
|
||||
QJsonObject outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "trojan", outbounds::GenerateTrojanOUT({ result }), {});
|
||||
outbound["streamSettings"] = stream;
|
||||
outbounds.append(outbound);
|
||||
JADD(outbounds)
|
||||
QJsonObject inbound = inbounds::GenerateInboundEntry();
|
||||
root["inbounds"] = QJsonArray { inbound };
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
} // namespace amnezia::serialization::trojan
|
||||
|
||||
256
client/core/serialization/vless.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include <QUrlQuery>
|
||||
#include "serialization.h"
|
||||
|
||||
namespace amnezia::serialization::vless
|
||||
{
|
||||
QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
|
||||
{
|
||||
// must start with vless://
|
||||
if (!str.startsWith("vless://"))
|
||||
{
|
||||
*errMessage = QObject::tr("VLESS link should start with vless://");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
// parse url
|
||||
QUrl url(str);
|
||||
if (!url.isValid())
|
||||
{
|
||||
*errMessage = QObject::tr("link parse failed: %1").arg(url.errorString());
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
// fetch host
|
||||
const auto hostRaw = url.host();
|
||||
if (hostRaw.isEmpty())
|
||||
{
|
||||
*errMessage = QObject::tr("empty host");
|
||||
return QJsonObject();
|
||||
}
|
||||
const auto host = (hostRaw.startsWith('[') && hostRaw.endsWith(']')) ? hostRaw.mid(1, hostRaw.length() - 2) : hostRaw;
|
||||
|
||||
// fetch port
|
||||
const auto port = url.port();
|
||||
if (port == -1)
|
||||
{
|
||||
*errMessage = QObject::tr("missing port");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
// fetch remarks
|
||||
const auto remarks = url.fragment();
|
||||
if (!remarks.isEmpty())
|
||||
{
|
||||
*alias = remarks;
|
||||
}
|
||||
|
||||
// fetch uuid
|
||||
const auto uuid = url.userInfo();
|
||||
if (uuid.isEmpty())
|
||||
{
|
||||
*errMessage = QObject::tr("missing uuid");
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
// initialize QJsonObject with basic info
|
||||
QJsonObject outbound;
|
||||
QJsonObject stream;
|
||||
|
||||
QJsonIO::SetValue(outbound, "vless", "protocol");
|
||||
QJsonIO::SetValue(outbound, host, { "settings", "vnext", 0, "address" });
|
||||
QJsonIO::SetValue(outbound, port, { "settings", "vnext", 0, "port" });
|
||||
QJsonIO::SetValue(outbound, uuid, { "settings", "vnext", 0, "users", 0, "id" });
|
||||
|
||||
// parse query
|
||||
QUrlQuery query(url.query());
|
||||
|
||||
// handle type
|
||||
const auto hasType = query.hasQueryItem("type");
|
||||
const auto type = hasType ? query.queryItemValue("type") : "tcp";
|
||||
if (type != "tcp")
|
||||
QJsonIO::SetValue(stream, type, "network");
|
||||
|
||||
// handle encryption
|
||||
const auto hasEncryption = query.hasQueryItem("encryption");
|
||||
const auto encryption = hasEncryption ? query.queryItemValue("encryption") : "none";
|
||||
QJsonIO::SetValue(outbound, encryption, { "settings", "vnext", 0, "users", 0, "encryption" });
|
||||
|
||||
// type-wise settings
|
||||
if (type == "kcp")
|
||||
{
|
||||
const auto hasSeed = query.hasQueryItem("seed");
|
||||
if (hasSeed)
|
||||
QJsonIO::SetValue(stream, query.queryItemValue("seed"), { "kcpSettings", "seed" });
|
||||
|
||||
const auto hasHeaderType = query.hasQueryItem("headerType");
|
||||
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
|
||||
if (headerType != "none")
|
||||
QJsonIO::SetValue(stream, headerType, { "kcpSettings", "header", "type" });
|
||||
}
|
||||
else if (type == "http")
|
||||
{
|
||||
const auto hasPath = query.hasQueryItem("path");
|
||||
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
|
||||
if (path != "/")
|
||||
QJsonIO::SetValue(stream, path, { "httpSettings", "path" });
|
||||
|
||||
const auto hasHost = query.hasQueryItem("host");
|
||||
if (hasHost)
|
||||
{
|
||||
const auto hosts = QJsonArray::fromStringList(query.queryItemValue("host").split(","));
|
||||
QJsonIO::SetValue(stream, hosts, { "httpSettings", "host" });
|
||||
}
|
||||
}
|
||||
else if (type == "ws")
|
||||
{
|
||||
const auto hasPath = query.hasQueryItem("path");
|
||||
const auto path = hasPath ? QUrl::fromPercentEncoding(query.queryItemValue("path").toUtf8()) : "/";
|
||||
if (path != "/")
|
||||
QJsonIO::SetValue(stream, path, { "wsSettings", "path" });
|
||||
|
||||
const auto hasHost = query.hasQueryItem("host");
|
||||
if (hasHost)
|
||||
{
|
||||
QJsonIO::SetValue(stream, query.queryItemValue("host"), { "wsSettings", "headers", "Host" });
|
||||
}
|
||||
}
|
||||
else if (type == "quic")
|
||||
{
|
||||
const auto hasQuicSecurity = query.hasQueryItem("quicSecurity");
|
||||
if (hasQuicSecurity)
|
||||
{
|
||||
const auto quicSecurity = query.queryItemValue("quicSecurity");
|
||||
QJsonIO::SetValue(stream, quicSecurity, { "quicSettings", "security" });
|
||||
|
||||
if (quicSecurity != "none")
|
||||
{
|
||||
const auto key = query.queryItemValue("key");
|
||||
QJsonIO::SetValue(stream, key, { "quicSettings", "key" });
|
||||
}
|
||||
|
||||
const auto hasHeaderType = query.hasQueryItem("headerType");
|
||||
const auto headerType = hasHeaderType ? query.queryItemValue("headerType") : "none";
|
||||
if (headerType != "none")
|
||||
QJsonIO::SetValue(stream, headerType, { "quicSettings", "header", "type" });
|
||||
}
|
||||
}
|
||||
else if (type == "grpc")
|
||||
{
|
||||
const auto hasServiceName = query.hasQueryItem("serviceName");
|
||||
if (hasServiceName)
|
||||
{
|
||||
const auto serviceName = QUrl::fromPercentEncoding(query.queryItemValue("serviceName").toUtf8());
|
||||
QJsonIO::SetValue(stream, serviceName, { "grpcSettings", "serviceName" });
|
||||
}
|
||||
|
||||
const auto hasMode = query.hasQueryItem("mode");
|
||||
if (hasMode)
|
||||
{
|
||||
const auto multiMode = QUrl::fromPercentEncoding(query.queryItemValue("mode").toUtf8()) == "multi";
|
||||
QJsonIO::SetValue(stream, multiMode, { "grpcSettings", "multiMode" });
|
||||
}
|
||||
}
|
||||
|
||||
// tls-wise settings
|
||||
const auto hasSecurity = query.hasQueryItem("security");
|
||||
const auto security = hasSecurity ? query.queryItemValue("security") : "none";
|
||||
const auto tlsKey = security == "xtls" ? "xtlsSettings" : ( security == "tls" ? "tlsSettings" : "realitySettings" );
|
||||
if (security != "none")
|
||||
{
|
||||
QJsonIO::SetValue(stream, security, "security");
|
||||
}
|
||||
// sni
|
||||
const auto hasSNI = query.hasQueryItem("sni");
|
||||
if (hasSNI)
|
||||
{
|
||||
const auto sni = query.queryItemValue("sni");
|
||||
QJsonIO::SetValue(stream, sni, { tlsKey, "serverName" });
|
||||
}
|
||||
// alpn
|
||||
const auto hasALPN = query.hasQueryItem("alpn");
|
||||
if (hasALPN)
|
||||
{
|
||||
const auto alpnRaw = QUrl::fromPercentEncoding(query.queryItemValue("alpn").toUtf8());
|
||||
QStringList aplnElems = alpnRaw.split(",");
|
||||
// h2 protocol is not supported by xray
|
||||
aplnElems.removeAll("h2");
|
||||
if (!aplnElems.isEmpty()) {
|
||||
const auto alpnArray = QJsonArray::fromStringList(aplnElems);
|
||||
QJsonIO::SetValue(stream, alpnArray, { tlsKey, "alpn" });
|
||||
}
|
||||
}
|
||||
// xtls-specific
|
||||
if (security == "xtls" || security == "reality")
|
||||
{
|
||||
const auto flow = query.queryItemValue("flow");
|
||||
QJsonIO::SetValue(outbound, flow, { "settings", "vnext", 0, "users", 0, "flow" });
|
||||
}
|
||||
|
||||
if (security == "reality")
|
||||
{
|
||||
if (query.hasQueryItem("fp"))
|
||||
{
|
||||
const auto fp = QUrl::fromPercentEncoding(query.queryItemValue("fp").toUtf8());
|
||||
QJsonIO::SetValue(stream, fp, { "realitySettings", "fingerprint" });
|
||||
}
|
||||
if (query.hasQueryItem("pbk"))
|
||||
{
|
||||
const auto pbk = QUrl::fromPercentEncoding(query.queryItemValue("pbk").toUtf8());
|
||||
QJsonIO::SetValue(stream, pbk, { "realitySettings", "publicKey" });
|
||||
}
|
||||
if (query.hasQueryItem("spiderX"))
|
||||
{
|
||||
const auto spiderX = QUrl::fromPercentEncoding(query.queryItemValue("spiderX").toUtf8());
|
||||
QJsonIO::SetValue(stream, spiderX, { "realitySettings", "spiderX" });
|
||||
}
|
||||
if (query.hasQueryItem("sid"))
|
||||
{
|
||||
const auto sid = QUrl::fromPercentEncoding(query.queryItemValue("sid").toUtf8());
|
||||
QJsonIO::SetValue(stream, sid, { "realitySettings", "shortId" });
|
||||
}
|
||||
}
|
||||
|
||||
// assembling config
|
||||
QJsonObject root;
|
||||
outbound["streamSettings"] = stream;
|
||||
QJsonObject inbound = inbounds::GenerateInboundEntry();
|
||||
root["outbounds"] = QJsonArray{ outbound };
|
||||
root["inbounds"] = QJsonArray { inbound };
|
||||
return root;
|
||||
}
|
||||
} // namespace amnezia::serialization::vless
|
||||
|
||||
344
client/core/serialization/vmess.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
#include <QJsonDocument>
|
||||
#include "transfer.h"
|
||||
#include "utilities.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#define nothing
|
||||
|
||||
#define OUTBOUND_TAG_PROXY "PROXY"
|
||||
|
||||
namespace amnezia::serialization::vmess
|
||||
{
|
||||
// From https://github.com/2dust/v2rayN/wiki/分享链接格式说明(ver-2)
|
||||
const QString Serialize(const StreamSettingsObject &transfer, const VMessServerObject &server, const QString &alias)
|
||||
{
|
||||
QJsonObject vmessUriRoot;
|
||||
// Constant
|
||||
vmessUriRoot["v"] = 2;
|
||||
vmessUriRoot["ps"] = alias;
|
||||
vmessUriRoot["add"] = server.address;
|
||||
vmessUriRoot["port"] = server.port;
|
||||
vmessUriRoot["id"] = server.users.front().id;
|
||||
vmessUriRoot["aid"] = server.users.front().alterId;
|
||||
const auto scy = server.users.front().security;
|
||||
vmessUriRoot["scy"] = (scy == "aes-128-gcm" || scy == "chacha20-poly1305" || scy == "none" || scy == "zero") ? scy : "auto";
|
||||
vmessUriRoot["net"] = transfer.network == "http" ? "h2" : transfer.network;
|
||||
vmessUriRoot["tls"] = (transfer.security == "tls" || transfer.security == "xtls") ? "tls" : "none";
|
||||
if (transfer.security == "tls")
|
||||
{
|
||||
vmessUriRoot["sni"] = transfer.tlsSettings.serverName;
|
||||
}
|
||||
else if (transfer.security == "xtls")
|
||||
{
|
||||
vmessUriRoot["sni"] = transfer.xtlsSettings.serverName;
|
||||
}
|
||||
|
||||
if (transfer.network == "tcp")
|
||||
{
|
||||
vmessUriRoot["type"] = transfer.tcpSettings.header.type;
|
||||
}
|
||||
else if (transfer.network == "kcp")
|
||||
{
|
||||
vmessUriRoot["type"] = transfer.kcpSettings.header.type;
|
||||
}
|
||||
else if (transfer.network == "quic")
|
||||
{
|
||||
vmessUriRoot["type"] = transfer.quicSettings.header.type;
|
||||
vmessUriRoot["host"] = transfer.quicSettings.security;
|
||||
vmessUriRoot["path"] = transfer.quicSettings.key;
|
||||
}
|
||||
else if (transfer.network == "ws")
|
||||
{
|
||||
auto x = transfer.wsSettings.headers;
|
||||
auto host = x.contains("host");
|
||||
auto CapHost = x.contains("Host");
|
||||
auto realHost = host ? x["host"] : (CapHost ? x["Host"] : "");
|
||||
//
|
||||
vmessUriRoot["host"] = realHost;
|
||||
vmessUriRoot["path"] = transfer.wsSettings.path;
|
||||
}
|
||||
else if (transfer.network == "h2" || transfer.network == "http")
|
||||
{
|
||||
vmessUriRoot["host"] = transfer.httpSettings.host.join(",");
|
||||
vmessUriRoot["path"] = transfer.httpSettings.path;
|
||||
}
|
||||
else if (transfer.network == "grpc")
|
||||
{
|
||||
vmessUriRoot["path"] = transfer.grpcSettings.serviceName;
|
||||
}
|
||||
|
||||
if (!vmessUriRoot.contains("type") || vmessUriRoot["type"].toString().isEmpty())
|
||||
{
|
||||
vmessUriRoot["type"] = "none";
|
||||
}
|
||||
|
||||
//
|
||||
QString jString = Utils::JsonToString(vmessUriRoot, QJsonDocument::JsonFormat::Compact);
|
||||
auto vmessPart = jString.toUtf8().toBase64();
|
||||
return "vmess://" + vmessPart;
|
||||
}
|
||||
|
||||
// This generates global config containing only one outbound....
|
||||
QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage)
|
||||
{
|
||||
#define default QJsonObject()
|
||||
QString vmess = vmessStr;
|
||||
|
||||
if (vmess.trimmed() != vmess)
|
||||
{
|
||||
vmess = vmessStr.trimmed();
|
||||
}
|
||||
|
||||
// Reset errMessage
|
||||
*errMessage = "";
|
||||
|
||||
if (!vmess.toLower().startsWith("vmess://"))
|
||||
{
|
||||
*errMessage = QObject::tr("VMess string should start with 'vmess://'");
|
||||
return default;
|
||||
}
|
||||
|
||||
const auto b64Str = vmess.mid(8, vmess.length() - 8);
|
||||
if (b64Str.isEmpty())
|
||||
{
|
||||
*errMessage = QObject::tr("VMess string should be a valid base64 string");
|
||||
return default;
|
||||
}
|
||||
|
||||
auto vmessString = Utils::SafeBase64Decode(b64Str);
|
||||
auto jsonErr = Utils::VerifyJsonString(vmessString);
|
||||
|
||||
if (!jsonErr.isEmpty())
|
||||
{
|
||||
*errMessage = jsonErr;
|
||||
return default;
|
||||
}
|
||||
|
||||
auto vmessConf = Utils::JsonFromString(vmessString);
|
||||
|
||||
if (vmessConf.isEmpty())
|
||||
{
|
||||
*errMessage = QObject::tr("JSON should not be empty");
|
||||
return default;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
QJsonObject root;
|
||||
QString ps, add, id, net, type, host, path, tls, scy, sni;
|
||||
int port, aid;
|
||||
//
|
||||
// __vmess_checker__func(key, values)
|
||||
//
|
||||
// - Key = Key in JSON and the variable name.
|
||||
// - Values = Candidate variable list, if not match, the first one is used as default.
|
||||
//
|
||||
// - [[val.size() <= 1]] is used when only the default value exists.
|
||||
//
|
||||
// - It can be empty, if so, if the key is not in the JSON, or the value is empty, report an error.
|
||||
// - Else if it contains one thing. if the key is not in the JSON, or the value is empty, use that one.
|
||||
// - Else if it contains many things, when the key IS in the JSON but not within the THINGS, use the first in the THINGS
|
||||
// - Else -------------------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> use the JSON value
|
||||
//
|
||||
#define __vmess_checker__func(key, values) \
|
||||
{ \
|
||||
auto val = QStringList() values; \
|
||||
if (vmessConf.contains(#key) && !vmessConf[#key].toVariant().toString().trimmed().isEmpty() && \
|
||||
(val.size() <= 1 || val.contains(vmessConf[#key].toVariant().toString()))) \
|
||||
{ \
|
||||
key = vmessConf[#key].toVariant().toString(); \
|
||||
} \
|
||||
else if (!val.isEmpty()) \
|
||||
{ \
|
||||
key = val.first(); \
|
||||
} \
|
||||
else \
|
||||
{ \
|
||||
*errMessage = QObject::tr(#key " does not exist."); \
|
||||
} \
|
||||
}
|
||||
|
||||
// vmess v1 upgrader
|
||||
if (!vmessConf.contains("v"))
|
||||
{
|
||||
qDebug() << "Detected deprecated vmess v1. Trying to upgrade...";
|
||||
if (const auto network = vmessConf["net"].toString(); network == "ws" || network == "h2")
|
||||
{
|
||||
const QStringList hostComponents = vmessConf["host"].toString().replace(" ", "").split(";");
|
||||
if (const auto nParts = hostComponents.length(); nParts == 1)
|
||||
vmessConf["path"] = hostComponents[0], vmessConf["host"] = "";
|
||||
else if (nParts == 2)
|
||||
vmessConf["path"] = hostComponents[0], vmessConf["host"] = hostComponents[1];
|
||||
else
|
||||
vmessConf["path"] = "/", vmessConf["host"] = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Strict check of VMess protocol, to check if the specified value
|
||||
// is in the correct range.
|
||||
//
|
||||
// Get Alias (AKA ps) from address and port.
|
||||
{
|
||||
// Some idiot vmess:// links are using alterId...
|
||||
aid = vmessConf.contains("aid") ? vmessConf.value("aid").toInt(VMESS_USER_ALTERID_DEFAULT) :
|
||||
vmessConf.value("alterId").toInt(VMESS_USER_ALTERID_DEFAULT);
|
||||
//
|
||||
//
|
||||
__vmess_checker__func(ps, << vmessConf["add"].toVariant().toString() + ":" + vmessConf["port"].toVariant().toString()); //
|
||||
__vmess_checker__func(add, nothing); //
|
||||
__vmess_checker__func(id, nothing); //
|
||||
__vmess_checker__func(scy, << "aes-128-gcm" //
|
||||
<< "chacha20-poly1305" //
|
||||
<< "auto" //
|
||||
<< "none" //
|
||||
<< "zero"); //
|
||||
//
|
||||
__vmess_checker__func(type, << "none" //
|
||||
<< "http" //
|
||||
<< "srtp" //
|
||||
<< "utp" //
|
||||
<< "wechat-video"); //
|
||||
//
|
||||
__vmess_checker__func(net, << "tcp" //
|
||||
<< "http" //
|
||||
<< "h2" //
|
||||
<< "ws" //
|
||||
<< "kcp" //
|
||||
<< "quic" //
|
||||
<< "grpc"); //
|
||||
//
|
||||
__vmess_checker__func(tls, << "none" //
|
||||
<< "tls"); //
|
||||
//
|
||||
path = vmessConf.contains("path") ? vmessConf["path"].toVariant().toString() : (net == "quic" ? "" : "/");
|
||||
host = vmessConf.contains("host") ? vmessConf["host"].toVariant().toString() : (net == "quic" ? "none" : "");
|
||||
}
|
||||
|
||||
// Respect connection type rather than obfs type
|
||||
if (QStringList{ "srtp", "utp", "wechat-video" }.contains(type)) //
|
||||
{ //
|
||||
if (net != "quic" && net != "kcp") //
|
||||
{ //
|
||||
type = "none"; //
|
||||
} //
|
||||
}
|
||||
|
||||
port = vmessConf["port"].toVariant().toInt();
|
||||
aid = vmessConf["aid"].toVariant().toInt();
|
||||
//
|
||||
// Apply the settings.
|
||||
// User
|
||||
VMessServerObject::UserObject user;
|
||||
user.id = id;
|
||||
user.alterId = aid;
|
||||
user.security = scy;
|
||||
//
|
||||
// Server
|
||||
VMessServerObject serv;
|
||||
serv.port = port;
|
||||
serv.address = add;
|
||||
serv.users.push_back(user);
|
||||
//
|
||||
//
|
||||
// Stream Settings
|
||||
StreamSettingsObject streaming;
|
||||
|
||||
if (net == "tcp")
|
||||
{
|
||||
streaming.tcpSettings.header.type = type;
|
||||
}
|
||||
else if (net == "http" || net == "h2")
|
||||
{
|
||||
// Fill hosts for HTTP
|
||||
for (const auto &_host : host.split(','))
|
||||
{
|
||||
if (!_host.isEmpty())
|
||||
{
|
||||
streaming.httpSettings.host << _host.trimmed();
|
||||
}
|
||||
}
|
||||
|
||||
streaming.httpSettings.path = path;
|
||||
}
|
||||
else if (net == "ws")
|
||||
{
|
||||
if (!host.isEmpty())
|
||||
streaming.wsSettings.headers["Host"] = host;
|
||||
streaming.wsSettings.path = path;
|
||||
}
|
||||
else if (net == "kcp")
|
||||
{
|
||||
streaming.kcpSettings.header.type = type;
|
||||
}
|
||||
else if (net == "quic")
|
||||
{
|
||||
streaming.quicSettings.security = host;
|
||||
streaming.quicSettings.header.type = type;
|
||||
streaming.quicSettings.key = path;
|
||||
}
|
||||
else if (net == "grpc")
|
||||
{
|
||||
streaming.grpcSettings.serviceName = path;
|
||||
}
|
||||
|
||||
streaming.security = tls;
|
||||
if (tls == "tls")
|
||||
{
|
||||
if (sni.isEmpty() && !host.isEmpty())
|
||||
sni = host;
|
||||
streaming.tlsSettings.serverName = sni;
|
||||
streaming.tlsSettings.allowInsecure = false;
|
||||
}
|
||||
//
|
||||
// Network type
|
||||
// NOTE(DuckSoft): Damn vmess:// just don't write 'http' properly
|
||||
if (net == "h2")
|
||||
net = "http";
|
||||
streaming.network = net;
|
||||
//
|
||||
// VMess root config
|
||||
QJsonObject vConf;
|
||||
vConf["vnext"] = QJsonArray{ serv.toJson() };
|
||||
const auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, streaming.toJson());
|
||||
QJsonObject inbound = inbounds::GenerateInboundEntry();
|
||||
root["outbounds"] = QJsonArray{ outbound };
|
||||
root["inbounds"] = QJsonArray{ inbound };
|
||||
// If previous alias is empty, just the PS is needed, else, append a "_"
|
||||
*alias = alias->trimmed().isEmpty() ? ps : *alias + "_" + ps;
|
||||
return root;
|
||||
#undef default
|
||||
}
|
||||
} // namespace amnezia::serialization::vmess
|
||||
|
||||
172
client/core/serialization/vmess_new.cpp
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Qv2ray, A Qt frontend for V2Ray. Written in C++.
|
||||
// This file is part of the Qv2ray VPN client.
|
||||
//
|
||||
// Qv2ray, A Qt frontend for V2Ray. Written in C++
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Copyright (c) 2024 AmneziaVPN
|
||||
// This file has been modified for AmneziaVPN
|
||||
//
|
||||
// This file is based on the work of the Qv2ray VPN client.
|
||||
// The original code of the Qv2ray, A Qt frontend for V2Ray. Written in C++ and licensed under GPL3.
|
||||
//
|
||||
// The modified version of this file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this file. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||
#include "3rd/QJsonStruct/QJsonStruct.hpp"
|
||||
#include "transfer.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#include <QUrlQuery>
|
||||
|
||||
#define OUTBOUND_TAG_PROXY "PROXY"
|
||||
|
||||
namespace amnezia::serialization::vmess_new
|
||||
{
|
||||
const static QStringList NetworkType{ "tcp", "http", "ws", "kcp", "quic", "grpc" };
|
||||
const static QStringList QuicSecurityTypes{ "none", "aes-128-gcm", "chacha20-poly1305" };
|
||||
const static QStringList QuicKcpHeaderTypes{ "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" };
|
||||
const static QStringList FalseTypes{ "false", "False", "No", "Off", "0" };
|
||||
|
||||
QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMessage)
|
||||
{
|
||||
QUrl url{ vmessStr };
|
||||
QUrlQuery query{ url };
|
||||
//
|
||||
#define default QJsonObject()
|
||||
if (!url.isValid())
|
||||
{
|
||||
*errMessage = QObject::tr("vmess:// url is invalid");
|
||||
return default;
|
||||
}
|
||||
|
||||
// If previous alias is empty, just the PS is needed, else, append a "_"
|
||||
const auto name = url.fragment(QUrl::FullyDecoded).trimmed();
|
||||
*alias = alias->isEmpty() ? name : (*alias + "_" + name);
|
||||
|
||||
VMessServerObject server;
|
||||
server.users << VMessServerObject::UserObject{};
|
||||
|
||||
StreamSettingsObject stream;
|
||||
QString net;
|
||||
bool tls = false;
|
||||
// Check streamSettings
|
||||
{
|
||||
for (const auto &_protocol : url.userName().split("+"))
|
||||
{
|
||||
if (_protocol == "tls")
|
||||
tls = true;
|
||||
else
|
||||
net = _protocol;
|
||||
}
|
||||
if (!NetworkType.contains(net))
|
||||
{
|
||||
*errMessage = QObject::tr("Invalid streamSettings protocol: ") + net;
|
||||
return default;
|
||||
}
|
||||
stream.network = net;
|
||||
stream.security = tls ? "tls" : "";
|
||||
}
|
||||
// Host Port UUID AlterID
|
||||
{
|
||||
const auto host = url.host();
|
||||
int port = url.port();
|
||||
QString uuid;
|
||||
int aid;
|
||||
{
|
||||
const auto pswd = url.password();
|
||||
const auto index = pswd.lastIndexOf("-");
|
||||
uuid = pswd.mid(0, index);
|
||||
aid = pswd.right(pswd.length() - index - 1).toInt();
|
||||
}
|
||||
server.address = host;
|
||||
server.port = port;
|
||||
server.users.first().id = uuid;
|
||||
server.users.first().alterId = aid;
|
||||
server.users.first().security = "auto";
|
||||
}
|
||||
|
||||
const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) {
|
||||
if (query.hasQueryItem(key))
|
||||
return query.queryItemValue(key, QUrl::FullyDecoded);
|
||||
else
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
//
|
||||
// Begin transport settings parser
|
||||
{
|
||||
if (net == "tcp")
|
||||
{
|
||||
stream.tcpSettings.header.type = getQueryValue("type", "none");
|
||||
}
|
||||
else if (net == "http")
|
||||
{
|
||||
stream.httpSettings.host.append(getQueryValue("host", ""));
|
||||
stream.httpSettings.path = getQueryValue("path", "/");
|
||||
}
|
||||
else if (net == "ws")
|
||||
{
|
||||
stream.wsSettings.headers["Host"] = getQueryValue("host", "");
|
||||
stream.wsSettings.path = getQueryValue("path", "/");
|
||||
}
|
||||
else if (net == "kcp")
|
||||
{
|
||||
stream.kcpSettings.seed = getQueryValue("seed", "");
|
||||
stream.kcpSettings.header.type = getQueryValue("type", "none");
|
||||
}
|
||||
else if (net == "quic")
|
||||
{
|
||||
stream.quicSettings.security = getQueryValue("security", "none");
|
||||
stream.quicSettings.key = getQueryValue("key", "");
|
||||
stream.quicSettings.header.type = getQueryValue("type", "none");
|
||||
}
|
||||
else if (net == "grpc")
|
||||
{
|
||||
stream.grpcSettings.serviceName = getQueryValue("serviceName", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
*errMessage = QObject::tr("Unknown transport method: ") + net;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
#undef default
|
||||
if (tls)
|
||||
{
|
||||
stream.tlsSettings.allowInsecure = !FalseTypes.contains(getQueryValue("allowInsecure", "false"));
|
||||
stream.tlsSettings.serverName = getQueryValue("tlsServerName", "");
|
||||
}
|
||||
QJsonObject root;
|
||||
QJsonObject vConf;
|
||||
QJsonArray vnextArray;
|
||||
vnextArray.append(server.toJson());
|
||||
vConf["vnext"] = vnextArray;
|
||||
auto outbound = outbounds::GenerateOutboundEntry(OUTBOUND_TAG_PROXY, "vmess", vConf, stream.toJson());
|
||||
QJsonObject inbound = inbounds::GenerateInboundEntry();
|
||||
|
||||
//
|
||||
root["outbounds"] = QJsonArray{ outbound };
|
||||
root["inbound"] = QJsonArray{ inbound };
|
||||
return root;
|
||||
}
|
||||
|
||||
} // namespace amnezia::serialization::vmess_new
|
||||