feat(panel, PANEL_ENHANCE_PLAN 批1-A): 访问级 4 位 PIN 鉴权 + 后端只读端点 power/overview/ams/svcd + audio 电平键

鉴权(用户定案:访问面板即需登录):
- PANEL.pin 存 sha256+salt(salt:hex,绝不含明文);与未来锁屏 PIN 同源预留 system.pin(g_sets 注记)。
- 未设 PIN → /api/auth/setup 首次设置流程;未设 PIN 前读/写端点全拦(require_auth 无条件,登录/设置流程除外)。
- token 128bit(16B→32hex) 只存进程内存(重启自然失效),3 天固定不滑动续期,多会话并存。
- 连错 5 次 PIN 锁 5 分钟(429 LOCKED,status 透出 lock_remaining_ms)。
- 旧密码机制 panel.auth_enabled/panel.auth_pass 兼容迁移:未设 PIN 时可暂用旧密码登录(status=legacy),设 PIN 后以 PIN 为准。

新只读端点(均需 PIN 会话):
- /api/power: meow_powermgr MSG_PM_QUERY(0xD0)→RESULT(0xD1) battery/estimate/breakdown,confidence 原样透传。
- /api/audio: 补 rms/peak/lvlseq(追加键,ABI 兼容),抽 audio_fill() 供 overview 复用。
- /api/ams: am list 解析 App 表(pkg/name/state/pid/launches/crashes/last_kind/last_code)。
- /api/svcd: meow_svcd status all 解析服务表(name/state/pid/restart/backoff_ms/trigger/src/exit/exec)。
- /api/overview: 单线程后端只暴露聚合——perf/power/audio/net/ams/svcd 六源顺序 fan-out,任一源失败该子块 available:false,绝不假数据。

前端最小支撑:登录页(PIN 输入+首次设置双态)+ 401 自动跳登录 + token 存 localStorage 随请求带 X-MeowToken(后端亦认 Authorization: Bearer)。
红线:仅新增读级端点,未加任何写端点(音量/路由/策略等);本任务不含批1-B 页面。

md5: meowpanel 3696e85b5eaf111f530c5c39fa68886e(mipsel 静态)/ meowpanel_host 3180e4f87e77db8717597619097805eb。
验证:host+mips -Werror 0 warning;假 IPC/am/svcd 桩六源聚合(含 power 宕机降级)PASS;PIN 全流程(设置→登录→连错5次锁→token 过期)PASS;qemu-mipsel 冒烟 PASS。
This commit is contained in:
MeowOS committed 2026-09-01 21:30:41 +08:00
1 parent 30e2ec901f
commit d12164305d
6 files changed
+627 -155

No files matched your search

