fixed open Qr QML & add check error code & add test

This commit is contained in:
dranik
2026-05-07 19:15:28 +03:00
parent 2cb7b30d8a
commit 5583c0a2a9
20 changed files with 884 additions and 76 deletions
+28 -9
View File
@@ -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
View File
@@ -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)
+33
View File
@@ -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"