Compare commits

..

1 Commits

Author SHA1 Message Date
Vladyslav Miachkov
dad2dc0ab0 Hide error on clear server button 2024-04-06 23:07:46 +03:00
1375 changed files with 32882 additions and 158014 deletions

View File

@@ -1,39 +0,0 @@
BasedOnStyle: WebKit
AccessModifierOffset: '-4'
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: 'true'
AlignTrailingComments: 'true'
AllowAllArgumentsOnNextLine: 'true'
AllowAllParametersOfDeclarationOnNextLine: 'true'
AllowShortBlocksOnASingleLine: 'false'
AllowShortCaseLabelsOnASingleLine: 'true'
AllowShortEnumsOnASingleLine: 'false'
AllowShortFunctionsOnASingleLine: None
AlwaysBreakTemplateDeclarations: 'No'
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakConstructorInitializers: BeforeColon
ColumnLimit: '120'
CommentPragmas: '"^!|^:"'
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
ConstructorInitializerIndentWidth: '4'
ContinuationIndentWidth: '8'
IndentPPDirectives: BeforeHash
NamespaceIndentation: All
PenaltyExcessCharacter: '10'
PointerAlignment: Right
SortIncludes: 'true'
SpaceAfterTemplateKeyword: 'false'
Standard: Auto

View File

@@ -1,20 +0,0 @@
/client/3rd
/client/3rd-prebuild
/client/android
/client/cmake
/client/core/utils/serialization
/client/daemon
/client/fonts
/client/images
/client/ios
/client/mozilla
/client/platforms/dummy
/client/platforms/linux
/client/platforms/macos
/client/platforms/windows
/client/server_scripts
/client/translations
/deploy
/docs
/metadata
/service/src

View File

@@ -1,38 +0,0 @@
# .github/actions/apple-install-cert/action.yml
name: Setup apple keychain
description: Creates and configures a temporary build keychain
inputs:
keychain-path:
description: Path to the keychain
required: true
keychain-password:
description: Password to the keychain
required: true
cert-base64:
description: Base64-encoded certificate
required: true
cert-password:
description: Certificate password
required: true
runs:
using: composite
steps:
- name: Create keychain
shell: bash
env:
KEYCHAIN_PATH: ${{ inputs.keychain-path }}
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
CERT_BASE64: ${{ inputs.cert-base64 }}
CERT_PASSWORD: ${{ inputs.cert-password }}
run: |
CERT_PATH=$(mktemp /tmp/cert_XXXXXX.p12)
trap "rm -f '$CERT_PATH'" EXIT
echo -n "$CERT_BASE64" | base64 --decode -o "$CERT_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$CERT_PASSWORD" -A -t cert -f pkcs12
security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

View File

@@ -1,57 +0,0 @@
# .github/actions/apple-setup-keychain/action.yml
name: Setup apple keychain
description: Creates and configures a temporary build keychain
inputs:
keychain-name:
description: Name of the keychain
required: false
default: "ci-amnezia"
keychain-password:
description: The keychain password
required: true
lock-timeout:
description: A timeout after exceeding which the keychain would be locked
required: false
default: "0"
outputs:
keychain-path:
description: "Full path to the keychain created"
value: ${{ steps.setup.outputs.keychain-path }}
keychain-name:
description: "Actual name of the keychain created"
value: ${{ steps.setup.outputs.keychain-name }}
runs:
using: composite
steps:
- name: Setup keychain
id: setup
shell: bash
env:
KEYCHAIN_NAME: ${{ inputs.keychain-name }}
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
LOCK_TIMEOUT: ${{ inputs.lock-timeout }}
run: |
KEYCHAIN_PATH="$HOME/Library/Keychains/$KEYCHAIN_NAME.keychain-db"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
if [[ "$LOCK_TIMEOUT" == "0" ]]; then
security set-keychain-settings "$KEYCHAIN_PATH"
else
security set-keychain-settings -u -t "$LOCK_TIMEOUT" "$KEYCHAIN_PATH"
fi
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "${{ github.action_path }}/DeveloperIDG2CA.cer" -k "$KEYCHAIN_PATH" -A
security import "${{ github.action_path }}/AppleWWDRCAG3.cer" -k "$KEYCHAIN_PATH" -A
security list-keychains -d user -s "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
echo "keychain-name=$KEYCHAIN_NAME" >> $GITHUB_OUTPUT
echo "keychain-path=$KEYCHAIN_PATH" >> $GITHUB_OUTPUT

View File

@@ -1,31 +0,0 @@
# .github/actions/apple-setup-provisioning-profile/action.yml
name: Setup provisioning profiles
description: Decodes and installs provisioning profiles
inputs:
provisioning_profile_base64:
description: Base64-encoded provisioning profile
required: true
runs:
using: composite
steps:
- name: Setup provisioning profile
shell: bash
run: |
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
TEMP_FILE=$(mktemp)
echo "${{ inputs.provisioning_profile_base64 }}" | base64 --decode > "$TEMP_FILE"
PROFILE_UUID=$(grep UUID -A1 -a "$TEMP_FILE" | grep -io "[-A-F0-9]\{36\}")
if [[ -z "$PROFILE_UUID" ]]; then
echo "Failed to extract UUID from provisioning profile"
rm -f "$TEMP_FILE"
exit 1
fi
mkdir -p "$PROFILES_DIR"
mv "$TEMP_FILE" "$PROFILES_DIR/$PROFILE_UUID.mobileprovision"
echo "Installed profile: $PROFILE_UUID"

File diff suppressed because it is too large Load Diff

View File

@@ -15,14 +15,6 @@ jobs:
env:
QT_VERSION: 6.4.1
QIF_VERSION: 4.5
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
FREE_V2_ENDPOINT: ${{ secrets.FREE_V2_ENDPOINT }}
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
steps:
- name: 'Install desktop Qt'

View File

@@ -1,41 +1,64 @@
name: 'Upload a new version'
on:
workflow_dispatch:
inputs:
RELEASE_VERSION:
description: 'Release version (e.g. 1.2.3.4)'
required: true
type: string
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+.[0-9]+'
jobs:
Upload-S3:
upload:
runs-on: ubuntu-latest
name: upload
steps:
- name: Checkout
- name: Checkout CMakeLists.txt
uses: actions/checkout@v4
with:
ref: ${{ inputs.RELEASE_VERSION }}
ref: ${{ github.ref_name }}
sparse-checkout: |
CMakeLists.txt
deploy/deploy_s3.sh
sparse-checkout-cone-mode: false
- name: Verify git tag
run: |
TAG_NAME=${{ inputs.RELEASE_VERSION }}
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
GIT_TAG=${{ github.ref_name }}
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
if [[ "$GIT_TAG" == "$CMAKE_TAG" ]]; then
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are the same. Continuing..."
else
echo "::error::Mismatch: Git tag ($TAG_NAME) != CMakeLists.txt version ($CMAKE_TAG). Exiting with error..."
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are not the same! Cancelling..."
exit 1
fi
- name: Setup Rclone
uses: AnimMouse/setup-rclone@v1
- name: Download artifacts from the "${{ github.ref_name }}" tag
uses: robinraju/release-downloader@v1.8
with:
rclone_config: ${{ secrets.RCLONE_CONFIG }}
tag: ${{ github.ref_name }}
fileName: "AmneziaVPN_(Linux_|)${{ github.ref_name }}*"
out-file-path: ${{ github.ref_name }}
- name: Send dist to S3
run: bash deploy/deploy_s3.sh ${{ inputs.RELEASE_VERSION }}
- name: Upload beta version
uses: jakejarvis/s3-sync-action@master
if: contains(github.event.base_ref, 'dev')
with:
args: --include "AmneziaVPN*" --delete
env:
AWS_S3_BUCKET: updates
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: https://${{ vars.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com
SOURCE_DIR: ${{ github.ref_name }}
DEST_DIR: beta/${{ github.ref_name }}
- name: Upload stable version
uses: jakejarvis/s3-sync-action@master
if: contains(github.event.base_ref, 'master')
with:
args: --include "AmneziaVPN*" --delete
env:
AWS_S3_BUCKET: updates
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: https://${{ vars.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com
SOURCE_DIR: ${{ github.ref_name }}
DEST_DIR: stable/${{ github.ref_name }}

12
.gitignore vendored
View File

@@ -9,7 +9,6 @@ deploy/build_32/*
deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
# Qt-es
@@ -81,7 +80,6 @@ client/.DS_Store
._.DS_Store
._*
*.dmg
deploy/data/macos/pf/amn.400.allowPIA.conf
# tmp files
*.*~
@@ -135,12 +133,4 @@ client/3rd/ShadowSocks/ss_ios.xcconfig
out/
# CMake files
CMakeFiles/
ios-ne-build.sh
macos-ne-build.sh
macos-signed-build.sh
macos-with-sign-build.sh
DeveloperIdApplicationCertificate.p12
DeveloperIdInstallerCertificate.p12
CMakeFiles/

13
.gitmodules vendored
View File

@@ -1,16 +1,15 @@
[submodule "client/3rd/OpenVPNAdapter"]
path = client/3rd/OpenVPNAdapter
url = https://github.com/amnezia-vpn/OpenVPNAdapter.git
[submodule "client/3rd/qtkeychain"]
path = client/3rd/qtkeychain
url = https://github.com/frankosterfeld/qtkeychain.git
[submodule "client/3rd/SortFilterProxyModel"]
path = client/3rd/SortFilterProxyModel
url = https://github.com/mitchcurtis/SortFilterProxyModel.git
[submodule "client/3rd-prebuilt"]
path = client/3rd-prebuilt
url = https://github.com/amnezia-vpn/3rd-prebuilt
[submodule "client/3rd/amneziawg-apple"]
path = client/3rd/amneziawg-apple
url = https://github.com/amnezia-vpn/amneziawg-apple
[submodule "client/3rd/QSimpleCrypto"]
path = client/3rd/QSimpleCrypto
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
[submodule "client/3rd/qtgamepad"]
path = client/3rd/qtgamepad
url = https://github.com/amnezia-vpn/qtgamepad.git
branch = 6.6

View File

@@ -1,34 +1,17 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.9.0.2)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
${CMAKE_SOURCE_DIR}/cmake/platform_settings.cmake
${CMAKE_SOURCE_DIR}/cmake/recipes_bootstrap.cmake
${CMAKE_SOURCE_DIR}/cmake/conan_provider.cmake
CACHE STRING "" FORCE)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
project(${PROJECT} VERSION 4.5.0.0
DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/"
)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
return()
endif()
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 2123)
set(APP_ANDROID_VERSION_CODE 50)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
@@ -45,34 +28,17 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
endif()
set(QT_BUILD_TOOLS_WHEN_CROSS_COMPILING ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(APPLE AND NOT IOS)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(AMN_PF_RULE_IDENTITY "user { root }")
else()
set(AMN_PF_RULE_IDENTITY "group { amnvpn }")
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in"
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
@ONLY
)
file(COPY_FILE
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
"${CMAKE_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf"
ONLY_IF_DIFFERENT
)
set(CMAKE_OSX_ARCHITECTURES "x86_64")
endif()
add_subdirectory(client)
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
if(NOT IOS AND NOT ANDROID)
add_subdirectory(service)
endif()
if ((LINUX AND NOT ANDROID) OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (WIN32))
include(${CMAKE_SOURCE_DIR}/cmake/CPack.cmake)
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
endif()

218
README.md
View File

@@ -1,51 +1,25 @@
# Amnezia VPN
### _The best client for self-hosted VPN_
## _The best client for self-hosted VPN_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md)
[Amnezia](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-en) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
### [Website](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-en) | [Alt website link](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-en-mirror) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting)
> [!TIP]
> If the [Amnezia website](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-en) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-en-mirror).
<a href="https://amnezia.org/en/downloads?utm_source=github&utm_campaign=amnezia_button-readme-en"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
<a href="https://storage.googleapis.com/amnezia/amnezia.org?m-path=/en/downloads&utm_source=github&utm_campaign=amnezia_button-readme-en-mirrow"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-alt.svg" width="150" style="max-width: 100%;"></a>
[All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
<br/>
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
## Features
- Very easy to use - enter your IP address, SSH login, password and Amnezia will automatically install VPN docker containers to your server and connect to the VPN.
- Classic VPN-protocols: OpenVPN, WireGuard and IKEv2 protocols.
- Protocols with traffic Masking (Obfuscation): OpenVPN over [Cloak](https://github.com/cbeuw/Cloak) plugin, Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
- Split tunneling support - add any sites to the client to enable VPN only for them or add Apps (only for Android and Desktop).
- 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.
- 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.
- Support for AmneziaWG protocol configuration on [Keenetic beta firmware](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
## Links
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium
[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)
## Tech
@@ -53,26 +27,11 @@ AmneziaVPN uses several open-source projects to work:
- [OpenSSL](https://www.openssl.org/)
- [OpenVPN](https://openvpn.net/)
- [ShadowSocks](https://shadowsocks.org/)
- [Qt](https://www.qt.io/)
- [LibSsh](https://libssh.org)
- [WireGuard](https://www.wireguard.com/)
- [Xray-core](https://xtls.github.io/en/)
- [Conan](https://conan.io/)
- [LibSsh](https://libssh.org) - forked from Qt Creator
- and more...
## Help us 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.
## Checking out the source code
Make sure to pull all submodules after checking out the repo.
@@ -81,104 +40,113 @@ Make sure to pull all submodules after checking out the repo.
git submodule update --init --recursive
```
## Hacking guide
## Development
Want to contribute? Welcome!
### Build requirements
### Building sources and deployment
* [`CMake`](https://cmake.org/download/)
* Compiler and underlying build system, depending on the target:
- [Linux] Any of `make` and `gcc`
- [Apple] [`Xcode`](https://developer.apple.com/xcode/) or [`Xcode command line tools`](https://developer.apple.com/xcode/)
- [Windows] [`Visual Studio 2022`](https://aka.ms/vs/17/release/vs_community.exe) or [`VS 2022 Build Tools`](https://aka.ms/vs/17/release/vs_buildtools.exe)
- [Android] [`Android SDK`](#installing-android-sdk) and [`Ninja`](https://ninja-build.org/)
* [`Qt 6.10+`](https://www.qt.io/download-open-source) with the following modules:
- Core module for targeting platform (Desktop/Android/iOS)
- Qt 5 Compatibility module
- Qt Remote Objects
* [`Conan`](https://conan.io/downloads) package manager
- On MacOS is enough just to use `homebrew` or install it in `.venv` in project root
- Other systems must have it in `PATH`
* (Optional) Installer dependencies:
- [Windows/Linux] [`Qt Installer Framework`](https://www.qt.io/download-open-source)
- [Windows] [`WIX toolset`](https://github.com/wixtoolset/wix/releases)
Check deploy folder for build scripts.
### Building the project using scripts
### How to build an iOS app from source code on MacOS
* Run scripts located in `deploy` directory
* Basically, if dependencies are located in default installation paths, the scripts will find them automatically.
* If they differ, specify them using the following variables:
- `QT_INSTALL_DIR` - Qt root installation folder
- `QT_ROOT_PATH` - Qt framework root directory
- `QIF_ROOT_PATH` - Qt Installer Framework root path
- `ANDROID_HOME` - Path to Android SDK root folder
- and others. Check scripts for more
1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher.
Unix-like:
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:
- MacOS
- iOS
- Qt 5 Compatibility Module
- Qt Shader Tools
- Additional Libraries:
- Qt Image Formats
- Qt Multimedia
- Qt Remote Objects
3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/)
4. You also need to install go >= v1.16. If you don't have it installed already,
download go from the [official website](https://golang.org/dl/) or use Homebrew.
The latest version is recommended. Install gomobile
```bash
# Build executables for the host platform
deploy/build.sh
# Or just
deploy/build.sh
# Build executables and installers for the host platform
deploy/build.sh --installer all
# Build Android APK and AAB
deploy/build.sh -t android --aab
# Call for help
deploy/build.sh -h
export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
Windows:
```batch
:: Build executables for Windows
deploy/build.bat
5. Build the project
```bash
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
export QT_IOS_BIN=$QT_BIN_DIR
export PATH=$PATH:~/go/bin
mkdir build-ios
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
```
Replace PATH-TO-QT-FOLDER and QT-VERSION to your environment
:: Build executables with IFW installer for Windows
deploy/build.bat --installer ifw
:: Build executables with IFW and WIX installer for Windows
deploy/build.bat --installer ifw --installer wix
:: Or just
deploy/build.bat --installer all
If you get `gomobile: command not found` make sure to set PATH to the location
of the bin folder where gomobile was installed. Usually, it's in `GOPATH`.
```bash
export PATH=$(PATH):/path/to/GOPATH/bin
```
### Developing the project in IDEs
6. Open the XCode project. You can then run /test/archive/ship the app.
* Basically, you can use any IDE that handles CMake and Qt kits properly to run configure and build steps, and to navigate through the code nicely. For example:
- `Qt Creator`
- `Visual Studio Code` with `Qt Extension Pack`
- and so on
If the build fails with the following error
```
make: ***
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
Error 1
```
Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with
key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
* To use `Xcode`, you have to configure project first by using `cmake`. The easiest way to do it is to use `Qt Creator` for configuration. Then open `AmneziaVPN.xcodeproj` file from the build folder by using `Xcode`. Note that none of the files changed are saved - the files actually getting changed in build directory. Copy them manually if necessary
if the above error persists on your M1 Mac, then most probably you need to install arch based CMake
```
arch -arm64 brew install cmake
```
* `Android studio` could be used in the same way - just configure the project by using `cmake` manually or by using `Qt Creator`. Open `<build-dir>/client/android-build` in `Android studio` then. Do not forget to copy the changes - everything you do is saved under the build directory actually.
Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that
require them. In this case, simply restart the build.
### Installing Android SDK
## How to build the Android app
* Android SDK could be installed using the following methods:
- Using `Qt Creator`. Use `Preferences`->`SDKs`
- Using `Android studio`. By default it installs necessary `SDKs` automatically during the installation
- Manually by using `sdk-manager`. Check [this](https://developer.android.com/tools) page for details
_Tested on Mac OS_
The Android app has the following requirements:
* JDK 11
* Android platform SDK 33
* CMake 3.25.0
After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`.
* set path to JDK 11
* set path to Android SDK ($ANDROID_HOME)
In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine! 
Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <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. 
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`.
That's it! You should be ready to compile the project from QT Creator!
### Development flow
After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes!
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`<path>/client/android-build/.`) and you should be good to go.
## License
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
GPL v3.0
## Donate
Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn)
Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
payeer.com: P2561305
ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn)
Bitcoin: bc1qmhtgcf9637rl3kqyy22r2a8wa8laka4t9rx2mf <br>
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
## Acknowledgments
This project is tested with BrowserStack.

View File

@@ -1,180 +0,0 @@
# Amnezia VPN
### _Лучший клиент для создания VPN на собственном сервере_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский
[AmneziaVPN](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-ru) — это open source VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
### [Сайт](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-ru) | [Зеркало сайта](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-ru-mirror) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
> [!TIP]
> Если [сайт Amnezia](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-ru) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-ru-mirror).
<a href="https://storage.googleapis.com/amnezia/amnezia.org?m-path=/ru/downloads&utm_source=github&utm_campaign=amnezia_button-readme-ru-mirror"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website-ru.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
<br/>
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
## Особенности
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
- Поддерживает платформы: Windows, macOS, Linux, Android, iOS.
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
## Ссылки
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
## Технологии
AmneziaVPN использует несколько проектов с открытым исходным кодом:
- [OpenSSL](https://www.openssl.org/)
- [OpenVPN](https://openvpn.net/)
- [Qt](https://www.qt.io/)
- [LibSsh](https://libssh.org)
- [WireGuard](https://www.wireguard.com/)
- [Xray-core](https://xtls.github.io/en/)
- [Conan](https://conan.io/)
- и другие...
## Помощь с переводами
Загрузите самые актуальные файлы перевода.
Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations".
Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка.
Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом.
## Проверка исходного кода
После клонирования репозитория обязательно загрузите все подмодули.
```bash
git submodule update --init --recursive
```
## Руководство по разработке
Хотите внести свой вклад? Добро пожаловать!
### Требования для сборки
* [`CMake`](https://cmake.org/download/)
* Компилятор и система сборки, в зависимости от таргета:
- [Linux] Любые `make` и `gcc`
- [Apple] [`Xcode`](https://developer.apple.com/xcode/) или [`Xcode command line tools`](https://developer.apple.com/xcode/)
- [Windows] [`Visual Studio 2022`](https://aka.ms/vs/17/release/vs_community.exe) или [`VS 2022 Build Tools`](https://aka.ms/vs/17/release/vs_buildtools.exe)
- [Android] [`Android SDK`](#установка-android-sdk) и [`Ninja`](https://ninja-build.org/)
* [`Qt 6.10+`](https://www.qt.io/download-open-source) со следующими модулями:
- Основные модули для таргета (Desktop/Android/iOS)
- Qt 5 Compatibility module
- Qt Remote Objects
* Пакетный менеджер [`Conan`](https://conan.io/downloads)
- На MacOS достаточно использовать `homebrew` или установить в `.venv` в корень проекта
- Для остальных систем необходимо прописать пути в `PATH`
* (Необязательно) Заивисимости для установщиков:
- [Windows/Linux] [`Qt Installer Framework`](https://www.qt.io/download-open-source)
- [Windows] [`WIX toolset`](https://github.com/wixtoolset/wix/releases)
### Сборка проекта через скрипты
* Запустите скрипты, находящиеся в папке `deploy`
* Если все зависимости установлены в стандартных локациях, скрипт найдёт их самостоятельно
* Если пути отличаются, их нужно явно указать используя:
- `QT_INSTALL_DIR` - корневая папка установки Qt
- `QT_ROOT_PATH` - корневая папка Qt Framework
- `QIF_ROOT_PATH` - корневая папка Qt Installer Framework
- `ANDROID_HOME` - путь к Android SDK
- и другие. Их можно получить из вышеуказанных скриптов
Unix-like:
```bash
# Build executables for the host platform
deploy/build.sh
# Or just
deploy/build.sh
# Build executables and installers for the host platform
deploy/build.sh --installer all
# Build Android APK and AAB
deploy/build.sh -t android --aab
# Call for help
deploy/build.sh -h
```
Windows:
```batch
:: Build executables for Windows
deploy/build.bat
:: Build executables with IFW installer for Windows
deploy/build.bat --installer ifw
:: Build executables with IFW and WIX installer for Windows
deploy/build.bat --installer ifw --installer wix
:: Or just
deploy/build.bat --installer all
```
### Разработка в IDE
* Можно использовать любые IDE которые умеют работать с CMake и находить Qt Kits. Например:
- `Qt Creator`
- `Visual Studio Code` with `Qt Extension Pack`
- и так далее
* Для использования `Xcode` нужно сконфигурировать проект с помощью `cmake`. Самый простой способ это сделать - использовать `Qt Creator` для конфигурации. Затем, нужно открыть файл `AmneziaVPN.xcodeproj` из папки сборки с помощью `Xcode`. Учтите, что никакие файлы фактически не сохраняются - они сохраняются в директории сборки. Если требуется, скопируйте файлы вручную
* `Android studio` может быть использована подобным вышеуказанному способу - нужно использовать `cmake` вручную или через `Qt Creator` для конфигурации. Далее, откройте `<build-dir>/client/android-build` в `Android studio`. Не забудьте скопировать изменённые файлы в папку с исходным кодом - все файлы, изменённые в IDE, сохраняются фактически в папке сборки.
### Установка Android SDK
* Android SDK может быть установлен следующими способами:
- Используя `Qt Creator`, через настройки в пунктах `Preferences`->`SDKs`
- Используя `Android studio`. По умолчанию необходимые `SDK` устанавливаются автоматически.
- Вручную, используя `sdk-manager`. Подробности можно найти [здесь](https://developer.android.com/tools)
## Лицензия
GPL v3.0
## Донаты
Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn)
Bitcoin: bc1qmhtgcf9637rl3kqyy22r2a8wa8laka4t9rx2mf <br>
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
## Благодарности
Этот проект тестируется с помощью BrowserStack.
Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта.

View File

@@ -1,149 +0,0 @@
# Third-Party Licenses
This project is licensed under the GNU General Public License v3.0.
This file lists third-party software components used by this repository.
Each component is distributed under its own license as linked below.
---
## QtKeychain
- Source: https://github.com/frankosterfeld/qtkeychain
- License: BSD License
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
---
## QSimpleCrypto
- Source: https://github.com/n1flh31mur/QSimpleCrypto
- License: Apache License 2.0
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
---
## SortFilterProxyModel
- Source: https://github.com/oKcerG/SortFilterProxyModel
- License: MIT License
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
---
## QJsonStruct
- Source: https://github.com/Qv2ray/QJsonStruct
- License: MIT License
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
---
## QR Code Generator (qrcodegen)
- Source: https://github.com/nayuki/QR-Code-generator
- License: MIT License
- License Text: https://www.nayuki.io/page/qr-code-generator-library
---
## Qt Gamepad
- Source: https://github.com/qt/qtgamepad
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
---
## AmneziaWG Apple (WireGuard)
- Source: https://github.com/amnezia-vpn/amneziawg-apple
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
---
## AmneziaWG Android
- Source: https://github.com/amnezia-vpn/amneziawg-go
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
---
## Xray Core
- Source: https://github.com/XTLS/Xray-core
- License: Mozilla Public License 2.0 (MPL-2.0)
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
---
## Cloak
- Source: https://github.com/cbeuw/Cloak
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
---
## Shadowsocks
- Source: https://github.com/shadowsocks/shadowsocks-libev
- License: GPL-3.0-or-later
- License Text: http://www.gnu.org/licenses/
---
## OpenSSL
- Source: https://github.com/openssl/openssl
- License: Apache License 2.0
- License Text: https://www.openssl.org/source/license.html
---
## libssh
- Source: https://www.libssh.org/
- License: GNU Lesser General Public License (LGPL)
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
---
## OpenVPNAdapter
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
---
## Wintun
- Source: https://www.wintun.net/
- License: Prebuilt Binaries License
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
---
## Mullvad Split Tunnel Driver
- Source: https://github.com/mullvad/win-split-tunnel
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
---
## tun2socks
- Source: https://github.com/eycorsican/go-tun2socks
- License: MIT License
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
---
## TAP-Windows Driver
- Source: https://github.com/OpenVPN/tap-windows6
- License: tap-windows6 license
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING

14
agw-sdk/.gitignore vendored
View File

@@ -1,14 +0,0 @@
# Локальные сборки
build/
build-*/
cmake-build-*/
# Conan
CMakeUserPresets.json
conan.lock
# Примеры
examples/dart_smoke/.dart_tool/
examples/dart_smoke/pubspec.lock
examples/c_smoke/smoke
test_package/

