257 lines
7.7 KiB
TypeScript
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,
|
|
})
|
|
}
|