Files
multi-simadmin/test/server-core.test.js
T
chick 2977f75129 fix: apply security and correctness review findings across both stacks
Legacy panel:
- upstream body reads now carry their own deadline and a 10 MB byte budget;
  a stalled modem can no longer hang /api/status fan-out forever nor OOM
  the proxy (request headers alone had the timeout, bodies had none)
- add X-Frame-Options DENY / CSP frame-ancestors none / nosniff; the panel
  (delete-instance and confirmed-write dialogs) is no longer clickjackable
- only send a JSON content-type when the API console request has a body, so
  payload-less dangerous writes stop failing with 400 and burning the
  one-use confirmation token
- register a form-urlencoded parser (the proxy branch was unreachable) and
  drop the multipart parser that buffered up to 60 MB before rejecting;
  bodyLimit drops to 2 MB; framework-level 415 keeps the stable error body
- remove /api/sms/send from readable paths: GET bypassed the write
  confirmation for a send endpoint
- /api/instances/:id/login maps upstream failures to a stable 502 instead
  of leaking raw error text
- guard MULTI_SIMADMIN_TIMEOUT_MS parsing (NaN aborted every request);
  prune dead code (buildClients, cookie expando no-op)

Control plane:
- deleting a notification channel detaches it from rules instead of leaving
  dangling ids that made every referencing rule unreadable and silently
  dropped future notifications; rule reads tolerate unknown ids
- startup sweep resets notification_queue rows stranded in 'sending' by a
  crash (mirrors the sms outbox sweep); terminal outbox rows are pruned on
  the retention timer
- /api/v1/metrics no longer emits operator-assigned node names on the
  session-free scrape; login limiter map is bounded and pruned; Secure
  cookie honors the gateway-declared x-forwarded-proto
- webhook delivery sets redirect: manual (signed payloads are not replayed)
- SMTP envelope sender is validated against CR/LF smuggling
- scheduled reboots with delaySeconds != 3 fail fast at the dispatcher with
  a clear reason instead of burning every retry; contract narrowed to the
  pinned baseline
2026-09-07 01:57:47 +08:00

84 lines
4.5 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { createSimAdminClient, normalizeInstance, summarizeInstanceSnapshot, redactInstance } from '../server/core.js'
test('normalizeInstance supports passwordless and password-protected instances without exposing password', () => {
const open = normalizeInstance({ id: 'open-1', name: '开放设备', url: 'http://192.0.2.12:3000/' }, 0)
assert.equal(open.auth.mode, 'none')
assert.equal(open.url, 'http://192.0.2.12:3000')
const protectedOne = normalizeInstance({ id: 'locked', url: 'http://192.0.2.13', auth: { password: 'secret' } }, 1)
assert.equal(protectedOne.auth.mode, 'password')
assert.equal(protectedOne.auth.password, 'secret')
assert.equal(redactInstance(protectedOne).auth.hasPassword, true)
assert.equal(redactInstance(protectedOne).auth.password, undefined)
})
test('client stores simadmin_session from login and sends it on later proxied requests', async () => {
const calls = []
const fetchImpl = async (url, options = {}) => {
calls.push({ url: String(url), options })
if (String(url).endsWith('/api/auth/status') && calls.length === 1) {
return new Response(JSON.stringify({ data: { configured: true, authenticated: false, settings: { password_protection_enabled: true } } }), { status: 200, headers: { 'content-type': 'application/json' } })
}
if (String(url).endsWith('/api/auth/login')) {
return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'set-cookie': 'simadmin_session=abc123; HttpOnly; Path=/; Max-Age=86400' } })
}
if (String(url).endsWith('/api/device')) {
return new Response(JSON.stringify({ data: { model: 'RM500Q' } }), { status: 200, headers: { 'content-type': 'application/json' } })
}
return new Response('{}', { status: 404 })
}
const client = createSimAdminClient(normalizeInstance({ id: 'locked', url: 'http://192.0.2.14', auth: { password: 'secret' } }, 0), { fetchImpl })
const auth = await client.ensureAuthenticated()
assert.equal(auth.authenticated, true)
const proxied = await client.fetchJson('/api/device')
assert.equal(proxied.status, 200)
assert.match(calls.at(-1).options.headers.get('cookie'), /simadmin_session=abc123/)
})
test('snapshot summary exposes device-monitoring fields from stats, sms, data and hardware endpoints', () => {
const snapshot = summarizeInstanceSnapshot({
device: { data: { model: 'CPE X', imei: '123', manufacturer: 'Quectel', powered: true, revision: 'RG500QEAAAR13A01' } },
sim: { data: { iccid: '8986', imsi: '460001234', phone_numbers: ['13800138000'], operator: 'CMCC', present: true } },
network: { data: { operator_name: '中国移动', signal_strength: -73, registration_status: 'registered', technology_preference: 'LTE' } },
smsStats: { data: { total: 12, incoming: 9, outgoing: 3, pushed: 8 } },
data: { data: { active: true } },
ota: { data: { current_version: '1.2.3', current_commit: 'abc123', pending_update: false } },
stats: { data: {
cpu_load: [0.2, 0.4, 0.6],
memory: { total: 1024, used: 512, free: 512 },
disk: { total: 2048, used: 1024, free: 1024 },
network_speed: { rx: 1024, tx: 2048 },
system_info: { os: 'OpenWrt', kernel: '6.6.1' },
temperature: { cpu: 47.5, modem: 41 },
uptime: 3600,
} },
})
assert.equal(snapshot.device.model, 'CPE X')
assert.equal(snapshot.device.manufacturer, 'Quectel')
assert.equal(snapshot.device.powered, true)
assert.equal(snapshot.sim.iccid, '8986')
assert.equal(snapshot.sim.present, true)
assert.equal(snapshot.sim.phoneNumber, '13800138000')
assert.equal(snapshot.network.operator, '中国移动')
assert.equal(snapshot.network.registration, 'registered')
assert.equal(snapshot.network.signal, -73)
assert.equal(snapshot.network.accessTechnology, 'LTE')
assert.equal(snapshot.sms.total, 12)
assert.equal(snapshot.sms.incoming, 9)
assert.equal(snapshot.sms.outgoing, 3)
assert.equal(snapshot.sms.pushed, 8)
assert.equal(snapshot.data.active, true)
assert.equal(snapshot.ota.currentVersion, '1.2.3')
assert.equal(snapshot.ota.currentCommit, 'abc123')
assert.equal(snapshot.ota.pendingUpdate, false)
assert.deepEqual(snapshot.system.temperature, { cpu: 47.5, modem: 41 })
assert.deepEqual(snapshot.system.networkSpeed, { rx: 1024, tx: 2048 })
assert.equal(snapshot.system.memory.used, 512)
assert.equal(snapshot.system.disk.free, 1024)
assert.equal(snapshot.system.info.os, 'OpenWrt')
assert.equal(snapshot.system.uptime, 3600)
})