View File

@@ -1,133 +0,0 @@
cmake_minimum_required(VERSION 3.21)
project(agw LANGUAGES CXX VERSION 0.1.0)
# --- стандарт ---------------------------------------------------------------
# floor C++20 (см. решения в docs/plans/gateway-sdk/README.md). C++23 — opt-in позже.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()
# --- опции ------------------------------------------------------------------
# Когда SDK подключают через add_subdirectory (отладочная сборка в клиенте), по умолчанию НЕ строим
# тесты и НЕ строим shared C-ABI, чтобы не пачкать родительскую сборку. Standalone — всё включено.
if(PROJECT_IS_TOP_LEVEL)
set(_agw_default_aux ON)
else()
set(_agw_default_aux OFF)
endif()
option(AGW_BUILD_TESTS "Build agw-sdk tests" ${_agw_default_aux})
# Режимы зависимостей (Фаза 5): shared-deps — общий OpenSSL из Conan; vendored — бандл.
# На Фазе 1 определяем only-флаг, реальный механизм бандла появится в Фазе 5.
set(AGW_DEPS_MODE "shared-deps" CACHE STRING "Dependency mode: shared-deps | vendored")
# --- зависимости ------------------------------------------------------------
find_package(OpenSSL REQUIRED)
find_package(Threads REQUIRED)
# libcurl (транспорт по умолчанию). Если не найден — SDK собирается без него, а клиент обязан
# передать свой IHttpClient через Config (см. default_client_fallback.cpp).
find_package(CURL QUIET)
# nlohmann/json: предпочтительно из Conan (find_package), иначе — вендоренный single-header
# (он лежит в tests/third_party и нужен только для локальной сборки без Conan).
find_package(nlohmann_json QUIET)
if(NOT nlohmann_json_FOUND)
add_library(agw_nlohmann_fallback INTERFACE)
target_include_directories(agw_nlohmann_fallback INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/tests/third_party)
add_library(nlohmann_json::nlohmann_json ALIAS agw_nlohmann_fallback)
message(STATUS "agw: using vendored nlohmann/json fallback (tests/third_party)")
endif()
# --- библиотека -------------------------------------------------------------
# Один раз компилируем объекты, из них собираем static (agw) и shared C-ABI (agw_capi).
add_library(agw_obj OBJECT
src/gateway_controller.cpp
src/c_abi.cpp
src/crypto/rng.cpp
src/crypto/aes.cpp
src/crypto/rsa.cpp
src/crypto/hash.cpp
src/http/curl_client.cpp
src/http/default_client_fallback.cpp
src/protocol/request_builder.cpp
src/protocol/response.cpp
src/protocol/error_mapping.cpp
src/failover/bypass_policy.cpp
src/failover/proxy_list.cpp
src/failover/proxy_picker.cpp
src/util/base64.cpp
src/util/uuid.cpp
src/util/json.cpp
src/util/url.cpp
src/util/thread_pool.cpp
)
target_include_directories(agw_obj
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
)
# PUBLIC, чтобы зависимости и include-пути дотекли до static/shared (и до тестов).
target_link_libraries(agw_obj
PUBLIC OpenSSL::Crypto nlohmann_json::nlohmann_json Threads::Threads
)
# Скрываем всё по умолчанию: наружу торчат только agw_* (AGW_API = visibility default).
set_target_properties(agw_obj PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
POSITION_INDEPENDENT_CODE ON
)
if(CURL_FOUND)
target_compile_definitions(agw_obj PRIVATE AGW_HAVE_CURL)
target_link_libraries(agw_obj PUBLIC CURL::libcurl)
message(STATUS "agw: libcurl found — default HTTP client enabled")
else()
message(STATUS "agw: libcurl NOT found — default HTTP client disabled (inject IHttpClient via Config)")
endif()
# Статическая библиотека (C++ API) — для наших приложений / тестов.
add_library(agw STATIC)
target_link_libraries(agw PUBLIC agw_obj)
add_library(agw::agw ALIAS agw)
# Shared C-ABI библиотека (для dart:ffi и сторонних). Экспортирует только agw_*.
option(AGW_BUILD_CAPI_SHARED "Build shared C-ABI library (agw_capi)" ${_agw_default_aux})
if(AGW_BUILD_CAPI_SHARED)
add_library(agw_capi SHARED $<TARGET_OBJECTS:agw_obj>)
target_link_libraries(agw_capi PRIVATE agw_obj)
target_compile_definitions(agw_capi PRIVATE AGW_BUILDING_SHARED)
set_target_properties(agw_capi PROPERTIES OUTPUT_NAME agw_capi)
message(STATUS "agw: deps mode = ${AGW_DEPS_MODE} (vendored — статическая линковка/скрытие символов, через Conan)")
endif()
set_target_properties(agw PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
POSITION_INDEPENDENT_CODE ON
)
# --- установка (только для Conan-пакета / standalone; не при add_subdirectory) ---------------
if(PROJECT_IS_TOP_LEVEL)
include(GNUInstallDirs)
install(TARGETS agw ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
if(AGW_BUILD_CAPI_SHARED)
install(TARGETS agw_capi
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/agw
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
endif()
# --- тесты ------------------------------------------------------------------
if(AGW_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

View File

@@ -1,110 +0,0 @@
# agw-sdk
Qt-free C++20 транспорт к API-шлюзу Amnezia (вынос `GatewayController`). Узкая поверхность —
`post` (sync/async) поверх крипты, выбора эндпоинта и обхода блокировок. Протокол воспроизводится
байт-в-байт.
План и решения: [../docs/plans/gateway-sdk/](../docs/plans/gateway-sdk/) — начни с
`agw-sdk-tier1-impl-plan.md` и `README.md` (таблица решений).
## Статус
Тир 1, в работе по фазам:
- [x] **Фаза 1** — каркас + крипта на OpenSSL EVP (AES-256-CBC, RSA-PKCS1 v1.5, SHA-512), base64
(std + url), UUID v4, Qt-Indented JSON-сериализатор, golden-тесты крипты.
- [x] **Фаза 2**`IHttpClient`(libcurl) + `Config`/`GatewayController`/`executePost` + sync `post`;
`request_builder`/`response`/`error_mapping`; интеграционный тест через in-process mock-шлюз
(полный round-trip: SDK шифрует → «сервер» расшифровывает → шифрует ответ → SDK расшифровывает).
- [x] **Фаза 3** — failover: `bypass_policy` (`shouldBypassProxy` дословно), `proxy_list`
(S3-пути + prod-расшифровка через `SHA-512(pubkey)`), `proxy_picker` (health-check `lmbd-health`),
встройка в `executePost` с кешем рабочего прокси на инстансе (под мьютексом). Интеграционный тест:
прямой ответ подозрителен → S3 → health → прокси → успех; повторный запрос идёт сразу на кеш.
- [x] **Фаза 4**`util::ThreadPool` (drain в деструкторе), `postAsync`(коллбэк на потоке пула)/
`postFuture`(`std::future`) поверх `executePost`, `CancellationToken` (проверки между шагами
failover + прерывание трансфера через progress-коллбэк curl → `ErrorCode::Cancelled`). Кеш прокси
под мьютексом, пул — последний член Impl (рушится первым, дожидаясь задач). TSan чисто; ASan+UBSan
10/10.
- [x] **Фаза 5** — C-ABI (`include/agw/c_abi.h` + `src/c_abi.cpp`, `agw_*`: создание/уничтожение,
sync/async `post`, токен отмены, освобождение результата; на границе только C-типы). Сборка:
object-библиотека → static `agw` + shared `agw_capi` (экспортирует **только** `agw_*`, остальное
скрыто). C-smoke (чистый C) и Dart-smoke (`dart:ffi`) проходят. `conan create` (shared-deps)
зелёный — пакет с `libagw.a` + `libagw_capi.dylib` + заголовками. Режим `vendored` (статические
зависимости) задан в conanfile (`-o deps_mode=vendored`).
- [~] **Фаза 6** — интеграция в Qt-клиент. Готово: `GatewayController` переписан тонким адаптером
над `agw::GatewayController` (сигнатуры один в один, байт-паритет payload, персистентный клиент на
окружение, `onBeforeRequest` = iOS inet + desktop kill-switch, async через `QPromise`+маршалинг);
проводка сборки (корневой `conanfile` requires `agw-sdk/0.1.0`, `client/cmake/3rdparty.cmake`
линкует `agw::agw`). Осталось (вне этого окружения): Qt-сборка под все платформы, перевод
синхронных вызовов (`subscription`/`servicesCatalog` `executeRequest`) на рабочий поток, регрессия
против dev/prod. См. `docs/plans/gateway-sdk/agw-sdk-tier1-phase6-integration.md`.
## Раскладка
```
include/agw/ публичные заголовки (types, config, client, http, cancellation, c_abi)
src/crypto/ AES, RSA, SHA-512, RNG
src/util/ base64, uuid, json (Qt-Indented), url, thread_pool
src/protocol/ имена полей API, request_builder, response, error_mapping
src/failover/ bypass_policy, proxy_list, proxy_picker
src/http/ curl_client (+ fallback)
src/c_abi.cpp C-ABI обёртка
tests/ unit + golden + integration (+ вендоренный nlohmann для офлайн-сборки)
examples/ c_smoke (чистый C), dart_smoke (dart:ffi)
```
## C-ABI и потребление из Dart/C
Публичный C-заголовок — `include/agw/c_abi.h`. Shared-библиотека `libagw_capi.*` экспортирует только
`agw_*`. Примеры:
```sh
# чистый C
cc -std=c11 -Iinclude examples/c_smoke/smoke.c -Lbuild-local -lagw_capi -o /tmp/agw_smoke
DYLD_LIBRARY_PATH=build-local /tmp/agw_smoke # → код 1105, OK
# Dart (dart:ffi)
cd examples/dart_smoke && dart pub get && dart run # → код 1105, OK
```
## Локальная сборка и тесты (без Conan)
Нужны CMake ≥ 3.21 и OpenSSL 3. nlohmann/json берётся из вендоренного single-header
(`tests/third_party`), если Conan-пакет не найден.
```sh
cmake -S . -B build-local -DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3)
cmake --build build-local -j
ctest --test-dir build-local --output-on-failure
```
Санитайзеры (macOS): TSan — на конкурентных тестах; ASan+UBSan — `detect_leaks=0` (LSan на Darwin
не поддержан):
```sh
cmake -S . -B build-asan -DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3) \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -g -O1" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build build-asan -j
ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build-asan --output-on-failure
```
## Сборка через Conan (как в проекте)
```sh
conan create . -o build_tests=True
```
Зависимости: `openssl/3.6.2`, `nlohmann_json/3.11.3` (как в корневом `conanfile.py`); `libcurl`
с Фазы 2.
## Заметки по паритету
Крипта сверена с `client/3rd/QSimpleCrypto` и `gatewayController.cpp`. Ключевое:
- AES-256-CBC, ключ 32 байта, IV генерится 32 — CBC берёт первые 16; salt (8 байт) в локальном AES
не участвует, уходит только в `key_payload`.
- RSA PKCS#1 v1.5 — паддинг рандомный, поэтому `key_payload` **не** воспроизводим байт-в-байт;
golden проверяет его round-trip, а `api_payload` (AES) — точные байты.
- JSON собирается в формате `QJsonDocument::toJson(Indented)`: отступ 4 пробела, завершающий `\n`,
**отсортированные ключи** (это даёт `aes_iv` раньше `aes_key`).

View File

@@ -1,65 +0,0 @@
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout
class AgwSdkConan(ConanFile):
name = "agw-sdk"
version = "0.1.0"
license = "TBD"
description = "AGW SDK — Qt-free C++ transport to the Amnezia API gateway (Tier 1)"
settings = "os", "compiler", "build_type", "arch"
# shared-deps: линкуем общий OpenSSL/curl/nlohmann из Conan (наши приложения).
# vendored: бандлим зависимости статически + скрытие символов (сторонние/standalone).
options = {
"deps_mode": ["shared-deps", "vendored"],
"build_tests": [True, False],
"build_capi_shared": [True, False],
}
default_options = {
"deps_mode": "shared-deps",
"build_tests": False,
"build_capi_shared": True,
}
exports_sources = "CMakeLists.txt", "include/*", "src/*", "tests/*"
def requirements(self):
# Версия OpenSSL совпадает с приложением (корневой conanfile.py) — без второго OpenSSL.
self.requires("openssl/3.6.2")
self.requires("libcurl/8.10.1")
self.requires("nlohmann_json/3.11.3")
def configure(self):
# vendored: тянем статические зависимости, чтобы забандлить их в библиотеку.
if self.options.deps_mode == "vendored":
self.options["openssl"].shared = False
self.options["libcurl"].shared = False
def layout(self):
cmake_layout(self)
def generate(self):
deps = CMakeDeps(self)
deps.generate()
tc = CMakeToolchain(self)
tc.variables["AGW_DEPS_MODE"] = str(self.options.deps_mode)
tc.variables["AGW_BUILD_TESTS"] = bool(self.options.build_tests)
tc.variables["AGW_BUILD_CAPI_SHARED"] = bool(self.options.build_capi_shared)
tc.generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
def package_info(self):
self.cpp_info.libs = ["agw"]
self.cpp_info.includedirs = ["include"]
# Потребитель подключает: find_package(agw-sdk) + target agw::agw
self.cpp_info.set_property("cmake_file_name", "agw-sdk")
self.cpp_info.set_property("cmake_target_name", "agw::agw")

View File

@@ -1,39 +0,0 @@
/*
* Чистый C-потребитель C-ABI: доказывает, что agw_* линкуется и работает из C без C++/Qt.
* Детерминированный путь без сети: невалидный публичный ключ → ApiMissingAgwPublicKey (1105).
*
* Сборка (пример, macOS):
* cc -std=c11 -I ../../include smoke.c -L ../../build-local -lagw_capi -o smoke
* DYLD_LIBRARY_PATH=../../build-local ./smoke
*/
#include <stdio.h>
#include <string.h>
#include "agw/c_abi.h"
int main(void)
{
agw_config cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.gateway_endpoint = "gw.example.test";
cfg.agw_public_key_pem = "not a real pem key"; /* → 1105 без обращения к сети */
cfg.request_timeout_msecs = 5000;
agw_client *client = agw_client_create(&cfg);
if (client == NULL) {
printf("FAIL: agw_client_create returned NULL\n");
return 1;
}
agw_response r = agw_client_post(client, "https://%1/api/v1/test", "{\"x\":1}", "", "", NULL);
printf("post error code = %d\n", r.error);
int ok = (r.error == 1105); /* ApiMissingAgwPublicKey */
agw_response_free(&r);
agw_client_destroy(client);
printf(ok ? "OK\n" : "FAIL\n");
return ok ? 0 : 1;
}

View File

@@ -1,235 +0,0 @@
// Dart-демо C-ABI agw-sdk через dart:ffi.
//
// Показывает поток запроса: подключает лог-хук SDK и onBeforeRequest, поэтому печатаются строки
// [agw] (post START -> direct request url -> direct response -> failover -> post DONE) — видно,
// что запрос ушёл и ответ пришёл. Делает синхронный post, а при AGW_ASYNC=1 — ещё и асинхронный
// (agw_client_post_async + NativeCallable.listener: коллбэк прилетает с потока пула SDK).
//
// Конфиг через переменные окружения (все опциональны):
// AGW_GATEWAY хост шлюза с "%1"-подстановкой, напр. "http://gw.dev.amzsvc.com:80/"
// AGW_PUBKEY_FILE путь к PEM ПУБЛИЧНОГО ключа шлюза (по умолчанию — тестовый фикстур)
// AGW_S3_PRIMARY список S3-адресов через запятую (failover)
// AGW_DEV "1" → dev-режим (S3-список открытым текстом)
// AGW_ENDPOINT шаблон пути, по умолчанию "%1v1/services"
// AGW_PAYLOAD JSON тела запроса
// AGW_ASYNC "1" → дополнительно прогнать асинхронный вызов
// AGW_CAPI_LIB путь к libagw_capi.* (иначе ../../build-local)
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
final class AgwConfig extends Struct {
external Pointer<Utf8> gatewayEndpoint;
external Pointer<Utf8> agwPublicKeyPem;
external Pointer<Pointer<Utf8>> s3Primary;
@Size()
external int s3PrimaryCount;
external Pointer<Pointer<Utf8>> s3Fallback;
@Size()
external int s3FallbackCount;
@Int32()
external int isDevEnvironment;
@Int32()
external int requestTimeoutMsecs;
@Int32()
external int proxyHealthTimeoutMsecs;
@Int32()
external int proxyStorageTimeoutMsecs;
@Int32()
external int threadPoolSize;
external Pointer<Void> onBeforeRequest;
external Pointer<Void> onBeforeRequestUserData;
external Pointer<Void> log;
external Pointer<Void> logUserData;
}
final class AgwResponse extends Struct {
@Int32()
external int error;
external Pointer<Utf8> body;
@Size()
external int bodyLen;
}
typedef _CreateC = Pointer<Void> Function(Pointer<AgwConfig>);
typedef _PostC = AgwResponse Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, Pointer<Void>);
typedef _PostAsyncC = Void Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, Pointer<Void>, Pointer<Void>, Pointer<Void>);
typedef _PostAsyncDart = void Function(Pointer<Void>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, Pointer<Void>, Pointer<Void>, Pointer<Void>);
typedef _FreeC = Void Function(Pointer<AgwResponse>);
typedef _FreeDart = void Function(Pointer<AgwResponse>);
typedef _DestroyC = Void Function(Pointer<Void>);
typedef _DestroyDart = void Function(Pointer<Void>);
typedef _LogNative = Void Function(Int32, Pointer<Utf8>, Pointer<Void>);
typedef _BeforeNative = Void Function(Pointer<Utf8>, Pointer<Void>);
typedef _PostCbNative = Void Function(AgwResponse, Pointer<Void>);
const _levels = ['DBG', 'INF', 'WRN', 'ERR'];
void _printLog(String tag, int level, Pointer<Utf8> message) {
final lvl = (level >= 0 && level < _levels.length) ? _levels[level] : '?';
stdout.writeln(' $tag [agw][$lvl] ${message.toDartString()}');
}
class _Cfg {
final Pointer<AgwConfig> ptr;
final List<Pointer<NativeType>> allocs;
_Cfg(this.ptr, this.allocs);
void free() {
for (final p in allocs) {
calloc.free(p);
}
calloc.free(ptr);
}
}
String _libPath() {
final env = Platform.environment['AGW_CAPI_LIB'];
if (env != null) return env;
final base = '${Directory.current.path}/../../build-local';
if (Platform.isMacOS) return '$base/libagw_capi.dylib';
if (Platform.isWindows) return '$base/agw_capi.dll';
return '$base/libagw_capi.so';
}
String _defaultPubKey() {
final f = File('${Directory.current.path}/../../tests/golden/fixtures/test_rsa_pub.pem');
return f.existsSync() ? f.readAsStringSync() : 'not a real pem key';
}
late String gateway, pubKey, payload;
late int isDev;
late List<String> s3List;
_Cfg buildConfig(Pointer<Void> logFn, Pointer<Void> beforeFn) {
final cfg = calloc<AgwConfig>();
final allocs = <Pointer<NativeType>>[];
final gw = gateway.toNativeUtf8();
final pk = pubKey.toNativeUtf8();
allocs.add(gw);
allocs.add(pk);
cfg.ref.gatewayEndpoint = gw;
cfg.ref.agwPublicKeyPem = pk;
cfg.ref.requestTimeoutMsecs = 8000;
cfg.ref.isDevEnvironment = isDev;
cfg.ref.onBeforeRequest = beforeFn;
cfg.ref.log = logFn;
if (s3List.isNotEmpty) {
final arr = calloc<Pointer<Utf8>>(s3List.length);
for (var i = 0; i < s3List.length; i++) {
arr[i] = s3List[i].toNativeUtf8();
allocs.add(arr[i]);
}
cfg.ref.s3Primary = arr;
cfg.ref.s3PrimaryCount = s3List.length;
allocs.add(arr);
}
return _Cfg(cfg, allocs);
}
Future<int> main() async {
final env = Platform.environment;
final lib = DynamicLibrary.open(_libPath());
final create = lib.lookupFunction<_CreateC, _CreateC>('agw_client_create');
final post = lib.lookupFunction<_PostC, _PostC>('agw_client_post');
final postAsync = lib.lookupFunction<_PostAsyncC, _PostAsyncDart>('agw_client_post_async');
final free = lib.lookupFunction<_FreeC, _FreeDart>('agw_response_free');
final destroy = lib.lookupFunction<_DestroyC, _DestroyDart>('agw_client_destroy');
gateway = env['AGW_GATEWAY'] ?? 'http://gw.example.test/';
final pubKeyFile = env['AGW_PUBKEY_FILE'];
pubKey = pubKeyFile != null ? File(pubKeyFile).readAsStringSync() : _defaultPubKey();
final endpoint = env['AGW_ENDPOINT'] ?? '%1v1/services';
payload = env['AGW_PAYLOAD'] ??
'{"os_version":"macos","app_version":"4.9.0","cli_name":"amnezia","app_language":"en"}';
isDev = (env['AGW_DEV'] == '1') ? 1 : 0;
s3List = (env['AGW_S3_PRIMARY'] ?? '')
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
stdout.writeln('=== agw-sdk Dart demo ===');
stdout.writeln('gateway=$gateway endpoint=$endpoint dev=$isDev s3primary=${s3List.length}');
stdout.writeln('pubkey=${pubKeyFile ?? "(test fixture)"}');
final endpointC = endpoint.toNativeUtf8();
final payloadC = payload.toNativeUtf8();
final svc = ''.toNativeUtf8();
// ---------- SYNC (коллбэки isolateLocal: ядро sync исполняется на этом потоке) ----------
stdout.writeln('\n--- SYNC post ---');
final logSync = NativeCallable<_LogNative>.isolateLocal(
(int lvl, Pointer<Utf8> m, Pointer<Void> _) => _printLog('[sync]', lvl, m));
final beforeSync = NativeCallable<_BeforeNative>.isolateLocal(
(Pointer<Utf8> h, Pointer<Void> _) =>
stdout.writeln(' [sync] → onBeforeRequest host=${h.toDartString()}'));
final cfgSync = buildConfig(logSync.nativeFunction.cast(), beforeSync.nativeFunction.cast());
final clientSync = create(cfgSync.ptr);
final resp = post(clientSync, endpointC, payloadC, svc, svc, nullptr);
stdout.writeln(' [sync] RESULT errorCode=${resp.error} bodyLen=${resp.bodyLen}');
if (resp.body != nullptr && resp.bodyLen > 0) {
final body = resp.body.toDartString(length: resp.bodyLen);
stdout.writeln(' [sync] body=${body.length > 200 ? "${body.substring(0, 200)}…" : body}');
}
final rp = calloc<AgwResponse>()
..ref.error = resp.error
..ref.body = resp.body
..ref.bodyLen = resp.bodyLen;
free(rp);
calloc.free(rp);
destroy(clientSync);
cfgSync.free();
logSync.close();
beforeSync.close();
// ---------- ASYNC (коллбэки listener: прилетают с потока пула SDK) ----------
if (env['AGW_ASYNC'] == '1') {
stdout.writeln('\n--- ASYNC post (коллбэк с потока пула) ---');
// ВАЖНО: лог-хук/onBeforeRequest сюда НЕ вешаем. Их const char* живут лишь во время вызова на
// потоке пула, а NativeCallable.listener выполняется позже на Dart event-loop → указатель был бы
// висячим. Result-коллбэк безопасен: body выделен в куче и принадлежит вызывающему (мы его освобождаем).
final done = Completer<void>();
final resultCb = NativeCallable<_PostCbNative>.listener((AgwResponse r, Pointer<Void> _) {
stdout.writeln(' [async] CALLBACK (прилетел с потока пула) errorCode=${r.error} bodyLen=${r.bodyLen}');
if (r.body != nullptr && r.bodyLen > 0) {
final body = r.body.toDartString(length: r.bodyLen);
stdout.writeln(' [async] body=${body.length > 200 ? "${body.substring(0, 200)}…" : body}');
}
final p = calloc<AgwResponse>()
..ref.error = r.error
..ref.body = r.body
..ref.bodyLen = r.bodyLen;
free(p);
calloc.free(p);
done.complete();
});
final cfgAsync = buildConfig(nullptr, nullptr); // без лог/before-хуков (см. выше)
final clientAsync = create(cfgAsync.ptr);
stdout.writeln(' [async] post_async отправлен, ждём коллбэк…');
postAsync(clientAsync, endpointC, payloadC, svc, svc, resultCb.nativeFunction.cast(),
nullptr, nullptr);
await done.future; // ждём, пока коллбэк прилетит с потока пула
destroy(clientAsync);
cfgAsync.free();
resultCb.close();
}
calloc.free(endpointC);
calloc.free(payloadC);
calloc.free(svc);
stdout.writeln('\n=== done ===');
return 0;
}

View File

@@ -1,9 +0,0 @@
name: agw_dart_smoke
description: Smoke-вызов C-ABI agw-sdk через dart:ffi.
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
ffi: ^2.1.0

View File

@@ -1,10 +0,0 @@
#ifndef AGW_AGW_H
#define AGW_AGW_H
#include "agw/cancellation.h"
#include "agw/gateway_controller.h"
#include "agw/config.h"
#include "agw/http.h"
#include "agw/types.h"
#endif

View File

@@ -1,80 +0,0 @@
#ifndef AGW_C_ABI_H
#define AGW_C_ABI_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#if defined(_WIN32)
#if defined(AGW_BUILDING_SHARED)
#define AGW_API __declspec(dllexport)
#elif defined(AGW_USING_SHARED)
#define AGW_API __declspec(dllimport)
#else
#define AGW_API
#endif
#else
#define AGW_API __attribute__((visibility("default")))
#endif
typedef struct agw_client agw_client;
typedef struct agw_cancel_token agw_cancel_token;
typedef void (*agw_before_request_fn)(const char *host, void *user_data);
typedef void (*agw_log_fn)(int level, const char *message, void *user_data);
typedef struct
{
const char *gateway_endpoint;
const char *agw_public_key_pem;
const char *const *s3_primary_endpoints;
size_t s3_primary_count;
const char *const *s3_fallback_endpoints;
size_t s3_fallback_count;
int is_dev_environment;
int request_timeout_msecs;
int proxy_health_timeout_msecs;
int proxy_storage_timeout_msecs;
int thread_pool_size;
agw_before_request_fn on_before_request;
void *on_before_request_user_data;
agw_log_fn log;
void *log_user_data;
} agw_config;
typedef struct
{
int error;
char *body;
size_t body_len;
} agw_response;
typedef void (*agw_post_callback)(agw_response response, void *user_data);
AGW_API agw_client *agw_client_create(const agw_config *config);
AGW_API void agw_client_destroy(agw_client *client);
AGW_API agw_response agw_client_post(agw_client *client, const char *endpoint, const char *payload,
const char *service_type, const char *user_country_code,
agw_cancel_token *cancel_token);
AGW_API void agw_client_post_async(agw_client *client, const char *endpoint, const char *payload,
const char *service_type, const char *user_country_code, agw_post_callback callback,
void *user_data, agw_cancel_token *cancel_token);
AGW_API void agw_response_free(agw_response *response);
AGW_API agw_cancel_token *agw_cancel_token_create(void);
AGW_API void agw_cancel_token_cancel(agw_cancel_token *token);
AGW_API void agw_cancel_token_destroy(agw_cancel_token *token);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,30 +0,0 @@
#ifndef AGW_CANCELLATION_H
#define AGW_CANCELLATION_H
#include <atomic>
namespace agw
{
class CancellationToken
{
public:
CancellationToken() = default;
CancellationToken(const CancellationToken &) = delete;
CancellationToken &operator=(const CancellationToken &) = delete;
void cancel() noexcept
{
m_cancelled.store(true, std::memory_order_relaxed);
}
bool isCancelled() const noexcept
{
return m_cancelled.load(std::memory_order_relaxed);
}
private:
std::atomic<bool> m_cancelled { false };
};
}
#endif

View File

@@ -1,38 +0,0 @@
#ifndef AGW_CONFIG_H
#define AGW_CONFIG_H
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "agw/http.h"
#include "agw/types.h"
namespace agw
{
struct Config
{
std::string gatewayEndpoint;
std::string agwPublicKeyPem;
std::vector<std::string> s3PrimaryEndpoints;
std::vector<std::string> s3FallbackEndpoints;
bool isDevEnvironment = false;
int requestTimeoutMsecs = 12000;
int proxyHealthTimeoutMsecs = 1000;
int proxyStorageTimeoutMsecs = 3000;
int threadPoolSize = 4;
std::function<void(const std::string &host)> onBeforeRequest;
std::function<void(LogLevel, const std::string &message)> log;
std::shared_ptr<IHttpClient> httpClient;
};
}
#endif

View File

@@ -1,40 +0,0 @@
#ifndef AGW_GATEWAY_CONTROLLER_H
#define AGW_GATEWAY_CONTROLLER_H
#include <functional>
#include <future>
#include <memory>
#include <string>
#include "agw/cancellation.h"
#include "agw/config.h"
#include "agw/types.h"
namespace agw {
class GatewayController {
public:
explicit GatewayController(Config config);
~GatewayController();
GatewayController(GatewayController &&) noexcept;
GatewayController &operator=(GatewayController &&) noexcept;
GatewayController(const GatewayController &) = delete;
GatewayController &operator=(const GatewayController &) = delete;
Response post(const std::string &endpoint, const std::string &payload, const FailoverContext &ctx,
CancellationToken *cancel = nullptr);
void postAsync(const std::string &endpoint, const std::string &payload,
std::function<void(Response)> onResult, const FailoverContext &ctx,
CancellationToken *cancel = nullptr);
std::future<Response> postFuture(const std::string &endpoint, const std::string &payload,
const FailoverContext &ctx, CancellationToken *cancel = nullptr);
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
}
#endif

View File

@@ -1,51 +0,0 @@
#ifndef AGW_HTTP_H
#define AGW_HTTP_H
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace agw
{
enum class TransportError {
None = 0,
Timeout,
Canceled,
OperationNotImplemented,
ConnectionError,
};
struct HttpRequest
{
std::string url;
std::string method;
std::string body;
std::vector<std::pair<std::string, std::string>> headers;
int timeoutMsecs = 0;
std::function<bool()> cancelCheck;
};
struct HttpResponse
{
TransportError error = TransportError::None;
std::string errorString;
int httpStatusCode = 0;
bool sslError = false;
std::string body;
};
class IHttpClient
{
public:
virtual ~IHttpClient() = default;
virtual HttpResponse send(const HttpRequest &request) = 0;
};
std::unique_ptr<IHttpClient> makeDefaultHttpClient();
}
#endif

View File

@@ -1,55 +0,0 @@
#ifndef AGW_TYPES_H
#define AGW_TYPES_H
#include <string>
namespace agw
{
enum class ErrorCode : int {
NoError = 0,
Cancelled = 1,
ApiConfigDownloadError = 1100,
ApiConfigAlreadyAdded = 1101,
ApiConfigEmptyError = 1102,
ApiConfigTimeoutError = 1103,
ApiConfigSslError = 1104,
ApiMissingAgwPublicKey = 1105,
ApiConfigDecryptionError = 1106,
ApiServicesMissingError = 1107,
ApiConfigLimitError = 1108,
ApiNotFoundError = 1109,
ApiMigrationError = 1110,
ApiUpdateRequestError = 1111,
ApiSubscriptionExpiredError = 1112,
ApiPurchaseError = 1113,
ApiSubscriptionNotActiveError = 1114,
ApiNoPurchasedSubscriptionsError = 1115,
ApiTrialAlreadyUsedError = 1116,
ApiCaptchaRequiredError = 1117,
ApiCaptchaInvalidError = 1118,
ApiCaptchaRefreshError = 1119,
ApiRateLimitError = 1120,
};
enum class LogLevel : int {
Debug,
Info,
Warning,
Error
};
struct Response
{
ErrorCode error = ErrorCode::NoError;
std::string body;
};
struct FailoverContext
{
std::string serviceType;
std::string userCountryCode;
};
}
#endif

View File

@@ -1,201 +0,0 @@
#include "agw/c_abi.h"
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include "agw/cancellation.h"
#include "agw/gateway_controller.h"
#include "agw/config.h"
#include "detail/test_hooks.h"
struct agw_client
{
explicit agw_client(agw::Config cfg) : client(std::move(cfg))
{
}
agw::GatewayController client;
};
struct agw_cancel_token
{
agw::CancellationToken token;
};
namespace agw::detail
{
namespace
{
std::mutex g_testHttpMutex;
std::shared_ptr<IHttpClient> g_testHttp;
}
void setNextTestHttpClient(std::shared_ptr<IHttpClient> http)
{
std::lock_guard<std::mutex> lock(g_testHttpMutex);
g_testHttp = std::move(http);
}
std::shared_ptr<IHttpClient> takeNextTestHttpClient()
{
std::lock_guard<std::mutex> lock(g_testHttpMutex);
std::shared_ptr<IHttpClient> h = std::move(g_testHttp);
g_testHttp.reset();
return h;
}
}
namespace
{
std::string cstr(const char *s)
{
return s ? std::string(s) : std::string();
}
agw_response toCResponse(const agw::Response &r)
{
agw_response out;
out.error = static_cast<int>(r.error);
out.body = nullptr;
out.body_len = r.body.size();
char *buf = static_cast<char *>(std::malloc(r.body.size() + 1));
if (buf != nullptr) {
if (!r.body.empty()) {
std::memcpy(buf, r.body.data(), r.body.size());
}
buf[r.body.size()] = '\0';
out.body = buf;
} else {
out.body_len = 0;
}
return out;
}
}
extern "C" {
agw_client *agw_client_create(const agw_config *config)
{
if (config == nullptr) {
return nullptr;
}
agw::Config cfg;
cfg.gatewayEndpoint = cstr(config->gateway_endpoint);
cfg.agwPublicKeyPem = cstr(config->agw_public_key_pem);
for (size_t i = 0; i < config->s3_primary_count; ++i) {
cfg.s3PrimaryEndpoints.push_back(cstr(config->s3_primary_endpoints[i]));
}
for (size_t i = 0; i < config->s3_fallback_count; ++i) {
cfg.s3FallbackEndpoints.push_back(cstr(config->s3_fallback_endpoints[i]));
}
cfg.isDevEnvironment = config->is_dev_environment != 0;
if (config->request_timeout_msecs > 0)
cfg.requestTimeoutMsecs = config->request_timeout_msecs;
if (config->proxy_health_timeout_msecs > 0)
cfg.proxyHealthTimeoutMsecs = config->proxy_health_timeout_msecs;
if (config->proxy_storage_timeout_msecs > 0)
cfg.proxyStorageTimeoutMsecs = config->proxy_storage_timeout_msecs;
if (config->thread_pool_size > 0)
cfg.threadPoolSize = config->thread_pool_size;
if (config->on_before_request != nullptr) {
agw_before_request_fn fn = config->on_before_request;
void *ud = config->on_before_request_user_data;
cfg.onBeforeRequest = [fn, ud](const std::string &host) { fn(host.c_str(), ud); };
}
if (config->log != nullptr) {
agw_log_fn fn = config->log;
void *ud = config->log_user_data;
cfg.log = [fn, ud](agw::LogLevel level, const std::string &msg) { fn(static_cast<int>(level), msg.c_str(), ud); };
}
if (auto http = agw::detail::takeNextTestHttpClient()) {
cfg.httpClient = http;
}
try {
return new agw_client(std::move(cfg));
} catch (...) {
return nullptr;
}
}
void agw_client_destroy(agw_client *client)
{
delete client;
}
agw_response agw_client_post(agw_client *client, const char *endpoint, const char *payload, const char *service_type,
const char *user_country_code, agw_cancel_token *cancel_token)
{
if (client == nullptr) {
agw_response out;
out.error = static_cast<int>(agw::ErrorCode::ApiConfigDownloadError);
out.body = nullptr;
out.body_len = 0;
return out;
}
agw::FailoverContext ctx { cstr(service_type), cstr(user_country_code) };
agw::CancellationToken *tk = cancel_token ? &cancel_token->token : nullptr;
agw::Response r = client->client.post(cstr(endpoint), cstr(payload), ctx, tk);
return toCResponse(r);
}
void agw_client_post_async(agw_client *client, const char *endpoint, const char *payload, const char *service_type,
const char *user_country_code, agw_post_callback callback, void *user_data,
agw_cancel_token *cancel_token)
{
if (client == nullptr || callback == nullptr) {
return;
}
agw::FailoverContext ctx { cstr(service_type), cstr(user_country_code) };
agw::CancellationToken *tk = cancel_token ? &cancel_token->token : nullptr;
client->client.postAsync(
cstr(endpoint), cstr(payload),
[callback, user_data](agw::Response r) {
agw_response cr = toCResponse(r);
callback(cr, user_data);
},
ctx, tk);
}
void agw_response_free(agw_response *response)
{
if (response == nullptr) {
return;
}
std::free(response->body);
response->body = nullptr;
response->body_len = 0;
}
agw_cancel_token *agw_cancel_token_create(void)
{
try {
return new agw_cancel_token();
} catch (...) {
return nullptr;
}
}
void agw_cancel_token_cancel(agw_cancel_token *token)
{
if (token != nullptr) {
token->token.cancel();
}
}
void agw_cancel_token_destroy(agw_cancel_token *token)
{
delete token;
}
}

View File

@@ -1,87 +0,0 @@
#include "aes.h"
#include <memory>
#include <stdexcept>
#include <openssl/evp.h>
namespace agw::crypto
{
namespace
{
using CtxPtr = std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)>;
constexpr int kAes256KeyLen = 32;
constexpr int kAesBlock = 16;
void checkKeyIv(const std::vector<std::uint8_t> &key, const std::vector<std::uint8_t> &iv)
{
if (key.size() != static_cast<std::size_t>(kAes256KeyLen)) {
throw std::runtime_error("agw::crypto::aes: key must be 32 bytes (AES-256)");
}
if (iv.size() < static_cast<std::size_t>(kAesBlock)) {
throw std::runtime_error("agw::crypto::aes: iv must be at least 16 bytes");
}
}
}
std::vector<std::uint8_t> aesEncryptCbc(const std::vector<std::uint8_t> &data, const std::vector<std::uint8_t> &key,
const std::vector<std::uint8_t> &iv)
{
checkKeyIv(key, iv);
CtxPtr ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);
if (!ctx) {
throw std::runtime_error("agw::crypto::aes: EVP_CIPHER_CTX_new failed");
}
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, key.data(), iv.data()) != 1) {
throw std::runtime_error("agw::crypto::aes: EVP_EncryptInit_ex failed");
}
std::vector<std::uint8_t> out(data.size() + kAesBlock);
int len = 0;
if (EVP_EncryptUpdate(ctx.get(), out.data(), &len, data.data(), static_cast<int>(data.size())) != 1) {
throw std::runtime_error("agw::crypto::aes: EVP_EncryptUpdate failed");
}
int total = len;
if (EVP_EncryptFinal_ex(ctx.get(), out.data() + total, &len) != 1) {
throw std::runtime_error("agw::crypto::aes: EVP_EncryptFinal_ex failed");
}
total += len;
out.resize(static_cast<std::size_t>(total));
return out;
}
std::vector<std::uint8_t> aesDecryptCbc(const std::vector<std::uint8_t> &data, const std::vector<std::uint8_t> &key,
const std::vector<std::uint8_t> &iv)
{
checkKeyIv(key, iv);
CtxPtr ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free);
if (!ctx) {
throw std::runtime_error("agw::crypto::aes: EVP_CIPHER_CTX_new failed");
}
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, key.data(), iv.data()) != 1) {
throw std::runtime_error("agw::crypto::aes: EVP_DecryptInit_ex failed");
}
std::vector<std::uint8_t> out(data.size() + kAesBlock);
int len = 0;
if (EVP_DecryptUpdate(ctx.get(), out.data(), &len, data.data(), static_cast<int>(data.size())) != 1) {
throw std::runtime_error("agw::crypto::aes: EVP_DecryptUpdate failed");
}
int total = len;
if (EVP_DecryptFinal_ex(ctx.get(), out.data() + total, &len) != 1) {
throw std::runtime_error("agw::crypto::aes: EVP_DecryptFinal_ex failed (bad key/iv/padding)");
}
total += len;
out.resize(static_cast<std::size_t>(total));
return out;
}
}

