mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
fix: keep client names and dates when Amnezia app rewrites clientsTable
Приложение AmneziaVPN при переподключении пересобирает clientsTable из awg0.conf и оставляет только clientId с именами «Client N» — имена, даты создания и last_config пропадают. Панель теперь держит снимок этих полей в DATA_DIR и восстанавливает их при следующем чтении состояния, попутно чиня файл в контейнере. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
75
scripts/clients-meta.mjs
Normal file
75
scripts/clients-meta.mjs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Страховка от затирания clientsTable приложением AmneziaVPN.
|
||||||
|
*
|
||||||
|
* Приложение при переподключении/перезагрузке сервера пересобирает clientsTable
|
||||||
|
* из awg0.conf: остаются только clientId и дефолтные имена «Client N», а имена,
|
||||||
|
* даты создания и last_config, которые ведёт панель, пропадают.
|
||||||
|
*
|
||||||
|
* Панель держит свою копию этих полей в DATA_DIR (том /data переживает
|
||||||
|
* пересоздание контейнера) и возвращает их на место при следующем чтении.
|
||||||
|
*/
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/** Поля userData, которые ведёт панель и которые нельзя терять. */
|
||||||
|
export const META_FIELDS = ["clientName", "creationDate", "last_config", "allowedIps"];
|
||||||
|
|
||||||
|
/** «Client 17» — дефолтное имя от приложения, оно не ценнее сохранённого. */
|
||||||
|
export function isPlaceholderName(name) {
|
||||||
|
return /^Client\s*\d+$/i.test(String(name || "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMeaningful(field, value) {
|
||||||
|
if (value == null || value === "") return false;
|
||||||
|
return field === "clientName" ? !isPlaceholderName(value) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaFile(dataDir, profileId) {
|
||||||
|
const safe = String(profileId || "default").replace(/[^\w.-]/g, "_");
|
||||||
|
return path.join(dataDir, "clients-meta", `${safe}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readClientsMeta(dataDir, profileId) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(metaFile(dataDir, profileId), "utf8"));
|
||||||
|
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeClientsMeta(dataDir, profileId, meta) {
|
||||||
|
const file = metaFile(dataDir, profileId);
|
||||||
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||||
|
fs.writeFileSync(file, `${JSON.stringify(meta, null, 2)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает клиентов с восстановленными полями и число восстановленных значений.
|
||||||
|
* Заодно обновляет снимок метаданных теми значениями, что сейчас на сервере;
|
||||||
|
* metaChanged=false — снимок писать не нужно (панель читает состояние часто).
|
||||||
|
*/
|
||||||
|
export function healClientsMeta(clients, meta) {
|
||||||
|
let restored = 0;
|
||||||
|
const before = JSON.stringify(meta);
|
||||||
|
const nextMeta = { ...meta };
|
||||||
|
const healed = clients.map((row) => {
|
||||||
|
const id = row?.clientId;
|
||||||
|
if (!id) return row;
|
||||||
|
const userData = { ...(row.userData || {}) };
|
||||||
|
const saved = nextMeta[id] || {};
|
||||||
|
for (const field of META_FIELDS) {
|
||||||
|
if (!isMeaningful(field, userData[field]) && isMeaningful(field, saved[field])) {
|
||||||
|
userData[field] = saved[field];
|
||||||
|
restored++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const keep = {};
|
||||||
|
for (const field of META_FIELDS) {
|
||||||
|
if (isMeaningful(field, userData[field])) keep[field] = userData[field];
|
||||||
|
}
|
||||||
|
if (Object.keys(keep).length) nextMeta[id] = keep;
|
||||||
|
return { ...row, userData };
|
||||||
|
});
|
||||||
|
return { clients: healed, restored, meta: nextMeta, metaChanged: JSON.stringify(nextMeta) !== before };
|
||||||
|
}
|
||||||
43
scripts/test-clients-meta.mjs
Normal file
43
scripts/test-clients-meta.mjs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// node scripts/test-clients-meta.mjs — самопроверка страховки clientsTable
|
||||||
|
import assert from "assert";
|
||||||
|
import { healClientsMeta, isPlaceholderName } from "./clients-meta.mjs";
|
||||||
|
|
||||||
|
const good = [
|
||||||
|
{ clientId: "A=", userData: { clientName: "Mama", creationDate: "Sat Jun 27 2026", last_config: "{...}" } },
|
||||||
|
{ clientId: "B=", userData: { clientName: "teplici", creationDate: "Sat Jul 04 2026" } },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1. Снимок берёт живые значения.
|
||||||
|
const snap = healClientsMeta(good, {});
|
||||||
|
assert.equal(snap.restored, 0);
|
||||||
|
assert.equal(snap.meta["A="].clientName, "Mama");
|
||||||
|
assert.equal(snap.metaChanged, true);
|
||||||
|
|
||||||
|
// 2. Приложение затёрло таблицу — имена и даты возвращаются, новый клиент не трогается.
|
||||||
|
const wiped = [
|
||||||
|
{ clientId: "A=", userData: { clientName: "Client 0" } },
|
||||||
|
{ clientId: "B=", userData: { clientName: "Client 1" } },
|
||||||
|
{ clientId: "C=", userData: { clientName: "Client 2" } },
|
||||||
|
];
|
||||||
|
const fixed = healClientsMeta(wiped, snap.meta);
|
||||||
|
assert.equal(fixed.clients[0].userData.clientName, "Mama");
|
||||||
|
assert.equal(fixed.clients[0].userData.last_config, "{...}");
|
||||||
|
assert.equal(fixed.clients[1].userData.creationDate, "Sat Jul 04 2026");
|
||||||
|
assert.equal(fixed.clients[2].userData.clientName, "Client 2");
|
||||||
|
assert.equal(fixed.restored, 5);
|
||||||
|
|
||||||
|
// 3. Повторный прогон уже целой таблицы ничего не чинит и не переписывает снимок.
|
||||||
|
const again = healClientsMeta(fixed.clients, fixed.meta);
|
||||||
|
assert.equal(again.restored, 0);
|
||||||
|
assert.equal(again.metaChanged, false);
|
||||||
|
|
||||||
|
// 4. Переименование в панели побеждает старый снимок.
|
||||||
|
const renamed = healClientsMeta(
|
||||||
|
[{ clientId: "A=", userData: { clientName: "Мама новая" } }],
|
||||||
|
snap.meta
|
||||||
|
);
|
||||||
|
assert.equal(renamed.clients[0].userData.clientName, "Мама новая");
|
||||||
|
assert.equal(renamed.meta["A="].clientName, "Мама новая");
|
||||||
|
|
||||||
|
assert.ok(isPlaceholderName("Client 17") && !isPlaceholderName("Vorot2"));
|
||||||
|
console.log("clients-meta: ok");
|
||||||
21
server.js
21
server.js
@@ -4,6 +4,7 @@ import crypto from "crypto";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
import { healClientsMeta, readClientsMeta, writeClientsMeta } from "./scripts/clients-meta.mjs";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -1007,7 +1008,25 @@ function createRuntime(profile) {
|
|||||||
dockerReadFile(activeClientsPath),
|
dockerReadFile(activeClientsPath),
|
||||||
]);
|
]);
|
||||||
const conf = splitAwgConf(confText);
|
const conf = splitAwgConf(confText);
|
||||||
const clients = parseClientsTable(tableText);
|
const parsed = parseClientsTable(tableText);
|
||||||
|
// ponytail: приложение AmneziaVPN пересобирает clientsTable из awg0.conf и стирает
|
||||||
|
// имена/даты/last_config — возвращаем их из снимка в /data и чиним файл на сервере.
|
||||||
|
const { clients, restored, meta, metaChanged } = healClientsMeta(
|
||||||
|
parsed,
|
||||||
|
readClientsMeta(DATA_DIR, profile.id)
|
||||||
|
);
|
||||||
|
if (metaChanged) writeClientsMeta(DATA_DIR, profile.id, meta);
|
||||||
|
if (restored) {
|
||||||
|
console.log(
|
||||||
|
`→ clientsTable: восстановлено ${restored} потерянных полей клиентов (${profile.label || profile.id})`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await backupRemoteFiles();
|
||||||
|
await dockerWriteFile(activeClientsPath, stringifyClientsTable(clients));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`clientsTable: восстановить файл на сервере не удалось: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
const peerByKey = new Map(conf.peers.map((p) => [p.publicKey, p]));
|
const peerByKey = new Map(conf.peers.map((p) => [p.publicKey, p]));
|
||||||
return { confText, conf, clients, peerByKey, confPath: activeConfPath, clientsPath: activeClientsPath };
|
return { confText, conf, clients, peerByKey, confPath: activeConfPath, clientsPath: activeClientsPath };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user