From 4e6ab2dd6ab10bea248ae0086f6bf0c7388bc0ea Mon Sep 17 00:00:00 2001 From: andrey271192 <76248502+andrey271192@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:29:08 +0300 Subject: [PATCH] fix: parse legacy clients table formats --- docs/panel-guide.md | 2 + package-lock.json | 4 +- package.json | 2 +- server.js | 111 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/docs/panel-guide.md b/docs/panel-guide.md index eef0316..bd8e5ac 100644 --- a/docs/panel-guide.md +++ b/docs/panel-guide.md @@ -40,6 +40,8 @@ Начиная с **v1.2.36** в блоке **«Пользователи»** есть кнопка **«Подтянуть из awg0.conf»**. Она создаёт недостающие строки `clientsTable` для уже активных `[Peer]`, которые были созданы в приложении Amnezia. VPN-конфиг и контейнер не пересоздаются. Экспорт `.conf` для таких пользователей появится только после импорта их клиентского `.conf`, потому приватный ключ клиента хранится в приложении, а не на сервере. +Начиная с **v1.2.37** парсер `clientsTable` поддерживает не только массив, но и object/map/nested-форматы разных установок Amnezia. Для диагностики источников есть `GET /api/clients/source-report?profileId=...`: показывает текущий контейнер, пути, количество `[Peer]`, количество строк `clientsTable`, missing/table-only счётчики и короткие префиксы ключей без публикации полных ключей. + ## Лендинг не поднимается (порт 80 занят) При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит. diff --git a/package-lock.json b/package-lock.json index b17f03c..5130bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "amnezia-admin", - "version": "1.2.36", + "version": "1.2.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "amnezia-admin", - "version": "1.2.36", + "version": "1.2.37", "license": "MIT", "dependencies": { "express": "^4.21.2" diff --git a/package.json b/package.json index c41145a..31a9d13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.2.36", + "version": "1.2.37", "private": false, "description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)", "license": "MIT", diff --git a/server.js b/server.js index f1a12fe..54f285f 100644 --- a/server.js +++ b/server.js @@ -955,16 +955,73 @@ function serializeAwgConf(head, peers) { function parseClientsTable(raw) { const data = JSON.parse(raw); - if (!Array.isArray(data)) throw new Error("clientsTable is not an array"); - return data; + return normalizeClientsTableRows(data); } function stringifyClientsTable(rows) { return `${JSON.stringify(rows, null, 4)}\n`; } +function looksLikePublicKey(s) { + return /^[A-Za-z0-9+/=_-]{32,80}$/.test(String(s || "").trim()); +} + +function normalizeClientTableRow(row, key = "") { + if (!row || typeof row !== "object" || Array.isArray(row)) return null; + const id = clientRowId(row) || (looksLikePublicKey(key) ? String(key).trim() : ""); + if (!id) return { ...row }; + const ud = clientRowUserData(row); + const userData = { ...ud }; + for (const [from, to] of [ + ["name", "clientName"], + ["clientName", "clientName"], + ["allowedIps", "allowedIps"], + ["allowedIPs", "allowedIps"], + ["last_config", "last_config"], + ["lastConfig", "last_config"], + ["creationDate", "creationDate"], + ]) { + if (row[from] != null && userData[to] == null) userData[to] = row[from]; + } + return { ...row, clientId: id, userData }; +} + +function normalizeClientsTableRows(data) { + if (Array.isArray(data)) { + return data.map((row) => normalizeClientTableRow(row)).filter(Boolean); + } + if (!data || typeof data !== "object") { + throw new Error("clientsTable JSON is not an object/array"); + } + for (const key of ["clients", "users", "rows", "items", "data", "clientList", "clientsTable"]) { + if (Array.isArray(data[key])) { + return data[key].map((row) => normalizeClientTableRow(row)).filter(Boolean); + } + } + const out = []; + for (const [key, value] of Object.entries(data)) { + if (value && typeof value === "object" && !Array.isArray(value)) { + const row = normalizeClientTableRow(value, key); + if (row) out.push(row); + } + } + if (out.length) return out; + throw new Error("clientsTable format not recognized"); +} + function clientRowId(row) { - return String(row?.clientId ?? row?.id ?? row?.publicKey ?? row?.client_id ?? "").trim(); + return String( + row?.clientId ?? + row?.id ?? + row?.publicKey ?? + row?.public_key ?? + row?.key ?? + row?.client_id ?? + row?.userData?.clientId ?? + row?.userData?.publicKey ?? + row?.userData?.public_key ?? + "", + ).trim(); } function clientRowUserData(row) { @@ -2392,6 +2449,54 @@ app.post("/api/clients/sync-peers", requireAuth, requireProTier, async (req, res } }); +app.get("/api/clients/source-report", requireAuth, async (req, res) => { + const rt = runtimeFromExportRequest(req); + try { + const container = await rt.resolveContainer(); + let confText = ""; + let tableText = ""; + let conf = { peers: [] }; + let clients = []; + let clientsError = null; + try { + confText = await rt.dockerReadFile(rt.confPath); + conf = splitAwgConf(confText); + } catch (e) { + return res.status(500).json({ error: `Не удалось прочитать ${rt.confPath}: ${String(e.message || e)}` }); + } + try { + tableText = await rt.dockerReadFile(rt.clientsPath); + clients = parseClientsTable(tableText); + } catch (e) { + clientsError = String(e.message || e); + } + const clientIds = new Set(clients.map(clientRowId).filter(Boolean)); + const peerIds = new Set(conf.peers.map((p) => p.publicKey).filter(Boolean)); + const missingInTable = [...peerIds].filter((id) => !clientIds.has(id)); + const tableOnly = [...clientIds].filter((id) => !peerIds.has(id)); + res.json({ + profileId: rt.profile.id, + profileLabel: rt.profile.label, + container, + confPath: rt.confPath, + clientsPath: rt.clientsPath, + confBytes: Buffer.byteLength(confText, "utf8"), + clientsTableBytes: Buffer.byteLength(tableText, "utf8"), + peerCount: conf.peers.length, + clientsTableCount: clients.length, + clientsTableParseError: clientsError, + missingInTableCount: missingInTable.length, + tableOnlyCount: tableOnly.length, + peerIdsShort: [...peerIds].map((id) => `${id.slice(0, 10)}…`), + clientIdsShort: [...clientIds].map((id) => `${id.slice(0, 10)}…`), + missingInTableShort: missingInTable.map((id) => `${id.slice(0, 10)}…`), + }); + } catch (e) { + console.error(e); + res.status(500).json({ error: String(e.message || e) }); + } +}); + async function serveClientConfigExport(req, res) { if (IS_COMMUNITY) { res.status(403).json({