View File

@@ -1,16 +0,0 @@
#ifndef AGW_CRYPTO_AES_H
#define AGW_CRYPTO_AES_H
#include <cstdint>
#include <vector>
namespace agw::crypto
{
std::vector<std::uint8_t> aesEncryptCbc(const std::vector<std::uint8_t> &data, const std::vector<std::uint8_t> &key,
const std::vector<std::uint8_t> &iv);
std::vector<std::uint8_t> aesDecryptCbc(const std::vector<std::uint8_t> &data, const std::vector<std::uint8_t> &key,
const std::vector<std::uint8_t> &iv);
}
#endif

View File

@@ -1,59 +0,0 @@
#include "hash.h"
#include <stdexcept>
#include <openssl/sha.h>
namespace agw::crypto
{
namespace
{
int hexNibble(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
}
std::vector<std::uint8_t> sha512(const std::vector<std::uint8_t> &data)
{
std::vector<std::uint8_t> out(SHA512_DIGEST_LENGTH);
SHA512(data.data(), data.size(), out.data());
return out;
}
std::string toHex(const std::vector<std::uint8_t> &data)
{
static const char *digits = "0123456789abcdef";
std::string out;
out.reserve(data.size() * 2);
for (std::uint8_t b : data) {
out.push_back(digits[b >> 4]);
out.push_back(digits[b & 0x0F]);
}
return out;
}
std::vector<std::uint8_t> fromHex(const std::string &hex)
{
if (hex.size() % 2 != 0) {
throw std::runtime_error("agw::crypto::fromHex: odd-length input");
}
std::vector<std::uint8_t> out;
out.reserve(hex.size() / 2);
for (std::size_t i = 0; i < hex.size(); i += 2) {
const int hi = hexNibble(hex[i]);
const int lo = hexNibble(hex[i + 1]);
if (hi < 0 || lo < 0) {
throw std::runtime_error("agw::crypto::fromHex: invalid hex character");
}
out.push_back(static_cast<std::uint8_t>((hi << 4) | lo));
}
return out;
}
}

View File

@@ -1,17 +0,0 @@
#ifndef AGW_CRYPTO_HASH_H
#define AGW_CRYPTO_HASH_H
#include <cstdint>
#include <string>
#include <vector>
namespace agw::crypto
{
std::vector<std::uint8_t> sha512(const std::vector<std::uint8_t> &data);
std::string toHex(const std::vector<std::uint8_t> &data);
std::vector<std::uint8_t> fromHex(const std::string &hex);
}
#endif

View File

@@ -1,20 +0,0 @@
#include "rng.h"
#include <stdexcept>
#include <openssl/rand.h>
namespace agw::crypto
{
std::vector<std::uint8_t> DefaultRng::bytes(std::size_t n)
{
std::vector<std::uint8_t> out(n);
if (n == 0) {
return out;
}
if (RAND_priv_bytes(out.data(), static_cast<int>(n)) != 1) {
throw std::runtime_error("agw::crypto::DefaultRng: RAND_priv_bytes failed");
}
return out;
}
}

View File

@@ -1,24 +0,0 @@
#ifndef AGW_CRYPTO_RNG_H
#define AGW_CRYPTO_RNG_H
#include <cstddef>
#include <cstdint>
#include <vector>
namespace agw::crypto
{
class IRng
{
public:
virtual ~IRng() = default;
virtual std::vector<std::uint8_t> bytes(std::size_t n) = 0;
};
class DefaultRng : public IRng
{
public:
std::vector<std::uint8_t> bytes(std::size_t n) override;
};
}
#endif

View File

@@ -1,111 +0,0 @@
#include "rsa.h"
#include <memory>
#include <stdexcept>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
namespace agw::crypto
{
namespace
{
using BioPtr = std::unique_ptr<BIO, decltype(&BIO_free)>;
using PkeyPtr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
using PkeyCtxPtr = std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)>;
PkeyPtr loadPublicKey(const std::string &pem)
{
BioPtr bio(BIO_new_mem_buf(pem.data(), static_cast<int>(pem.size())), BIO_free);
if (!bio) {
throw std::runtime_error("agw::crypto::rsa: BIO_new_mem_buf failed");
}
EVP_PKEY *raw = nullptr;
if (!PEM_read_bio_PUBKEY(bio.get(), &raw, nullptr, nullptr)) {
throw std::runtime_error("agw::crypto::rsa: PEM_read_bio_PUBKEY failed");
}
return PkeyPtr(raw, EVP_PKEY_free);
}
PkeyPtr loadPrivateKey(const std::string &pem)
{
BioPtr bio(BIO_new_mem_buf(pem.data(), static_cast<int>(pem.size())), BIO_free);
if (!bio) {
throw std::runtime_error("agw::crypto::rsa: BIO_new_mem_buf failed");
}
EVP_PKEY *raw = nullptr;
if (!PEM_read_bio_PrivateKey(bio.get(), &raw, nullptr, nullptr)) {
throw std::runtime_error("agw::crypto::rsa: PEM_read_bio_PrivateKey failed");
}
return PkeyPtr(raw, EVP_PKEY_free);
}
}
std::vector<std::uint8_t> rsaEncryptPublicPkcs1(const std::vector<std::uint8_t> &plaintext,
const std::string &publicKeyPem)
{
PkeyPtr key = loadPublicKey(publicKeyPem);
PkeyCtxPtr ctx(EVP_PKEY_CTX_new(key.get(), nullptr), EVP_PKEY_CTX_free);
if (!ctx) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_CTX_new failed");
}
if (EVP_PKEY_encrypt_init(ctx.get()) != 1) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_encrypt_init failed");
}
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PADDING) != 1) {
throw std::runtime_error("agw::crypto::rsa: set_rsa_padding failed");
}
std::size_t outLen = 0;
if (EVP_PKEY_encrypt(ctx.get(), nullptr, &outLen, plaintext.data(), plaintext.size()) != 1) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_encrypt (size) failed");
}
std::vector<std::uint8_t> out(outLen);
if (EVP_PKEY_encrypt(ctx.get(), out.data(), &outLen, plaintext.data(), plaintext.size()) != 1) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_encrypt failed");
}
out.resize(outLen);
return out;
}
bool rsaPublicKeyValid(const std::string &publicKeyPem)
{
try {
loadPublicKey(publicKeyPem);
return true;
} catch (...) {
return false;
}
}
std::vector<std::uint8_t> rsaDecryptPrivatePkcs1(const std::vector<std::uint8_t> &ciphertext,
const std::string &privateKeyPem)
{
PkeyPtr key = loadPrivateKey(privateKeyPem);
PkeyCtxPtr ctx(EVP_PKEY_CTX_new(key.get(), nullptr), EVP_PKEY_CTX_free);
if (!ctx) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_CTX_new failed");
}
if (EVP_PKEY_decrypt_init(ctx.get()) != 1) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_decrypt_init failed");
}
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PADDING) != 1) {
throw std::runtime_error("agw::crypto::rsa: set_rsa_padding failed");
}
std::size_t outLen = 0;
if (EVP_PKEY_decrypt(ctx.get(), nullptr, &outLen, ciphertext.data(), ciphertext.size()) != 1) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_decrypt (size) failed");
}
std::vector<std::uint8_t> out(outLen);
if (EVP_PKEY_decrypt(ctx.get(), out.data(), &outLen, ciphertext.data(), ciphertext.size()) != 1) {
throw std::runtime_error("agw::crypto::rsa: EVP_PKEY_decrypt failed");
}
out.resize(outLen);
return out;
}
}

