diff --git a/backend/src/services/mikrotik.ts b/backend/src/services/mikrotik.ts index f43ef0b..357bfb3 100644 --- a/backend/src/services/mikrotik.ts +++ b/backend/src/services/mikrotik.ts @@ -696,21 +696,32 @@ export class MikrotikClient { : new Error(`Не удалось скачать файл ${normalized} с RouterOS`) } - /** Создание ключевой пары + заявки: /certificate add (поля common-name, key-size, key-usage…). */ - async addCertificate(body: Record, timeoutMs = 30_000): Promise { - // Набор параметров `/certificate/add` зависит от версии RouterOS (напр. `comment`). - // Деградируем: при 400 «unknown parameter X» убираем X из тела и повторяем. + /** + * POST с деградацией по несовместимым параметрам: набор полей `/certificate/*` зависит от версии + * RouterOS (напр. `comment`, `days-valid`). При 400 «unknown parameter X» убираем X и повторяем. + */ + private async postTolerant( + path: string, + body: Record, + timeoutMs: number, + maxAttempts = 4, + ): Promise { const payload: Record = { ...body } - for (let attempt = 0; attempt < 4; attempt += 1) { + for (let attempt = 0; attempt < maxAttempts; attempt += 1) { try { - return await this.post("/certificate/add", payload, timeoutMs) + return await this.post(path, 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(", ")}`) + throw new Error(`RouterOS: не удалось выполнить ${path} (несовместимые параметры): ${Object.keys(body).join(", ")}`) + } + + /** Создание ключевой пары + заявки: /certificate add (поля common-name, key-size, key-usage…). */ + async addCertificate(body: Record, timeoutMs = 30_000): Promise { + return this.postTolerant("/certificate/add", body, timeoutMs) } /** Подпись сертификата локальным CA; sign небыстрый — увеличенный таймаут. */ @@ -723,14 +734,14 @@ export class MikrotikClient { if (params.ca) body.ca = params.ca if (params.daysValid != null) body["days-valid"] = String(params.daysValid) try { - return await this.post("/certificate/sign", body, timeoutMs) + return await this.postTolerant("/certificate/sign", body, timeoutMs) } catch (e) { // Некоторые версии REST принимают цель подписи только как .id. const certs = await this.getCertificates() const row = certs.find((c) => String(c.name ?? "") === params.name) const id = row?.[".id"] if (!id) throw e - return await this.post("/certificate/sign", { ".id": id, ...body }, timeoutMs) + return await this.postTolerant("/certificate/sign", { ".id": id, ...body }, timeoutMs) } } @@ -744,12 +755,12 @@ export class MikrotikClient { if (params.passphrase?.trim()) body["export-passphrase"] = params.passphrase.trim() let raw: unknown try { - raw = await this.post("/certificate/export-certificate", body, timeoutMs) + raw = await this.postTolerant("/certificate/export-certificate", body, timeoutMs) } catch (e) { const certs = await this.getCertificates() const id = certs.find((c) => String(c.name ?? "") === params.name)?.[".id"] if (!id) throw e - raw = await this.post("/certificate/export-certificate", { ".id": id, ...body }, timeoutMs) + raw = await this.postTolerant("/certificate/export-certificate", { ".id": id, ...body }, timeoutMs) } void raw // RouterOS создаёт cert_export_.p12 либо .p12 — ищем по списку файлов.