mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-27 12:37:38 +03:00
Compare commits
388 Commits
without_ls
...
feat/agw-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
163cb11319 | ||
|
|
71ca24d56b | ||
|
|
1d55372da4 | ||
|
|
f85b810efe | ||
|
|
70222943dd | ||
|
|
1fbd49f55b | ||
|
|
ff8ebbfdc9 | ||
|
|
7736fa1946 | ||
|
|
7e6e70d1d3 | ||
|
|
c859128cb2 | ||
|
|
5d16645b84 | ||
|
|
203a092dc9 | ||
|
|
d8b8590bc4 | ||
|
|
9b8bfaa6f8 | ||
|
|
890103a16a | ||
|
|
56ab82f87f | ||
|
|
3984acbb44 | ||
|
|
cc404378f9 | ||
|
|
594635e5cf | ||
|
|
f9b106cf5b | ||
|
|
a9861d18b7 | ||
|
|
c14138f031 | ||
|
|
60686fde24 | ||
|
|
bd0747296e | ||
|
|
ba61019a50 | ||
|
|
113f967006 | ||
|
|
bcee58b08b | ||
|
|
52de1acebf | ||
|
|
027a12a1df | ||
|
|
0a659a2d74 | ||
|
|
6f119cd083 | ||
|
|
1753aed3fc | ||
|
|
c714d98bd1 | ||
|
|
4787f3915b | ||
|
|
7a383116b2 | ||
|
|
d3de5f0f48 | ||
|
|
8749d683e3 | ||
|
|
9de9d082bc | ||
|
|
a4233fef41 | ||
|
|
4890dd1d74 | ||
|
|
564630827e | ||
|
|
fbe15d965b | ||
|
|
b29515c380 | ||
|
|
0658a8f565 | ||
|
|
482ec04b4a | ||
|
|
d40d24fcf9 | ||
|
|
fb5666057b | ||
|
|
a49892c7e7 | ||
|
|
277b295fd8 | ||
|
|
8c33779fc3 | ||
|
|
f0299ca9fe | ||
|
|
c7b1c2809f | ||
|
|
c9ed0baf3b | ||
|
|
2a3e3126ac | ||
|
|
98771027b7 | ||
|
|
0433e03bdc | ||
|
|
cb48667b91 | ||
|
|
d0a1af0381 | ||
|
|
fd0c773918 | ||
|
|
06372c8fd7 | ||
|
|
009ca981d5 | ||
|
|
c0cae0ff01 | ||
|
|
c28452a5da | ||
|
|
396ce23228 | ||
|
|
b05ee0a654 | ||
|
|
fd5051262d | ||
|
|
847bb6923b | ||
|
|
2edd7de413 | ||
|
|
f0da2b003f | ||
|
|
650c1c6ebb | ||
|
|
8dbded1624 | ||
|
|
cebfcc846e | ||
|
|
4c18ceaa50 | ||
|
|
ebe3a5dac6 | ||
|
|
92deee5f67 | ||
|
|
a75bd0cf5e | ||
|
|
46f5b3894b | ||
|
|
493ee22883 | ||
|
|
ad14847eb5 | ||
|
|
cd50e0b8a5 | ||
|
|
78f504e35c | ||
|
|
bf3d11e5c4 | ||
|
|
9a0222aee3 | ||
|
|
f0f0f7c5be | ||
|
|
36b1a863bf | ||
|
|
4103c5bbcf | ||
|
|
fa69da6d56 | ||
|
|
aaf2c9ddeb | ||
|
|
dbbc7119ec | ||
|
|
c57162c4cc | ||
|
|
40e39895c9 | ||
|
|
ec3ab2a03c | ||
|
|
ddecfcad26 | ||
|
|
67bd880cdf | ||
|
|
477afb9d85 | ||
|
|
f969fcdbb8 | ||
|
|
b0ca16d861 | ||
|
|
9963359948 | ||
|
|
ca639d293d | ||
|
|
83d045af64 | ||
|
|
aea8ff4961 | ||
|
|
1892db4375 | ||
|
|
c86a641e05 | ||
|
|
befb2bf19a | ||
|
|
7ad6bc340c | ||
|
|
9164e38c34 | ||
|
|
8f7559f01b | ||
|
|
af56200735 | ||
|
|
3874050fae | ||
|
|
3087163e34 | ||
|
|
1fa152845c | ||
|
|
50e23ef233 | ||
|
|
ea648466de | ||
|
|
b782775016 | ||
|
|
89a7fe1081 | ||
|
|
e8bb096025 | ||
|
|
fd5c7c8322 | ||
|
|
e798d0f503 | ||
|
|
bbb0abb596 | ||
|
|
0925aec86a | ||
|
|
b084c4c284 | ||
|
|
87288ebccd | ||
|
|
fcd7eadf4c | ||
|
|
0373338fb7 | ||
|
|
42f070fe9d | ||
|
|
02be6dc5f9 | ||
|
|
bfcf7f0305 | ||
|
|
2bce595ade | ||
|
|
cd1e561fd4 | ||
|
|
9bd1e6a0f5 | ||
|
|
5058c9aa6f | ||
|
|
d78416835c | ||
|
|
40e6c6aae3 | ||
|
|
911a999c64 | ||
|
|
b4f4184aa6 | ||
|
|
5c6db4b7a4 | ||
|
|
f6277cdbb2 | ||
|
|
99312e61d3 | ||
|
|
9f0ae75a2f | ||
|
|
7960d8015d | ||
|
|
5dcc64e5e5 | ||
|
|
964436ad43 | ||
|
|
4fc3900fd5 | ||
|
|
8f5e42dd61 | ||
|
|
24895752c1 | ||
|
|
87eccfb4ca | ||
|
|
a983d0504e | ||
|
|
d0b8535395 | ||
|
|
f84480cf56 | ||
|
|
de7a026ec1 | ||
|
|
a128c7d247 | ||
|
|
f316f0e25a | ||
|
|
ea5242e29b | ||
|
|
b31a62c55f | ||
|
|
02e3107a23 | ||
|
|
1862850108 | ||
|
|
f73792844c | ||
|
|
a7199ca6f5 | ||
|
|
5e757cdd3b | ||
|
|
92af1f3268 | ||
|
|
aad9d6dae2 | ||
|
|
423fe3fd4f | ||
|
|
b591dd7445 | ||
|
|
a45bb5ea4f | ||
|
|
d859b111ca | ||
|
|
52031efc48 | ||
|
|
d78202c612 | ||
|
|
6bac948633 | ||
|
|
a4c4ef71fb | ||
|
|
127f85f4f0 | ||
|
|
13d4ddd292 | ||
|
|
7265e09c85 | ||
|
|
2e629b6dac | ||
|
|
92aba49705 | ||
|
|
bec06b3a5e | ||
|
|
91cd9474ea | ||
|
|
6178b05643 | ||
|
|
46ce22b85c | ||
|
|
36edafb985 | ||
|
|
d77eaba500 | ||
|
|
6a3d43fbb0 | ||
|
|
4975955bbe | ||
|
|
8f508783e3 | ||
|
|
f50817c43c | ||
|
|
54f67b3d82 | ||
|
|
d669adb707 | ||
|
|
5103bc640e | ||
|
|
3e6f0c0342 | ||
|
|
40950b92ee | ||
|
|
ac77b4ee75 | ||
|
|
fbf652f818 | ||
|
|
bbbf4891e6 | ||
|
|
20d005d66c | ||
|
|
c81ae2b060 | ||
|
|
105c42db1c | ||
|
|
89818ff63d | ||
|
|
414c422177 | ||
|
|
b39ac8556c | ||
|
|
5e1742262d | ||
|
|
5a07a1274f | ||
|
|
7b8ff1fd6e | ||
|
|
c7221832e0 | ||
|
|
eb7d031c7d | ||
|
|
3b3a0aaceb | ||
|
|
01ec79b7d5 | ||
|
|
3d6339e2dd | ||
|
|
b4d78d865a | ||
|
|
b53cdcff08 | ||
|
|
3cc18c5807 | ||
|
|
5fdce1e49e | ||
|
|
2ee61a040b | ||
|
|
741b5cc0f9 | ||
|
|
aaf0e070dc | ||
|
|
e0e126eda8 | ||
|
|
236daf6b3b | ||
|
|
f1481b1b1f | ||
|
|
f6e7d3ccf1 | ||
|
|
a754a11913 | ||
|
|
4d25e3b6f6 | ||
|
|
1fac280497 | ||
|
|
c886c5e6a7 | ||
|
|
cd7f78b9ca | ||
|
|
a587d3230f | ||
|
|
93e7b45136 | ||
|
|
e024f71ce1 | ||
|
|
50d1be7b4a | ||
|
|
3ec6d8973b | ||
|
|
3ea47d31a9 | ||
|
|
30c8cc4548 | ||
|
|
98586d2dd9 | ||
|
|
c66d8ecca0 | ||
|
|
db535f7e7d | ||
|
|
89f30d8c31 | ||
|
|
8bce432824 | ||
|
|
f3539b2632 | ||
|
|
7a96c212f3 | ||
|
|
2d5dc54e0f | ||
|
|
cef4c262e9 | ||
|
|
34309261a8 | ||
|
|
657eeb40c7 | ||
|
|
b4938c2cc9 | ||
|
|
524fefc5cb | ||
|
|
73f13404bb | ||
|
|
5fc68cca83 | ||
|
|
fcb7b8fa8d | ||
|
|
a81e32ff95 | ||
|
|
c897052107 | ||
|
|
4d0efc7ea5 | ||
|
|
a77842c9e3 | ||
|
|
0ded9db780 | ||
|
|
58d480fcb5 | ||
|
|
7154428d26 | ||
|
|
02a52d0169 | ||
|
|
ec60764072 | ||
|
|
17d2fa5532 | ||
|
|
3ca8b534e8 | ||
|
|
e88f7c5e46 | ||
|
|
3ac5d7bd1f | ||
|
|
19cad00a00 | ||
|
|
1ea716a163 | ||
|
|
4551659c2a | ||
|
|
c568bf8c24 | ||
|
|
a412d91105 | ||
|
|
ad01f23bbe | ||
|
|
656070b132 | ||
|
|
c907f5ca36 | ||
|
|
94a13b2b54 | ||
|
|
169f11d9c7 | ||
|
|
816dc3af95 | ||
|
|
b802863de5 | ||
|
|
8dc2a4b76c | ||
|
|
beb1c6dbf2 | ||
|
|
3eb06916c7 | ||
|
|
30d0f84a4f | ||
|
|
251f2aa5db | ||
|
|
16d92ddb7c | ||
|
|
e9d4fd8482 | ||
|
|
9fdcf5ab13 | ||
|
|
a6e6de33c8 | ||
|
|
53c7fd4d81 | ||
|
|
2608ea4367 | ||
|
|
d20ed4ad01 | ||
|
|
eae2936449 | ||
|
|
da8ad1f6ba | ||
|
|
5472347969 | ||
|
|
a43f7a6926 | ||
|
|
47f917de0b | ||
|
|
dbeb7edd7a | ||
|
|
6cede712f5 | ||
|
|
d328739192 | ||
|
|
d15c0bd962 | ||
|
|
d53c794936 | ||
|
|
e5dcb25a4a | ||
|
|
f9002b4f43 | ||
|
|
0531508a75 | ||
|
|
174e85a20a | ||
|
|
e9abb6f1e2 | ||
|
|
5be44f9596 | ||
|
|
90efaaff92 | ||
|
|
99b554e7c3 | ||
|
|
ac0ce8a6f6 | ||
|
|
9f9da885b7 | ||
|
|
f51fd2bf3e | ||
|
|
c8378fd32d | ||
|
|
d767214f10 | ||
|
|
e027c504ae | ||
|
|
669a95d975 | ||
|
|
a96df5d518 | ||
|
|
c5c81735a0 | ||
|
|
c933745707 | ||
|
|
6710fd18b3 | ||
|
|
1b78a71529 | ||
|
|
1909d3c94e | ||
|
|
10a107716c | ||
|
|
5445e6637b | ||
|
|
2380cd5cfb | ||
|
|
42661618dc | ||
|
|
8a7e901d7a | ||
|
|
f8bea71716 | ||
|
|
efcc0b7efc | ||
|
|
4d17e913b5 | ||
|
|
b341934863 | ||
|
|
127f8ed3bb | ||
|
|
9dca80de18 | ||
|
|
b0a6bcc055 | ||
|
|
f0626e2eca | ||
|
|
979ab42c5a | ||
|
|
e152e84ddc | ||
|
|
2605978889 | ||
|
|
a2d30efaab | ||
|
|
d3715d00ae | ||
|
|
c37662dbe2 | ||
|
|
768ca1e73d | ||
|
|
a20516850c | ||
|
|
7a203868ec | ||
|
|
43c3ce9a6e | ||
|
|
369e08844f | ||
|
|
48a5452a65 | ||
|
|
c2f9340db6 | ||
|
|
a6508e642a | ||
|
|
a3e73797c2 | ||
|
|
df7bf204ea | ||
|
|
e16243ff55 | ||
|
|
e23cbe67ad | ||
|
|
7702f2f74c | ||
|
|
b457ef9a3f | ||
|
|
a28ed6a977 | ||
|
|
0c73682cfc | ||
|
|
7e380b6cfb | ||
|
|
63b5257986 | ||
|
|
acc4485e81 | ||
|
|
2c44999a31 | ||
|
|
e59a48f9f4 | ||
|
|
b86356b0cc | ||
|
|
f6d7552b58 | ||
|
|
5bd88ac2e9 | ||
|
|
94fa5b59f3 | ||
|
|
7169480999 | ||
|
|
c44ce0d77c | ||
|
|
7fd71a8408 | ||
|
|
68db721089 | ||
|
|
a180e12bdf | ||
|
|
f3a4a1b1be | ||
|
|
6977a8ecbc | ||
|
|
d00f64e6ad | ||
|
|
d5b3da6ba3 | ||
|
|
c245318339 | ||
|
|
b3b0fec2e1 | ||
|
|
9d571a4c71 | ||
|
|
f283858490 | ||
|
|
76fe203767 | ||
|
|
b9a47f2f50 | ||
|
|
27cb17c640 | ||
|
|
ef8fb89eb3 | ||
|
|
f1b045f8a8 | ||
|
|
050066132b | ||
|
|
2a6e6a1e24 | ||
|
|
92689d084c | ||
|
|
00f314039d | ||
|
|
fcb75e837d | ||
|
|
9fbea76b74 | ||
|
|
b3ff120bcf | ||
|
|
9dea98f020 | ||
|
|
c4701d4e7a | ||
|
|
48903ca3a1 | ||
|
|
0c9fd4aef4 | ||
|
|
b2af2e46ac | ||
|
|
efc76a0683 |
@@ -2,7 +2,7 @@
|
||||
/client/3rd-prebuild
|
||||
/client/android
|
||||
/client/cmake
|
||||
/client/core/serialization
|
||||
/client/core/utils/serialization
|
||||
/client/daemon
|
||||
/client/fonts
|
||||
/client/images
|
||||
|
||||
38
.github/actions/apple-install-cert/action.yml
vendored
Normal file
38
.github/actions/apple-install-cert/action.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# .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"
|
||||
BIN
.github/actions/apple-setup-keychain/AppleWWDRCAG3.cer
vendored
Normal file
BIN
.github/actions/apple-setup-keychain/AppleWWDRCAG3.cer
vendored
Normal file
Binary file not shown.
BIN
.github/actions/apple-setup-keychain/DeveloperIDG2CA.cer
vendored
Normal file
BIN
.github/actions/apple-setup-keychain/DeveloperIDG2CA.cer
vendored
Normal file
Binary file not shown.
57
.github/actions/apple-setup-keychain/action.yml
vendored
Normal file
57
.github/actions/apple-setup-keychain/action.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# .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
|
||||
31
.github/actions/apple-setup-provisioning-profile/action.yml
vendored
Normal file
31
.github/actions/apple-setup-provisioning-profile/action.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# .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"
|
||||
825
.github/workflows/deploy.yml
vendored
825
.github/workflows/deploy.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.github/workflows/tag-deploy.yml
vendored
3
.github/workflows/tag-deploy.yml
vendored
@@ -17,9 +17,12 @@ jobs:
|
||||
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'
|
||||
|
||||
63
.github/workflows/tag-upload.yml
vendored
63
.github/workflows/tag-upload.yml
vendored
@@ -1,64 +1,41 @@
|
||||
name: 'Upload a new version'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
RELEASE_VERSION:
|
||||
description: 'Release version (e.g. 1.2.3.4)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
Upload-S3:
|
||||
runs-on: ubuntu-latest
|
||||
name: upload
|
||||
steps:
|
||||
- name: Checkout CMakeLists.txt
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
ref: ${{ inputs.RELEASE_VERSION }}
|
||||
sparse-checkout: |
|
||||
CMakeLists.txt
|
||||
deploy/deploy_s3.sh
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Verify git tag
|
||||
run: |
|
||||
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..."
|
||||
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)."
|
||||
else
|
||||
echo "Git tag ($GIT_TAG) and version in CMakeLists.txt ($CMAKE_TAG) are not the same! Cancelling..."
|
||||
echo "::error::Mismatch: Git tag ($TAG_NAME) != CMakeLists.txt version ($CMAKE_TAG). Exiting with error..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download artifacts from the "${{ github.ref_name }}" tag
|
||||
uses: robinraju/release-downloader@v1.8
|
||||
- name: Setup Rclone
|
||||
uses: AnimMouse/setup-rclone@v1
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
fileName: "AmneziaVPN_(Linux_|)${{ github.ref_name }}*"
|
||||
out-file-path: ${{ github.ref_name }}
|
||||
rclone_config: ${{ secrets.RCLONE_CONFIG }}
|
||||
|
||||
- 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 }}
|
||||
- name: Send dist to S3
|
||||
run: bash deploy/deploy_s3.sh ${{ inputs.RELEASE_VERSION }}
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -9,6 +9,7 @@ deploy/build_32/*
|
||||
deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
|
||||
|
||||
# Qt-es
|
||||
@@ -80,6 +81,7 @@ client/.DS_Store
|
||||
._.DS_Store
|
||||
._*
|
||||
*.dmg
|
||||
deploy/data/macos/pf/amn.400.allowPIA.conf
|
||||
|
||||
# tmp files
|
||||
*.*~
|
||||
@@ -133,4 +135,12 @@ client/3rd/ShadowSocks/ss_ios.xcconfig
|
||||
out/
|
||||
|
||||
# CMake files
|
||||
CMakeFiles/
|
||||
CMakeFiles/
|
||||
|
||||
ios-ne-build.sh
|
||||
macos-ne-build.sh
|
||||
macos-signed-build.sh
|
||||
macos-with-sign-build.sh
|
||||
DeveloperIdApplicationCertificate.p12
|
||||
DeveloperIdInstallerCertificate.p12
|
||||
|
||||
|
||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -4,12 +4,13 @@
|
||||
[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
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
project(${PROJECT} VERSION 4.8.4.3
|
||||
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}
|
||||
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 2080)
|
||||
set(APP_ANDROID_VERSION_CODE 2123)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
@@ -28,17 +45,34 @@ 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)
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64")
|
||||
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
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
add_subdirectory(client)
|
||||
|
||||
if(NOT IOS AND NOT ANDROID)
|
||||
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
add_subdirectory(service)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
|
||||
endif()
|
||||
|
||||
if ((LINUX AND NOT ANDROID) OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (WIN32))
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/CPack.cmake)
|
||||
endif()
|
||||
|
||||
179
README.md
179
README.md
@@ -9,17 +9,17 @@
|
||||
### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md)
|
||||
|
||||
|
||||
[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
[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.
|
||||
|
||||
[](https://amnezia.org)
|
||||
|
||||
### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/amnezia/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting)
|
||||
### [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) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/amnezia/amnezia.org ).
|
||||
> 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/downloads"><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/q9p19109"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-alt.svg" width="150" style="max-width: 100%;"></a>
|
||||
<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)
|
||||
|
||||
@@ -53,24 +53,14 @@ AmneziaVPN uses several open-source projects to work:
|
||||
|
||||
- [OpenSSL](https://www.openssl.org/)
|
||||
- [OpenVPN](https://openvpn.net/)
|
||||
- [Shadowsocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [LibSsh](https://libssh.org) - forked from Qt Creator
|
||||
- [LibSsh](https://libssh.org)
|
||||
- [WireGuard](https://www.wireguard.com/)
|
||||
- [Xray-core](https://xtls.github.io/en/)
|
||||
- [Conan](https://conan.io/)
|
||||
- and more...
|
||||
|
||||
## Checking out the source code
|
||||
|
||||
Make sure to pull all submodules after checking out the repo.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Want to contribute? Welcome!
|
||||
|
||||
### Help with translations
|
||||
## Help us with translations
|
||||
|
||||
Download the most actual translation files.
|
||||
|
||||
@@ -83,103 +73,102 @@ Each *.ts file contains strings for one corresponding language.
|
||||
Translate or correct some strings in one or multiple *.ts files and commit them back to this repository into the ``client/translations`` folder.
|
||||
You can do it via a web-interface or any other method you're familiar with.
|
||||
|
||||
### Building sources and deployment
|
||||
## Checking out the source code
|
||||
|
||||
Check deploy folder for build scripts.
|
||||
Make sure to pull all submodules after checking out the repo.
|
||||
|
||||
### How to build an iOS app from source code on MacOS
|
||||
|
||||
1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher.
|
||||
|
||||
2. We use QT to generate the XCode project. We need QT version 6.6.2. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules:
|
||||
- MacOS
|
||||
- iOS
|
||||
- Qt 5 Compatibility Module
|
||||
- Qt Shader Tools
|
||||
- Additional Libraries:
|
||||
- Qt Image Formats
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
|
||||
3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/)
|
||||
|
||||
4. You also need to install go >= v1.16. If you don't have it installed already,
|
||||
download go from the [official website](https://golang.org/dl/) or use Homebrew.
|
||||
The latest version is recommended. Install gomobile
|
||||
```bash
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
5. Build the project
|
||||
## Hacking guide
|
||||
|
||||
Want to contribute? Welcome!
|
||||
|
||||
### Build requirements
|
||||
|
||||
* [`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)
|
||||
|
||||
### Building the project using scripts
|
||||
|
||||
* 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
|
||||
|
||||
Unix-like:
|
||||
```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 for the host platform
|
||||
deploy/build.sh
|
||||
|
||||
# Or just
|
||||
deploy/build.sh
|
||||
|
||||
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
|
||||
# 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
|
||||
```
|
||||
|
||||
6. Open the XCode project. You can then run /test/archive/ship the app.
|
||||
Windows:
|
||||
```batch
|
||||
:: Build executables for Windows
|
||||
deploy/build.bat
|
||||
|
||||
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`.
|
||||
:: Build executables with IFW installer for Windows
|
||||
deploy/build.bat --installer ifw
|
||||
|
||||
if the above error persists on your M1 Mac, then most probably you need to install arch based CMake
|
||||
```
|
||||
arch -arm64 brew install cmake
|
||||
:: Build executables with IFW and WIX installer for Windows
|
||||
deploy/build.bat --installer ifw --installer wix
|
||||
|
||||
:: Or just
|
||||
deploy/build.bat --installer all
|
||||
```
|
||||
|
||||
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.
|
||||
### Developing the project in IDEs
|
||||
|
||||
## How to build the Android 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
|
||||
|
||||
_Tested on Mac OS_
|
||||
* 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
|
||||
|
||||
The Android app has the following requirements:
|
||||
* JDK 11
|
||||
* Android platform SDK 33
|
||||
* CMake 3.25.0
|
||||
* `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.
|
||||
|
||||
After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly.
|
||||
### Installing Android SDK
|
||||
|
||||
- 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.
|
||||
* 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
|
||||
|
||||
## License
|
||||
|
||||
GPL v3.0
|
||||
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).
|
||||
|
||||
## Donate
|
||||
|
||||
|
||||
177
README_RU.md
177
README_RU.md
@@ -6,16 +6,16 @@
|
||||
[](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
|
||||
|
||||
### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский
|
||||
[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
|
||||
[AmneziaVPN](https://amnezia.org?utm_source=github&utm_campaign=amnezia_website-readme-ru) — это open source VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
|
||||
|
||||
[](https://amnezia.org)
|
||||
|
||||
### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/amnezia/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
|
||||
### [Сайт](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) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/amnezia/amnezia.org).
|
||||
> Если [сайт 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/q9p19109"><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>
|
||||
<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)
|
||||
@@ -30,7 +30,7 @@
|
||||
- Классические 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.
|
||||
- Поддерживает платформы: 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).
|
||||
|
||||
## Ссылки
|
||||
@@ -38,10 +38,10 @@
|
||||
- [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://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\)
|
||||
|
||||
## Технологии
|
||||
@@ -50,23 +50,14 @@ AmneziaVPN использует несколько проектов с откр
|
||||
|
||||
- [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/)
|
||||
- и другие...
|
||||
|
||||
## Проверка исходного кода
|
||||
После клонирования репозитория обязательно загрузите все подмодули.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
|
||||
## Разработка
|
||||
Хотите внести свой вклад? Добро пожаловать!
|
||||
|
||||
### Помощь с переводами
|
||||
## Помощь с переводами
|
||||
|
||||
Загрузите самые актуальные файлы перевода.
|
||||
|
||||
@@ -76,90 +67,98 @@ git submodule update --init --recursive
|
||||
|
||||
Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом.
|
||||
|
||||
### Сборка исходного кода и деплой
|
||||
Проверьте папку deploy для скриптов сборки.
|
||||
## Проверка исходного кода
|
||||
|
||||
### Как собрать iOS-приложение из исходного кода на MacOS
|
||||
1. Убедитесь, что у вас установлен XCode версии 14 или выше.
|
||||
2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
|
||||
- MacOS
|
||||
- iOS
|
||||
- Модуль совместимости с Qt 5
|
||||
- Qt Shader Tools
|
||||
- Дополнительные библиотеки:
|
||||
- Qt Image Formats
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
|
||||
|
||||
3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь.
|
||||
4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile:
|
||||
После клонирования репозитория обязательно загрузите все подмодули.
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
5. Соберите проект:
|
||||
## Руководство по разработке
|
||||
|
||||
Хотите внести свой вклад? Добро пожаловать!
|
||||
|
||||
### Требования для сборки
|
||||
|
||||
* [`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
|
||||
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
|
||||
```
|
||||
Замените <PATH-TO-QT-FOLDER> и <QT-VERSION> на ваши значения.
|
||||
# Build executables for the host platform
|
||||
deploy/build.sh
|
||||
|
||||
Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile:
|
||||
```bash
|
||||
export PATH=$(PATH):/path/to/GOPATH/bin
|
||||
# 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
|
||||
```
|
||||
|
||||
6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение.
|
||||
|
||||
Если сборка завершится с ошибкой:
|
||||
```
|
||||
make: ***
|
||||
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
|
||||
Error 1
|
||||
```
|
||||
Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
|
||||
Windows:
|
||||
```batch
|
||||
:: Build executables for Windows
|
||||
deploy/build.bat
|
||||
|
||||
Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM:
|
||||
```
|
||||
arch -arm64 brew install cmake
|
||||
:: 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
|
||||
```
|
||||
|
||||
При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку.
|
||||
### Разработка в IDE
|
||||
|
||||
* Можно использовать любые IDE которые умеют работать с CMake и находить Qt Kits. Например:
|
||||
- `Qt Creator`
|
||||
- `Visual Studio Code` with `Qt Extension Pack`
|
||||
- и так далее
|
||||
|
||||
## Как собрать Android-приложение
|
||||
Сборка тестировалась на MacOS. Требования:
|
||||
- JDK 11
|
||||
- Android SDK 33
|
||||
- CMake 3.25.0
|
||||
|
||||
Установите QT, QT Creator и Android Studio.
|
||||
Настройте QT Creator:
|
||||
* Для использования `Xcode` нужно сконфигурировать проект с помощью `cmake`. Самый простой способ это сделать - использовать `Qt Creator` для конфигурации. Затем, нужно открыть файл `AmneziaVPN.xcodeproj` из папки сборки с помощью `Xcode`. Учтите, что никакие файлы фактически не сохраняются - они сохраняются в директории сборки. Если требуется, скопируйте файлы вручную
|
||||
|
||||
- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`.
|
||||
- Укажите путь к JDK 11.
|
||||
- Укажите путь к Android SDK (`$ANDROID_HOME`)
|
||||
* `Android studio` может быть использована подобным вышеуказанному способу - нужно использовать `cmake` вручную или через `Qt Creator` для конфигурации. Далее, откройте `<build-dir>/client/android-build` в `Android studio`. Не забудьте скопировать изменённые файлы в папку с исходным кодом - все файлы, изменённые в IDE, сохраняются фактически в папке сборки.
|
||||
|
||||
Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере!
|
||||
|
||||
Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake.
|
||||
|
||||
Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK.
|
||||
|
||||
### Разработка Android-компонентов
|
||||
|
||||
После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` в качестве корневой.
|
||||
Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения.
|
||||
Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`<path>/client/android-build/.`).
|
||||
### Установка Android SDK
|
||||
|
||||
* Android SDK может быть установлен следующими способами:
|
||||
- Используя `Qt Creator`, через настройки в пунктах `Preferences`->`SDKs`
|
||||
- Используя `Android studio`. По умолчанию необходимые `SDK` устанавливаются автоматически.
|
||||
- Вручную, используя `sdk-manager`. Подробности можно найти [здесь](https://developer.android.com/tools)
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
149
THIRD_PARTY_LICENSES.md
Normal file
149
THIRD_PARTY_LICENSES.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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
Normal file
14
agw-sdk/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Локальные сборки
|
||||
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/
|
||||
133
agw-sdk/CMakeLists.txt
Normal file
133
agw-sdk/CMakeLists.txt
Normal file
@@ -0,0 +1,133 @@
|
||||
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()
|
||||
110
agw-sdk/README.md
Normal file
110
agw-sdk/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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`).
|
||||
65
agw-sdk/conanfile.py
Normal file
65
agw-sdk/conanfile.py
Normal file
@@ -0,0 +1,65 @@
|
||||
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")
|
||||
39
agw-sdk/examples/c_smoke/smoke.c
Normal file
39
agw-sdk/examples/c_smoke/smoke.c
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Чистый 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;
|
||||
}
|
||||
235
agw-sdk/examples/dart_smoke/bin/smoke.dart
Normal file
235
agw-sdk/examples/dart_smoke/bin/smoke.dart
Normal file
@@ -0,0 +1,235 @@
|
||||
// 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;
|
||||
}
|
||||
9
agw-sdk/examples/dart_smoke/pubspec.yaml
Normal file
9
agw-sdk/examples/dart_smoke/pubspec.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
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
|
||||
10
agw-sdk/include/agw/agw.h
Normal file
10
agw-sdk/include/agw/agw.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#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
|
||||
80
agw-sdk/include/agw/c_abi.h
Normal file
80
agw-sdk/include/agw/c_abi.h
Normal file
@@ -0,0 +1,80 @@
|
||||
#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
|
||||
30
agw-sdk/include/agw/cancellation.h
Normal file
30
agw-sdk/include/agw/cancellation.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#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
|
||||
38
agw-sdk/include/agw/config.h
Normal file
38
agw-sdk/include/agw/config.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#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
|
||||
40
agw-sdk/include/agw/gateway_controller.h
Normal file
40
agw-sdk/include/agw/gateway_controller.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#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
|
||||
51
agw-sdk/include/agw/http.h
Normal file
51
agw-sdk/include/agw/http.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#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
|
||||
55
agw-sdk/include/agw/types.h
Normal file
55
agw-sdk/include/agw/types.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#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
|
||||
201
agw-sdk/src/c_abi.cpp
Normal file
201
agw-sdk/src/c_abi.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
87
agw-sdk/src/crypto/aes.cpp
Normal file
87
agw-sdk/src/crypto/aes.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
16
agw-sdk/src/crypto/aes.h
Normal file
16
agw-sdk/src/crypto/aes.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#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
|
||||
59
agw-sdk/src/crypto/hash.cpp
Normal file
59
agw-sdk/src/crypto/hash.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
17
agw-sdk/src/crypto/hash.h
Normal file
17
agw-sdk/src/crypto/hash.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#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
|
||||
20
agw-sdk/src/crypto/rng.cpp
Normal file
20
agw-sdk/src/crypto/rng.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
24
agw-sdk/src/crypto/rng.h
Normal file
24
agw-sdk/src/crypto/rng.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#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
|
||||
111
agw-sdk/src/crypto/rsa.cpp
Normal file
111
agw-sdk/src/crypto/rsa.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
19
agw-sdk/src/crypto/rsa.h
Normal file
19
agw-sdk/src/crypto/rsa.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#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
|
||||
14
agw-sdk/src/detail/test_hooks.h
Normal file
14
agw-sdk/src/detail/test_hooks.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#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
|
||||
100
agw-sdk/src/failover/bypass_policy.cpp
Normal file
100
agw-sdk/src/failover/bypass_policy.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
13
agw-sdk/src/failover/bypass_policy.h
Normal file
13
agw-sdk/src/failover/bypass_policy.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
71
agw-sdk/src/failover/proxy_list.cpp
Normal file
71
agw-sdk/src/failover/proxy_list.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#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()));
|
||||
}
|
||||
}
|
||||
19
agw-sdk/src/failover/proxy_list.h
Normal file
19
agw-sdk/src/failover/proxy_list.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#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
|
||||
21
agw-sdk/src/failover/proxy_picker.cpp
Normal file
21
agw-sdk/src/failover/proxy_picker.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#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 { };
|
||||
}
|
||||
}
|
||||
14
agw-sdk/src/failover/proxy_picker.h
Normal file
14
agw-sdk/src/failover/proxy_picker.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#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
|
||||
364
agw-sdk/src/gateway_controller.cpp
Normal file
364
agw-sdk/src/gateway_controller.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
132
agw-sdk/src/http/curl_client.cpp
Normal file
132
agw-sdk/src/http/curl_client.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#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
|
||||
17
agw-sdk/src/http/curl_client.h
Normal file
17
agw-sdk/src/http/curl_client.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#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
|
||||
15
agw-sdk/src/http/default_client_fallback.cpp
Normal file
15
agw-sdk/src/http/default_client_fallback.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#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
|
||||
133
agw-sdk/src/protocol/error_mapping.cpp
Normal file
133
agw-sdk/src/protocol/error_mapping.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
14
agw-sdk/src/protocol/error_mapping.h
Normal file
14
agw-sdk/src/protocol/error_mapping.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#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
|
||||
18
agw-sdk/src/protocol/keys.h
Normal file
18
agw-sdk/src/protocol/keys.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#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
|
||||
55
agw-sdk/src/protocol/request_builder.cpp
Normal file
55
agw-sdk/src/protocol/request_builder.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
26
agw-sdk/src/protocol/request_builder.h
Normal file
26
agw-sdk/src/protocol/request_builder.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#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
|
||||
24
agw-sdk/src/protocol/response.cpp
Normal file
24
agw-sdk/src/protocol/response.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
20
agw-sdk/src/protocol/response.h
Normal file
20
agw-sdk/src/protocol/response.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#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
|
||||
108
agw-sdk/src/util/base64.cpp
Normal file
108
agw-sdk/src/util/base64.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
19
agw-sdk/src/util/base64.h
Normal file
19
agw-sdk/src/util/base64.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#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
|
||||
108
agw-sdk/src/util/json.cpp
Normal file
108
agw-sdk/src/util/json.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
15
agw-sdk/src/util/json.h
Normal file
15
agw-sdk/src/util/json.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#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
|
||||
59
agw-sdk/src/util/thread_pool.cpp
Normal file
59
agw-sdk/src/util/thread_pool.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#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();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
agw-sdk/src/util/thread_pool.h
Normal file
36
agw-sdk/src/util/thread_pool.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#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
|
||||
48
agw-sdk/src/util/url.cpp
Normal file
48
agw-sdk/src/util/url.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
13
agw-sdk/src/util/url.h
Normal file
13
agw-sdk/src/util/url.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
36
agw-sdk/src/util/uuid.cpp
Normal file
36
agw-sdk/src/util/uuid.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
13
agw-sdk/src/util/uuid.h
Normal file
13
agw-sdk/src/util/uuid.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#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
|
||||
27
agw-sdk/tests/CMakeLists.txt
Normal file
27
agw-sdk/tests/CMakeLists.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
# Тесты 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)
|
||||
39
agw-sdk/tests/agw_test.h
Normal file
39
agw-sdk/tests/agw_test.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#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
|
||||
1
agw-sdk/tests/golden/fixtures/aes_cipher.bin
Normal file
1
agw-sdk/tests/golden/fixtures/aes_cipher.bin
Normal file
@@ -0,0 +1 @@
|
||||
ыaГцЖ7И~▐>ъl╛▌Ц╗ЦaJ╩┤ъ%фTхO╒╢^
|
||||
1
agw-sdk/tests/golden/fixtures/aes_plaintext.bin
Normal file
1
agw-sdk/tests/golden/fixtures/aes_plaintext.bin
Normal file
@@ -0,0 +1 @@
|
||||
{"hello":"world"}
|
||||
4
agw-sdk/tests/golden/fixtures/aes_vector.txt
Normal file
4
agw-sdk/tests/golden/fixtures/aes_vector.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
AES_KEY_HEX=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
|
||||
AES_IV_HEX=101112131415161718191a1b1c1d1e1f
|
||||
AES_PLAINTEXT={"hello":"world"}
|
||||
AES_CIPHER_B64=2WHnAcP2N+l+jz7fbKyO46jjYUq7h98lxlTIT6K0Xg8=
|
||||
28
agw-sdk/tests/golden/fixtures/test_rsa_priv.pem
Normal file
28
agw-sdk/tests/golden/fixtures/test_rsa_priv.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----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-----
|
||||
9
agw-sdk/tests/golden/fixtures/test_rsa_pub.pem
Normal file
9
agw-sdk/tests/golden/fixtures/test_rsa_pub.pem
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy96cRO7iN0WnEI78hc+b
|
||||
W5C/O2qqRQbwFRUtDhc1S2wKWiep79o2TX4nWzwjeUjHjaGtpDUwvjGTUKjkYK++
|
||||
04GkbhWqeXUrp/anItJZeWP8wUB2/P14oWofGKZGzv/EldWhVi7VikIkC8axUw/U
|
||||
oVwu0uiEK3RnJ4FIS0oYp1po73mSf/we56N8FD98TxfhJE106uUMg/0n0P8NAVKL
|
||||
MuebAp+Tsj+okieHKrowO1ghHePF6Uuc4HHqnZxPfJr44a4jA3zkV7Ep8cprA5z8
|
||||
l/8JKyzsp6TeSj6AjMF0tJJ00hgA1DzJvxEF2uX4xA8YNDpyUHCVf5mSZOEYIvoG
|
||||
SQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
97
agw-sdk/tests/golden/test_golden.cpp
Normal file
97
agw-sdk/tests/golden/test_golden.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#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();
|
||||
}
|
||||
174
agw-sdk/tests/integration/test_async.cpp
Normal file
174
agw-sdk/tests/integration/test_async.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#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();
|
||||
}
|
||||
118
agw-sdk/tests/integration/test_c_abi.cpp
Normal file
118
agw-sdk/tests/integration/test_c_abi.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#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();
|
||||
}
|
||||
125
agw-sdk/tests/integration/test_failover.cpp
Normal file
125
agw-sdk/tests/integration/test_failover.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
#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();
|
||||
}
|
||||
103
agw-sdk/tests/integration/test_post.cpp
Normal file
103
agw-sdk/tests/integration/test_post.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#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();
|
||||
}
|
||||
77
agw-sdk/tests/mock_gateway/mock_gateway.h
Normal file
77
agw-sdk/tests/mock_gateway/mock_gateway.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#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
|
||||
24765
agw-sdk/tests/third_party/nlohmann/json.hpp
vendored
Normal file
24765
agw-sdk/tests/third_party/nlohmann/json.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
48
agw-sdk/tests/unit/test_bypass_policy.cpp
Normal file
48
agw-sdk/tests/unit/test_bypass_policy.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#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();
|
||||
}
|
||||
123
agw-sdk/tests/unit/test_crypto.cpp
Normal file
123
agw-sdk/tests/unit/test_crypto.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#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();
|
||||
}
|
||||
54
agw-sdk/tests/unit/test_error_mapping.cpp
Normal file
54
agw-sdk/tests/unit/test_error_mapping.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#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();
|
||||
}
|
||||
61
agw-sdk/tests/unit/test_json.cpp
Normal file
61
agw-sdk/tests/unit/test_json.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#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();
|
||||
}
|
||||
87
agw-sdk/tests/unit/test_proxy_list.cpp
Normal file
87
agw-sdk/tests/unit/test_proxy_list.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#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();
|
||||
}
|
||||
33
agw-sdk/tests/unit/test_thread_pool.cpp
Normal file
33
agw-sdk/tests/unit/test_thread_pool.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#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();
|
||||
}
|
||||
Submodule client/3rd-prebuilt deleted from e555c78bcf
2
client/3rd/amneziawg-apple
vendored
2
client/3rd/amneziawg-apple
vendored
Submodule client/3rd/amneziawg-apple updated: 76e7db556a...cf63135331
1
client/3rd/qtgamepad
vendored
Submodule
1
client/3rd/qtgamepad
vendored
Submodule
Submodule client/3rd/qtgamepad added at f72b3e0c62
@@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
set(PROJECT AmneziaVPN)
|
||||
project(${PROJECT})
|
||||
|
||||
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER "Autogen")
|
||||
set_property(GLOBAL PROPERTY AUTOMOC_TARGETS_FOLDER "Autogen")
|
||||
@@ -26,14 +25,14 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
|
||||
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
||||
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
||||
add_definitions(-DFALLBACK_S3_ENDPOINT="$ENV{FALLBACK_S3_ENDPOINT}")
|
||||
|
||||
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
||||
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||
add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
|
||||
|
||||
if(IOS)
|
||||
set(PACKAGES ${PACKAGES} Multimedia)
|
||||
endif()
|
||||
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
@@ -48,28 +47,30 @@ set(LIBS ${LIBS}
|
||||
Qt6::Core5Compat Qt6::Concurrent
|
||||
)
|
||||
|
||||
if(IOS)
|
||||
set(LIBS ${LIBS} Qt6::Multimedia)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
qt_add_executable(${PROJECT} MANUAL_FINALIZATION)
|
||||
target_include_directories(${PROJECT} PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
||||
)
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
|
||||
endif()
|
||||
|
||||
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
|
||||
qt6_add_resources(QRC ${QRC}
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/ui/qml/qml.qrc
|
||||
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
|
||||
)
|
||||
|
||||
# -- i18n begin
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(AMNEZIAVPN_TS_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru_RU.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
|
||||
@@ -81,19 +82,10 @@ set(AMNEZIAVPN_TS_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_hi_IN.ts
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
||||
|
||||
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
|
||||
|
||||
set(QM_FILE_LIST "")
|
||||
foreach(FILE ${AMNEZIAVPN_QM_FILES})
|
||||
get_filename_component(QM_FILE_NAME ${FILE} NAME)
|
||||
list(APPEND QM_FILE_LIST "<file>${QM_FILE_NAME}</file>")
|
||||
endforeach()
|
||||
string(REPLACE ";" "" QM_FILE_LIST ${QM_FILE_LIST})
|
||||
|
||||
configure_file(${CMAKE_CURRENT_LIST_DIR}/translations/translations.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc)
|
||||
qt6_add_resources(QRC ${I18NQRC} ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc)
|
||||
qt6_add_translations(${PROJECT}
|
||||
TS_FILES ${AMNEZIAVPN_TS_FILES}
|
||||
RESOURCE_PREFIX "/translations"
|
||||
)
|
||||
# -- i18n end
|
||||
|
||||
set(IS_CI ${CI})
|
||||
@@ -115,6 +107,15 @@ include_directories(
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
if(MACOS_NE)
|
||||
message("MACOS_NE is ON")
|
||||
add_definitions(-DQ_OS_MAC)
|
||||
add_definitions(-DMACOS_NE)
|
||||
message("Add macros for MacOS Network Extension")
|
||||
else()
|
||||
message("MACOS_NE is OFF")
|
||||
endif()
|
||||
|
||||
include_directories(mozilla)
|
||||
include_directories(mozilla/shared)
|
||||
include_directories(mozilla/models)
|
||||
@@ -144,7 +145,7 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
cmake_policy(SET CMP0099 OLD)
|
||||
cmake_policy(SET CMP0099 NEW)
|
||||
cmake_policy(SET CMP0114 NEW)
|
||||
|
||||
if(NOT BUILD_OSX_APP_IDENTIFIER)
|
||||
@@ -163,7 +164,10 @@ if(APPLE)
|
||||
set(CMAKE_XCODE_GENERATE_SCHEME FALSE)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${BUILD_VPN_DEVELOPMENT_TEAM})
|
||||
set(CMAKE_XCODE_ATTRIBUTE_GROUP_ID_IOS ${BUILD_IOS_GROUP_IDENTIFIER})
|
||||
|
||||
|
||||
if (BUILD_VPN_KEYCHAIN)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--keychain ${BUILD_VPN_KEYCHAIN}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LINUX AND NOT ANDROID)
|
||||
@@ -171,8 +175,7 @@ if(LINUX AND NOT ANDROID)
|
||||
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
message("Client desktop build")
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
add_compile_definitions(AMNEZIA_DESKTOP)
|
||||
endif()
|
||||
|
||||
@@ -183,44 +186,95 @@ endif()
|
||||
if(IOS)
|
||||
include(cmake/ios.cmake)
|
||||
include(cmake/ios-arch-fixup.cmake)
|
||||
elseif(APPLE AND NOT IOS)
|
||||
elseif(APPLE AND MACOS_NE)
|
||||
include(cmake/macos_ne.cmake)
|
||||
elseif(APPLE)
|
||||
include(cmake/osxtools.cmake)
|
||||
include(cmake/macos.cmake)
|
||||
endif()
|
||||
|
||||
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
|
||||
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
|
||||
|
||||
# deploy artifacts required to run the application to the debug build folder
|
||||
if(WIN32)
|
||||
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
|
||||
set(DEPLOY_PLATFORM_PATH "windows/x64")
|
||||
else()
|
||||
set(DEPLOY_PLATFORM_PATH "windows/x32")
|
||||
endif()
|
||||
elseif(LINUX)
|
||||
set(DEPLOY_PLATFORM_PATH "linux/client")
|
||||
elseif(APPLE AND NOT IOS)
|
||||
set(DEPLOY_PLATFORM_PATH "macos")
|
||||
endif()
|
||||
|
||||
if(NOT IOS AND NOT ANDROID)
|
||||
add_custom_command(
|
||||
TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
||||
${CMAKE_SOURCE_DIR}/deploy/data/${DEPLOY_PLATFORM_PATH}
|
||||
$<TARGET_FILE_DIR:${PROJECT}>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
add_custom_command(
|
||||
TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
||||
${CMAKE_SOURCE_DIR}/client/3rd-prebuilt/deploy-prebuilt/${DEPLOY_PLATFORM_PATH}
|
||||
$<TARGET_FILE_DIR:${PROJECT}>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
|
||||
endif()
|
||||
|
||||
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||
qt_finalize_target(${PROJECT})
|
||||
|
||||
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||
if(COMMAND qt_import_qml_plugins)
|
||||
qt_import_qml_plugins(${PROJECT})
|
||||
endif()
|
||||
if(COMMAND qt_finalize_executable)
|
||||
qt_finalize_executable(${PROJECT})
|
||||
else()
|
||||
qt_finalize_target(${PROJECT})
|
||||
endif()
|
||||
|
||||
install(TARGETS ${PROJECT}
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
RUNTIME_DEPENDENCY_SET client_deps
|
||||
COMPONENT AmneziaVPN
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/AmneziaVPN.app/Contents/Frameworks)
|
||||
else()
|
||||
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
|
||||
endif()
|
||||
|
||||
install(RUNTIME_DEPENDENCY_SET client_deps
|
||||
PRE_EXCLUDE_REGEXES
|
||||
[[api-ms-win-.*]]
|
||||
[[ext-ms-.*]]
|
||||
[[kernel32\.dll]]
|
||||
[[hvsifiletrust\.dll]]
|
||||
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
|
||||
[[.*\.framework]]
|
||||
[[^[Qq]t.*]]
|
||||
POST_EXCLUDE_REGEXES
|
||||
[[^.*[\\/]system32[\\/].*\.dll$]]
|
||||
[[^/lib.*]]
|
||||
[[^/usr/lib.*]]
|
||||
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
|
||||
COMPONENT AmneziaVPN
|
||||
DESTINATION "${RUNTIME_DEPS_DIR}"
|
||||
)
|
||||
|
||||
set(deploy_tool_options "")
|
||||
if(WIN32)
|
||||
set(deploy_tool_options "--force-openssl --force")
|
||||
endif()
|
||||
|
||||
qt_generate_deploy_qml_app_script(
|
||||
TARGET ${PROJECT}
|
||||
OUTPUT_SCRIPT QT_DEPLOY_SCRIPT
|
||||
NO_UNSUPPORTED_PLATFORM_ERROR
|
||||
DEPLOY_TOOL_OPTIONS ${deploy_tool_options}
|
||||
)
|
||||
install(SCRIPT ${QT_DEPLOY_SCRIPT}
|
||||
COMPONENT AmneziaVPN
|
||||
)
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT MACOS_NE)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/macos/update-resolv-conf.sh")
|
||||
endif()
|
||||
if (LINUX AND NOT ANDROID)
|
||||
list(APPEND OVPN_SCRIPTS "${CMAKE_SOURCE_DIR}/deploy/data/linux/update-resolv-conf.sh")
|
||||
endif()
|
||||
|
||||
if(OVPN_SCRIPTS)
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${OVPN_SCRIPTS}
|
||||
"$<TARGET_FILE_DIR:${PROJECT}>"
|
||||
)
|
||||
|
||||
install(FILES ${OVPN_SCRIPTS}
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
COMPONENT AmneziaVPN
|
||||
PERMISSIONS
|
||||
OWNER_READ OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "amnezia_application.h"
|
||||
#include "amneziaApplication.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFontDatabase>
|
||||
@@ -12,18 +12,30 @@
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#include <QEvent>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QtQuick/QQuickWindow>
|
||||
#include <QWindow>
|
||||
|
||||
#include "core/protocols/qmlRegisterProtocols.h"
|
||||
#include "logger.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/models/installedAppsModel.h"
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
|
||||
|
||||
#include "protocols/qml_register_protocols.h"
|
||||
bool AmneziaApplication::m_forceQuit = false;
|
||||
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv)
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv),
|
||||
m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")),
|
||||
m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs")),
|
||||
m_optConnect ({QStringLiteral("connect")}, QStringLiteral("Connect to server by index on startup"), QStringLiteral("index")),
|
||||
m_optImport ({QStringLiteral("import")}, QStringLiteral("Import configuration from data string"), QStringLiteral("data"))
|
||||
{
|
||||
setDesktopFileName(QStringLiteral(APPLICATION_NAME));
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
// Fix config file permissions
|
||||
@@ -42,59 +54,113 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
|
||||
QFile::setPermissions(configLoc2, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
#endif
|
||||
|
||||
m_settings = std::shared_ptr<Settings>(new Settings);
|
||||
m_settings = new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, this);
|
||||
m_nam = new QNetworkAccessManager(this);
|
||||
}
|
||||
|
||||
AmneziaApplication::~AmneziaApplication()
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_vpnConnection && m_vpnConnectionThread.isRunning()) {
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::BlockingQueuedConnection);
|
||||
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::BlockingQueuedConnection);
|
||||
}
|
||||
#endif
|
||||
|
||||
m_vpnConnectionThread.requestInterruption();
|
||||
m_vpnConnectionThread.quit();
|
||||
m_vpnConnectionThread.wait(3000);
|
||||
|
||||
if (!m_vpnConnectionThread.wait(3000)) {
|
||||
m_vpnConnectionThread.terminate();
|
||||
m_vpnConnectionThread.wait(500);
|
||||
}
|
||||
|
||||
if (m_engine) {
|
||||
QObject::disconnect(m_engine, 0, 0, 0);
|
||||
delete m_engine;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
namespace {
|
||||
static void clearQtCaches()
|
||||
{
|
||||
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
if (!cacheRoot.isEmpty()) {
|
||||
QDir(cacheRoot + "/QtShaderCache").removeRecursively();
|
||||
QDir(cacheRoot + "/qmlcache").removeRecursively();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void AmneziaApplication::init()
|
||||
{
|
||||
m_engine = new QQmlApplicationEngine;
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
|
||||
QObject::connect(
|
||||
m_engine, &QQmlApplicationEngine::objectCreated, this,
|
||||
[url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl)
|
||||
QCoreApplication::exit(-1);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
m_engine, &QQmlApplicationEngine::objectCreated, this,
|
||||
[this, url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl) {
|
||||
QCoreApplication::exit(-1);
|
||||
return;
|
||||
}
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
win->installEventFilter(this);
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||
qWarning() << "Scene graph error (suppressed):" << msg;
|
||||
});
|
||||
// Keep graphics context alive across hide/show cycles to avoid
|
||||
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->show();
|
||||
#else
|
||||
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
|
||||
win->show();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
m_engine->rootContext()->setContextProperty("Debug", &Logger::Instance());
|
||||
|
||||
m_vpnConnection.reset(new VpnConnection(m_settings));
|
||||
#ifdef MACOS_NE
|
||||
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", true);
|
||||
#else
|
||||
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
|
||||
#endif
|
||||
|
||||
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
|
||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
||||
m_vpnConnectionThread.start();
|
||||
|
||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
|
||||
if (m_parser.isSet(m_optImport)) {
|
||||
const QString data = m_parser.value(m_optImport);
|
||||
if (!data.isEmpty()) {
|
||||
if (m_coreController) {
|
||||
m_coreController->importConfigFromData(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_engine->load(url);
|
||||
|
||||
m_coreController->setQmlRoot();
|
||||
|
||||
bool enabled = m_settings->isSaveLogs();
|
||||
#ifndef Q_OS_ANDROID
|
||||
if (enabled) {
|
||||
if (!Logger::init(false)) {
|
||||
qWarning() << "Initialization of debug subsystem failed";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Logger::setServiceLogsEnabled(enabled);
|
||||
|
||||
#ifdef Q_OS_WIN //TODO
|
||||
if (m_parser.isSet("a"))
|
||||
if (m_parser.isSet(m_optAutostart))
|
||||
m_coreController->pageController()->showOnStartup();
|
||||
else
|
||||
emit m_coreController->pageController()->raiseMainWindow();
|
||||
@@ -118,6 +184,18 @@ void AmneziaApplication::init()
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
if (m_parser.isSet(m_optConnect)) {
|
||||
bool ok = false;
|
||||
int idx = m_parser.value(m_optConnect).toInt(&ok);
|
||||
if (ok) {
|
||||
QTimer::singleShot(0, this, [this, idx]() {
|
||||
if (m_coreController) {
|
||||
m_coreController->openConnectionByIndex(idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaApplication::registerTypes()
|
||||
@@ -125,13 +203,11 @@ void AmneziaApplication::registerTypes()
|
||||
qRegisterMetaType<ServerCredentials>("ServerCredentials");
|
||||
|
||||
qRegisterMetaType<DockerContainer>("DockerContainer");
|
||||
using namespace amnezia::ProtocolEnumNS;
|
||||
qRegisterMetaType<TransportProto>("TransportProto");
|
||||
qRegisterMetaType<Proto>("Proto");
|
||||
qRegisterMetaType<ServiceType>("ServiceType");
|
||||
|
||||
declareQmlProtocolEnum();
|
||||
declareQmlContainerEnum();
|
||||
|
||||
qmlRegisterType<QRCodeReader>("QRCodeReader", 1, 0, "QRCodeReader");
|
||||
|
||||
m_containerProps.reset(new ContainerProps());
|
||||
@@ -145,6 +221,7 @@ void AmneziaApplication::registerTypes()
|
||||
|
||||
qmlRegisterType<InstalledAppsModel>("InstalledAppsModel", 1, 0, "InstalledAppsModel");
|
||||
|
||||
amnezia::declareQmlProtocolEnum();
|
||||
Vpn::declareQmlVpnConnectionStateEnum();
|
||||
PageLoader::declareQmlPageEnum();
|
||||
}
|
||||
@@ -162,15 +239,14 @@ bool AmneziaApplication::parseCommands()
|
||||
m_parser.addHelpOption();
|
||||
m_parser.addVersionOption();
|
||||
|
||||
QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" };
|
||||
m_parser.addOption(c_autostart);
|
||||
|
||||
QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" };
|
||||
m_parser.addOption(c_cleanup);
|
||||
|
||||
m_parser.addOption(m_optAutostart);
|
||||
m_parser.addOption(m_optCleanup);
|
||||
m_parser.addOption(m_optConnect);
|
||||
m_parser.addOption(m_optImport);
|
||||
|
||||
m_parser.process(*this);
|
||||
|
||||
if (m_parser.isSet(c_cleanup)) {
|
||||
if (m_parser.isSet(m_optCleanup)) {
|
||||
Logger::cleanUp();
|
||||
QTimer::singleShot(100, this, [this] { quit(); });
|
||||
exec();
|
||||
@@ -179,9 +255,8 @@ bool AmneziaApplication::parseCommands()
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void AmneziaApplication::startLocalServer()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
void AmneziaApplication::startLocalServer() {
|
||||
const QString serverName("AmneziaVPNInstance");
|
||||
QLocalServer::removeServer(serverName);
|
||||
|
||||
@@ -198,6 +273,32 @@ void AmneziaApplication::startLocalServer()
|
||||
}
|
||||
#endif
|
||||
|
||||
bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Close) {
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
quit();
|
||||
#else
|
||||
if (m_forceQuit) {
|
||||
quit();
|
||||
} else {
|
||||
if (m_coreController && m_coreController->pageController()) {
|
||||
m_coreController->pageController()->hideMainWindow();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true; // eat the close
|
||||
}
|
||||
// call base QObject::eventFilter
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void AmneziaApplication::forceQuit()
|
||||
{
|
||||
m_forceQuit = true;
|
||||
quit();
|
||||
}
|
||||
|
||||
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
||||
{
|
||||
return m_engine;
|
||||
@@ -7,22 +7,24 @@
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#include <QGuiApplication>
|
||||
#include <QGuiApplication>
|
||||
#else
|
||||
#include <QApplication>
|
||||
#include <QApplication>
|
||||
#endif
|
||||
#include <QClipboard>
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "settings.h"
|
||||
#include "vpnconnection.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "ui/models/containerProps.h"
|
||||
#include "ui/models/protocolProps.h"
|
||||
|
||||
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#define AMNEZIA_BASE_CLASS QGuiApplication
|
||||
#define AMNEZIA_BASE_CLASS QGuiApplication
|
||||
#else
|
||||
#define AMNEZIA_BASE_CLASS QApplication
|
||||
#define AMNEZIA_BASE_CLASS QApplication
|
||||
#endif
|
||||
|
||||
class AmneziaApplication : public AMNEZIA_BASE_CLASS
|
||||
@@ -37,7 +39,7 @@ public:
|
||||
void loadFonts();
|
||||
bool parseCommands();
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
void startLocalServer();
|
||||
#endif
|
||||
|
||||
@@ -45,9 +47,13 @@ public:
|
||||
QNetworkAccessManager *networkManager();
|
||||
QClipboard *getClipboard();
|
||||
|
||||
public slots:
|
||||
void forceQuit();
|
||||
|
||||
private:
|
||||
static bool m_forceQuit;
|
||||
QQmlApplicationEngine *m_engine {};
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QScopedPointer<CoreController> m_coreController;
|
||||
|
||||
@@ -56,10 +62,17 @@ private:
|
||||
|
||||
QCommandLineParser m_parser;
|
||||
|
||||
QCommandLineOption m_optAutostart;
|
||||
QCommandLineOption m_optCleanup;
|
||||
QCommandLineOption m_optConnect;
|
||||
QCommandLineOption m_optImport;
|
||||
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QThread m_vpnConnectionThread;
|
||||
|
||||
QNetworkAccessManager *m_nam;
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
};
|
||||
|
||||
#endif // AMNEZIA_APPLICATION_H
|
||||
@@ -45,7 +45,8 @@
|
||||
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|
||||
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
|
||||
android:launchMode="singleInstance"
|
||||
android:windowSoftInputMode="stateUnchanged|adjustResize"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -214,4 +215,4 @@
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -39,6 +39,7 @@ android {
|
||||
|
||||
// keeps language resources for only the locales specified below
|
||||
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
|
||||
ndk.abiFilters += qtTargetAbiList.split(",")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -52,50 +53,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
storeFile = providers.environmentVariable("ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
storePassword = providers.environmentVariable("ANDROID_KEYSTORE_KEY_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("ANDROID_KEYSTORE_KEY_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("ANDROID_KEYSTORE_KEY_PASS").orNull
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// exclude coroutine debug resource from release build
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
|
||||
create("fdroid") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = null
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*qtTargetAbiList.split(',').toTypedArray())
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
// fix for Qt Creator to allow deploying the application to a device
|
||||
// to enable this fix, add the line outputBaseName=android-build to local.properties
|
||||
if (outputBaseName.isNotEmpty()) {
|
||||
applicationVariants.all {
|
||||
outputs.map { it as BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.outputFileName.endsWith(".apk")) {
|
||||
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +74,6 @@ dependencies {
|
||||
implementation(project(":wireguard"))
|
||||
implementation(project(":awg"))
|
||||
implementation(project(":openvpn"))
|
||||
implementation(project(":cloak"))
|
||||
implementation(project(":xray"))
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.protocol.cloak"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
compileOnly(project(":protocolApi"))
|
||||
implementation(project(":openvpn"))
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.amnezia.vpn.protocol.cloak
|
||||
|
||||
import android.util.Base64
|
||||
import net.openvpn.ovpn3.ClientAPI_Config
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpn
|
||||
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
|
||||
import org.json.JSONObject
|
||||
|
||||
class Cloak : OpenVpn() {
|
||||
|
||||
override fun internalInit() {
|
||||
super.internalInit()
|
||||
if (!isInitialized) loadSharedLibrary(context, "ck-ovpn-plugin")
|
||||
}
|
||||
|
||||
override fun parseConfig(config: JSONObject): ClientAPI_Config {
|
||||
val openVpnConfig = ClientAPI_Config()
|
||||
|
||||
val openVpnConfigStr = config.getJSONObject("openvpn_config_data").getString("config")
|
||||
val cloakConfigJson = checkCloakJson(config.getJSONObject("cloak_config_data"))
|
||||
val cloakConfigStr = Base64.encodeToString(cloakConfigJson.toString().toByteArray(), Base64.DEFAULT)
|
||||
|
||||
val configStr = "$openVpnConfigStr\n<cloak>\n$cloakConfigStr\n</cloak>\n"
|
||||
|
||||
openVpnConfig.usePluggableTransports = true
|
||||
openVpnConfig.content = configStr
|
||||
return openVpnConfig
|
||||
}
|
||||
|
||||
private fun checkCloakJson(cloakConfigJson: JSONObject): JSONObject {
|
||||
cloakConfigJson.put("NumConn", 1)
|
||||
cloakConfigJson.put("ProxyMethod", "openvpn")
|
||||
if (cloakConfigJson.has("port")) {
|
||||
val port = cloakConfigJson["port"]
|
||||
cloakConfigJson.remove("port")
|
||||
cloakConfigJson.put("RemotePort", port)
|
||||
}
|
||||
if (cloakConfigJson.has("remote")) {
|
||||
val remote = cloakConfigJson["remote"]
|
||||
cloakConfigJson.remove("remote")
|
||||
cloakConfigJson.put("RemoteHost", remote)
|
||||
}
|
||||
return cloakConfigJson
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
[versions]
|
||||
agp = "8.5.2"
|
||||
agp = "8.6.1"
|
||||
kotlin = "1.9.24"
|
||||
androidx-core = "1.13.1"
|
||||
androidx-activity = "1.9.1"
|
||||
androidx-annotation = "1.8.2"
|
||||
androidx-biometric = "1.2.0-alpha05"
|
||||
androidx-camera = "1.3.4"
|
||||
androidx-camera = "1.5.3"
|
||||
androidx-fragment = "1.8.2"
|
||||
androidx-security-crypto = "1.1.0-alpha06"
|
||||
androidx-datastore = "1.1.1"
|
||||
|
||||
@@ -93,7 +93,7 @@ open class OpenVpn : Protocol() {
|
||||
openVpnClient = null
|
||||
}
|
||||
|
||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
||||
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
openVpnClient?.let {
|
||||
it.establish = makeEstablish(vpnBuilder)
|
||||
it.reconnect(0)
|
||||
|
||||
@@ -42,7 +42,7 @@ abstract class Protocol {
|
||||
|
||||
abstract fun stopVpn()
|
||||
|
||||
abstract fun reconnectVpn(vpnBuilder: Builder)
|
||||
abstract fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean)
|
||||
|
||||
protected fun ProtocolConfig.Builder.configSplitTunneling(config: JSONObject) {
|
||||
if (!allowSplitTunneling) {
|
||||
|
||||
10
client/android/res/drawable/ic_launcher_background.xml
Normal file
10
client/android/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:angle="135"
|
||||
android:startColor="#2A2A2E"
|
||||
android:centerColor="#17171A"
|
||||
android:endColor="#0E0E11" />
|
||||
</shape>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user