View File

@@ -1,19 +0,0 @@
#ifndef AGW_CRYPTO_RSA_H
#define AGW_CRYPTO_RSA_H
#include <cstdint>
#include <string>
#include <vector>
namespace agw::crypto
{
std::vector<std::uint8_t> rsaEncryptPublicPkcs1(const std::vector<std::uint8_t> &plaintext,
const std::string &publicKeyPem);
std::vector<std::uint8_t> rsaDecryptPrivatePkcs1(const std::vector<std::uint8_t> &ciphertext,
const std::string &privateKeyPem);
bool rsaPublicKeyValid(const std::string &publicKeyPem);
}
#endif

View File

@@ -1,14 +0,0 @@
#ifndef AGW_DETAIL_TEST_HOOKS_H
#define AGW_DETAIL_TEST_HOOKS_H
#include <memory>
#include "agw/http.h"
namespace agw::detail
{
void setNextTestHttpClient(std::shared_ptr<IHttpClient> http);
std::shared_ptr<IHttpClient> takeNextTestHttpClient();
}
#endif

View File

@@ -1,100 +0,0 @@
#include "failover/bypass_policy.h"
#include "protocol/keys.h"
#include "util/json.h"
namespace agw::failover
{
namespace
{
constexpr const char *kPattern1 = "No active configuration found for";
constexpr const char *kPattern2 = "No non-revoked public key found for";
constexpr const char *kPattern3 = "Account not found.";
constexpr const char *kPatternQrSessionNotFound = "QR session not found";
constexpr const char *kPatternSessionNotFound = "Session not found";
constexpr const char *kUpdateRequestPattern = "client version update is required";
constexpr const char *kUnprocessableSubscriptionMessage =
"Failed to retrieve subscription information. Is it activated?";
constexpr int kNotFound = 404;
constexpr int kNotImplemented = 501;
constexpr int kPaymentRequired = 402;
constexpr int kConflict = 409;
constexpr int kRequestTimeout = 408;
constexpr int kUnprocessableEntity = 422;
bool contains(const std::string &body, const char *needle)
{
return body.find(needle) != std::string::npos;
}
std::string trim(const std::string &s)
{
std::size_t b = 0, e = s.size();
while (b < e && (s[b] == ' ' || s[b] == '\t' || s[b] == '\n' || s[b] == '\r'))
++b;
while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t' || s[e - 1] == '\n' || s[e - 1] == '\r'))
--e;
return s.substr(b, e - b);
}
}
bool shouldBypassProxy(TransportError transportError, const std::string &decryptedBody, bool decryptionSuccessful)
{
if (!decryptionSuccessful) {
return true;
}
int apiHttpStatus = -1;
std::string apiErrorMessage;
try {
util::Json obj = util::Json::parse(decryptedBody);
if (obj.is_object()) {
if (auto it = obj.find(protocol::keys::httpStatus); it != obj.end() && it->is_number_integer()) {
apiHttpStatus = it->get<int>();
}
if (auto it = obj.find(protocol::keys::message); it != obj.end() && it->is_string()) {
apiErrorMessage = trim(it->get<std::string>());
}
}
} catch (...) {
}
if (transportError == TransportError::Canceled || transportError == TransportError::Timeout) {
return true;
}
if (contains(decryptedBody, "html")) {
return true;
}
if (apiHttpStatus == kRequestTimeout) {
return false;
}
if (apiHttpStatus == kNotFound) {
if (contains(decryptedBody, kPattern1) || contains(decryptedBody, kPattern2)
|| contains(decryptedBody, kPattern3) || contains(decryptedBody, kPatternQrSessionNotFound)
|| contains(decryptedBody, kPatternSessionNotFound)) {
return false;
}
return true;
}
if (apiHttpStatus == kNotImplemented) {
if (contains(decryptedBody, kUpdateRequestPattern)) {
return false;
}
return true;
}
if (apiHttpStatus == kConflict) {
return false;
}
if (apiHttpStatus == kPaymentRequired) {
return false;
}
if (apiHttpStatus == kUnprocessableEntity) {
return apiErrorMessage != kUnprocessableSubscriptionMessage;
}
if (transportError != TransportError::None) {
return true;
}
return false;
}
}

View File

@@ -1,13 +0,0 @@
#ifndef AGW_FAILOVER_BYPASS_POLICY_H
#define AGW_FAILOVER_BYPASS_POLICY_H
#include <string>
#include "agw/http.h"
namespace agw::failover
{
bool shouldBypassProxy(TransportError transportError, const std::string &decryptedBody, bool decryptionSuccessful);
}
#endif

View File

@@ -1,71 +0,0 @@
#include "failover/proxy_list.h"
#include "crypto/aes.h"
#include "crypto/hash.h"
#include "util/base64.h"
#include "util/json.h"
namespace agw::failover
{
namespace
{
void appendStorageUrls(const std::vector<std::string> &baseUrls, const FailoverContext &ctx,
std::vector<std::string> &target)
{
if (!ctx.serviceType.empty()) {
const std::string token = "endpoints-" + ctx.serviceType + "-" + ctx.userCountryCode;
const std::string encoded =
util::base64UrlEncodeNoPad(std::vector<std::uint8_t>(token.begin(), token.end()));
for (const auto &base : baseUrls) {
target.push_back(base + encoded + ".json");
}
}
for (const auto &base : baseUrls) {
target.push_back(base + "endpoints.json");
}
}
std::vector<std::string> parseEndpointsArray(const std::string &json)
{
std::vector<std::string> out;
try {
util::Json doc = util::Json::parse(json);
if (doc.is_array()) {
for (const auto &el : doc) {
if (el.is_string()) {
out.push_back(el.get<std::string>());
}
}
}
} catch (...) {
}
return out;
}
}
std::vector<std::string> buildStorageUrls(const std::vector<std::string> &primaryBaseUrls,
const std::vector<std::string> &fallbackBaseUrls,
const FailoverContext &ctx)
{
std::vector<std::string> result;
appendStorageUrls(primaryBaseUrls, ctx, result);
appendStorageUrls(fallbackBaseUrls, ctx, result);
return result;
}
std::vector<std::string> decodeProxyList(const std::string &body, bool isDevEnvironment, const std::string &pubKeyPem)
{
if (isDevEnvironment) {
return parseEndpointsArray(body);
}
const std::vector<std::uint8_t> pubBytes(pubKeyPem.begin(), pubKeyPem.end());
const std::string h = crypto::toHex(crypto::sha512(pubBytes));
const std::vector<std::uint8_t> key = crypto::fromHex(h.substr(0, 64));
const std::vector<std::uint8_t> iv = crypto::fromHex(h.substr(64, 32));
const std::vector<std::uint8_t> cipher = util::base64Decode(body);
const std::vector<std::uint8_t> plain = crypto::aesDecryptCbc(cipher, key, iv);
return parseEndpointsArray(std::string(plain.begin(), plain.end()));
}
}

