fix(backend): улучшить обработку сертификатов и добавить поддержку PUT запросов

This commit is contained in:
Denozordec committed 2026-05-12 20:29:06 +07:00
1 parent 53ea0e74a3
commit 60024facab
2 files changed
+132 -15

No files matched your search

+115 -2
View File
@@ -156,6 +156,62 @@ function rosPost(
})
}
function rosPut(
params: MikrotikConnectParams,
path: string,
body: Record<string, string>,
timeoutMs: number,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const basePath = params.apiPath ?? "/rest"
const authHeader = "Basic " + Buffer.from(`${params.username}:${params.password}`).toString("base64")
const payload = JSON.stringify(body)
const options: https.RequestOptions = {
hostname: params.host,
port: params.port,
path: basePath + path,
method: "PUT",
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
},
rejectUnauthorized: params.useSsl ? params.verifySsl : undefined,
}
const lib = params.useSsl ? https : http
const timer = setTimeout(() => {
req.destroy(new Error(`Connection to ${params.host}:${params.port} timed out after ${timeoutMs / 1000}s`))
}, timeoutMs)
const req = lib.request(options, (res) => {
let buf = ""
res.setEncoding("utf8")
res.on("data", (chunk: string) => { buf += chunk })
res.on("end", () => {
clearTimeout(timer)
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
reject(new MikrotikError(res.statusCode ?? 0, path, buf))
return
}
try {
resolve(buf.trim() ? JSON.parse(buf) : {})
} catch {
reject(new Error(`Invalid JSON from RouterOS: ${buf.slice(0, 200)}`))
}
})
})
req.on("error", (err) => {
clearTimeout(timer)
reject(err)
})
req.write(payload)
req.end()
})
}
function rosDelete(
params: MikrotikConnectParams,
path: string,
@@ -270,6 +326,18 @@ function unwrapRestSingle<T extends object>(raw: unknown): T {
throw new Error("RouterOS REST: неожиданное тело ответа")
}
function routerFileBasename(fileName: string): string {
return fileName.replace(/^\/+/, "").split("/").pop() ?? fileName
}
function matchesUploadedFile(entryName: string, requested: string): boolean {
const base = routerFileBasename(requested)
return entryName === requested
|| entryName === base
|| entryName === `flash/${base}`
|| entryName.endsWith(`/${base}`)
}
// ── MikrotikClient ─────────────────────────────────────────────────────────────
export class MikrotikClient {
@@ -295,6 +363,10 @@ export class MikrotikClient {
return rosPost(this.params, path, body, timeoutMs, signal) as Promise<T>
}
async put<T>(path: string, body: Record<string, string>, timeoutMs = 15_000): Promise<T> {
return rosPut(this.params, path, body, timeoutMs) as Promise<T>
}
async delete(path: string, timeoutMs = 10_000): Promise<void> {
return rosDelete(this.params, path, timeoutMs)
}
@@ -437,8 +509,49 @@ export class MikrotikClient {
return raw.filter((row): row is Record<string, string | undefined> => row != null && typeof row === "object")
}
async uploadTextFile(fileName: string, contents: string, timeoutMs = 30_000): Promise<void> {
await this.post("/file", { name: fileName, contents }, timeoutMs)
async listFiles(): Promise<Array<{ name: string }>> {
const raw = await this.get<unknown>("/file")
if (!Array.isArray(raw)) return []
return raw
.filter((row): row is Record<string, unknown> => row != null && typeof row === "object")
.map((row) => ({ name: String(row.name ?? "") }))
.filter((row) => row.name.length > 0)
}
private async resolveUploadedFileName(requested: string): Promise<string> {
const files = await this.listFiles()
const match = files.find((file) => matchesUploadedFile(file.name, requested))
if (!match) {
throw new Error(`Файл ${routerFileBasename(requested)} не найден на RouterOS после загрузки`)
}
return match.name
}
async uploadTextFile(fileName: string, contents: string, timeoutMs = 30_000): Promise<string> {
const normalized = routerFileBasename(fileName)
const payload = contents.endsWith("\n") ? contents : `${contents}\n`
const candidates = [`flash/${normalized}`, normalized]
let lastError: unknown
for (const name of candidates) {
try {
await this.put("/file", { name, contents: payload }, timeoutMs)
return await this.resolveUploadedFileName(name)
} catch (error) {
lastError = error
}
try {
await this.post("/file/add", { name, contents: payload, type: "file" }, timeoutMs)
return await this.resolveUploadedFileName(name)
} catch (error) {
lastError = error
}
}
throw lastError instanceof Error
? lastError
: new Error(`Не удалось загрузить файл ${normalized} на RouterOS`)
}
async importCertificate(params: {