From 752256f12e646ab3929b3bf3986851a18b5275dc Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 13 Sep 2026 00:10:10 +0700 Subject: [PATCH] =?UTF-8?q?fix(ipsec):=20=D1=82=D1=80=D0=B5=D0=B1=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=20.p12=20=D0=BD=D0=B5=20=D0=BA=D0=BE=D1=80=D0=BE=D1=87=D0=B5?= =?UTF-8?q?=208=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- backend/src/routes/ipsec.ts | 23 ++++++++++++++++++++--- components/ipsec/ipsec-cert-sheet.tsx | 20 ++++++++++++++++---- components/ipsec/ipsec-user-sheet.tsx | 14 ++++++++++++-- packages/contracts/src/ipsec.ts | 14 ++++++++++---- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/backend/src/routes/ipsec.ts b/backend/src/routes/ipsec.ts index 0520cda..efeb0a9 100644 --- a/backend/src/routes/ipsec.ts +++ b/backend/src/routes/ipsec.ts @@ -1,6 +1,7 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" import type { FastifyReply } from "fastify" import { + IPSEC_MIN_PASSPHRASE, ipsecCertDeleteRequestSchema, ipsecCertExportByNameRequestSchema, ipsecCertExportRequestSchema, @@ -97,6 +98,22 @@ function errReply(reply: FastifyReply, e: unknown) { return reply.status(502).send({ error: `RouterOS: ${msg}` }) } +/** + * 400 по невалидному телу. Для короткого пароля .p12 — понятный текст вместо generic-сообщения: + * RouterOS отклоняет `export-passphrase` короче 8 символов. + */ +function badBodyReply(reply: FastifyReply, error: z.ZodError) { + const shortPassphrase = error.issues.some( + (issue) => issue.path[0] === "passphrase" && issue.code === "too_small", + ) + if (shortPassphrase) { + return reply.status(400).send({ + error: `Пароль архива .p12 должен быть не короче ${IPSEC_MIN_PASSPHRASE} символов (требование RouterOS)`, + }) + } + return reply.status(400).send({ error: "Некорректное тело запроса", details: error.flatten() }) +} + async function recordIpsec( server: NonNullable>>, source: ConfigRevisionSource, @@ -243,7 +260,7 @@ const ipsecRoutes: FastifyPluginAsyncZod = async (app) => { app.post("/ipsec/users", async (req, reply) => { const parsed = ipsecUserCreateRequestSchema.safeParse(req.body ?? {}) if (!parsed.success) { - return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) + return badBodyReply(reply, parsed.error) } const body = parsed.data const server = await getEnabledIpsecServerById(body.serverId) @@ -608,7 +625,7 @@ const ipsecRoutes: FastifyPluginAsyncZod = async (app) => { const { serverId, rosId } = req.params as { serverId: string; rosId: string } const parsed = ipsecCertExportRequestSchema.safeParse({ ...(req.body as object), serverId, clientId: rosId }) if (!parsed.success) { - return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) + return badBodyReply(reply, parsed.error) } const body = parsed.data const server = await getEnabledIpsecServerById(serverIdParam(serverId)) @@ -667,7 +684,7 @@ const ipsecRoutes: FastifyPluginAsyncZod = async (app) => { const { serverId } = req.params as { serverId: string } const parsed = ipsecCertExportByNameRequestSchema.safeParse(req.body ?? {}) if (!parsed.success) { - return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) + return badBodyReply(reply, parsed.error) } const { name, passphrase } = parsed.data const server = await getEnabledIpsecServerById(serverIdParam(serverId)) diff --git a/components/ipsec/ipsec-cert-sheet.tsx b/components/ipsec/ipsec-cert-sheet.tsx index 449a05b..4d601f9 100644 --- a/components/ipsec/ipsec-cert-sheet.tsx +++ b/components/ipsec/ipsec-cert-sheet.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useMemo, useState } from "react" -import type { IpsecCertBundle } from "@mmapp/contracts/ipsec" +import { IPSEC_MIN_PASSPHRASE, type IpsecCertBundle } from "@mmapp/contracts/ipsec" import { FormField, SectionTitle } from "@/components/form-kit" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -33,7 +33,8 @@ function downloadB64(filename: string, b64: string, mime: string) { } function randomPassphrase(): string { - const bytes = new Uint8Array(9) + // RouterOS требует ≥ IPSEC_MIN_PASSPHRASE символов + const bytes = new Uint8Array(IPSEC_MIN_PASSPHRASE + 1) crypto.getRandomValues(bytes) let s = "" for (const b of bytes) s += "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"[b % 56] @@ -62,7 +63,10 @@ function IpsecCertSheet({ queueMicrotask(() => setPassphrase(initial)) }, [open, bundle]) - const canDownload = useMemo(() => Boolean(bundle && passphrase.trim().length >= 4), [bundle, passphrase]) + const canDownload = useMemo( + () => Boolean(bundle && passphrase.trim().length >= IPSEC_MIN_PASSPHRASE), + [bundle, passphrase], + ) return ( @@ -83,7 +87,15 @@ function IpsecCertSheet({ <>
Пароль архива .p12 - + 0 && passphrase.trim().length < IPSEC_MIN_PASSPHRASE + ? `Минимум ${IPSEC_MIN_PASSPHRASE} символов — требование RouterOS` + : `Нужна при импорте .p12 на устройстве (минимум ${IPSEC_MIN_PASSPHRASE} символов)` + } + >
0 && passphrase.length < IPSEC_MIN_PASSPHRASE) return false return true }, [form, editing]) @@ -201,7 +204,14 @@ function IpsecUserSheet({ ) : ( !editing ? ( - + 0 && form.passphrase.trim().length < IPSEC_MIN_PASSPHRASE + ? `Минимум ${IPSEC_MIN_PASSPHRASE} символов — требование RouterOS` + : `Пусто — сгенерируем автоматически (минимум ${IPSEC_MIN_PASSPHRASE} символов)` + } + >