View File

@@ -1,19 +0,0 @@
#ifndef AGW_FAILOVER_PROXY_LIST_H
#define AGW_FAILOVER_PROXY_LIST_H
#include <string>
#include <vector>
#include "agw/types.h"
namespace agw::failover
{
std::vector<std::string> buildStorageUrls(const std::vector<std::string> &primaryBaseUrls,
const std::vector<std::string> &fallbackBaseUrls,
const FailoverContext &ctx);
std::vector<std::string> decodeProxyList(const std::string &body, bool isDevEnvironment,
const std::string &pubKeyPem);
}
#endif

View File

@@ -1,21 +0,0 @@
#include "failover/proxy_picker.h"
namespace agw::failover
{
std::string pickHealthyProxy(IHttpClient &http, const std::vector<std::string> &proxyUrls, int timeoutMsecs)
{
for (const auto &proxy : proxyUrls) {
HttpRequest req;
req.url = proxy + "lmbd-health";
req.method = "GET";
req.headers = { { "Content-Type", "application/json" } };
req.timeoutMsecs = timeoutMsecs;
const HttpResponse resp = http.send(req);
if (resp.error == TransportError::None && !resp.sslError) {
return proxy;
}
}
return { };
}
}

View File

@@ -1,14 +0,0 @@
#ifndef AGW_FAILOVER_PROXY_PICKER_H
#define AGW_FAILOVER_PROXY_PICKER_H
#include <string>
#include <vector>
#include "agw/http.h"
namespace agw::failover
{
std::string pickHealthyProxy(IHttpClient &http, const std::vector<std::string> &proxyUrls, int timeoutMsecs);
}
#endif

View File

@@ -1,364 +0,0 @@
#include "agw/gateway_controller.h"
#include <algorithm>
#include <chrono>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <random>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "crypto/rng.h"
#include "failover/bypass_policy.h"
#include "failover/proxy_list.h"
#include "failover/proxy_picker.h"
#include "protocol/error_mapping.h"
#include "protocol/request_builder.h"
#include "protocol/response.h"
#include "util/thread_pool.h"
#include "util/url.h"
#include "util/uuid.h"
namespace agw {
namespace {
bool isCancelled(const CancellationToken *cancel)
{
return cancel != nullptr && cancel->isCancelled();
}
std::function<bool()> makeCancelCheck(CancellationToken *cancel)
{
if (cancel == nullptr) {
return {};
}
return [cancel] { return cancel->isCancelled(); };
}
std::string threadIdStr()
{
std::ostringstream oss;
oss << std::this_thread::get_id();
return oss.str();
}
const char *transportErrorName(TransportError e)
{
switch (e) {
case TransportError::None: return "None";
case TransportError::Timeout: return "Timeout";
case TransportError::Canceled: return "Canceled";
case TransportError::OperationNotImplemented: return "OperationNotImplemented";
case TransportError::ConnectionError: return "ConnectionError";
}
return "?";
}
}
struct GatewayController::Impl {
Config config;
std::shared_ptr<IHttpClient> http;
std::unique_ptr<crypto::IRng> rng;
std::mutex proxyMutex;
std::string cachedProxy;
util::ThreadPool pool;
explicit Impl(Config cfg)
: config(std::move(cfg)),
rng(std::make_unique<crypto::DefaultRng>()),
pool(static_cast<std::size_t>(config.threadPoolSize))
{
http = config.httpClient ? config.httpClient
: std::shared_ptr<IHttpClient>(makeDefaultHttpClient());
log(LogLevel::Info,
"client created: dev=" + std::string(config.isDevEnvironment ? "1" : "0")
+ " timeout=" + std::to_string(config.requestTimeoutMsecs) + "ms"
+ " pool=" + std::to_string(config.threadPoolSize)
+ " s3primary=" + std::to_string(config.s3PrimaryEndpoints.size())
+ " s3fallback=" + std::to_string(config.s3FallbackEndpoints.size())
+ " customHttp=" + std::string(config.httpClient ? "1" : "0"));
}
void log(LogLevel level, const std::string &message) const
{
if (config.log) {
config.log(level, message);
}
}
void dbg(const std::string &message) const { log(LogLevel::Debug, message); }
std::string getCachedProxy()
{
std::lock_guard<std::mutex> lock(proxyMutex);
return cachedProxy;
}
void setCachedProxy(const std::string &proxy)
{
std::lock_guard<std::mutex> lock(proxyMutex);
cachedProxy = proxy;
}
bool attempt(const std::string &endpoint, const std::string &host, const HttpRequest &baseReq,
const std::vector<std::uint8_t> &key, const std::vector<std::uint8_t> &iv,
HttpResponse &resp, protocol::DecryptResult &dec)
{
HttpRequest req = baseReq;
req.url = util::formatEndpoint(endpoint, host);
dbg(" proxy attempt: POST " + req.url);
const auto t0 = std::chrono::steady_clock::now();
resp = http->send(req);
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
dec = protocol::tryDecryptResponse(resp.body, key, iv);
const bool bypass = resp.sslError || failover::shouldBypassProxy(resp.error, dec.decryptedBody, dec.ok);
dbg(" proxy attempt result: transport=" + std::string(transportErrorName(resp.error))
+ " ssl=" + std::string(resp.sslError ? "1" : "0") + " http=" + std::to_string(resp.httpStatusCode)
+ " bodyLen=" + std::to_string(resp.body.size()) + " decryptOk=" + std::string(dec.ok ? "1" : "0")
+ " bypassAgain=" + std::string(bypass ? "1" : "0") + " (" + std::to_string(ms) + "ms)");
return !bypass;
}
void runFailover(const std::string &endpoint, const HttpRequest &baseReq, const FailoverContext &ctx,
const std::vector<std::uint8_t> &key, const std::vector<std::uint8_t> &iv,
HttpResponse &resp, protocol::DecryptResult &dec, CancellationToken *cancel)
{
if (isCancelled(cancel)) {
dbg("failover: cancelled before start");
return;
}
std::random_device rd;
std::mt19937 gen(rd());
std::vector<std::string> primary = config.s3PrimaryEndpoints;
std::vector<std::string> fallback = config.s3FallbackEndpoints;
std::shuffle(primary.begin(), primary.end(), gen);
std::shuffle(fallback.begin(), fallback.end(), gen);
const std::vector<std::string> storageUrls = failover::buildStorageUrls(primary, fallback, ctx);
dbg("failover: storage urls=" + std::to_string(storageUrls.size())
+ " service='" + ctx.serviceType + "' country='" + ctx.userCountryCode + "'");
std::vector<std::string> proxyUrls;
for (const auto &storageUrl : storageUrls) {
if (isCancelled(cancel)) {
dbg("failover: cancelled during storage fetch");
return;
}
HttpRequest g;
g.url = storageUrl;
g.method = "GET";
g.headers = {{"Content-Type", "application/json"}};
g.timeoutMsecs = config.proxyStorageTimeoutMsecs;
g.cancelCheck = makeCancelCheck(cancel);
const HttpResponse gr = http->send(g);
dbg(" storage GET " + storageUrl + " → transport=" + std::string(transportErrorName(gr.error))
+ " ssl=" + std::string(gr.sslError ? "1" : "0") + " http=" + std::to_string(gr.httpStatusCode)
+ " bodyLen=" + std::to_string(gr.body.size()));
if (gr.error != TransportError::None || gr.sslError) {
continue;
}
try {
proxyUrls = failover::decodeProxyList(gr.body, config.isDevEnvironment, config.agwPublicKeyPem);
dbg(" decoded proxy list: " + std::to_string(proxyUrls.size()) + " proxies");
break;
} catch (...) {
dbg(" proxy list decode failed → next storage");
continue;
}
}
std::shuffle(proxyUrls.begin(), proxyUrls.end(), gen);
std::string proxy = getCachedProxy();
if (proxy.empty()) {
if (isCancelled(cancel)) {
dbg("failover: cancelled before health-check");
return;
}
dbg("failover: no cached proxy → health-check of " + std::to_string(proxyUrls.size()) + " proxies");
proxy = failover::pickHealthyProxy(*http, proxyUrls, config.proxyHealthTimeoutMsecs);
if (!proxy.empty()) {
dbg("failover: healthy proxy = " + proxy + " (cached)");
setCachedProxy(proxy);
} else {
dbg("failover: no healthy proxy found");
}
} else {
dbg("failover: using cached proxy = " + proxy);
}
if (!proxy.empty()) {
if (isCancelled(cancel)) {
return;
}
if (attempt(endpoint, proxy, baseReq, key, iv, resp, dec)) {
dbg("failover: succeeded via cached/first proxy");
return;
}
}
for (const auto &p : proxyUrls) {
if (isCancelled(cancel)) {
return;
}
if (attempt(endpoint, p, baseReq, key, iv, resp, dec)) {
dbg("failover: succeeded via proxy " + p + " (cached)");
setCachedProxy(p);
return;
}
}
dbg("failover: exhausted all proxies (using last attempt result)");
}
Response executePost(const std::string &endpoint, const std::string &payload,
const FailoverContext &ctx, CancellationToken *cancel)
{
const auto tStart = std::chrono::steady_clock::now();
log(LogLevel::Info, "post START endpoint='" + endpoint + "' service='" + ctx.serviceType
+ "' country='" + ctx.userCountryCode + "' payloadLen=" + std::to_string(payload.size())
+ " thread=" + threadIdStr());
if (isCancelled(cancel)) {
log(LogLevel::Info, "post: cancelled before start");
return Response{ErrorCode::Cancelled, std::string()};
}
protocol::EncryptedRequest enc =
protocol::buildEncryptedRequest(payload, config.agwPublicKeyPem, *rng);
if (enc.error != ErrorCode::NoError) {
log(LogLevel::Warning, "post: request build failed error="
+ std::to_string(static_cast<int>(enc.error)));
return Response{enc.error, std::string()};
}
dbg("request built: bodyLen=" + std::to_string(enc.body.size()) + " (key/iv/salt generated)");
if (isCancelled(cancel)) {
return Response{ErrorCode::Cancelled, std::string()};
}
const std::string requestId = util::makeUuidV4(*rng);
const std::string cached = getCachedProxy();
const std::string directHost = cached.empty() ? config.gatewayEndpoint : cached;
const std::string url = util::formatEndpoint(endpoint, directHost);
dbg("direct request: url=" + url + " reqId=" + requestId
+ " viaCachedProxy=" + std::string(cached.empty() ? "0" : "1"));
if (config.onBeforeRequest) {
const std::string host = util::extractHost(url);
dbg("onBeforeRequest(host=" + host + ")");
config.onBeforeRequest(host);
}
HttpRequest req;
req.url = url;
req.method = "POST";
req.body = enc.body;
req.headers = {
{"Content-Type", "application/json"},
{"X-Client-Request-ID", requestId},
};
req.timeoutMsecs = config.requestTimeoutMsecs;
req.cancelCheck = makeCancelCheck(cancel);
const auto t0 = std::chrono::steady_clock::now();
HttpResponse resp = http->send(req);
const auto httpMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
if (isCancelled(cancel)) {
log(LogLevel::Info, "post: cancelled after direct send");
return Response{ErrorCode::Cancelled, std::string()};
}
protocol::DecryptResult dec = protocol::tryDecryptResponse(resp.body, enc.key, enc.iv);
dbg("direct response: transport=" + std::string(transportErrorName(resp.error))
+ " ssl=" + std::string(resp.sslError ? "1" : "0") + " http=" + std::to_string(resp.httpStatusCode)
+ " bodyLen=" + std::to_string(resp.body.size()) + " decryptOk=" + std::string(dec.ok ? "1" : "0")
+ " (" + std::to_string(httpMs) + "ms)");
const bool bypass = !resp.sslError
&& failover::shouldBypassProxy(resp.error, dec.decryptedBody, dec.ok);
if (bypass) {
log(LogLevel::Info, "direct response suspicious — running failover");
runFailover(endpoint, req, ctx, enc.key, enc.iv, resp, dec, cancel);
if (isCancelled(cancel)) {
log(LogLevel::Info, "post: cancelled during failover");
return Response{ErrorCode::Cancelled, std::string()};
}
} else {
dbg("direct response accepted (no failover)");
}
Response out;
out.body = dec.decryptedBody;
const ErrorCode mapped = protocol::mapResponseError(resp.sslError, resp.error, dec.decryptedBody);
const auto totalMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - tStart).count();
if (mapped != ErrorCode::NoError) {
out.error = mapped;
log(LogLevel::Warning, "post DONE error=" + std::to_string(static_cast<int>(mapped))
+ " bodyLen=" + std::to_string(out.body.size()) + " (" + std::to_string(totalMs) + "ms)");
return out;
}
if (!dec.ok) {
out.error = ErrorCode::ApiConfigDecryptionError;
log(LogLevel::Error, "post DONE: response decryption failed (1106) ("
+ std::to_string(totalMs) + "ms)");
return out;
}
out.error = ErrorCode::NoError;
log(LogLevel::Info, "post DONE ok bodyLen=" + std::to_string(out.body.size())
+ " (" + std::to_string(totalMs) + "ms)");
return out;
}
};
GatewayController::GatewayController(Config config) : m_impl(std::make_unique<Impl>(std::move(config))) {}
GatewayController::~GatewayController() = default;
GatewayController::GatewayController(GatewayController &&) noexcept = default;
GatewayController &GatewayController::operator=(GatewayController &&) noexcept = default;
Response GatewayController::post(const std::string &endpoint, const std::string &payload,
const FailoverContext &ctx, CancellationToken *cancel)
{
return m_impl->executePost(endpoint, payload, ctx, cancel);
}
void GatewayController::postAsync(const std::string &endpoint, const std::string &payload,
std::function<void(Response)> onResult, const FailoverContext &ctx,
CancellationToken *cancel)
{
Impl *impl = m_impl.get();
impl->dbg("postAsync: submitting to pool (caller thread=" + threadIdStr() + ")");
impl->pool.submit([impl, endpoint, payload, onResult = std::move(onResult), ctx, cancel]() {
impl->dbg("postAsync: running on pool thread=" + threadIdStr());
Response r = impl->executePost(endpoint, payload, ctx, cancel);
if (onResult) {
onResult(std::move(r));
}
});
}
std::future<Response> GatewayController::postFuture(const std::string &endpoint, const std::string &payload,
const FailoverContext &ctx, CancellationToken *cancel)
{
auto promise = std::make_shared<std::promise<Response>>();
std::future<Response> fut = promise->get_future();
Impl *impl = m_impl.get();
impl->dbg("postFuture: submitting to pool (caller thread=" + threadIdStr() + ")");
impl->pool.submit([impl, endpoint, payload, ctx, cancel, promise]() {
impl->dbg("postFuture: running on pool thread=" + threadIdStr());
promise->set_value(impl->executePost(endpoint, payload, ctx, cancel));
});
return fut;
}
}

View File

@@ -1,132 +0,0 @@
#ifdef AGW_HAVE_CURL
#include "http/curl_client.h"
#include <mutex>
#include <string>
#include <curl/curl.h>
namespace agw
{
namespace
{
std::once_flag g_curlInitOnce;
void ensureCurlGlobalInit()
{
std::call_once(g_curlInitOnce, []() { curl_global_init(CURL_GLOBAL_DEFAULT); });
}
std::size_t writeCallback(char *ptr, std::size_t size, std::size_t nmemb, void *userdata)
{
const std::size_t total = size * nmemb;
auto *buf = static_cast<std::string *>(userdata);
buf->append(ptr, total);
return total;
}
int xferCallback(void *clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t)
{
auto *check = static_cast<const std::function<bool()> *>(clientp);
if (check && *check && (*check)()) {
return 1;
}
return 0;
}
TransportError mapCurlError(CURLcode code, bool &sslError)
{
sslError = false;
switch (code) {
case CURLE_OK: return TransportError::None;
case CURLE_OPERATION_TIMEDOUT: return TransportError::Timeout;
case CURLE_ABORTED_BY_CALLBACK: return TransportError::Canceled;
case CURLE_SSL_CONNECT_ERROR:
case CURLE_PEER_FAILED_VERIFICATION:
case CURLE_SSL_CERTPROBLEM:
case CURLE_SSL_CIPHER:
case CURLE_SSL_CACERT_BADFILE:
case CURLE_SSL_ISSUER_ERROR: sslError = true; return TransportError::ConnectionError;
default: return TransportError::ConnectionError;
}
}
}
CurlHttpClient::CurlHttpClient()
{
ensureCurlGlobalInit();
}
CurlHttpClient::~CurlHttpClient() = default;
HttpResponse CurlHttpClient::send(const HttpRequest &request)
{
HttpResponse response;
CURL *curl = curl_easy_init();
if (!curl) {
response.error = TransportError::ConnectionError;
response.errorString = "curl_easy_init failed";
return response;
}
struct curl_slist *headers = nullptr;
for (const auto &h : request.headers) {
const std::string line = h.first + ": " + h.second;
headers = curl_slist_append(headers, line.c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
if (headers) {
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
}
if (request.timeoutMsecs > 0) {
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, static_cast<long>(request.timeoutMsecs));
}
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
if (request.cancelCheck) {
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCallback);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &request.cancelCheck);
}
if (request.method == "POST") {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.data());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(request.body.size()));
} else {
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
}
const CURLcode code = curl_easy_perform(curl);
bool sslError = false;
response.error = mapCurlError(code, sslError);
response.sslError = sslError;
if (code != CURLE_OK) {
response.errorString = curl_easy_strerror(code);
}
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
response.httpStatusCode = static_cast<int>(httpCode);
if (headers) {
curl_slist_free_all(headers);
}
curl_easy_cleanup(curl);
return response;
}
std::unique_ptr<IHttpClient> makeDefaultHttpClient()
{
return std::make_unique<CurlHttpClient>();
}
}
#endif

View File

@@ -1,17 +0,0 @@
#ifndef AGW_HTTP_CURL_CLIENT_H
#define AGW_HTTP_CURL_CLIENT_H
#include "agw/http.h"
namespace agw
{
class CurlHttpClient : public IHttpClient
{
public:
CurlHttpClient();
~CurlHttpClient() override;
HttpResponse send(const HttpRequest &request) override;
};
}
#endif

View File

@@ -1,15 +0,0 @@
#ifndef AGW_HAVE_CURL
#include <stdexcept>
#include "agw/http.h"
namespace agw
{
std::unique_ptr<IHttpClient> makeDefaultHttpClient()
{
throw std::runtime_error("agw: SDK built without libcurl; provide Config::httpClient (your own IHttpClient)");
}
}
#endif

View File

