diff --git a/backend/src/services/certificate-parse.test.ts b/backend/src/services/certificate-parse.test.ts index 3c1cf8b..3599013 100644 --- a/backend/src/services/certificate-parse.test.ts +++ b/backend/src/services/certificate-parse.test.ts @@ -48,6 +48,9 @@ const anakondra: RosCertificateRow = { ...client1, name: "anakondra", "common-na // reference-based важнее key-usage: у client1 в key-usage есть tls-server, но он client assert.equal(certificateRole(client1, ctx), "client") assert.equal(certificateRole(anakondra, ctx), "client") + // без reference-контекста: подпись локальным CA делает серт клиентским, несмотря на tls-server в usage + assert.equal(certificateRole(client1), "client") + assert.equal(certificateRole(anakondra), "client") // без контекста — по key-usage (default содержит оба, поэтому server) assert.equal(certificateRole({ name: "x", "key-usage": "tls-client" }), "client") assert.equal(certificateRole({ name: "y", "key-usage": "digital-signature" }), "other") diff --git a/backend/src/services/certificate-parse.ts b/backend/src/services/certificate-parse.ts index f8aa83e..4f0e83a 100644 --- a/backend/src/services/certificate-parse.ts +++ b/backend/src/services/certificate-parse.ts @@ -50,6 +50,10 @@ export function certificateRole( if (isCaCertificate(row)) return "ca" if (name && new Set(ctx.peerCertNames ?? []).has(name)) return "server" if (name && new Set(ctx.identityCertNames ?? []).has(name)) return "client" + // Серт, подписанный локальным CA (`ca` заполнен), но не используемый как peer-серт — клиентский. + // `client1`/`anakondra` имеют дефолтный key-usage (tls-server+tls-client), поэтому по usage + // они выглядели бы «server». + if ((row.ca ?? "").trim() !== "") return "client" const usage = row["key-usage"] ?? "" if (usage.includes("tls-server")) return "server" if (usage.includes("tls-client")) return "client" diff --git a/backend/src/services/mikrotik.ts b/backend/src/services/mikrotik.ts index dc8a793..f43ef0b 100644 --- a/backend/src/services/mikrotik.ts +++ b/backend/src/services/mikrotik.ts @@ -415,6 +415,13 @@ export function encodeRosId(rosId: string): string { // ── MikrotikClient ───────────────────────────────────────────────────────────── +/** Имя параметра из ошибки RouterOS `unknown parameter ` (400) — для деградации запроса. */ +function unknownParameterName(error: MikrotikError): string | undefined { + if (error.statusCode !== 400) return undefined + const match = error.body.match(/unknown parameter\s+"?([A-Za-z0-9_-]+)"?/i) + return match?.[1] +} + export class MikrotikClient { constructor(private readonly params: MikrotikConnectParams) {} @@ -691,7 +698,19 @@ export class MikrotikClient { /** Создание ключевой пары + заявки: /certificate add (поля common-name, key-size, key-usage…). */ async addCertificate(body: Record, timeoutMs = 30_000): Promise { - return this.post("/certificate/add", body, timeoutMs) + // Набор параметров `/certificate/add` зависит от версии RouterOS (напр. `comment`). + // Деградируем: при 400 «unknown parameter X» убираем X из тела и повторяем. + const payload: Record = { ...body } + for (let attempt = 0; attempt < 4; attempt += 1) { + try { + return await this.post("/certificate/add", payload, timeoutMs) + } catch (error) { + const param = error instanceof MikrotikError ? unknownParameterName(error) : undefined + if (!param || !(param in payload)) throw error + delete payload[param] + } + } + throw new Error(`RouterOS: не удалось добавить сертификат (несовместимые параметры): ${Object.keys(body).join(", ")}`) } /** Подпись сертификата локальным CA; sign небыстрый — увеличенный таймаут. */