外观
第 6 章 接口与数据参考
接口与数据表的完整定义见 uanlink/SPEC.md,本章为其摘要与说明。名字与格式:租户码 ^[a-z][a-z0-9]{1,30}$;相机名 camNN(两位补零,过 99 三位);推流路径 t_<租户>/<相机>;推流账号 p_<租户>_<相机>;盒子 id bx_ 加 26 位 ULID;时间 RFC3339 UTC。
6.1 鉴权方式
| 主体 | 凭据 | 说明 |
|---|---|---|
| 运营 | x-admin-key | 等于 UANLINK_ADMIN_KEY,只从 127.0.0.1 或 nginx 白名单进 |
| 盒子 | x-box-id、x-ts、x-sig | Ed25519 对 METHOD\nPATH_AND_QUERY\nts\nhex(sha256(body)) 签名;时间差 300 秒内;同 ts 同签名 300 秒内重放 401 |
| 租户 | x-tenant-key | sha256 后与租户 key_hash 常数时间比较;错 5 次按(IP,租户码)锁 15 分钟;别的租户的钥匙 403;中心代调带 x-uanlink-actor-user 进审计 |
| 节点 | x-hub-key | 每个节点一把,库存哈希 |
6.2 数据表(sqlite,WAL)
tenant(code PK, name, hub_id, state, cfg_rev, key_hash, created_at)
hub(hub_id PK, key_hash, signal_port, public_v4, public_v6, capacity_streams, udp_open, shed_json, cfg_rev, updated_at)
site(site_id PK, tenant, name, failback, created_at)
join_code(code_hash PK, site_id, expires_at, used_at, used_by_box, fail_count, locked_until, created_by, created_at)
box(box_id PK, tenant, site_id, pubkey_b64, hw_fingerprint, state[active|suspended|revoked], cfg_rev, last_seen_at, last_ip,
agent_version, probe_json, name, paused, seal_pub, secrets_rev, secrets_rev_applied, discovery_at, up_since, drill_until, created_at)
camera(tenant, cam, box_id, assigned_box, site_id, source_kind[rtsp_url|nvr_channel], source_ref, stream_role, publish_user, publish_hash,
state[enabled|paused|removed], name, created_at, updated_at, PK(tenant, cam))
camera_secret(tenant, cam, secret) -- 推流口令明文,只在这里
discovery(box_id, ip, mac, json, seen_at, PK(box_id, ip, mac))
box_secret(box_id, ip, sealed, rev, updated_at, PK(box_id, ip))
box_action(box_id, kind, created_at, PK(box_id, kind))
ledger_hour(tenant, cam, hour, hub_bytes, box_bytes, sessions, reconnects, pair_kind, PK(tenant, cam, hour))
audit(id PK, at, actor, action, object, detail_json)字段说明:box_id 是登记时的首选盒子,assigned_box 是当前实际推送的盒子;source_ref 只是 ip 或 ip/通道,带 @ 的一律拒;box.paused 是租户按的"停止上送";up_since 只在心跳从离线变在线时写;老库启动时 add_column 幂等补列。
6.3 配置版本与长轮询
db::bump(tenant, boxes, hubs):租户版本加一,受影响的盒子取这个版本,受影响的节点各自加一,回要唤醒的键 b:<盒子> / h:<节点>。提交事务后 App::notify 用 tokio::sync::watch 叫醒挂着的长轮询(最多 25 秒,停机时立刻回 304)。盒子 GET config?rev= 与节点 GET publishers?rev= 版本没变就挂着等。
6.4 运营侧 /v1/admin/
建改节点 PUT hubs/{h}(回 hub_key 一次)、建改租户 PUT tenants/{t}(新建回 tenant_key 一次,?rotate_key=1 轮换)、建现场、发接入码、登记相机、吊销盒子 POST boxes/{b}/revoke、台账 GET ledger、nginx 映射渲染。
6.5 盒子侧 /v1/box/{id}/
| 方法与路径 | 要点 |
|---|---|
POST /v1/join | 接入码、公钥、硬件指纹、可选 seal_pub_b64;接入码一次性、24 小时、错 5 次按 IP 锁 15 分钟 |
POST probe | 自检结果:时钟、HTTPS、UDP 与 NAT 类型、IPv6、相机网段;返回判定(ready_v4 / ready_v6 / blocked 等)与面向网络负责人的说明文字 |
POST heartbeat | 30 秒一次;每路 state / cred / err / codec / bframes / chosen_stream / fps;seal_pub_b64 每次都带;secrets_rev 已应用版本;hw_fingerprint 90 秒内换了就挂起。回 cfg_rev 与 actions(reload、discover、probe、recheck、revoke) |
GET config?rev= | BoxConfig {cfg_rev, cameras[{cam, path, publish_url, source_kind, source_ref, stream_role, state}], paused, secrets_rev};state 取 enabled / paused / shed / removed;租户停用时全部 paused |
POST discovery | 整份覆盖,最多 256 台,同 ip 不同 mac 两条都留 |
GET secrets | 只回本盒子的 {rev, items[{ip, sealed}]} |
POST ledger | 每路发出的字节,小时差分 |
盒子收到配置后的动作:state=paused/shed 不推不重试;removed 从配置里拿掉;某路连续 60 秒没建连退避 60 / 300 / 900 秒;收到 revoke 删令牌与 box.key、清空推流、退出不再自启。
6.6 租户侧 /v1/t/{tenant}/
| 方法与路径 | 要点 | |:--------------------------|:------------------------------------|:-------------|:---------------------| | GET sites / POST sites / POST sites/{s}/join-codes | 现场与接入码;GET sites 带 failback | | PUT sites/{s}/failback {enabled} | 现场回切开关 | | PUT sites/{s}/secrets {items[{box_id, ip, sealed}]} | 同一 IP 给现场内每台盒子各一份密文,单个事务写入;任一盒子不属于该现场时整批拒绝 | | GET boxes?site= | 不回硬件指纹;online = 90 秒内有心跳;streams/sys 取内存里最近一次心跳;带 up_since、drill_until | | PUT boxes/{b} {name} / POST boxes/{b}/pause|resume | 改名、停止上送 | | POST boxes/{b}/actions {type} | discover / probe / recheck,落库排队,下一次心跳带走;recheck 5 分钟一次 | | POST boxes/{b}/drill {minutes} | 演练切换;minutes=0 结束演练;最长 60 分钟 | | GET boxes/{b}/discovery / PUT boxes/{b}/secrets | 发现清单;单盒的密文写入 | | POST cameras/bulk | 1 到 256 项,整批校验;同现场同 source_ref 回原来的相机;名字缺省取发现清单的自报名 | | GET cameras | 含已移除的;带 assigned_box | | PUT cameras/{cam} / stream-role / pause|resume|remove | 改名、改码流意图(auto / third / sub / main)、单路状态 | | POST pause-all / resume-all | 总闸 | | GET ledger / GET audit | 台账;审计不回指纹与来源 IP |
6.7 节点侧 /v1/hub/{hub_id}/
GET publishers?rev=:该放行的推流账号(租户 active 且挂在这个节点、相机 enabled、代推盒子 active 且没按停止上送),按现场分组带 rank(卸路时后加的先让)。POST status:每路收到字节、就绪、候选对类型、卸路名单;卸路名单有变,受影响盒子的配置版本加一。
6.8 密封格式 sealed_v1
盒子公钥 P-256 未压缩点 65 字节,标准 base64
共享秘密 ECDH(临时私钥, 盒子公钥) 取 256 位
AES 钥 HKDF-SHA256,salt 32 个 0,info "uanbox-camsecret-v1|<box_id>|<ip>",32 字节
IV 12 字节随机;AAD "<box_id>|<ip>"
明文 JSON {"user":"…","pass":"…"}
密文串 JSON {"v":1,"epk":"<临时公钥 base64>","iv":"<base64>","ct":"<含 16 字节 tag>"}浏览器只用 WebCrypto,不引第三方库;frontend/scripts/seal_selftest.mjs 和盒子的 uanbox-agent seal-selftest 做互通测试。crypto.subtle 只在 https 或 localhost 下有,页面走 http 时拿不到会报 no_webcrypto。
6.9 中心接入页接口
中心代登录用户调上面的租户侧接口,读接口对非超级管理员按园区过滤,写接口只给超级管理员并进中心审计。回包 {ok: true, result},控制面连不上或回错一律 502 带中文原因。overview 把现场、盒子(带发现清单)、相机三表合一,并上本中心的相机配置与在线状态。