mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
fixed open Qr QML & add check error code & add test
This commit is contained in:
@@ -22,26 +22,45 @@ go run .
|
||||
|
||||
После `git pull` обязательно **остановите старый процесс** на 8080 (`Ctrl+C` в терминале или `kill <PID>`), иначе будет крутиться бинарник без правок.
|
||||
|
||||
В логах должно появиться сообщение вида:
|
||||
В логах при старте: `plaintext mock on tcp4 0.0.0.0:8080 — see ... README.md for paths`. Каждый запрос дополнительно пишется как `REQ <METHOD> <path>`.
|
||||
|
||||
`plaintext mock listening on 0.0.0.0:8080 GET / POST /v1/services POST /v1/config POST /api/v1/generate_qr POST /api/v1/scan_qr`
|
||||
Проверка без клиента (mock должен быть запущен):
|
||||
|
||||
```bash
|
||||
./verify.sh
|
||||
# или
|
||||
bash verify.sh http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|--------|------|------------|
|
||||
| `GET` | `/` | Короткий текст для проверки из браузера / телефона. |
|
||||
| `POST` | `/v1/services` | Минимальный ответ со списком сервисов (в т.ч. `amnezia-free` / `awg`). |
|
||||
| `POST` | `/v1/config` | Импорт конфига: лимит/CAPTCHA (`dchest/captcha`), проверка решения, мок-ответы. |
|
||||
| `POST` | `/api/v1/generate_qr` | Регистрация pairing-сессии по `qr_uuid` + long-poll (**120s** в этом mock; **30s** на production gateway). |
|
||||
| `POST` | `/api/v1/scan_qr` | Завершение pairing-сессии: передача `config` + `service_info` + `supported_protocols` по `qr_uuid`. |
|
||||
| `GET` | `/VERSION` | Версия для цепочки обновлений (`UpdateController`: после `updater_endpoint`). Значение `0.0.1` — ниже клиента, «обновление не найдено». |
|
||||
| `GET` | `/CHANGELOG` | Пустое тело, успех. |
|
||||
| `GET` | `/RELEASE_DATE` | Пустое тело, успех. |
|
||||
| `POST` | `/v1/account_info` | Экран API‑подписки (`getAccountInfo`). |
|
||||
| `POST` | `/v1/services` | Каталог сервисов (`ServicesCatalogController`). |
|
||||
| `POST` | `/v1/config` | Amnezia Free: CAPTCHA/лимит; иначе короткий мок‑ответ (полноценный premium `vpn://` здесь не строится). |
|
||||
| `POST` | `/v1/news` | Лента новостей (`NewsController`), пустой `news`. |
|
||||
| `POST` | `/v1/renewal_link` | Ссылка продления (`renewal_url`). |
|
||||
| `POST` | `/v1/updater_endpoint` | `{"url":"http://127.0.0.1:8080"}` → затем GET `/VERSION` на этом хосте. |
|
||||
| `POST` | `/v1/revoke_config` | Успех, тело не разбирается при `NoError`. |
|
||||
| `POST` | `/v1/revoke_native_config` | То же. |
|
||||
| `POST` | `/api/v1/generate_qr` | Pairing: long-poll (**120s** mock). |
|
||||
| `POST` | `/api/v1/scan_qr` | Pairing: завершение по `qr_uuid`. |
|
||||
|
||||
Других маршрутов нет (кроме `GET /`).
|
||||
**Не реализовано** (нужен осмысленный `vpn://` / IAP): `POST /v1/trial`, `POST /v1/subscriptions`, `POST /v1/native_config`, `POST /v1/proxy_config` (Telegram). При необходимости — отдельная доработка или прод gateway.
|
||||
|
||||
**Обновление premium** (`updateServiceFromGateway` → `POST /v1/config` с `amnezia-premium`) требует валидного поля `config` с `vpn://…` в ответе; текущий mock для premium не подменяет полный конфиг — избегайте «Reload API config» на полностью локальном стенде или расширяйте mock.
|
||||
|
||||
## Связка с клиентом AmneziaVPN
|
||||
|
||||
1. Соберите клиент с флагом CMake **`AMNEZIA_LOCAL_GATEWAY=ON`** — тогда для `localhost` запросы к gateway уходят **plaintext JSON** без RSA/AES (см. `GatewayController`, `SecureAppSettingsRepository`).
|
||||
2. В настройках приложения endpoint gateway должен указывать на **`http://localhost:8080/`** (или `http://127.0.0.1:8080/`). При включённом `AMNEZIA_LOCAL_GATEWAY` дефолтный URL в коде уже `http://localhost:8080/`.
|
||||
1. Соберите клиент с определением **`AMNEZIA_LOCAL_GATEWAY`** (см. `client/CMakeLists.txt`, `target_compile_definitions`) — тогда для **`127.0.0.1`** и **`localhost`** запросы к gateway уходят **plaintext JSON** без RSA/AES (см. `GatewayController`, `SecureAppSettingsRepository`).
|
||||
2. В настройках приложения endpoint gateway: **`http://127.0.0.1:8080/`** (дефолт при `AMNEZIA_LOCAL_GATEWAY` в коде). Допустим и `http://localhost:8080/` — тоже plaintext.
|
||||
|
||||
Пошаговый план (включая следующие этапы вроде `/v1/account_info`): **`docs/local-gateway-mock.md`**.
|
||||
|
||||
После этого сценарии вроде **Amnezia Free → Continue** будут ходить в этот mock.
|
||||
|
||||
|
||||
+128
-9
@@ -80,6 +80,19 @@ func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func drainBody(r *http.Request) {
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
_ = r.Body.Close()
|
||||
}
|
||||
|
||||
// logReq logs every request (step 5 in docs/local-gateway-mock.md).
|
||||
func logReq(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("REQ %s %s", r.Method, r.URL.Path)
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupExpiredSessions(now time.Time) {
|
||||
for uuid, session := range sessions {
|
||||
if now.After(session.ExpiresAt) {
|
||||
@@ -243,8 +256,7 @@ func handleServices(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
_ = r.Body.Close()
|
||||
drainBody(r)
|
||||
|
||||
// Minimal shape for ApiServicesModel::updateModel + importFreeFromGateway (service_protocol "awg").
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -385,17 +397,124 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("local_gateway plaintext mock\nPOST /api/v1/generate_qr, /api/v1/scan_qr, /v1/services, /v1/config\n"))
|
||||
_, _ = w.Write([]byte("local_gateway plaintext mock — full path list: tools/local_gateway/README.md\n"))
|
||||
}
|
||||
|
||||
// POST /v1/account_info — same path as SubscriptionController::getAccountInfo (ApiAccountInfoModel::updateModel).
|
||||
func handleAccountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
drainBody(r)
|
||||
|
||||
// Keys match client/core/utils/constants/apiKeys.h (snake_case).
|
||||
endDate := time.Now().UTC().AddDate(1, 0, 0).Format(time.RFC3339)
|
||||
resp := map[string]any{
|
||||
"active_device_count": 1,
|
||||
"max_device_count": 5,
|
||||
"subscription_end_date": endDate,
|
||||
"subscription_description": "Local mock (tools/local_gateway)",
|
||||
"is_renewal_available": false,
|
||||
"supported_protocols": []string{"awg", "vless"},
|
||||
"available_countries": []any{},
|
||||
"issued_configs": []any{},
|
||||
"support_info": map[string]any{
|
||||
"telegram": "amnezia_support",
|
||||
"email": "support@example.com",
|
||||
"billing_email": "billing@example.com",
|
||||
"website": "https://amnezia.org",
|
||||
"website_name": "Amnezia",
|
||||
},
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// POST /v1/news — NewsController::fetchNews (empty list is fine).
|
||||
func handleNews(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
drainBody(r)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"news": []any{}})
|
||||
}
|
||||
|
||||
// POST /v1/renewal_link — SubscriptionController::getRenewalLink.
|
||||
func handleRenewalLink(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
drainBody(r)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"renewal_url": "https://amnezia.org/"})
|
||||
}
|
||||
|
||||
// POST /v1/updater_endpoint — UpdateController::fetchGatewayUrl, then GET {url}/VERSION.
|
||||
func handleUpdaterEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
drainBody(r)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"url": "http://127.0.0.1:8080"})
|
||||
}
|
||||
|
||||
// POST /v1/revoke_config, /v1/revoke_native_config — success body ignored if error is NoError.
|
||||
func handleRevokeNoop(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
drainBody(r)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "mock"})
|
||||
}
|
||||
|
||||
func handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("0.0.1"))
|
||||
}
|
||||
|
||||
func handleGetChangelog(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func handleGetReleaseDate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", handleRoot)
|
||||
http.HandleFunc("/v1/services", handleServices)
|
||||
http.HandleFunc("/v1/config", handleConfig)
|
||||
http.HandleFunc("/api/v1/generate_qr", handleGenerateQR)
|
||||
http.HandleFunc("/api/v1/scan_qr", handleScanQR)
|
||||
http.HandleFunc("/", logReq(handleRoot))
|
||||
http.HandleFunc("/VERSION", logReq(handleGetVersion))
|
||||
http.HandleFunc("/CHANGELOG", logReq(handleGetChangelog))
|
||||
http.HandleFunc("/RELEASE_DATE", logReq(handleGetReleaseDate))
|
||||
http.HandleFunc("/v1/account_info", logReq(handleAccountInfo))
|
||||
http.HandleFunc("/v1/services", logReq(handleServices))
|
||||
http.HandleFunc("/v1/config", logReq(handleConfig))
|
||||
http.HandleFunc("/v1/news", logReq(handleNews))
|
||||
http.HandleFunc("/v1/renewal_link", logReq(handleRenewalLink))
|
||||
http.HandleFunc("/v1/updater_endpoint", logReq(handleUpdaterEndpoint))
|
||||
http.HandleFunc("/v1/revoke_config", logReq(handleRevokeNoop))
|
||||
http.HandleFunc("/v1/revoke_native_config", logReq(handleRevokeNoop))
|
||||
http.HandleFunc("/api/v1/generate_qr", logReq(handleGenerateQR))
|
||||
http.HandleFunc("/api/v1/scan_qr", logReq(handleScanQR))
|
||||
const addr = "0.0.0.0:8080"
|
||||
log.Printf("plaintext mock listening on tcp4 %s GET / POST /v1/services POST /v1/config POST /api/v1/generate_qr POST /api/v1/scan_qr\n", addr)
|
||||
log.Printf("plaintext mock on tcp4 %s — see tools/local_gateway/README.md for paths\n", addr)
|
||||
ln, err := net.Listen("tcp4", addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Smoke-test routes used by AmneziaVPN against a running local_gateway.
|
||||
# Prerequisite: in another terminal: cd tools/local_gateway && go run .
|
||||
# Usage: ./verify.sh [base_url] default: http://127.0.0.1:8080
|
||||
set -euo pipefail
|
||||
BASE="${1:-http://127.0.0.1:8080}"
|
||||
|
||||
echo "== GET / =="
|
||||
curl -sfS "$BASE/" | head -n 2
|
||||
|
||||
echo "== GET updater follow-up =="
|
||||
curl -sfS "$BASE/VERSION" | head -c 20
|
||||
echo
|
||||
curl -sfS -o /dev/null -w "CHANGELOG %{http_code}\n" "$BASE/CHANGELOG"
|
||||
curl -sfS -o /dev/null -w "RELEASE_DATE %{http_code}\n" "$BASE/RELEASE_DATE"
|
||||
|
||||
echo "== POST /v1/* (empty JSON) =="
|
||||
for path in account_info services config news renewal_link updater_endpoint revoke_config revoke_native_config; do
|
||||
code=$(curl -sS -o /dev/null -w "%{http_code}" -X POST "$BASE/v1/$path" \
|
||||
-H "Content-Type: application/json" -d '{}')
|
||||
echo "POST /v1/$path -> HTTP $code"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
echo "expected 200"; exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "== POST pairing bad payload -> 400 =="
|
||||
code=$(curl -sS -o /dev/null -w "%{http_code}" -X POST "$BASE/api/v1/generate_qr" \
|
||||
-H "Content-Type: application/json" -d '{}')
|
||||
echo "POST /api/v1/generate_qr (invalid) -> HTTP $code"
|
||||
[[ "$code" == "400" ]]
|
||||
|
||||
echo "OK"
|
||||
Reference in New Issue
Block a user