+60 -22
View File
@@ -28,7 +28,8 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
- 产出(本次构建实测):
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
体积约 **218 KB**(固件阈值内)。md5:`015f9cf1c12e36d133b01cf6b6648edd`(本次含 `/api/audio` 音频状态端点 + 前端「音频」「字体」页)。
体积约 **236 KB**(固件阈值内)。md5:`3696e85b5eaf111f530c5c39fa68886e`(批1-A:访问级 **4 位 PIN 鉴权** + `/api/{power,overview,ams,svcd}` + `/api/audio` 补 `rms/peak/lvlseq`。
**重防火墙注意**:运行中二进制先 `.new` 再 `mv`,切勿直接覆盖运行中的 `meowpanel`)。
- `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **148 KB**。
- 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自
`meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。
@@ -73,9 +74,10 @@ http://<host>.local:8080/
- mDNS 由板载 `meow_mdns` 应答 `<host>.local`。
- 端口默认 **8080**,可用 `-p PORT` / 环境变量 `MEOWPORT` / 与后端同目录 `meowpanel.conf`(`port=N`)覆盖。
(优先级:`-p` > `MEOWPORT` > `meowpanel.conf` > 默认 8080。)
- 无鉴权(设备内网信任模型;**可选**密码鉴权已实现,见 §5.9)。默认关=零回归;开启后除登录/登出/状态外 `/api/*` 需会话。
- **访问级 PIN 鉴权(用户 2026-09-01 定案)**:访问面板即需登录(4 位 PIN,`panel.pin`,见 §5.9);未认证请求一律 401。
除 `/api/auth/{login,setup,logout,status}` 外全部 `/api/` 需有效会话(token 存面板进程内存,重启失效,3 天固定不滑动续期)。
- 上传/下载/管理类请求统一带 `X-MeowToken: <token>`(或 `Authorization: Bearer <token>` / `Cookie: meow_token=<token>`);
缺/错/过期 → 401 `UNAUTHORIZED`。**默认无鉴权时请勿把 8080 暴露公网**。
缺/错/过期 → 401 `UNAUTHORIZED`。**HTTP 明文(无 TLS),内网使用;请勿把 8080 暴露公网**。
---
@@ -88,8 +90,9 @@ http://<host>.local:8080/
| GET | `/`、`/meowpanel.css`、`/meowpanel.js`、`/assets/<f>` | 静态托管 | ✅ |
| GET | `/api/screen.jpeg` | 实时画面:fb0 当前帧 → RGB565→JPEG(`?q=1-100` 质量,默认 70;`?delta=1` 开启增量帧,见 §5.10) | ✅(阶段 C) |
| POST | `/api/input/tap` | 触摸注入:`{x,y}`(480×720 原生坐标;write 进 goodix event1,本机实测可投递);本机触摸后 5s 内忽略注入 | ✅(阶段 C) |
| GET | `/api/auth/status` | 鉴权状态(enabled/会话有效) | ✅(公开) |
| POST | `/api/auth/login` | 密码登录发 token(sha256 带盐校验) | ✅(公开;错→401) |
| GET | `/api/auth/status` | 访问鉴权状态(auth_enabled/session/pin_set/legacy/locked/lock_remaining_ms) | ✅(公开) |
| POST | `/api/auth/setup` | 首次设置 4 位 PIN(仅未设 PIN 时;`{pin}` 4 位数字;已设→409) | ✅(公开;非法→422) |
| POST | `/api/auth/login` | PIN 登录发 token(`{pin}`;未设 PIN 时接受旧密码 `{password}` 迁移;错→401,连错 5 次→429 LOCKED) | ✅(公开) |
| POST | `/api/auth/logout` | 登出失效 token | ✅(公开;幂等) |
| GET | `/api/status` | 性能+系统(`meow_svc_perf` IPC subset=all 透传) | ✅ |
| GET | `/api/perf` | 仅性能(subset=perf 透传) | ✅ |
@@ -112,7 +115,11 @@ http://<host>.local:8080/
| GET | `/api/fonts` | 字体库列表(扫描 `MeowOS/fonts/<family>/<weight>/` 三件套齐) | ✅(无卡→503) |
| POST | `/api/fonts/import` | 上传字体库 `.tar.gz`(multipart `file`,可带 `?force=1` 覆盖) | ✅(缺件/坏json/穿越→400,冲突→409,超限→413) |
| GET | `/api/fonts/export` | 导出 `<family>/<weight>/` 三件套为 `.tar.gz`(`?family=&weight=`) | ✅(缺件→404) |
| GET | `/api/audio` | 音频状态快照:meow_audiod IPC `MSG_AUDIO_QUERY→STATE` 透传(当前流/路由/音量/underrun,见 §5.12) | ✅(audiod 不在→`available:false`,不报错) |
| GET | `/api/audio` | 音频状态快照:meow_audiod IPC `MSG_AUDIO_QUERY→STATE` 透传(当前流/路由/音量/underrun + **`rms/peak/lvlseq` 电平**,见 §5.12) | ✅(audiod 不在→`available:false`,不报错) |
| GET | `/api/power` | 功耗管理:meow_powermgr IPC `MSG_PM_QUERY→RESULT`(battery/estimate/breakdown,confidence 原样透传) | ✅(powermgr 不在→503 `POWER_UNAVAILABLE`) |
| GET | `/api/ams` | App 运行表:`am list`(pkg/name/state/pid/launches/crashes/last_kind/last_code) | ✅(amsd 不在→`available:false`) |
| GET | `/api/svcd` | 服务表:`meow_svcd status all`(name/state/pid/restart/backoff_ms/trigger/src/exit/exec) | ✅(svcd 不在→`available:false`) |
| GET | `/api/overview` | **首页聚合**(单线程后端只暴露聚合):后端顺序 fan-out perf/power/audio/net/ams/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) |
| GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ |
| GET | `/api/apps/:pkg` | 应用详情 | ✅ |
| POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ |
@@ -237,21 +244,26 @@ http://<host>.local:8080/
TF 路径仅为 launcher 不可达时的诚实降级(会显示自动过期的历史通知,直到 clear)。
- **并发**:IPC 可用时面板不写 TF(撤清由 launcher 自行归档),避免与 launcher `O_APPEND` 竞写;仅 IPC 回退到「写临时 + rename」。
### 5.9 可选鉴权(panel.auth_enabled / panel.auth_pass)
- **设置键**:`panel.auth_enabled`(bool,默认 `0`=关)+ `panel.auth_pass`(str,存 `salt:hex`)。两者写 `settings.conf`,
与实体设置页/CLI 同一存储契约(面板为独立进程,直接镜像文件)。`panel.auth_pass` 的 `GET /api/settings` 恒回
`"value":"" + "set":<bool>`,**绝不回传存量哈希/明文**。
- **门禁**:`require_auth` 在路由分发前执行——未启用直接放行(零回归);启用后除 `/api/auth/{login,logout,status}` 外全部
`/api/` 需有效会话(token 头 / `X-MeowToken` / `Cookie meow_token`),缺/错/过期 → 401 `UNAUTHORIZED`。
危险端点(reboot/shutdown/ota、key 注入)在未开启时**不额外限制**(保持内网模型),开启后亦统一纳入会话校验。
- **密码**:`PUT /api/settings/panel.auth_pass` 传明文,后端生成 16B 盐(/dev/urandom)→ `salt:hex`(sha256(salt||password))
落盘;空串=清除。校验时重算并恒定比较。**哈希/明文绝不进日志/响应**。`panel.auth_enabled` 置 1 时若未设密码 → 422 `请先设置面板密码`。
- **会话**:`POST /api/auth/login` 校验后发 token(32B → 64 hex),存内存(`AUTH_MAX_SESS`=8,满淘汰最早过期)+ 卡上
`<web_root>/meowpanel.session`(`<token> <expires_at>`,重启加载、防吊销失效),有效期 12h。`logout` 失效当前 token。
- **静态屏蔽**:`serve_static` 额外拒绝后端可执行文件与 `meowpanel.session`(403),防 token 文件被 HTTP 读取。
- **前端**:api 客户端统一带 `X-MeowToken`,收到 401 → 弹登录页(`#login` overlay);登录成功 token 存 `localStorage`;
首次进入以 `/api/auth/status` 决定是否弹登录。设置页「面板鉴权」卡片含开关(`panel.auth_enabled`)+「设置/修改密码」
按钮(`panel.auth_pass`,走 `passwordDlg` 输入)。
### 5.9 访问级 PIN 鉴权(panel.pin;用户定案 2026-09-01)
- **模型(覆盖 282 报告「默认无鉴权」前提)**:**访问面板即需登录**。未认证请求一律 401(静态登录页自身 + css/js 放行——
`serve_static` 先于 `require_auth`);未设 PIN 前无法建立会话,故**读端点也全拦**(登录/设置流程除外),写端点自然一律拒绝。
- **设置键**:`panel.pin`(str,存 `salt:hex` = sha256(salt||pin) 带盐哈希,绝不存明文),写板载 `settings.conf`(与实体设置页/CLI
同一存储契约,面板独立进程直接镜像文件);**与未来 MeowOS 锁屏 PIN 同源**:设置键 `system.pin` 预留(本后端只读面板键,见 `g_sets` 注记)。
`panel.pin` 的 `GET /api/settings` 恒回 `"value":"" + "set":<bool>`,**绝不回传哈希/明文**。
- **PIN**:4 位数字(`pin_validate`);校验时重算 sha256(salt||pin) 并恒定比较(`memcmp`)。
- **门禁**:`require_auth` 在路由分发前执行——除 `/api/auth/{login,setup,logout,status}` 外全部 `/api/` 需有效会话;
缺/错/过期 → 401 `UNAUTHORIZED`。危险端点(reboot/shutdown/ota、key 注入、settings 写等)一律纳入会话校验(不再有「未开启」旁路)。
- **会话 token**:`/dev/urandom` 16B → **32 hex(128bit)**,**只存面板进程内存**(`g_sess[]`,`AUTH_MAX_SESS`=8,满淘汰最早过期),
**无卡上持久化**——设备重启自然清(会话即失效)。有效期 **3 天**(`AUTH_TOKEN_TTL`,**固定不滑动续期**,逐 token 校验到期戳);
多会话并存。`logout` 失效当前 token。
- **锁定(防爆破)**:连续 `PIN_MAX_FAILS=5` 次错 PIN → 锁 `PIN_LOCK_SEC=300`s(`429 LOCKED`,响应附 `lock_remaining_ms`;
`status` 透出 `locked/lock_remaining_ms`);内存态,重启清。
- **旧密码机制兼容迁移**(`panel.auth_enabled`/`panel.auth_pass` 保留):旧密码存在且未设 PIN 时,视为「已设访问屏障」的同等物——
登录可用旧密码(`{"password":…}`),`status` 透出 `legacy:true` 并提示改设 4 位 PIN;**一旦设置 `panel.pin`,新代码以 PIN 为准**
(登录只认 PIN,旧密码不再作为凭据)。`PUT /api/settings/panel.auth_pass` 仍可写(引导迁移),`panel.auth_enabled` 已不作门禁(保留兼容)。
- **前端**:api 客户端统一带 `X-MeowToken`(token 存 `localStorage`);收到 401 → 自动切登录页;未设 PIN → 首次设置流程
(`#login` overlay 双态:设置=输入+确认,登录=单输入);429 `LOCKED` → 显示锁定提示。设置页「面板鉴权」卡片沿用旧密码项。
- **静态屏蔽**:`serve_static` 额外拒绝后端可执行文件(403),防可执行被 HTTP 读取。
### 5.10 实时画面(`/api/screen.jpeg`,含增量帧 `?delta=1`)+ 触摸注入(`/api/input/tap`)— 阶段 C
- **实时画面(整帧,默认)**:`GET /api/screen.jpeg` 读 `/dev/fb0` **当前显示帧**(`FBIOGET_VSCREENINFO` 取 `yoffset`/`xres`/`yres`/`bpp`,
@@ -314,8 +326,10 @@ http://<host>.local:8080/
(`active=.. app=.. sid=.. rate=.. ch=.. fmt=.. written=.. played=.. underrun=.. paused=.. route_mode=.. port=.. vol=..`,
空格分隔 k=v、NUL 结尾)。**audiod 无 MSG_HELLO 握手**(见 meow_audio.c CLI),与 perf/power/launcher 路径不同。
- **响应**:解析 k=v 为 JSON——`{available:true, active, app, sid, rate, ch, fmt, written_frames, played_frames,
underrun, paused, route_mode("auto"/"manual"), port, vol}`;`port` 为 amixer numid=9 路由口
underrun, paused, route_mode("auto"/"manual"), port, vol, **rms, peak, lvlseq**}`;`port` 为 amixer numid=9 路由口
(0=空闲 1=lineout 2=3.5 3=4.4 4=Type-C SPDIF;宿主无 amixer 时为 -1)。
- **电平(追加键,ABI/协议兼容)**:`rms`/`peak` = deci-dB(`-990`=‑inf;如 `-139`=‑13.9dBFS),`lvlseq` = 快照代际
(每次结算/开流复位递增,前端可用其判「新刻度」驱动 sparkline)。仅追加键,不破坏既有字段。
- **audiod 不在**(socket 缺失/连接拒/超时/非预期应答)→ HTTP 200 `{"available":false}`,**不报错**(面板音频页显示
「audiod 不可用」)。只读端点;不放测试音(真机乱响风险,tone 留给 CLI `meow_audio tone`)。
- **前端**:「音频」页(`web/#/audio`)5s 轮询展示——当前流(app/rate/ch/fmt/已写帧/已播放帧/underrun/暂停态)+
@@ -328,6 +342,30 @@ http://<host>.local:8080/
`family/weight` 与 `warning`(sha256 不符仅提示)/`hint`。
- 设置页原「字体库」卡保留(同一后端,互不影响)。
### 5.14 批1-A 新增只读端点(供批1-B 前端消费;JSON 形状)
> 本批只做**后端端点 + 最小前端支撑(登录)**,首页/电源/进程页归批1-B。前端调用统一带 PIN 会话 token(见 §5.9)。
- **`GET /api/overview`**(首页生命线,单线程后端只暴露聚合):
```json
{ "ts": 1788268891,
"perf": { "...": "meow_svc_perf 聚合 JSON(cpu/memory/storage/tfcard/temp/system/network,透传)" },
"power": { "mode":"meow","battery":{"state","pct","mv","health","technology","temp_c10","charger_temp_c10","cycle_count","nominal_mah_note"},
"estimate":{"est_mw","confidence":"estimated|derived|measured|none","calibrated","degraded"},
"breakdown":{"baseline_mw","cpu_mw","cpu_util_pct","backlight_mw","panel_mw","brightness_pct","wifi_mw","wifi_state","bt_mw","bt_state","audio_mw","audio_state","degraded"},
"policy": null },
"audio": { "available": true, "active","app","sid","rate","ch","fmt","written_frames","played_frames","underrun","paused",
"route_mode","port","vol","rms","peak","lvlseq" },
"net": { "...": "netmgrd 概览 JSON(mode/ifaces[]/wifi/bt/rfkill/hostname_local,透传)" },
"ams": { "available": true, "count": 3, "apps":[ {"pkg","name","state":"fg|bg|stop","pid","launches","crashes","last_kind":"clean|exit|sig","last_code"} ] },
"svcd": { "available": true, "count": 3, "services":[ {"name","state":"running|stopped|stopping|disabled|failed","pid","restart","restart_str","backoff_ms","trigger","src":"board|tf","exit","exit_time","exec"} ] }
}
```
- **任一源失败 → 该子块 `{"available":false}`**(`perf`/`power`/`net` 失败也是 `{"available":false}`,绝不假数据);
`audio`/`ams`/`svcd` 成功时自带 `available:true`。`perf`/`power`/`net` 成功时为对应源**原始 JSON**(无 `available` 字段)。
- **`GET /api/power`**:即 overview 的 `power` 子块(可 503 `POWER_UNAVAILABLE`,非 `available:false`)。
- **`GET /api/ams`** / **`GET /api/svcd`**:即 overview 的 `ams`/`svcd` 子块;源不在 → `{"available":false}`。
- **`GET /api/audio`**:overview 的 `audio` 子块 + rms/peak/lvlseq 消费说明见 §5.12。
---
## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md)