fix(api): проверять параметры запуска теста скорости до операции
This commit is contained in:
1 parent
b0e114b56d
commit
b59944b573
7 files changed
+114
-62
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import assert from "node:assert/strict"
|
||||
import Fastify from "fastify"
|
||||
import { SignJWT } from "jose"
|
||||
import { speedTestCommandSchema, type SpeedTestCommand } from "@mmapp/contracts"
|
||||
import { createSpeedTestRoute } from "./speed-test.js"
|
||||
import { SpeedProbeEndpointsChanged } from "../services/speed-run-persistence.js"
|
||||
const commands: SpeedTestCommand[] = [], failures: Array<SpeedTestCommand & { message: string }> = []
|
||||
let lookups = 0, missing = false, problem: Error | null = null
|
||||
const result = (c: SpeedTestCommand) => ({ runId: c.runId ?? "ad-hoc", srcInterface: c.srcInterface || null, dstInterface: c.dstInterface || null, address: "192.0.2.2", srcAddress: "192.0.2.1", dstAddress: "192.0.2.2", srcInterfaceAddress: null, dstInterfaceAddress: null, protocol: c.protocol, direction: c.direction, durationSec: c.durationSec, txAvgMbps: null, rxAvgMbps: 0, txSeries: [], rxSeries: [0], raw: [{ status: "done testing" }], afterBtPing: { rttMs: null, lossPct: null, error: null } })
|
||||
const deps = {
|
||||
serverExists: async (_id: number) => { lookups++; return !missing },
|
||||
run: async (c: SpeedTestCommand) => { commands.push(c); if (problem) throw problem; return result(c) },
|
||||
failure: async (c: SpeedTestCommand & { message: string }) => { failures.push(c) },
|
||||
}
|
||||
const app = Fastify(); await app.register(createSpeedTestRoute(deps), { prefix: "/api" })
|
||||
const send = (body: unknown) => app.inject({ method: "POST", url: "/api/uptime/speed-test", headers: { "content-type": "application/json" }, payload: JSON.stringify(body) })
|
||||
const base = { srcServerId: 1, dstServerId: 2 }
|
||||
const bad = [null, [], {}, { ...base, srcServerId: "1junk" }, { ...base, srcServerId: "1.0" }, { ...base, srcServerId: "01" }, { ...base, srcServerId: true }, { ...base, srcServerId: 0 }, { ...base, srcServerId: -1 }, { ...base, srcServerId: 1.5 }, { ...base, srcServerId: 9007199254740992 }, { ...base, dstServerId: "1" }, { ...base, protocol: "TCP" }, { ...base, protocol: null }, { ...base, direction: "other" }, { ...base, durationSec: "10junk" }, { ...base, durationSec: "3.5" }, { ...base, durationSec: 2 }, { ...base, durationSec: 2147483648 }, { ...base, srcInterface: null }, { ...base, runId: 123 }, { ...base, probeId: false }, ...["runId", "probeId", "srcInterface", "dstInterface"].map(key => ({ ...base, [key]: "bad" + String.fromCharCode(0) }))]
|
||||
for (const body of bad) assert.equal((await send(body)).statusCode, 400, JSON.stringify(body))
|
||||
assert.equal(lookups, 0); assert.equal(commands.length, 0); assert.equal(failures.length, 0)
|
||||
assert.equal(speedTestCommandSchema.safeParse({ ...base, durationSec: Infinity }).success, false)
|
||||
const first = await send(base); assert.equal(first.statusCode, 200); assert.equal(first.json().result.txAvgMbps, null); assert.equal(first.json().result.rxAvgMbps, 0)
|
||||
assert.deepEqual(commands[0], { ...base, srcInterface: "", dstInterface: "", protocol: "tcp", direction: "both", durationSec: 10 })
|
||||
const explicit = await send({ srcServerId: "1", dstServerId: "2", runId: " run-2 ", probeId: " probe ", srcInterface: " ether1 ", protocol: "udp", direction: "receive", durationSec: "3", lastTxAvgMbps: 999 })
|
||||
assert.equal(explicit.statusCode, 200); assert.equal(commands[1].runId, "run-2"); assert.equal(commands[1].probeId, "probe"); assert.equal(commands[1].srcInterface, "ether1"); assert.equal(commands[1].protocol, "udp"); assert.equal(commands[1].direction, "receive"); assert.equal(commands[1].durationSec, 3); assert.equal("lastTxAvgMbps" in commands[1], false)
|
||||
assert.equal(speedTestCommandSchema.parse({ ...base, runId: " ", probeId: "" }).probeId, undefined)
|
||||
missing = true; assert.equal((await send(base)).statusCode, 404); assert.equal(commands.length, 2); assert.equal(failures.length, 0); missing = false
|
||||
for (const [error, code] of [[new SpeedProbeEndpointsChanged(), 409], [Error("На интерфейсе назначения нет активного IP"), 400], [Error("Server not found"), 400], [Error("authentication failed"), 502], [Error("socket hang up"), 502]] as const) {
|
||||
problem = error; const response = await send({ ...base, runId: "failed", direction: "transmit", durationSec: 20 }); assert.equal(response.statusCode, code); assert.equal(failures.at(-1)?.runId, "failed"); assert.equal(failures.at(-1)?.durationSec, 20); assert.equal(failures.at(-1)?.direction, "transmit")
|
||||
}
|
||||
await app.close(); console.log("Speed-test HTTP:26 invalid inputs before lookups/native/history; defaults/ad-hoc/trim/nullable/zero/404/409/400/502")
|
||||
process.env.AUTH_REQUIRED = "true"; process.env.AUTH_JWT_SECRET = "speed-command-test-secret"; process.env.AUTH_ISSUER = "https://auth.test.local"; process.env.NODE_ENV = "test"
|
||||
const { default: authPlugin, requireAuth } = await import("../plugins/auth.js")
|
||||
const secured = Fastify(); await secured.register(authPlugin); secured.addHook("preHandler", requireAuth)
|
||||
problem = null; const before = { lookups, runs: commands.length }
|
||||
await secured.register(createSpeedTestRoute(deps), { prefix: "/api" })
|
||||
const authorized = (token?: string) => secured.inject({ method: "POST", url: "/api/uptime/speed-test", payload: base, headers: token ? { authorization: "Bearer " + token } : {} })
|
||||
assert.equal((await authorized()).statusCode, 401)
|
||||
for (const permission of ["mm:uptime:read", "mm:servers:write", "mm:uptime:write"]) {
|
||||
const token = await new SignJWT({ sub: "fixture", apps: ["mm"], permissions: [permission] }).setProtectedHeader({ alg: "HS256" }).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("speed-command-test-secret"))
|
||||
assert.equal((await authorized(token)).statusCode, permission === "mm:uptime:write" ? 200 : 403)
|
||||
}
|
||||
assert.equal(lookups, before.lookups + 2); assert.equal(commands.length, before.runs + 1)
|
||||
await secured.close(); console.log("Speed-test RBAC:401/403 before dependencies, uptime writer accepted; no PostgreSQL or RouterOS")
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import { speedTestCommandSchema, type SpeedTestCommand } from "@mmapp/contracts"
|
||||
import type { SpeedTestRunResult } from "../services/uptime-speed-service.js"
|
||||
import { SpeedProbeEndpointsChanged } from "../services/speed-run-persistence.js"
|
||||
|
||||
export function createSpeedTestRoute(deps: {
|
||||
serverExists: (id: number) => Promise<boolean>
|
||||
run: (command: SpeedTestCommand) => Promise<SpeedTestRunResult>
|
||||
failure: (command: SpeedTestCommand & { message: string }) => Promise<void>
|
||||
}): FastifyPluginAsyncZod {
|
||||
return async app => {
|
||||
app.post("/uptime/speed-test", async (req, reply) => {
|
||||
const parsed = speedTestCommandSchema.safeParse(req.body)
|
||||
if (!parsed.success) return reply.code(400).send({ error: "Проверьте источник, назначение, протокол, направление и целую длительность теста от 3 секунд." })
|
||||
const command = parsed.data
|
||||
const exists = await Promise.all([deps.serverExists(command.srcServerId), deps.serverExists(command.dstServerId)])
|
||||
if (exists.some(value => !value)) return reply.code(404).send({ error: "Server not found" })
|
||||
try {
|
||||
return reply.send({ ok: true, result: await deps.run(command) })
|
||||
} catch (error) {
|
||||
const raw = error instanceof Error ? error.message : "Bandwidth test failed"
|
||||
await deps.failure({ ...command, message: raw })
|
||||
const message = /timed out|socket hang up|ECONNRESET/i.test(raw)
|
||||
? "BTTest timeout/socket hang up: проверь доступность destination, /tool bandwidth-server, firewall и корректность интерфейсов"
|
||||
: raw
|
||||
if (error instanceof SpeedProbeEndpointsChanged) return reply.code(409).send({ error: message })
|
||||
if (/активного IP|Server not found/i.test(raw)) return reply.code(400).send({ error: message })
|
||||
return reply.code(502).send({ error: message })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { emptySpeedProbeObservations } from "@mmapp/contracts"
|
||||
import { SpeedProbeEndpointsChanged } from "../services/speed-run-persistence.js"
|
||||
import { createSpeedTestRoute } from "./speed-test.js"
|
||||
import { createPingProbesWriteRoute } from "./ping-probes-write.js"
|
||||
import { createSpeedProbesWriteRoute } from "./speed-probes-write.js"
|
||||
import { replaceSpeedProbes } from "../services/speed-probe-replacement.js"
|
||||
@@ -259,64 +259,11 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/uptime/speed-test", async (req, reply) => {
|
||||
const body = req.body as {
|
||||
runId?: string
|
||||
probeId?: string
|
||||
srcServerId?: string | number
|
||||
dstServerId?: string | number
|
||||
srcInterface?: string
|
||||
dstInterface?: string
|
||||
protocol?: "tcp" | "udp"
|
||||
direction?: "transmit" | "receive" | "both"
|
||||
durationSec?: string | number
|
||||
}
|
||||
const srcId = Number.parseInt(String(body.srcServerId ?? ""), 10)
|
||||
const dstId = Number.parseInt(String(body.dstServerId ?? ""), 10)
|
||||
if (!Number.isFinite(srcId) || !Number.isFinite(dstId)) {
|
||||
return reply.status(400).send({ error: "Invalid src/dst server id" })
|
||||
}
|
||||
if (srcId === dstId) return reply.status(400).send({ error: "Source and destination must be different" })
|
||||
const src = (await db.select().from(servers).where(eq(servers.id, srcId)).limit(1))[0]
|
||||
const dst = (await db.select().from(servers).where(eq(servers.id, dstId)).limit(1))[0]
|
||||
if (!src || !dst) return reply.status(404).send({ error: "Server not found" })
|
||||
try {
|
||||
const result = await runBandwidthSpeedTest({
|
||||
runId: String(body.runId ?? "").trim() || undefined,
|
||||
probeId: String(body.probeId ?? "").trim() || undefined,
|
||||
srcServerId: srcId,
|
||||
dstServerId: dstId,
|
||||
srcInterface: String(body.srcInterface ?? "").trim() || undefined,
|
||||
dstInterface: String(body.dstInterface ?? "").trim() || undefined,
|
||||
protocol: body.protocol === "udp" ? "udp" : "tcp",
|
||||
direction: body.direction === "transmit" || body.direction === "receive" ? body.direction : "both",
|
||||
durationSec: Math.max(3, Number.parseInt(String(body.durationSec ?? ""), 10) || 10),
|
||||
})
|
||||
return reply.send({ ok: true, result })
|
||||
} catch (e) {
|
||||
const raw = e instanceof Error ? e.message : "Bandwidth test failed"
|
||||
await recordSpeedTestFailure({
|
||||
probeId: String(body.probeId ?? "").trim() || undefined,
|
||||
runId: String(body.runId ?? "").trim() || undefined,
|
||||
srcServerId: srcId,
|
||||
dstServerId: dstId,
|
||||
srcInterface: String(body.srcInterface ?? "").trim(),
|
||||
dstInterface: String(body.dstInterface ?? "").trim(),
|
||||
protocol: body.protocol === "udp" ? "udp" : "tcp",
|
||||
direction: body.direction === "transmit" || body.direction === "receive" ? body.direction : "both",
|
||||
durationSec: Math.max(3, Number.parseInt(String(body.durationSec ?? ""), 10) || 10),
|
||||
message: raw,
|
||||
})
|
||||
const message = /timed out|socket hang up|ECONNRESET/i.test(raw)
|
||||
? "BTTest timeout/socket hang up: проверь доступность destination, /tool bandwidth-server, firewall и корректность интерфейсов"
|
||||
: raw
|
||||
if (e instanceof SpeedProbeEndpointsChanged) return reply.status(409).send({ error: message })
|
||||
if (/активного IP|Server not found/i.test(raw)) {
|
||||
return reply.status(400).send({ error: message })
|
||||
}
|
||||
return reply.status(502).send({ error: message })
|
||||
}
|
||||
})
|
||||
await app.register(createSpeedTestRoute({
|
||||
serverExists: async id => Boolean((await db.select({ id: servers.id }).from(servers).where(eq(servers.id, id)).limit(1))[0]),
|
||||
run: runBandwidthSpeedTest,
|
||||
failure: recordSpeedTestFailure,
|
||||
}))
|
||||
|
||||
app.get("/uptime/speed-test/endpoints/:id/interfaces", async (req, reply) => {
|
||||
const params = req.params as { id?: string | number }
|
||||
|
||||
Reference in new issue
Block a user