67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import { Router } from 'express'
|
|
import axios from 'axios'
|
|
|
|
const router = Router()
|
|
|
|
const HISTORY_SIZE = 20
|
|
const history: { ts: number; rx: number; tx: number }[] = []
|
|
|
|
function soap(action: string, service: string, body = ''): string {
|
|
return `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:${action} xmlns:u="${service}">${body}</u:${action}></s:Body></s:Envelope>`
|
|
}
|
|
|
|
function tag(xml: string, name: string): string {
|
|
const m = xml.match(new RegExp(`<(?:[^:>]*:)?${name}>([^<]*)<`))
|
|
return m?.[1]?.trim() ?? ''
|
|
}
|
|
|
|
async function soapReq(host: string, path: string, service: string, action: string): Promise<string> {
|
|
const res = await axios.post(`${host}:49000${path}`, soap(action, service), {
|
|
headers: {
|
|
'Content-Type': 'text/xml; charset="utf-8"',
|
|
SOAPAction: `"${service}#${action}"`,
|
|
},
|
|
timeout: 5000,
|
|
})
|
|
return res.data as string
|
|
}
|
|
|
|
router.get('/status', async (_req, res) => {
|
|
try {
|
|
const host = process.env.FRITZBOX_HOST
|
|
if (!host) {
|
|
res.status(503).json({ error: 'FRITZBOX_HOST not configured' })
|
|
return
|
|
}
|
|
|
|
const [addonXml, ipXml, statusXml] = await Promise.all([
|
|
soapReq(host, '/igdupnp/control/WANCommonIFC1',
|
|
'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1', 'GetAddonInfos'),
|
|
soapReq(host, '/igdupnp/control/WANIPConn1',
|
|
'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetExternalIPAddress'),
|
|
soapReq(host, '/igdupnp/control/WANIPConn1',
|
|
'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetStatusInfo'),
|
|
])
|
|
|
|
// Bytes/sec — FritzBox provides instantaneous rates
|
|
const rxRate = Number(tag(addonXml, 'NewByteReceiveRate') || tag(addonXml, 'NewBytesReceiveRate') || '0')
|
|
const txRate = Number(tag(addonXml, 'NewByteSendRate') || tag(addonXml, 'NewBytesSendRate') || '0')
|
|
|
|
history.push({ ts: Date.now(), rx: rxRate, tx: txRate })
|
|
if (history.length > HISTORY_SIZE) history.shift()
|
|
|
|
res.json({
|
|
connected: tag(statusXml, 'NewConnectionStatus') === 'Connected',
|
|
externalIp: tag(ipXml, 'NewExternalIPAddress'),
|
|
rxMbps: parseFloat((rxRate * 8 / 1_000_000).toFixed(2)),
|
|
txMbps: parseFloat((txRate * 8 / 1_000_000).toFixed(2)),
|
|
history: history.map(h => ({ rx: h.rx, tx: h.tx })),
|
|
})
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : 'Unknown error'
|
|
res.status(500).json({ error: msg })
|
|
}
|
|
})
|
|
|
|
export default router
|