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.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 занят)
|
## Лендинг не поднимается (порт 80 занят)
|
||||||
|
|
||||||
При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит.
|
При ошибке 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",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.36",
|
"version": "1.2.37",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.36",
|
"version": "1.2.37",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.2"
|
"express": "^4.21.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.2.36",
|
"version": "1.2.37",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
"description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
111
server.js
111
server.js
@@ -955,16 +955,73 @@ function serializeAwgConf(head, peers) {
|
|||||||
|
|
||||||
function parseClientsTable(raw) {
|
function parseClientsTable(raw) {
|
||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
if (!Array.isArray(data)) throw new Error("clientsTable is not an array");
|
return normalizeClientsTableRows(data);
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyClientsTable(rows) {
|
function stringifyClientsTable(rows) {
|
||||||
return `${JSON.stringify(rows, null, 4)}\n`;
|
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) {
|
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) {
|
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) {
|
async function serveClientConfigExport(req, res) {
|
||||||
if (IS_COMMUNITY) {
|
if (IS_COMMUNITY) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
|
|||||||
Reference in New Issue
Block a user