fix(api): уточнить причины отказа входа на маршрутизатор
This commit is contained in:
1 parent
552d5f831f
commit
25ae76dc3f
5 files changed
+128
-6
No files matched your search
@@ -12,7 +12,7 @@ import {
|
||||
type TestConnectionRequest,
|
||||
} from "../types/server.js"
|
||||
import { pollServer } from "../services/poller.js"
|
||||
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
||||
import { MikrotikClient, MikrotikError, routerOsAccessHint } from "../services/mikrotik.js"
|
||||
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
|
||||
import {
|
||||
createServer,
|
||||
@@ -56,9 +56,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
let message = "Не удалось подключиться"
|
||||
|
||||
if (err instanceof MikrotikError) {
|
||||
if (err.statusCode === 401) message = "Неверный логин или пароль (401 Unauthorized)"
|
||||
else if (err.statusCode === 403) message = "Нет прав доступа к REST API (403 Forbidden)"
|
||||
else message = `Ошибка RouterOS API: ${err.statusCode}`
|
||||
message = routerOsAccessHint(err.statusCode, err.body) ?? `Ошибка RouterOS API: ${err.statusCode}`
|
||||
} else if (err instanceof Error) {
|
||||
const msg = err.message.toLowerCase()
|
||||
if (msg.includes("timeout") || msg.includes("abort"))
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
import { test } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { createServer } from "node:http"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { MikrotikClient, MikrotikError, routerOsAccessHint } from "./mikrotik.js"
|
||||
|
||||
test("RouterOS access diagnostics distinguish login saturation without assuming a wrong password or retrying", async () => {
|
||||
let requests = 0
|
||||
const body = '{"detail":"too many concurrent logins (6)","error":500,"message":"Internal Server Error"}'
|
||||
const server = createServer((req, res) => { requests++; req.resume(); res.writeHead(500); res.end(body) })
|
||||
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
|
||||
const address = server.address(); assert(address && typeof address !== "string")
|
||||
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
|
||||
try {
|
||||
for (const operation of [() => client.get("/interface"), () => client.put("/own", {})]) {
|
||||
await assert.rejects(operation, (error: unknown) => {
|
||||
assert(error instanceof MikrotikError)
|
||||
assert.equal(error.statusCode, 500); assert.equal(error.body, body)
|
||||
assert.match(error.message, /превышен лимит одновременных входов/)
|
||||
return true
|
||||
})
|
||||
}
|
||||
assert.equal(requests, 2, "Neither reads nor writes may silently retry a refused login")
|
||||
assert.match(routerOsAccessHint(401, '{"message":"Unauthorized"}')!, /ограничения входа/)
|
||||
assert.doesNotMatch(routerOsAccessHint(401, "")!, /Неверный логин или пароль/)
|
||||
for (const unknown of ["not JSON", "null", '{"detail":6}', '{"detail":"disk failure"}']) {
|
||||
assert.equal(routerOsAccessHint(500, unknown), undefined)
|
||||
}
|
||||
assert.equal(routerOsAccessHint(400, body), undefined)
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
|
||||
}
|
||||
})
|
||||
|
||||
test("RouterOS print preserves native query arrays and property selection", async () => {
|
||||
let received: unknown, target = "", method = ""
|
||||
|
||||
@@ -1110,13 +1110,29 @@ export class MikrotikClient {
|
||||
|
||||
// ── Error type ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** HTTP 401 alone does not distinguish wrong credentials from a refused login. */
|
||||
export function routerOsAccessHint(statusCode: number, body: string): string | undefined {
|
||||
if (statusCode === 401) return "RouterOS отклонил авторизацию: проверьте учётные данные, права API/REST и ограничения входа"
|
||||
if (statusCode === 403) return "RouterOS запретил доступ к REST API"
|
||||
if (statusCode === 500) {
|
||||
try {
|
||||
const detail: unknown = JSON.parse(body)?.detail
|
||||
if (typeof detail === "string" && /^too many concurrent logins(?:\s*\(\d+\))?$/i.test(detail.trim())) {
|
||||
return "RouterOS занят: превышен лимит одновременных входов"
|
||||
}
|
||||
} catch { /* Keep an unrecognized response unchanged. */ }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export class MikrotikError extends Error {
|
||||
constructor(
|
||||
public readonly statusCode: number,
|
||||
public readonly path: string,
|
||||
public readonly body: string,
|
||||
) {
|
||||
super(`RouterOS API error ${statusCode} on ${path}: ${body}`)
|
||||
const hint = routerOsAccessHint(statusCode, body)
|
||||
super(`${hint ? `${hint}. ` : ""}RouterOS API error ${statusCode} on ${path}: ${body}`)
|
||||
this.name = "MikrotikError"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user