@@ -1,133 +0,0 @@
#include "protocol/error_mapping.h"
#include <algorithm>
#include <cctype>
#include "protocol/keys.h"
#include "util/json.h"
namespace agw::protocol
{
namespace
{
constexpr int kConflict = 409;
constexpr int kNotFound = 404;
constexpr int kNotImplemented = 501;
constexpr int kPaymentRequired = 402;
constexpr int kTooManyRequests = 429;
constexpr int kRequestTimeout = 408;
constexpr int kUnprocessableEntity = 422;
constexpr const char *kUnprocessableSubscriptionMessage =
"Failed to retrieve subscription information. Is it activated?";
constexpr const char *kTrialAlreadyUsedMessage = "trial subscription already used";
std::string trim(const std::string &s)
{
std::size_t b = 0;
std::size_t e = s.size();
while (b < e && std::isspace(static_cast<unsigned char>(s[b])))
++b;
while (e > b && std::isspace(static_cast<unsigned char>(s[e - 1])))
--e;
return s.substr(b, e - b);
}
std::string toLower(std::string s)
{
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
bool containsCI(const std::string &haystack, const std::string &needle)
{
return toLower(haystack).find(toLower(needle)) != std::string::npos;
}
std::string messageFrom(const util::Json &obj)
{
auto it = obj.find(keys::message);
if (it != obj.end() && it->is_string()) {
return trim(it->get<std::string>());
}
return { };
}
}
ErrorCode mapResponseError(bool sslError, TransportError transportError, const std::string &decryptedBody)
{
if (sslError) {
return ErrorCode::ApiConfigSslError;
}
if (transportError == TransportError::Timeout || transportError == TransportError::Canceled) {
return ErrorCode::ApiConfigTimeoutError;
}
if (transportError == TransportError::OperationNotImplemented) {
return ErrorCode::ApiUpdateRequestError;
}
util::Json obj;
bool isObject = false;
try {
obj = util::Json::parse(decryptedBody);
isObject = obj.is_object();
} catch (...) {
isObject = false;
}
if (isObject) {
int httpStatus = -1;
if (auto it = obj.find(keys::httpStatus); it != obj.end() && it->is_number_integer()) {
httpStatus = it->get<int>();
}
const std::string message = messageFrom(obj);
if (httpStatus == kTooManyRequests) {
return ErrorCode::ApiRateLimitError;
}
if (httpStatus == kConflict) {
if (containsCI(message, kTrialAlreadyUsedMessage)) {
return ErrorCode::ApiTrialAlreadyUsedError;
}
return ErrorCode::ApiConfigLimitError;
}
if (httpStatus == kNotFound) {
return ErrorCode::ApiNotFoundError;
}
if (httpStatus == kRequestTimeout) {
return ErrorCode::ApiConfigTimeoutError;
}
if (httpStatus == kNotImplemented) {
return ErrorCode::ApiUpdateRequestError;
}
if (httpStatus == kUnprocessableEntity) {
if (message == kUnprocessableSubscriptionMessage) {
return ErrorCode::ApiSubscriptionExpiredError;
}
return ErrorCode::ApiConfigDownloadError;
}
if (httpStatus == kPaymentRequired) {
if (containsCI(message, "refresh_captcha")) {
return ErrorCode::ApiCaptchaRefreshError;
}
if (containsCI(message, "invalid_captcha")) {
return ErrorCode::ApiCaptchaInvalidError;
}
if (obj.contains("captcha_id") || obj.contains("captcha_image")
|| containsCI(message, "rate_limit_exceeded")) {
return ErrorCode::ApiCaptchaRequiredError;
}
return ErrorCode::ApiSubscriptionNotActiveError;
}
if (httpStatus >= 300) {
return ErrorCode::ApiConfigDownloadError;
}
}
if (transportError == TransportError::None) {
return ErrorCode::NoError;
}
return ErrorCode::ApiConfigDownloadError;
}
}

View File

@@ -1,14 +0,0 @@
#ifndef AGW_PROTOCOL_ERROR_MAPPING_H
#define AGW_PROTOCOL_ERROR_MAPPING_H
#include <string>
#include "agw/http.h"
#include "agw/types.h"
namespace agw::protocol
{
ErrorCode mapResponseError(bool sslError, TransportError transportError, const std::string &decryptedBody);
}
#endif

View File

@@ -1,18 +0,0 @@
#ifndef AGW_PROTOCOL_KEYS_H
#define AGW_PROTOCOL_KEYS_H
namespace agw::protocol::keys {
inline constexpr const char *aesKey = "aes_key";
inline constexpr const char *aesIv = "aes_iv";
inline constexpr const char *aesSalt = "aes_salt";
inline constexpr const char *apiPayload = "api_payload";
inline constexpr const char *keyPayload = "key_payload";
inline constexpr const char *serviceType = "service_type";
inline constexpr const char *userCountryCode = "user_country_code";
inline constexpr const char *httpStatus = "http_status";
inline constexpr const char *message = "message";
}
#endif

View File

@@ -1,55 +0,0 @@
#include "protocol/request_builder.h"
#include "crypto/aes.h"
#include "crypto/rsa.h"
#include "protocol/keys.h"
#include "util/base64.h"
#include "util/json.h"
namespace agw::protocol
{
namespace
{
std::vector<std::uint8_t> bytesOf(const std::string &s)
{
return std::vector<std::uint8_t>(s.begin(), s.end());
}
}
EncryptedRequest buildEncryptedRequest(const std::string &payload, const std::string &publicKeyPem, crypto::IRng &rng)
{
namespace k = keys;
EncryptedRequest out;
out.key = rng.bytes(32);
out.iv = rng.bytes(32);
out.salt = rng.bytes(8);
if (!crypto::rsaPublicKeyValid(publicKeyPem)) {
out.error = ErrorCode::ApiMissingAgwPublicKey;
return out;
}
util::Json keysJson;
keysJson[k::aesKey] = util::base64Encode(out.key);
keysJson[k::aesIv] = util::base64Encode(out.iv);
keysJson[k::aesSalt] = util::base64Encode(out.salt);
const std::string keysSerialized = util::qtIndentedDump(keysJson);
std::string keyPayloadB64;
std::string apiPayloadB64;
try {
keyPayloadB64 = util::base64Encode(crypto::rsaEncryptPublicPkcs1(bytesOf(keysSerialized), publicKeyPem));
apiPayloadB64 = util::base64Encode(crypto::aesEncryptCbc(bytesOf(payload), out.key, out.iv));
} catch (...) {
out.error = ErrorCode::ApiConfigDecryptionError;
return out;
}
util::Json body;
body[k::keyPayload] = keyPayloadB64;
body[k::apiPayload] = apiPayloadB64;
out.body = util::qtIndentedDump(body);
return out;
}
}

View File

@@ -1,26 +0,0 @@
#ifndef AGW_PROTOCOL_REQUEST_BUILDER_H
#define AGW_PROTOCOL_REQUEST_BUILDER_H
#include <cstdint>
#include <string>
#include <vector>
#include "agw/types.h"
#include "crypto/rng.h"
namespace agw::protocol
{
struct EncryptedRequest
{
std::string body;
std::vector<std::uint8_t> key;
std::vector<std::uint8_t> iv;
std::vector<std::uint8_t> salt;
ErrorCode error = ErrorCode::NoError;
};
EncryptedRequest buildEncryptedRequest(const std::string &payload, const std::string &publicKeyPem,
crypto::IRng &rng);
}
#endif

View File

@@ -1,24 +0,0 @@
#include "protocol/response.h"
#include "crypto/aes.h"
namespace agw::protocol
{
DecryptResult tryDecryptResponse(const std::string &encrypted, const std::vector<std::uint8_t> &key,
const std::vector<std::uint8_t> &iv)
{
DecryptResult result;
result.decryptedBody = encrypted;
result.ok = false;
try {
const std::vector<std::uint8_t> in(encrypted.begin(), encrypted.end());
const std::vector<std::uint8_t> out = crypto::aesDecryptCbc(in, key, iv);
result.decryptedBody.assign(out.begin(), out.end());
result.ok = true;
} catch (...) {
result.decryptedBody = encrypted;
result.ok = false;
}
return result;
}
}

View File

@@ -1,20 +0,0 @@
#ifndef AGW_PROTOCOL_RESPONSE_H
#define AGW_PROTOCOL_RESPONSE_H
#include <cstdint>
#include <string>
#include <vector>
namespace agw::protocol
{
struct DecryptResult
{
std::string decryptedBody;
bool ok = false;
};
DecryptResult tryDecryptResponse(const std::string &encrypted, const std::vector<std::uint8_t> &key,
const std::vector<std::uint8_t> &iv);
}
#endif

View File

@@ -1,108 +0,0 @@
#include "base64.h"
#include <array>
namespace agw::util
{
namespace
{
const char *kStd = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const char *kUrl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
std::string encode(const std::vector<std::uint8_t> &data, const char *alphabet, bool pad)
{
std::string out;
out.reserve((data.size() + 2) / 3 * 4);
std::size_t i = 0;
const std::size_t n = data.size();
while (i + 3 <= n) {
const std::uint32_t v = (std::uint32_t(data[i]) << 16) | (std::uint32_t(data[i + 1]) << 8) | data[i + 2];
out.push_back(alphabet[(v >> 18) & 0x3F]);
out.push_back(alphabet[(v >> 12) & 0x3F]);
out.push_back(alphabet[(v >> 6) & 0x3F]);
out.push_back(alphabet[v & 0x3F]);
i += 3;
}
const std::size_t rem = n - i;
if (rem == 1) {
const std::uint32_t v = std::uint32_t(data[i]) << 16;
out.push_back(alphabet[(v >> 18) & 0x3F]);
out.push_back(alphabet[(v >> 12) & 0x3F]);
if (pad) {
out.push_back('=');
out.push_back('=');
}
} else if (rem == 2) {
const std::uint32_t v = (std::uint32_t(data[i]) << 16) | (std::uint32_t(data[i + 1]) << 8);
out.push_back(alphabet[(v >> 18) & 0x3F]);
out.push_back(alphabet[(v >> 12) & 0x3F]);
out.push_back(alphabet[(v >> 6) & 0x3F]);
if (pad) {
out.push_back('=');
}
}
return out;
}
int decodeChar(char c)
{
if (c >= 'A' && c <= 'Z')
return c - 'A';
if (c >= 'a' && c <= 'z')
return c - 'a' + 26;
if (c >= '0' && c <= '9')
return c - '0' + 52;
if (c == '+' || c == '-')
return 62;
if (c == '/' || c == '_')
return 63;
return -1;
}
}
std::string base64Encode(const std::vector<std::uint8_t> &data)
{
return encode(data, kStd, true);
}
std::string base64UrlEncodeNoPad(const std::vector<std::uint8_t> &data)
{
return encode(data, kUrl, false);
}
std::string base64Encode(const std::string &data)
{
return base64Encode(std::vector<std::uint8_t>(data.begin(), data.end()));
}
std::vector<std::uint8_t> base64Decode(const std::string &text)
{
std::vector<std::uint8_t> out;
out.reserve(text.size() / 4 * 3 + 3);
std::array<int, 4> quad { };
int count = 0;
for (char c : text) {
const int v = decodeChar(c);
if (v < 0) {
continue;
}
quad[count++] = v;
if (count == 4) {
out.push_back(static_cast<std::uint8_t>((quad[0] << 2) | (quad[1] >> 4)));
out.push_back(static_cast<std::uint8_t>((quad[1] << 4) | (quad[2] >> 2)));
out.push_back(static_cast<std::uint8_t>((quad[2] << 6) | quad[3]));
count = 0;
}
}
if (count == 2) {
out.push_back(static_cast<std::uint8_t>((quad[0] << 2) | (quad[1] >> 4)));
} else if (count == 3) {
out.push_back(static_cast<std::uint8_t>((quad[0] << 2) | (quad[1] >> 4)));
out.push_back(static_cast<std::uint8_t>((quad[1] << 4) | (quad[2] >> 2)));
}
return out;
}
}

View File

@@ -1,19 +0,0 @@
#ifndef AGW_UTIL_BASE64_H
#define AGW_UTIL_BASE64_H
#include <cstdint>
#include <string>
#include <vector>
namespace agw::util
{
std::string base64Encode(const std::vector<std::uint8_t> &data);
std::string base64UrlEncodeNoPad(const std::vector<std::uint8_t> &data);
std::vector<std::uint8_t> base64Decode(const std::string &text);
std::string base64Encode(const std::string &data);
}
#endif

View File

@@ -1,108 +0,0 @@
#include "json.h"
#include <cstdint>
namespace agw::util
{
namespace
{
const char *kHex = "0123456789abcdef";
void appendEscaped(std::string &out, const std::string &s)
{
out.push_back('"');
for (unsigned char c : s) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20) {
out += "\\u00";
out.push_back(kHex[c >> 4]);
out.push_back(kHex[c & 0x0F]);
} else {
out.push_back(static_cast<char>(c));
}
}
}
out.push_back('"');
}
void appendIndent(std::string &out, int level)
{
out.append(static_cast<std::size_t>(level) * 4, ' ');
}
void dumpValue(std::string &out, const Json &j, int indent);
void dumpObject(std::string &out, const Json &j, int indent)
{
out += "{\n";
const int inner = indent + 1;
std::size_t i = 0;
const std::size_t n = j.size();
for (auto it = j.begin(); it != j.end(); ++it, ++i) {
appendIndent(out, inner);
appendEscaped(out, it.key());
out += ": ";
dumpValue(out, it.value(), inner);
if (i + 1 < n) {
out.push_back(',');
}
out.push_back('\n');
}
appendIndent(out, indent);
out.push_back('}');
}
void dumpArray(std::string &out, const Json &j, int indent)
{
out += "[\n";
const int inner = indent + 1;
std::size_t i = 0;
const std::size_t n = j.size();
for (const auto &el : j) {
appendIndent(out, inner);
dumpValue(out, el, inner);
if (i + 1 < n) {
out.push_back(',');
}
out.push_back('\n');
++i;
}
appendIndent(out, indent);
out.push_back(']');
}
void dumpValue(std::string &out, const Json &j, int indent)
{
switch (j.type()) {
case Json::value_t::object: dumpObject(out, j, indent); break;
case Json::value_t::array: dumpArray(out, j, indent); break;
case Json::value_t::string: appendEscaped(out, j.get<std::string>()); break;
case Json::value_t::boolean: out += j.get<bool>() ? "true" : "false"; break;
case Json::value_t::null: out += "null"; break;
case Json::value_t::number_integer:
case Json::value_t::number_unsigned:
case Json::value_t::number_float:
default:
out += j.dump();
break;
}
}
}
std::string qtIndentedDump(const Json &j)
{
std::string out;
dumpValue(out, j, 0);
out.push_back('\n');
return out;
}
}

View File

@@ -1,15 +0,0 @@
#ifndef AGW_UTIL_JSON_H
#define AGW_UTIL_JSON_H
#include <string>
#include <nlohmann/json.hpp>
namespace agw::util
{
using Json = nlohmann::json;
std::string qtIndentedDump(const Json &j);
}
#endif

View File

@@ -1,59 +0,0 @@
#include "util/thread_pool.h"
namespace agw::util
{
ThreadPool::ThreadPool(std::size_t threadCount)
{
if (threadCount == 0) {
threadCount = 1;
}
m_workers.reserve(threadCount);
for (std::size_t i = 0; i < threadCount; ++i) {
m_workers.emplace_back([this] { workerLoop(); });
}
}
ThreadPool::~ThreadPool()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stopping = true;
}
m_cv.notify_all();
for (auto &w : m_workers) {
if (w.joinable()) {
w.join();
}
}
}
void ThreadPool::submit(std::function<void()> task)
{
{
std::lock_guard<std::mutex> lock(m_mutex);
m_tasks.push(std::move(task));
}
m_cv.notify_one();
}
void ThreadPool::workerLoop()
{
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(m_mutex);
m_cv.wait(lock, [this] { return m_stopping || !m_tasks.empty(); });
if (m_tasks.empty()) {
if (m_stopping) {
return;
}
continue;
}
task = std::move(m_tasks.front());
m_tasks.pop();
}
task();
}
}
}

View File

@@ -1,36 +0,0 @@
#ifndef AGW_UTIL_THREAD_POOL_H
#define AGW_UTIL_THREAD_POOL_H
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
namespace agw::util
{
class ThreadPool
{
public:
explicit ThreadPool(std::size_t threadCount);
~ThreadPool();
ThreadPool(const ThreadPool &) = delete;
ThreadPool &operator=(const ThreadPool &) = delete;
void submit(std::function<void()> task);
private:
void workerLoop();
std::vector<std::thread> m_workers;
std::queue<std::function<void()>> m_tasks;
std::mutex m_mutex;
std::condition_variable m_cv;
bool m_stopping = false;
};
}
#endif

View File

@@ -1,48 +0,0 @@
#include "util/url.h"
namespace agw::util
{
std::string formatEndpoint(const std::string &endpoint, const std::string &host)
{
const std::string token = "%1";
const std::size_t pos = endpoint.find(token);
if (pos == std::string::npos) {
return endpoint;
}
std::string out = endpoint;
out.replace(pos, token.size(), host);
return out;
}
std::string extractHost(const std::string &url)
{
std::size_t start = 0;
const std::size_t scheme = url.find("://");
if (scheme != std::string::npos) {
start = scheme + 3;
}
std::size_t end = url.size();
for (std::size_t i = start; i < url.size(); ++i) {
const char c = url[i];
if (c == '/' || c == '?' || c == '#') {
end = i;
break;
}
}
std::string authority = url.substr(start, end - start);
const std::size_t at = authority.find('@');
if (at != std::string::npos) {
authority = authority.substr(at + 1);
}
const std::size_t colon = authority.find(':');
if (colon != std::string::npos) {
authority = authority.substr(0, colon);
}
return authority;
}
}

View File

@@ -1,13 +0,0 @@
#ifndef AGW_UTIL_URL_H
#define AGW_UTIL_URL_H
#include <string>
namespace agw::util
{
std::string formatEndpoint(const std::string &endpoint, const std::string &host);
std::string extractHost(const std::string &url);
}
#endif

View File

@@ -1,36 +0,0 @@
#include "uuid.h"
#include <cstdint>
#include <vector>
namespace agw::util
{
namespace
{
const char *kHex = "0123456789abcdef";
void appendHex(std::string &out, std::uint8_t b)
{
out.push_back(kHex[b >> 4]);
out.push_back(kHex[b & 0x0F]);
}
}
std::string makeUuidV4(crypto::IRng &rng)
{
std::vector<std::uint8_t> b = rng.bytes(16);
b[6] = static_cast<std::uint8_t>((b[6] & 0x0F) | 0x40);
b[8] = static_cast<std::uint8_t>((b[8] & 0x3F) | 0x80);
std::string out;
out.reserve(36);
for (int i = 0; i < 16; ++i) {
if (i == 4 || i == 6 || i == 8 || i == 10) {
out.push_back('-');
}
appendHex(out, b[i]);
}
return out;
}
}

View File

@@ -1,13 +0,0 @@
#ifndef AGW_UTIL_UUID_H
#define AGW_UTIL_UUID_H
#include <string>
#include "crypto/rng.h"
namespace agw::util
{
std::string makeUuidV4(crypto::IRng &rng);
}
#endif

View File

