mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
fix: parse legacy clients table formats
This commit is contained in:
@@ -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) от этого не зависит.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
111
server.js
111
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({
|
||||
|
||||
Reference in New Issue
Block a user