Files
MikrotikManager/lib/api.ts
T
2026-05-02 01:17:08 +07:00

257 lines
7.7 KiB
TypeScript

/**
* RouterOS 7.x REST API client
*
* RouterOS 7.1+ exposes a REST API at https://<router>/rest/
* All responses are JSON; authentication is HTTP Basic or Bearer token.
*
* Usage (browser / server-component):
* const client = new RouterOsApiClient({ host: "10.0.0.1", port: 443, ... })
* const ifaces = await client.getInterfaces()
*/
export interface ApiClientConfig {
host: string // "10.0.0.1"
port: number // 443 (HTTPS) or 80 (HTTP)
tls: boolean // use HTTPS
username: string
password: string
tlsVerify: boolean // verify TLS certificate (false for self-signed)
timeout: number // ms, default 8000
}
// ─── RouterOS REST response shapes ───────────────────────────────────────────
export interface RosSystemIdentity {
name: string
}
export interface RosSystemResource {
uptime: string
version: string
"build-time": string
"free-memory": string
"total-memory": string
"cpu": string
"cpu-count": string
"cpu-load": string
"board-name": string
"platform": string
"architecture-name": string
"bad-blocks": string
}
export interface RosInterface {
".id": string
name: string
type: string
mtu: string
"actual-mtu": string
"mac-address": string
running: string
disabled: string
comment?: string
}
export interface RosBgpSession {
".id": string
name: string
"remote.address": string
"remote.as": string
state: string
"uptime": string
"prefix-count": string
disabled: string
}
export interface RosWireGuardInterface {
".id": string
name: string
"listen-port": string
mtu: string
"public-key": string
running: string
disabled: string
comment?: string
}
export interface RosWireGuardPeer {
".id": string
interface: string
"public-key": string
"endpoint-address"?: string
"endpoint-port"?: string
"allowed-address": string
"last-handshake"?: string
"rx": string
"tx": string
disabled: string
comment?: string
}
// ─── Error class ─────────────────────────────────────────────────────────────
export class RouterOsApiError extends Error {
constructor(
public readonly status: number,
public readonly detail: string,
message: string,
) {
super(message)
this.name = "RouterOsApiError"
}
}
// ─── Client class ─────────────────────────────────────────────────────────────
export class RouterOsApiClient {
private readonly baseUrl: string
private readonly headers: Record<string, string>
private readonly timeout: number
constructor(private readonly cfg: ApiClientConfig) {
const scheme = cfg.tls ? "https" : "http"
this.baseUrl = `${scheme}://${cfg.host}:${cfg.port}/rest`
const creds = btoa(`${cfg.username}:${cfg.password}`)
this.headers = {
"Authorization": `Basic ${creds}`,
"Content-Type": "application/json",
}
this.timeout = cfg.timeout ?? 8000
}
// ── low-level fetch ──────────────────────────────────────────────────────
private async request<T>(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
body?: unknown,
): Promise<T> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), this.timeout)
try {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: this.headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: controller.signal,
})
clearTimeout(timer)
if (!res.ok) {
let detail = ""
try { detail = (await res.json())?.message ?? "" } catch { /* ignore */ }
throw new RouterOsApiError(res.status, detail, `RouterOS API ${method} ${path}${res.status}: ${detail}`)
}
if (res.status === 204) return undefined as T
return res.json() as Promise<T>
} catch (e) {
clearTimeout(timer)
throw e
}
}
async get<T>(path: string): Promise<T> {
return this.request<T>("GET", path)
}
async post<T>(path: string, body: unknown): Promise<T> {
return this.request<T>("POST", path, body)
}
/** RouterOS uses PUT for updates on a specific item by /.id */
async put<T>(path: string, id: string, body: unknown): Promise<T> {
return this.request<T>("PUT", `${path}/${encodeURIComponent(id)}`, body)
}
async patch<T>(path: string, id: string, body: unknown): Promise<T> {
return this.request<T>("PATCH", `${path}/${encodeURIComponent(id)}`, body)
}
async delete(path: string, id: string): Promise<void> {
return this.request<void>("DELETE", `${path}/${encodeURIComponent(id)}`)
}
// ── high-level helpers ───────────────────────────────────────────────────
async getSystemIdentity(): Promise<RosSystemIdentity> {
return this.get<RosSystemIdentity>("/system/identity")
}
async getSystemResource(): Promise<RosSystemResource> {
return this.get<RosSystemResource>("/system/resource")
}
async getInterfaces(): Promise<RosInterface[]> {
return this.get<RosInterface[]>("/interface")
}
async getBgpSessions(): Promise<RosBgpSession[]> {
return this.get<RosBgpSession[]>("/routing/bgp/session")
}
async getWireGuardInterfaces(): Promise<RosWireGuardInterface[]> {
return this.get<RosWireGuardInterface[]>("/interface/wireguard")
}
async getWireGuardPeers(): Promise<RosWireGuardPeer[]> {
return this.get<RosWireGuardPeer[]>("/interface/wireguard/peers")
}
/** Add a WireGuard interface */
async addWireGuardInterface(params: {
name: string
listenPort: number
mtu?: number
comment?: string
}): Promise<RosWireGuardInterface> {
return this.post<RosWireGuardInterface>("/interface/wireguard", {
name: params.name,
"listen-port": String(params.listenPort),
mtu: String(params.mtu ?? 1420),
comment: params.comment ?? "",
})
}
/** Add a WireGuard peer */
async addWireGuardPeer(params: {
iface: string
publicKey: string
allowedAddresses: string[]
endpoint?: string
endpointPort?: number
persistentKeepalive?: number
comment?: string
}): Promise<RosWireGuardPeer> {
return this.post<RosWireGuardPeer>("/interface/wireguard/peers", {
interface: params.iface,
"public-key": params.publicKey,
"allowed-address": params.allowedAddresses.join(","),
...(params.endpoint ? { "endpoint-address": params.endpoint } : {}),
...(params.endpointPort ? { "endpoint-port": String(params.endpointPort) } : {}),
...(params.persistentKeepalive ? { "persistent-keepalive": String(params.persistentKeepalive) } : {}),
comment: params.comment ?? "",
})
}
/** Ping test from the router — uses /tool/ping */
async ping(address: string, count = 4): Promise<{ "avg-rtt": string; "packet-loss": string }[]> {
return this.post("/tool/ping", { address, count: String(count) })
}
}
// ─── Factory helper ───────────────────────────────────────────────────────────
export function createClient(overrides: Partial<ApiClientConfig> & { host: string }): RouterOsApiClient {
return new RouterOsApiClient({
port: 443,
tls: true,
username: "admin",
password: "",
tlsVerify: false,
timeout: 8000,
...overrides,
})
}