@@ -1,27 +0,0 @@
# Тесты agw-sdk. Локально — на встроенном harness (agw_test.h), без внешнего фреймворка.
set(AGW_FIXTURES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/golden/fixtures")
function(agw_add_test name src)
add_executable(${name} ${src})
target_include_directories(${name} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/src
)
target_link_libraries(${name} PRIVATE agw OpenSSL::Crypto nlohmann_json::nlohmann_json Threads::Threads)
target_compile_definitions(${name} PRIVATE AGW_FIXTURES_DIR="${AGW_FIXTURES_DIR}")
add_test(NAME ${name} COMMAND ${name})
endfunction()
agw_add_test(test_crypto unit/test_crypto.cpp)
agw_add_test(test_json unit/test_json.cpp)
agw_add_test(test_golden golden/test_golden.cpp)
agw_add_test(test_error_mapping unit/test_error_mapping.cpp)
agw_add_test(test_bypass_policy unit/test_bypass_policy.cpp)
agw_add_test(test_proxy_list unit/test_proxy_list.cpp)
agw_add_test(test_thread_pool unit/test_thread_pool.cpp)
agw_add_test(test_post integration/test_post.cpp)
agw_add_test(test_failover integration/test_failover.cpp)
agw_add_test(test_async integration/test_async.cpp)
agw_add_test(test_c_abi integration/test_c_abi.cpp)

View File

@@ -1,39 +0,0 @@
#ifndef AGW_TEST_H
#define AGW_TEST_H
#include <cstdio>
#include <cstdlib>
#include <string>
namespace agw_test {
inline int &failCount()
{
static int n = 0;
return n;
}
inline void report(bool ok, const char *expr, const char *file, int line)
{
if (!ok) {
std::fprintf(stderr, "FAIL: %s\n at %s:%d\n", expr, file, line);
++failCount();
}
}
inline void reportEq(const std::string &a, const std::string &b, const char *expr, const char *file, int line)
{
if (a != b) {
std::fprintf(stderr, "FAIL: %s\n at %s:%d\n lhs=[%s]\n rhs=[%s]\n",
expr, file, line, a.c_str(), b.c_str());
++failCount();
}
}
}
#define CHECK(expr) ::agw_test::report((expr), #expr, __FILE__, __LINE__)
#define CHECK_EQ(a, b) ::agw_test::reportEq((a), (b), #a " == " #b, __FILE__, __LINE__)
#define AGW_TEST_MAIN_RETURN() \
(::agw_test::failCount() == 0 ? (std::printf("OK\n"), 0) : (std::fprintf(stderr, "%d check(s) failed\n", ::agw_test::failCount()), 1))
#endif

View File

@@ -1 +0,0 @@
ыaГцЖ7И~▐>ъl╛▌Ц╗ЦaJ╩┤ъ%фTхO╒╢^

View File

@@ -1 +0,0 @@
{"hello":"world"}

View File

@@ -1,4 +0,0 @@
AES_KEY_HEX=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
AES_IV_HEX=101112131415161718191a1b1c1d1e1f
AES_PLAINTEXT={"hello":"world"}
AES_CIPHER_B64=2WHnAcP2N+l+jz7fbKyO46jjYUq7h98lxlTIT6K0Xg8=

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL3pxE7uI3RacQ
jvyFz5tbkL87aqpFBvAVFS0OFzVLbApaJ6nv2jZNfidbPCN5SMeNoa2kNTC+MZNQ
qORgr77TgaRuFap5dSun9qci0ll5Y/zBQHb8/Xihah8YpkbO/8SV1aFWLtWKQiQL
xrFTD9ShXC7S6IQrdGcngUhLShinWmjveZJ//B7no3wUP3xPF+EkTXTq5QyD/SfQ
/w0BUosy55sCn5OyP6iSJ4cqujA7WCEd48XpS5zgceqdnE98mvjhriMDfORXsSnx
ymsDnPyX/wkrLOynpN5KPoCMwXS0knTSGADUPMm/EQXa5fjEDxg0OnJQcJV/mZJk
4Rgi+gZJAgMBAAECggEAXWW2ob3u1POL/gIDninmOqStd0L+jnEHPCFfar0nJU5x
z6usJr4JcqcA0MNUXRQCl9gh/MCBfCCqJKG7PrBE9BDIi8ZROyN6xJAzMbi8VOiB
uucVnAFjak97v4ctmVeDcEFWkG0UVyrF6L82LZ9rAiGBMg5jvqStPWP1AskHUmM+
drJZYsrvqqqZLVbB34I6logBcD0IKEib8uBM3brLrO1t86XpLvOIJEjCsD+GRtmJ
vnjjVcIqHFM+VyA+RvpgMnfbTcG3D4YZhDtdgsbnOHs4mydM/I6C7a5pLftKsdoC
lKgb6CIqJoXvW2goljHfQiBre56hwhBjRgzmdo+BoQKBgQD+aM+P1+nAsIUxWmXf
jexL8LIQ+POxNztz/d9EIYLXOztiS3epwt8e/Xqffu4B5+D1j6u/gtnK90SNcMi2
IhetBwkTGdz6s/JHTt92f7okRbSnzwZwj03Ppkgg3VAPp3LL4nP/a1kcHi+9aqBB
kPPkQ0k9BS/JOmEVPOKpOx0ABwKBgQDNJOjDyhKesjjLK33xJdD6sdhwxFJrSMs3
TtMd0KvPu26Z+gn+5ybE/rzOoO7YZIUmB8AXvNKLu8U/5FRE6eOeaumz2LvFNCzE
IC6J2Oixt/lVxxuR0mSRTJI/hR0CtrofXRke8YjeU3VjtkMW6k2QrPAAMMTPieRW
fkfb8oWTLwKBgQCt0B/W77XFLxSgplkphgYlz/loPR4JOmoFEjLCkn6Y29/zhQnp
UrkrrBRl+ctUQ/7e5lx5yEVSNOOCGscWIG66iS76/NWL9vsVGt7zT8p106XcbEXD
CzUnJDztLybuuwFkKIAFxmqoGjuVls6MXSM0FYBpDy0Ztyfy4Zkd88QZawKBgHB8
rKWvSEZ8s2e0kXqJoe3VVzl+bTMm10eckWbn5U4jGKKV2KVNWpTqmd0zocRGWjxg
Q5TAlTLJ438FVK/1EDrtpPhY/51C3sksXFh5+B57It1GMHflRf/mXMs30pCKYcSQ
6BVvm/1NBjGG34LRN3b9XRy9oS2sDujelcilU1lBAoGBAMHx0oFSlNUHGofigo3p
26kWJw7BE/o8HVIrfI2vekXOgTVgJWGiubjp3qUKgNf1lbRy/Ur9F4ElW7hEINjC
aMJUZWc/xYwAmKHe/7ITZByfRXdGMwlvo8QudbzDC6vvCqn7wQW56kyTTEEHF54k
21jK19EBX4JWibzJotv8ShkU
-----END PRIVATE KEY-----

View File

@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy96cRO7iN0WnEI78hc+b
W5C/O2qqRQbwFRUtDhc1S2wKWiep79o2TX4nWzwjeUjHjaGtpDUwvjGTUKjkYK++
04GkbhWqeXUrp/anItJZeWP8wUB2/P14oWofGKZGzv/EldWhVi7VikIkC8axUw/U
oVwu0uiEK3RnJ4FIS0oYp1po73mSf/we56N8FD98TxfhJE106uUMg/0n0P8NAVKL
MuebAp+Tsj+okieHKrowO1ghHePF6Uuc4HHqnZxPfJr44a4jA3zkV7Ep8cprA5z8
l/8JKyzsp6TeSj6AjMF0tJJ00hgA1DzJvxEF2uX4xA8YNDpyUHCVf5mSZOEYIvoG
SQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -1,97 +0,0 @@
#include "agw_test.h"
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include "crypto/aes.h"
#include "crypto/hash.h"
#include "crypto/rsa.h"
#include "protocol/keys.h"
#include "util/base64.h"
#include "util/json.h"
using namespace agw;
namespace {
std::vector<std::uint8_t> bytesOf(const std::string &s)
{
return std::vector<std::uint8_t>(s.begin(), s.end());
}
std::string toStr(const std::vector<std::uint8_t> &v)
{
return std::string(v.begin(), v.end());
}
std::string readFile(const std::string &path)
{
std::ifstream f(path, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
}
int main()
{
namespace k = protocol::keys;
const auto key = crypto::fromHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
const auto iv = crypto::fromHex("101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f");
const auto salt = crypto::fromHex("a0a1a2a3a4a5a6a7");
const std::string payload = "{\"hello\":\"world\"}";
util::Json keysJson;
keysJson[k::aesKey] = util::base64Encode(key);
keysJson[k::aesIv] = util::base64Encode(iv);
keysJson[k::aesSalt] = util::base64Encode(salt);
const std::string keysSerialized = util::qtIndentedDump(keysJson);
const std::string expectedKeysJson =
"{\n"
" \"aes_iv\": \"EBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8=\",\n"
" \"aes_key\": \"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\",\n"
" \"aes_salt\": \"oKGio6Slpqc=\"\n"
"}\n";
CHECK_EQ(keysSerialized, expectedKeysJson);
const auto apiCipher = crypto::aesEncryptCbc(bytesOf(payload), key, iv);
const std::string apiPayloadB64 = util::base64Encode(apiCipher);
CHECK_EQ(apiPayloadB64, std::string("2WHnAcP2N+l+jz7fbKyO46jjYUq7h98lxlTIT6K0Xg8="));
const std::string pub = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_pub.pem");
const std::string priv = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_priv.pem");
CHECK(!pub.empty());
CHECK(!priv.empty());
const auto keyCipher = crypto::rsaEncryptPublicPkcs1(bytesOf(keysSerialized), pub);
const std::string keyPayloadB64 = util::base64Encode(keyCipher);
const auto keyCipherBack = util::base64Decode(keyPayloadB64);
const auto recovered = crypto::rsaDecryptPrivatePkcs1(keyCipherBack, priv);
CHECK_EQ(toStr(recovered), keysSerialized);
util::Json body;
body[k::keyPayload] = keyPayloadB64;
body[k::apiPayload] = apiPayloadB64;
const std::string bodySerialized = util::qtIndentedDump(body);
util::Json parsed = util::Json::parse(bodySerialized);
CHECK_EQ(parsed[k::apiPayload].get<std::string>(), apiPayloadB64);
{
const auto cBack = util::base64Decode(parsed[k::keyPayload].get<std::string>());
const auto rec = crypto::rsaDecryptPrivatePkcs1(cBack, priv);
CHECK_EQ(toStr(rec), keysSerialized);
}
{
const auto respPlain = bytesOf("{\"ok\":true}");
const auto respCipher = crypto::aesEncryptCbc(respPlain, key, iv);
const auto back = crypto::aesDecryptCbc(respCipher, key, iv);
CHECK(back == respPlain);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,174 +0,0 @@
#include "agw_test.h"
#include <atomic>
#include <chrono>
#include <fstream>
#include <future>
#include <memory>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include "agw/cancellation.h"
#include "agw/gateway_controller.h"
#include "agw/config.h"
#include "crypto/aes.h"
#include "crypto/rsa.h"
#include "mock_gateway/mock_gateway.h"
#include "protocol/keys.h"
#include "util/base64.h"
#include "util/json.h"
using namespace agw;
namespace {
std::string readFile(const std::string &path)
{
std::ifstream f(path, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
Config baseConfig(std::shared_ptr<IHttpClient> http, const std::string &pub)
{
Config c;
c.gatewayEndpoint = "gw.example.test";
c.agwPublicKeyPem = pub;
c.requestTimeoutMsecs = 5000;
c.httpClient = std::move(http);
return c;
}
class BlockingUntilCancelMock : public IHttpClient {
public:
std::atomic<int> entered{0};
HttpResponse send(const HttpRequest &req) override
{
entered.fetch_add(1);
while (!(req.cancelCheck && req.cancelCheck())) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
HttpResponse r;
r.error = TransportError::Canceled;
return r;
}
};
class StatelessMock : public IHttpClient {
public:
explicit StatelessMock(std::string priv) : m_priv(std::move(priv)) {}
std::atomic<int> count{0};
HttpResponse send(const HttpRequest &req) override
{
count.fetch_add(1);
namespace k = protocol::keys;
util::Json body = util::Json::parse(req.body);
const auto keyCipher = util::base64Decode(body[k::keyPayload].get<std::string>());
const auto keysBytes = crypto::rsaDecryptPrivatePkcs1(keyCipher, m_priv);
util::Json keysJson = util::Json::parse(std::string(keysBytes.begin(), keysBytes.end()));
const auto aesKey = util::base64Decode(keysJson[k::aesKey].get<std::string>());
const auto aesIv = util::base64Decode(keysJson[k::aesIv].get<std::string>());
const std::string plain = R"({"ok":true})";
const std::vector<std::uint8_t> pv(plain.begin(), plain.end());
const auto cipher = crypto::aesEncryptCbc(pv, aesKey, aesIv);
HttpResponse r;
r.httpStatusCode = 200;
r.body.assign(cipher.begin(), cipher.end());
return r;
}
private:
std::string m_priv;
};
}
int main()
{
const std::string pub = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_pub.pem");
const std::string priv = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_priv.pem");
const std::string endpoint = "https://%1/api/v1/test";
const FailoverContext ctx{"prem", "US"};
const std::string payload = R"({"hello":"world"})";
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->responsePlain = R"({"ok":true,"v":1})";
GatewayController client(baseConfig(mock, pub));
std::future<Response> f = client.postFuture(endpoint, payload, ctx);
Response r = f.get();
CHECK(r.error == ErrorCode::NoError);
CHECK_EQ(r.body, std::string(R"({"ok":true,"v":1})"));
CHECK_EQ(mock->lastDecryptedPayload, payload);
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->responsePlain = R"({"async":true})";
GatewayController client(baseConfig(mock, pub));
std::promise<Response> p;
std::future<Response> f = p.get_future();
client.postAsync(
endpoint, payload, [&p](Response r) { p.set_value(std::move(r)); }, ctx);
Response r = f.get();
CHECK(r.error == ErrorCode::NoError);
CHECK_EQ(r.body, std::string(R"({"async":true})"));
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
GatewayController client(baseConfig(mock, pub));
CancellationToken token;
token.cancel();
std::future<Response> f = client.postFuture(endpoint, payload, ctx, &token);
Response r = f.get();
CHECK(r.error == ErrorCode::Cancelled);
CHECK(mock->requestCount == 0);
}
{
auto mock = std::make_shared<BlockingUntilCancelMock>();
GatewayController client(baseConfig(mock, pub));
CancellationToken token;
std::promise<Response> p;
std::future<Response> f = p.get_future();
client.postAsync(
endpoint, payload, [&p](Response r) { p.set_value(std::move(r)); }, ctx, &token);
while (mock->entered.load() == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
token.cancel();
Response r = f.get();
CHECK(r.error == ErrorCode::Cancelled);
}
{
auto mock = std::make_shared<StatelessMock>(priv);
Config cfg = baseConfig(mock, pub);
cfg.threadPoolSize = 8;
GatewayController client(std::move(cfg));
constexpr int N = 64;
std::vector<std::future<Response>> futs;
futs.reserve(N);
for (int i = 0; i < N; ++i) {
futs.push_back(client.postFuture(endpoint, payload, ctx));
}
int ok = 0;
for (auto &fut : futs) {
Response r = fut.get();
if (r.error == ErrorCode::NoError && r.body == R"({"ok":true})") {
++ok;
}
}
CHECK(ok == N);
CHECK(mock->count.load() == N);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,118 +0,0 @@
#include "agw_test.h"
#include <fstream>
#include <future>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include "agw/c_abi.h"
#include "agw/types.h"
#include "detail/test_hooks.h"
#include "mock_gateway/mock_gateway.h"
namespace {
std::string readFile(const std::string &path)
{
std::ifstream f(path, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
agw_config makeConfig(const char *gateway, const char *pem)
{
agw_config c{};
c.gateway_endpoint = gateway;
c.agw_public_key_pem = pem;
c.request_timeout_msecs = 5000;
return c;
}
struct AsyncSink {
std::promise<std::pair<int, std::string>> promise;
};
void asyncCallback(agw_response r, void *ud)
{
auto *sink = static_cast<AsyncSink *>(ud);
sink->promise.set_value({r.error, r.body ? std::string(r.body, r.body_len) : std::string()});
agw_response_free(&r);
}
}
int main()
{
const std::string pub = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_pub.pem");
const std::string priv = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_priv.pem");
const std::string payload = R"({"hello":"world"})";
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->responsePlain = R"({"ok":true,"c":1})";
agw::detail::setNextTestHttpClient(mock);
agw_config cfg = makeConfig("gw.example.test", pub.c_str());
agw_client *client = agw_client_create(&cfg);
CHECK(client != nullptr);
agw_response r = agw_client_post(client, "https://%1/api/v1/test", payload.c_str(), "prem", "US", nullptr);
CHECK(r.error == 0);
CHECK(r.body != nullptr);
CHECK_EQ(std::string(r.body, r.body_len), std::string(R"({"ok":true,"c":1})"));
CHECK_EQ(mock->lastDecryptedPayload, payload);
agw_response_free(&r);
CHECK(r.body == nullptr);
mock->responsePlain = R"({"async":1})";
AsyncSink sink;
auto fut = sink.promise.get_future();
agw_client_post_async(client, "https://%1/api/v1/test", payload.c_str(), "prem", "US",
&asyncCallback, &sink, nullptr);
auto [err, body] = fut.get();
CHECK(err == 0);
CHECK_EQ(body, std::string(R"({"async":1})"));
agw_client_destroy(client);
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
agw::detail::setNextTestHttpClient(mock);
agw_config cfg = makeConfig("gw.example.test", pub.c_str());
agw_client *client = agw_client_create(&cfg);
agw_cancel_token *token = agw_cancel_token_create();
CHECK(token != nullptr);
agw_cancel_token_cancel(token);
agw_response r = agw_client_post(client, "https://%1/api/v1/test", payload.c_str(), "", "", token);
CHECK(r.error == static_cast<int>(agw::ErrorCode::Cancelled));
CHECK(mock->requestCount == 0);
agw_response_free(&r);
agw_cancel_token_destroy(token);
agw_client_destroy(client);
}
{
agw_config cfg = makeConfig("gw.example.test", "not a pem");
agw_client *client = agw_client_create(&cfg);
CHECK(client != nullptr);
agw_response r = agw_client_post(client, "https://%1/x", payload.c_str(), "", "", nullptr);
CHECK(r.error == static_cast<int>(agw::ErrorCode::ApiMissingAgwPublicKey));
agw_response_free(&r);
agw_client_destroy(client);
}
{
CHECK(agw_client_create(nullptr) == nullptr);
agw_response r = agw_client_post(nullptr, "e", "p", "", "", nullptr);
CHECK(r.error != 0);
agw_response_free(&r);
agw_client_destroy(nullptr);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,125 +0,0 @@
#include "agw_test.h"
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "agw/gateway_controller.h"
#include "agw/config.h"
#include "crypto/aes.h"
#include "crypto/rsa.h"
#include "protocol/keys.h"
#include "util/base64.h"
#include "util/json.h"
using namespace agw;
namespace {
std::string readFile(const std::string &path)
{
std::ifstream f(path, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
bool contains(const std::string &h, const std::string &n) { return h.find(n) != std::string::npos; }
class FailoverMock : public IHttpClient {
public:
explicit FailoverMock(std::string priv) : m_priv(std::move(priv)) {}
int directPosts = 0, proxyPosts = 0, storageGets = 0, healthGets = 0;
HttpResponse send(const HttpRequest &req) override
{
HttpResponse resp;
resp.httpStatusCode = 200;
if (req.method == "GET") {
if (contains(req.url, "lmbd-health")) {
++healthGets;
if (!contains(req.url, "proxy.good.test")) {
resp.error = TransportError::ConnectionError;
}
return resp;
}
++storageGets;
resp.body = R"(["https://proxy.good.test/"])";
return resp;
}
namespace k = protocol::keys;
util::Json body = util::Json::parse(req.body);
const auto keyCipher = util::base64Decode(body[k::keyPayload].get<std::string>());
const auto keysBytes = crypto::rsaDecryptPrivatePkcs1(keyCipher, m_priv);
util::Json keysJson = util::Json::parse(std::string(keysBytes.begin(), keysBytes.end()));
const auto aesKey = util::base64Decode(keysJson[k::aesKey].get<std::string>());
const auto aesIv = util::base64Decode(keysJson[k::aesIv].get<std::string>());
std::string plain;
if (contains(req.url, "proxy.good.test")) {
++proxyPosts;
plain = R"({"ok":true,"via":"proxy"})";
} else {
++directPosts;
plain = R"({"http_status":404,"message":"blocked"})";
}
const std::vector<std::uint8_t> pv(plain.begin(), plain.end());
const auto cipher = crypto::aesEncryptCbc(pv, aesKey, aesIv);
resp.body.assign(cipher.begin(), cipher.end());
return resp;
}
private:
std::string m_priv;
};
}
int main()
{
const std::string pub = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_pub.pem");
const std::string priv = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_priv.pem");
auto mock = std::make_shared<FailoverMock>(priv);
Config cfg;
cfg.gatewayEndpoint = "https://gw.example.test/";
cfg.agwPublicKeyPem = pub;
cfg.isDevEnvironment = true;
cfg.s3PrimaryEndpoints = {"https://s3.example.test/"};
cfg.requestTimeoutMsecs = 5000;
cfg.httpClient = mock;
GatewayController client(std::move(cfg));
const std::string endpoint = "%1api/v1/test";
const FailoverContext ctx{"prem", "US"};
const std::string payload = R"({"hello":"world"})";
{
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::NoError);
CHECK_EQ(r.body, std::string(R"({"ok":true,"via":"proxy"})"));
CHECK(mock->directPosts == 1);
CHECK(mock->storageGets >= 1);
CHECK(mock->healthGets >= 1);
CHECK(mock->proxyPosts == 1);
}
{
const int storageBefore = mock->storageGets;
const int healthBefore = mock->healthGets;
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::NoError);
CHECK_EQ(r.body, std::string(R"({"ok":true,"via":"proxy"})"));
CHECK(mock->storageGets == storageBefore);
CHECK(mock->healthGets == healthBefore);
CHECK(mock->directPosts == 1);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,103 +0,0 @@
#include "agw_test.h"
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include "agw/gateway_controller.h"
#include "agw/config.h"
#include "mock_gateway/mock_gateway.h"
using namespace agw;
namespace {
std::string readFile(const std::string &path)
{
std::ifstream f(path, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
Config baseConfig(std::shared_ptr<IHttpClient> http, const std::string &pubPem)
{
Config c;
c.gatewayEndpoint = "gw.example.test";
c.agwPublicKeyPem = pubPem;
c.requestTimeoutMsecs = 5000;
c.httpClient = std::move(http);
return c;
}
}
int main()
{
const std::string pub = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_pub.pem");
const std::string priv = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_priv.pem");
const std::string endpoint = "https://%1/api/v1/test";
const FailoverContext ctx{"premium", "US"};
const std::string payload = R"({"hello":"world","n":42})";
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->responsePlain = R"({"ok":true,"data":"hi"})";
std::string seenHost;
Config cfg = baseConfig(mock, pub);
cfg.onBeforeRequest = [&](const std::string &h) { seenHost = h; };
GatewayController client(std::move(cfg));
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::NoError);
CHECK_EQ(r.body, std::string(R"({"ok":true,"data":"hi"})"));
CHECK_EQ(mock->lastDecryptedPayload, payload);
CHECK_EQ(mock->lastUrl, std::string("https://gw.example.test/api/v1/test"));
CHECK_EQ(seenHost, std::string("gw.example.test"));
CHECK(mock->requestCount == 1);
CHECK(mock->lastRequestId.size() == 36);
CHECK(mock->lastRequestId[14] == '4');
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->responsePlain = R"({"http_status":409,"message":"limit"})";
GatewayController client(baseConfig(mock, pub));
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::ApiConfigLimitError);
CHECK_EQ(r.body, std::string(R"({"http_status":409,"message":"limit"})"));
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->simulateSsl = true;
GatewayController client(baseConfig(mock, pub));
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::ApiConfigSslError);
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
mock->simulateTransport = TransportError::Timeout;
GatewayController client(baseConfig(mock, pub));
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::ApiConfigTimeoutError);
}
{
auto mock = std::make_shared<agw_test::MockGateway>(priv);
Config cfg = baseConfig(mock, "not a pem key");
GatewayController client(std::move(cfg));
Response r = client.post(endpoint, payload, ctx);
CHECK(r.error == ErrorCode::ApiMissingAgwPublicKey);
CHECK(mock->requestCount == 0);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,77 +0,0 @@
#ifndef AGW_TEST_MOCK_GATEWAY_H
#define AGW_TEST_MOCK_GATEWAY_H
#include <string>
#include <vector>
#include "agw/http.h"
#include "crypto/aes.h"
#include "crypto/rsa.h"
#include "protocol/keys.h"
#include "util/base64.h"
#include "util/json.h"
namespace agw_test {
class MockGateway : public agw::IHttpClient {
public:
explicit MockGateway(std::string privateKeyPem) : m_priv(std::move(privateKeyPem)) {}
std::string responsePlain = "{\"ok\":true}";
bool simulateSsl = false;
agw::TransportError simulateTransport = agw::TransportError::None;
int httpStatusCode = 200;
std::string lastUrl;
std::string lastRequestId;
std::string lastDecryptedPayload;
int requestCount = 0;
agw::HttpResponse send(const agw::HttpRequest &req) override
{
++requestCount;
lastUrl = req.url;
for (const auto &h : req.headers) {
if (h.first == "X-Client-Request-ID") {
lastRequestId = h.second;
}
}
agw::HttpResponse resp;
resp.httpStatusCode = httpStatusCode;
if (simulateSsl) {
resp.sslError = true;
resp.error = agw::TransportError::ConnectionError;
return resp;
}
if (simulateTransport != agw::TransportError::None) {
resp.error = simulateTransport;
return resp;
}
namespace k = agw::protocol::keys;
agw::util::Json body = agw::util::Json::parse(req.body);
const auto keyCipher = agw::util::base64Decode(body[k::keyPayload].get<std::string>());
const auto keysBytes = agw::crypto::rsaDecryptPrivatePkcs1(keyCipher, m_priv);
agw::util::Json keysJson = agw::util::Json::parse(std::string(keysBytes.begin(), keysBytes.end()));
const auto aesKey = agw::util::base64Decode(keysJson[k::aesKey].get<std::string>());
const auto aesIv = agw::util::base64Decode(keysJson[k::aesIv].get<std::string>());
const auto apiCipher = agw::util::base64Decode(body[k::apiPayload].get<std::string>());
const auto payloadBytes = agw::crypto::aesDecryptCbc(apiCipher, aesKey, aesIv);
lastDecryptedPayload.assign(payloadBytes.begin(), payloadBytes.end());
const std::vector<std::uint8_t> respPlain(responsePlain.begin(), responsePlain.end());
const auto respCipher = agw::crypto::aesEncryptCbc(respPlain, aesKey, aesIv);
resp.body.assign(respCipher.begin(), respCipher.end());
resp.error = agw::TransportError::None;
return resp;
}
private:
std::string m_priv;
};
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
#include "agw_test.h"
#include <string>
#include "failover/bypass_policy.h"
using namespace agw;
using agw::failover::shouldBypassProxy;
namespace {
bool bypassBody(const std::string &body)
{
return shouldBypassProxy(TransportError::None, body, true);
}
}
int main()
{
CHECK(shouldBypassProxy(TransportError::None, "garbage", false) == true);
CHECK(shouldBypassProxy(TransportError::Timeout, R"({"http_status":200})", true) == true);
CHECK(shouldBypassProxy(TransportError::Canceled, R"({"http_status":200})", true) == true);
CHECK(bypassBody("<html><body>blocked</body></html>") == true);
CHECK(bypassBody(R"({"http_status":408})") == false);
CHECK(bypassBody(R"({"http_status":409})") == false);
CHECK(bypassBody(R"({"http_status":402})") == false);
CHECK(bypassBody(R"({"http_status":404,"message":"whatever"})") == true);
CHECK(bypassBody(R"({"http_status":404,"message":"No active configuration found for x"})") == false);
CHECK(bypassBody(R"({"http_status":404,"detail":"Account not found."})") == false);
CHECK(bypassBody(R"({"http_status":404,"message":"Session not found"})") == false);
CHECK(bypassBody(R"({"http_status":501})") == true);
CHECK(bypassBody(R"({"http_status":501,"message":"client version update is required"})") == false);
CHECK(bypassBody(R"({"http_status":422,"message":"Failed to retrieve subscription information. Is it activated?"})") == false);
CHECK(bypassBody(R"({"http_status":422,"message":"other"})") == true);
CHECK(bypassBody(R"({"http_status":200})") == false);
CHECK(bypassBody("plain ok") == false);
CHECK(shouldBypassProxy(TransportError::ConnectionError, R"({"http_status":200})", true) == true);
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,123 +0,0 @@
#include "agw_test.h"
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include "crypto/aes.h"
#include "crypto/hash.h"
#include "crypto/rng.h"
#include "crypto/rsa.h"
#include "util/base64.h"
#include "util/uuid.h"
using namespace agw;
namespace {
std::vector<std::uint8_t> bytesOf(const std::string &s)
{
return std::vector<std::uint8_t>(s.begin(), s.end());
}
std::string readFile(const std::string &path)
{
std::ifstream f(path, std::ios::binary);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
class FixedRng : public crypto::IRng {
public:
explicit FixedRng(std::vector<std::uint8_t> data) : m_data(std::move(data)) {}
std::vector<std::uint8_t> bytes(std::size_t n) override
{
std::vector<std::uint8_t> out(n);
for (std::size_t i = 0; i < n; ++i) {
out[i] = m_data[(m_pos + i) % m_data.size()];
}
m_pos += n;
return out;
}
private:
std::vector<std::uint8_t> m_data;
std::size_t m_pos = 0;
};
}
int main()
{
CHECK_EQ(util::base64Encode(std::string("")), std::string(""));
CHECK_EQ(util::base64Encode(std::string("f")), std::string("Zg=="));
CHECK_EQ(util::base64Encode(std::string("fo")), std::string("Zm8="));
CHECK_EQ(util::base64Encode(std::string("foo")), std::string("Zm9v"));
CHECK_EQ(util::base64Encode(std::string("foob")), std::string("Zm9vYg=="));
CHECK_EQ(util::base64Encode(std::string("fooba")), std::string("Zm9vYmE="));
CHECK_EQ(util::base64Encode(std::string("foobar")), std::string("Zm9vYmFy"));
{
std::vector<std::uint8_t> v{0xfb, 0xff, 0xbf};
CHECK_EQ(util::base64UrlEncodeNoPad(v), std::string("-_-_"));
CHECK_EQ(util::base64Encode(v), std::string("+/+/"));
}
{
auto v = bytesOf("any carnal pleasure.");
CHECK(util::base64Decode(util::base64Encode(v)) == v);
CHECK(util::base64Decode(util::base64UrlEncodeNoPad(v)) == v);
}
CHECK_EQ(crypto::toHex(crypto::sha512(bytesOf("abc"))),
std::string("ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a"
"2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"));
{
std::vector<std::uint8_t> v{0x00, 0x01, 0xab, 0xff};
CHECK_EQ(crypto::toHex(v), std::string("0001abff"));
CHECK(crypto::fromHex("0001abff") == v);
}
{
auto key = crypto::fromHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
auto iv = crypto::fromHex("101112131415161718191a1b1c1d1e1f");
auto pt = bytesOf("{\"hello\":\"world\"}");
auto ct = crypto::aesEncryptCbc(pt, key, iv);
CHECK_EQ(util::base64Encode(ct), std::string("2WHnAcP2N+l+jz7fbKyO46jjYUq7h98lxlTIT6K0Xg8="));
CHECK(crypto::aesDecryptCbc(ct, key, iv) == pt);
}
{
auto key = crypto::fromHex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
auto iv16 = crypto::fromHex("101112131415161718191a1b1c1d1e1f");
auto iv32 = crypto::fromHex("101112131415161718191a1b1c1d1e1fdeadbeefdeadbeefdeadbeefdeadbeef");
auto pt = bytesOf("{\"hello\":\"world\"}");
CHECK(crypto::aesEncryptCbc(pt, key, iv16) == crypto::aesEncryptCbc(pt, key, iv32));
}
{
std::string pub = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_pub.pem");
std::string priv = readFile(std::string(AGW_FIXTURES_DIR) + "/test_rsa_priv.pem");
CHECK(!pub.empty());
CHECK(!priv.empty());
auto msg = bytesOf("{\"aes_key\":\"...\",\"aes_iv\":\"...\",\"aes_salt\":\"...\"}");
auto ct = crypto::rsaEncryptPublicPkcs1(msg, pub);
auto rt = crypto::rsaDecryptPrivatePkcs1(ct, priv);
CHECK(rt == msg);
auto ct2 = crypto::rsaEncryptPublicPkcs1(msg, pub);
CHECK(ct != ct2);
}
{
FixedRng rng(std::vector<std::uint8_t>(16, 0xFF));
std::string u = util::makeUuidV4(rng);
CHECK_EQ(u, std::string("ffffffff-ffff-4fff-bfff-ffffffffffff"));
CHECK(u.size() == 36);
CHECK(u[14] == '4');
CHECK(u[19] == '8' || u[19] == '9' || u[19] == 'a' || u[19] == 'b');
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,54 +0,0 @@
#include "agw_test.h"
#include <string>
#include "protocol/error_mapping.h"
using namespace agw;
using agw::protocol::mapResponseError;
namespace {
int code(ErrorCode e) { return static_cast<int>(e); }
ErrorCode mapBody(const std::string &body)
{
return mapResponseError(false, TransportError::None, body);
}
}
int main()
{
CHECK(mapResponseError(true, TransportError::None, "") == ErrorCode::ApiConfigSslError);
CHECK(mapResponseError(false, TransportError::Timeout, "") == ErrorCode::ApiConfigTimeoutError);
CHECK(mapResponseError(false, TransportError::Canceled, "") == ErrorCode::ApiConfigTimeoutError);
CHECK(mapResponseError(false, TransportError::OperationNotImplemented, "") == ErrorCode::ApiUpdateRequestError);
CHECK(mapResponseError(false, TransportError::ConnectionError, "") == ErrorCode::ApiConfigDownloadError);
CHECK(mapResponseError(false, TransportError::None, "") == ErrorCode::NoError);
CHECK(mapBody("not a json") == ErrorCode::NoError);
CHECK(mapBody(R"({"http_status":429})") == ErrorCode::ApiRateLimitError);
CHECK(mapBody(R"({"http_status":409})") == ErrorCode::ApiConfigLimitError);
CHECK(mapBody(R"({"http_status":409,"message":"Trial Subscription Already Used"})") == ErrorCode::ApiTrialAlreadyUsedError);
CHECK(mapBody(R"({"http_status":404})") == ErrorCode::ApiNotFoundError);
CHECK(mapBody(R"({"http_status":408})") == ErrorCode::ApiConfigTimeoutError);
CHECK(mapBody(R"({"http_status":501})") == ErrorCode::ApiUpdateRequestError);
CHECK(mapBody(R"({"http_status":422,"message":"Failed to retrieve subscription information. Is it activated?"})")
== ErrorCode::ApiSubscriptionExpiredError);
CHECK(mapBody(R"({"http_status":422,"message":"something else"})") == ErrorCode::ApiConfigDownloadError);
CHECK(mapBody(R"({"http_status":402,"message":"refresh_captcha"})") == ErrorCode::ApiCaptchaRefreshError);
CHECK(mapBody(R"({"http_status":402,"message":"invalid_captcha"})") == ErrorCode::ApiCaptchaInvalidError);
CHECK(mapBody(R"({"http_status":402,"captcha_id":"x"})") == ErrorCode::ApiCaptchaRequiredError);
CHECK(mapBody(R"({"http_status":402,"captcha_image":"x"})") == ErrorCode::ApiCaptchaRequiredError);
CHECK(mapBody(R"({"http_status":402,"message":"rate_limit_exceeded"})") == ErrorCode::ApiCaptchaRequiredError);
CHECK(mapBody(R"({"http_status":402,"message":"nope"})") == ErrorCode::ApiSubscriptionNotActiveError);
CHECK(mapBody(R"({"http_status":500})") == ErrorCode::ApiConfigDownloadError);
CHECK(mapBody(R"({"http_status":200})") == ErrorCode::NoError);
(void)code;
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,61 +0,0 @@
#include "agw_test.h"
#include <string>
#include "util/json.h"
using namespace agw;
int main()
{
{
util::Json j;
j["aes_key"] = "KEY";
j["aes_iv"] = "IV";
j["aes_salt"] = "SALT";
const std::string expected =
"{\n"
" \"aes_iv\": \"IV\",\n"
" \"aes_key\": \"KEY\",\n"
" \"aes_salt\": \"SALT\"\n"
"}\n";
CHECK_EQ(util::qtIndentedDump(j), expected);
}
{
util::Json j;
j["key_payload"] = "K";
j["api_payload"] = "A";
const std::string expected =
"{\n"
" \"api_payload\": \"A\",\n"
" \"key_payload\": \"K\"\n"
"}\n";
CHECK_EQ(util::qtIndentedDump(j), expected);
}
{
util::Json j;
j["s"] = std::string("a\"b\\c\nd\te\x01");
const std::string expected =
"{\n"
" \"s\": \"a\\\"b\\\\c\\nd\\te\\u0001\"\n"
"}\n";
CHECK_EQ(util::qtIndentedDump(j), expected);
}
{
util::Json j;
j["outer"]["inner"] = "v";
const std::string expected =
"{\n"
" \"outer\": {\n"
" \"inner\": \"v\"\n"
" }\n"
"}\n";
CHECK_EQ(util::qtIndentedDump(j), expected);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,87 +0,0 @@
#include "agw_test.h"
#include <string>
#include <vector>
#include "crypto/aes.h"
#include "crypto/hash.h"
#include "failover/proxy_list.h"
#include "util/base64.h"
using namespace agw;
namespace {
std::vector<std::uint8_t> bytesOf(const std::string &s)
{
return std::vector<std::uint8_t>(s.begin(), s.end());
}
}
int main()
{
{
const std::vector<std::string> primary{"https://a/", "https://b/"};
const std::vector<std::string> fallback{"https://f/"};
const FailoverContext ctx{"prem", "US"};
const std::string enc =
util::base64UrlEncodeNoPad(bytesOf("endpoints-prem-US"));
const auto urls = failover::buildStorageUrls(primary, fallback, ctx);
const std::vector<std::string> expected{
"https://a/" + enc + ".json",
"https://b/" + enc + ".json",
"https://a/endpoints.json",
"https://b/endpoints.json",
"https://f/" + enc + ".json",
"https://f/endpoints.json",
};
CHECK(urls == expected);
}
{
const std::vector<std::string> primary{"https://a/", "https://b/"};
const std::vector<std::string> fallback{"https://f/"};
const FailoverContext ctx{"", ""};
const auto urls = failover::buildStorageUrls(primary, fallback, ctx);
const std::vector<std::string> expected{
"https://a/endpoints.json",
"https://b/endpoints.json",
"https://f/endpoints.json",
};
CHECK(urls == expected);
}
{
const auto list = failover::decodeProxyList(R"(["https://p1/","https://p2/"])", true, "");
const std::vector<std::string> expected{"https://p1/", "https://p2/"};
CHECK(list == expected);
CHECK(failover::decodeProxyList(R"({"x":1})", true, "").empty());
}
{
const std::string pub = "PUBKEYDATA-pem-like";
const std::string h = crypto::toHex(crypto::sha512(bytesOf(pub)));
const auto key = crypto::fromHex(h.substr(0, 64));
const auto iv = crypto::fromHex(h.substr(64, 32));
const std::string arr = R"(["https://prod1/","https://prod2/"])";
const auto cipher = crypto::aesEncryptCbc(bytesOf(arr), key, iv);
const std::string b64 = util::base64Encode(cipher);
const auto list = failover::decodeProxyList(b64, false, pub);
const std::vector<std::string> expected{"https://prod1/", "https://prod2/"};
CHECK(list == expected);
bool threw = false;
try {
failover::decodeProxyList("###not base64 cipher###", false, pub);
} catch (...) {
threw = true;
}
CHECK(threw);
}
return AGW_TEST_MAIN_RETURN();
}

View File

@@ -1,33 +0,0 @@
#include "agw_test.h"
#include <atomic>
#include <memory>
#include "util/thread_pool.h"
using namespace agw;
int main()
{
{
std::atomic<int> counter{0};
{
util::ThreadPool pool(4);
for (int i = 0; i < 1000; ++i) {
pool.submit([&counter] { counter.fetch_add(1, std::memory_order_relaxed); });
}
}
CHECK(counter.load() == 1000);
}
{
std::atomic<bool> ran{false};
{
util::ThreadPool pool(0);
pool.submit([&ran] { ran.store(true); });
}
CHECK(ran.load());
}
return AGW_TEST_MAIN_RETURN();
}

1
client/3rd-prebuilt Submodule

Submodule client/3rd-prebuilt added at ab4e6b680d

1
client/3rd/OpenVPNAdapter vendored Submodule

View File

@@ -1,2 +0,0 @@
*.user
build/

View File

@@ -1,19 +0,0 @@
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()

View File

@@ -1,21 +0,0 @@
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.

View File

@@ -1,190 +0,0 @@
#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 &current, 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 &current, 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();
}
};

View File

@@ -1,5 +0,0 @@
include_directories(${CMAKE_CURRENT_LIST_DIR})
set(QJSONSTRUCT_SOURCES
${CMAKE_CURRENT_LIST_DIR}/QJsonStruct.hpp
${CMAKE_CURRENT_LIST_DIR}/QJsonIO.hpp)

View File

@@ -1,215 +0,0 @@
#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;
}
};

View File

@@ -1,74 +0,0 @@
#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__)

View File

@@ -1,16 +0,0 @@
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)

View File

@@ -1,45 +0,0 @@
#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(){};
};

View File

@@ -1,19 +0,0 @@
#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)
};

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More