feat(meowpanel): 通知中心+文件管理后端补全 (agent-182) — /api/notifications(读TF归档,环形200条)+单删+dismiss_all(保留媒体/进度活动通知); 文件域修4bug(normalize_path丢前导/致白名单必失败、upload multipart 文件名截断恒NULL、rename拼错路径恒ENOTDIR、read>1MB改truncated) + 上传8MB上限+文件名净化+双重白名单防逃逸
meowpanel 8108852f→58a7b186; mipsel/host -Wall -Wextra 0 warning; 宿主 curl 自测全过 需launcher配合项(已记录): MSG_NOTIFY_QUERY 查询帧 + dismiss 广播 IPC
This commit is contained in:
1 parent
c187b7bce3
commit
563a9be1d6
5 files changed
+438
-75
No files matched your search
@@ -102,18 +102,22 @@ http://<host>.local:8080/
|
|||||||
| POST | `/api/key` | 批量/顺序注入 | ✅ |
|
| POST | `/api/key` | 批量/顺序注入 | ✅ |
|
||||||
| GET | `/api/logs` | `MeowOS/logs` 文件列表 | ✅(无卡→503) |
|
| GET | `/api/logs` | `MeowOS/logs` 文件列表 | ✅(无卡→503) |
|
||||||
| GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ |
|
| GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ |
|
||||||
| GET | `/api/notifications` | **501** | ⚠️ |
|
| GET | `/api/notifications` | 通知列表(读 TF 转储日志;见 §5.8) | ✅(无源→空态 `source:none`,不 503) |
|
||||||
| DELETE | `/api/notifications/:id` | **501** | ⚠️ |
|
| DELETE | `/api/notifications/:id` | 单条删除(重写日志移除该 id) | ✅(不存在→404) |
|
||||||
| POST | `/api/notifications/dismiss_all` | **501** | ⚠️ |
|
| POST | `/api/notifications/dismiss_all`(别名 `/api/notifications/clear`) | 一键清空:**保留活动中的媒体/进度通知**,移除其余 | ✅ |
|
||||||
|
| GET | `/api/files/read?path=` | 读文本:预览上限 1MB,超限 `truncated:true`(不再 404);二进制→415 | ✅ |
|
||||||
|
|
||||||
### 返回 501 `NOT_IMPLEMENTED` 的端点(诚实标注)
|
### 返回 501 `NOT_IMPLEMENTED` 的端点(诚实标注)
|
||||||
|
|
||||||
| 端点 | 原因 |
|
| 端点 | 原因 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/api/notifications`、`/api/notifications/:id`、`/api/notifications/dismiss_all` | 通知存储为 **launcher 进程内存**(`meow_notif.c g_slots[]`,NOTIFICATION_SYSTEM §4),后端为独立 svcd 子进程,**跨进程不可达**那批内存槽;框架只在关机前 dump 到 TF `MeowOS/notifications/notifications.log`(CSV 追加,非活跃快照)。**需为通知后端另开 IPC 查询/清撤通道**(如 `MSG_NOTIFY_LIST` 应答)或持久化活跃集,见 MEOWPANEL_PROTOCOL §6 待定案。 |
|
|
||||||
| `/api/key/:code` 的 `press:"hold"` | `hold` 需常驻持续注入通道(阶段 D),后端未实现;`tap`/`long` 已实现。 |
|
| `/api/key/:code` 的 `press:"hold"` | `hold` 需常驻持续注入通道(阶段 D),后端未实现;`tap`/`long` 已实现。 |
|
||||||
| OTA 升级模式(`/api/system/ota` 带 `url`) | 升级为草案(MEOWPANEL_PROTOCOL §6 待定案);当前 `POST /api/system/ota` 空 body 触发 `meow_svc_power` 的 `POWER_UPGRADE`(写 mtd5 进升级模式),带 `url` 的下载+升级未实现。 |
|
| OTA 升级模式(`/api/system/ota` 带 `url`) | 升级为草案(MEOWPANEL_PROTOCOL §6 待定案);当前 `POST /api/system/ota` 空 body 触发 `meow_svc_power` 的 `POWER_UPGRADE`(写 mtd5 进升级模式),带 `url` 的下载+升级未实现。 |
|
||||||
|
|
||||||
|
> 通知端点已从 501 落地为「读 TF 卡通知转储日志」实现。因 launcher 侧 **`MSG_NOTIFY_LIST` 为下行推送(守护→App),
|
||||||
|
> 无「客户端查询通知清单」请求类型**(launcher `util.c` `case MSG_NOTIFY_LIST: "not dispatched here"`),后端(svcd 独立进程)
|
||||||
|
> 无法经 IPC 取活跃清单,故回退到 TF 转储文件。详见 §5.8 与输出「需 launcher 侧配合项」。
|
||||||
|
|
||||||
> 前端对 `501` 显示「该功能后端尚未实现」(`ok:false,error.code=NOT_IMPLEMENTED`)。
|
> 前端对 `501` 显示「该功能后端尚未实现」(`ok:false,error.code=NOT_IMPLEMENTED`)。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -189,9 +193,23 @@ http://<host>.local:8080/
|
|||||||
- `GET /api/logs/read?file=&lines=`:`file` 必须相对 `MeowOS/logs/`,**禁 `..`/绝对路径 → 403**,敏感名 → 403;
|
- `GET /api/logs/read?file=&lines=`:`file` 必须相对 `MeowOS/logs/`,**禁 `..`/绝对路径 → 403**,敏感名 → 403;
|
||||||
缺省 `lines=200`,上限 `1000`;返回 `{file,lines[],total,truncated}`。
|
缺省 `lines=200`,上限 `1000`;返回 `{file,lines[],total,truncated}`。
|
||||||
|
|
||||||
### 5.8 通知(501 及其依据)
|
### 5.8 通知(数据源 = TF 卡通知转储日志)
|
||||||
见 §4 501 表:通知存储为 launcher 进程内存,后端跨进程不可读/清撤。已在源码 `api_notifications*` 注释「为何未实现 +
|
- **数据源优先级**:① launcher IPC 查询 →② TF 转储文件 →③ 空态。当前 ① 不可用(launcher 侧 `MSG_NOTIFY_LIST` 仅下行推送、
|
||||||
后续如何接」(通知后端加 `MSG_NOTIFY_LIST` 查询/清撤 IPC,或持久化活跃集到 TF `MeowOS/notifications/` 供只读)。
|
无客户端查询请求类型,见「需 launcher 侧配合项」),故以 ② 为准。
|
||||||
|
- **TF 转储文件**:`<卡根>/MeowOS/notifications/notifications.log`(launcher `meow_notif_dump_tf` 一行一条 CSV,
|
||||||
|
字段 `timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,ongoing,media,dismissed`;
|
||||||
|
`meow_notif_clear_tf` 开机清空 → 会话级归档)。后端逐行流式读取(环形保留最新 `NOTIF_PREVIEW_MAX=200` 条,不全量进内存)。
|
||||||
|
- **`GET /api/notifications`**:返回 `{count, items:[...], notifications:[...], source}`;`items`= `notifications` 别名
|
||||||
|
(兼容前端读 `.notifications` 与任务约定空态 `items:[]`)。字段含 `id/app_id/app_name/app_icon/category/summary/body/
|
||||||
|
urgency/ts/progress/ongoing/media/target_activity/dismissed`。`progress<0`→`null`;`app_name` 对 `system`→`系统`,其余用 `app_id` 兜底。
|
||||||
|
无源(无卡/文件不存在)→ `{count:0,items:[],notifications:[],source:"none"}`(**不 503、不伪造数据**)。
|
||||||
|
- **`DELETE /api/notifications/:id`**:从日志移除该行(重写临时文件后 rename)。不存在 → 404。
|
||||||
|
- **`POST /api/notifications/dismiss_all`(别名 `/api/notifications/clear`)**:重写日志,**只保留活动中的媒体/进度通知**
|
||||||
|
(`ongoing||media||progress>=0||category=="media"`),移除其余;返回 `{dismissed:<移除数>}`。
|
||||||
|
- **语义边界**:日志是**归档快照**(归档时 `dismissed` 尚未置 1,故该列恒 ≈0 不可靠),非活跃快照;且面板清空只改
|
||||||
|
日志视图,**不联动 launcher 进程内存**(launcher 通知中心仍显示其内存活跃集)。无 launcher 查询/清撤 IPC 前的诚实降级。
|
||||||
|
- **并发**:面板清空/删除用「写临时 + rename」重写日志;launcher 侧仍以 `O_APPEND` 追加,二者为竞写(低并发、会话级日志,
|
||||||
|
可接受),已写入「已知边界」(§7)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -208,6 +226,17 @@ http://<host>.local:8080/
|
|||||||
5. **`/api/files` 上传/删除/重命名等 path**:前端对这些操作传**绝对路径**(`d.path/子项`),列表传**相对 root**;
|
5. **`/api/files` 上传/删除/重命名等 path**:前端对这些操作传**绝对路径**(`d.path/子项`),列表传**相对 root**;
|
||||||
后端 `normalize_path` 对两者均容错(相对则拼 root,绝对则直用),符合协议「白名单根 + 规范化」。
|
后端 `normalize_path` 对两者均容错(相对则拼 root,绝对则直用),符合协议「白名单根 + 规范化」。
|
||||||
6. **`GET /api/files` 返回值**:`files_list` 返回 `500` 语义时以 500 封装(协议未定义 `root` 之外错误枚举,用通用错误信封)。
|
6. **`GET /api/files` 返回值**:`files_list` 返回 `500` 语义时以 500 封装(协议未定义 `root` 之外错误枚举,用通用错误信封)。
|
||||||
|
7. **通知清单数据源**:协议 §3.8 称后端「经通知后端读取」;因 launcher 无「客户端查询通知清单」IPC(`MSG_NOTIFY_LIST` 仅下行),
|
||||||
|
后端**改读 TF 卡通知转储日志**(`MeowOS/notifications/notifications.log`)。响应额外带 `source`(`tf`/`none`)与 `items` 别名
|
||||||
|
(与 `notifications` 同内容);`items` 为任务约定空态形状,`notifications` 为前端实际读取字段,二者并存。`clear` 语义为
|
||||||
|
「保留活动中的媒体/进度通知」(`ongoing||media||progress>=0`),对应通知中心既定语义。
|
||||||
|
8. **`/api/files/read` 文本截断**:协议 §3.4 称「仅文本(<1MB)→ 二进制 415」;现实现为「预览上限 1MB,超限 `truncated:true`,
|
||||||
|
不再整体 404」,二进制仍 415(比协议更友好,返回文件真实 `size` + `truncated`)。
|
||||||
|
9. **`/api/files/upload`/`rename` 修正**:上传文件名经 `safe_filename` 取 basename、拒绝 `..`/分隔符(防 `../../etc/passwd` 逃逸),
|
||||||
|
落盘前对 dir 与拼接后路径双重 `normalize_path`+白名单校验;rename 的新路径 = **原路径父目录** + `new_name`(原实现误拼到文件尾部,
|
||||||
|
恒 ENOTDIR 失败)。上传体上限 `MAX_UPLOAD_BODY`=8MB,超限 → 413 `PAYLOAD_TOO_LARGE`。
|
||||||
|
10. **TF 卡根可覆盖(宿主测试)**:`env MEOW_TF_ROOT` 覆盖卡根(设备上不设,恒用 `/usr/data/mnt/sd_0`);`env MEOW_NOTIF_LOG`
|
||||||
|
覆盖通知转储日志路径。设 `MEOW_TF_ROOT` 时 `tf_present()` 恒真(测试用)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -223,6 +252,12 @@ http://<host>.local:8080/
|
|||||||
- **`/api/files/upload` 上传路径**:multipart 需要 `path` 字段(目标目录);缺失时上传文件会被忽略(前端总是带着 `path`)。
|
- **`/api/files/upload` 上传路径**:multipart 需要 `path` 字段(目标目录);缺失时上传文件会被忽略(前端总是带着 `path`)。
|
||||||
- **数据一致性**:设置直接写 `settings.conf`;`display.brightness`/`boot.default` 的 clamp/同步与 launcher `meow_settings_*` 同口径。
|
- **数据一致性**:设置直接写 `settings.conf`;`display.brightness`/`boot.default` 的 clamp/同步与 launcher `meow_settings_*` 同口径。
|
||||||
若 launcher 未来改 clamp 逻辑,需同步本表。
|
若 launcher 未来改 clamp 逻辑,需同步本表。
|
||||||
|
- **通知视图为「会话级归档」**:TF 转储日志由 launcher 开机清空、每通知归档时追加一次;非活动(非 ongoing/无进度)通知
|
||||||
|
10s TTL 自动归档后仍留在日志,故面板会**显示已自动过期但未手动清理的历史通知**(直到 clear)。媒体/进度/ongoing 通知
|
||||||
|
持续保留。无 launcher 查询 IPC 前的诚实降级(见 §5.8)。
|
||||||
|
- **通知清空不联动 launcher 内存**:面板 clear/delete 只重写日志视图,launcher 通知中心内存活跃集不受影响(两者数据源分离)。
|
||||||
|
- **上传为「限长缓冲」非全流式**:multipart 请求体整体读入(上限 8MB,超限 413),下载为流式(8KB 分块)。严格「socket 逐字节流式上传
|
||||||
|
解析」未实现(需增量 multipart 解析器),列为后续;14MB 内存约束下 8MB 上限 + 流式下载已覆盖大文件读侧。
|
||||||
- **存储为 `meowpanel` 编译时刻写死**的 socket/路径会因 `--sock-perf/--sock-power`(qemu sim)可覆盖,但默认走 `/usr/data/meow_*.sock`。
|
- **存储为 `meowpanel` 编译时刻写死**的 socket/路径会因 `--sock-perf/--sock-power`(qemu sim)可覆盖,但默认走 `/usr/data/meow_*.sock`。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -231,9 +266,9 @@ http://<host>.local:8080/
|
|||||||
|
|
||||||
```
|
```
|
||||||
patch_v2/meowpanel/
|
patch_v2/meowpanel/
|
||||||
├── meowpanel.c # 后端源码(单文件,约 2700 行)
|
├── meowpanel.c # 后端源码(单文件,约 3150 行,-Wall -Wextra 0 warning)
|
||||||
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning)
|
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning)
|
||||||
├── meowpanel # 交叉产物(mipsel 静态,约 166 KB,md5 11ed76ec…)
|
├── meowpanel # 交叉产物(mipsel 静态,约 174 KB,md5 58a7b186…)
|
||||||
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
||||||
├── services.d/
|
├── services.d/
|
||||||
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
||||||
|
|||||||
Binary file not shown.
+382
-64
@@ -56,10 +56,21 @@
|
|||||||
#define DEFAULT_PORT 8080
|
#define DEFAULT_PORT 8080
|
||||||
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
|
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||||||
#define SETTINGS_HDR "# meow-settings 1"
|
#define SETTINGS_HDR "# meow-settings 1"
|
||||||
#define TF_ROOT "/usr/data/mnt/sd_0"
|
/* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。
|
||||||
#define TF_CARD_DIR "/usr/data/mnt/sd_0/MeowOS"
|
* TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */
|
||||||
#define MEOWAPPS_DIR "/usr/data/mnt/sd_0/MeowAPPs"
|
static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
|
||||||
#define LOGS_DIR "/usr/data/mnt/sd_0/MeowOS/logs"
|
static char g_tf_card_buf[2048];
|
||||||
|
static char g_apps_dir_buf[2048];
|
||||||
|
static char g_logs_dir_buf[2048];
|
||||||
|
static char g_notif_log_buf[2048];
|
||||||
|
static const char *g_tf_card = NULL; /* 派生:<root>/MeowOS */
|
||||||
|
static const char *g_apps_dir = NULL; /* 派生:<root>/MeowAPPs */
|
||||||
|
static const char *g_logs_dir = NULL; /* 派生:<card>/logs */
|
||||||
|
static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notifications/notifications.log */
|
||||||
|
#define TF_ROOT g_tf_root_buf
|
||||||
|
#define TF_CARD_DIR g_tf_card
|
||||||
|
#define MEOWAPPS_DIR g_apps_dir
|
||||||
|
#define LOGS_DIR g_logs_dir
|
||||||
#define PERF_SOCK "/usr/data/meow_perf.sock"
|
#define PERF_SOCK "/usr/data/meow_perf.sock"
|
||||||
#define POWER_SOCK "/usr/data/meow_power.sock"
|
#define POWER_SOCK "/usr/data/meow_power.sock"
|
||||||
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
|
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
|
||||||
@@ -70,10 +81,13 @@
|
|||||||
|
|
||||||
#define IPC_TIMEOUT_MS 2000
|
#define IPC_TIMEOUT_MS 2000
|
||||||
#define MAX_HTTP_HEADER (64 * 1024)
|
#define MAX_HTTP_HEADER (64 * 1024)
|
||||||
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 上传上限 */
|
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */
|
||||||
|
#define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */
|
||||||
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
|
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
|
||||||
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
|
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
|
||||||
#define LOG_MAX_LINES 1000
|
#define LOG_MAX_LINES 1000
|
||||||
|
#define NOTIF_PREVIEW_MAX 200 /* 通知列表返回上限(每条约 0.2KB,总 <50KB) */
|
||||||
|
#define TEXT_PREVIEW_MAX (1024 * 1024) /* 文本预览上限(超出截断,truncated=true;14MB 约束下 1MB 缓冲安全) */
|
||||||
|
|
||||||
/* 白名单子目录(TF 卡根下) */
|
/* 白名单子目录(TF 卡根下) */
|
||||||
static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
|
static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
|
||||||
@@ -89,6 +103,29 @@ static const char *g_sensitive_names[] = {
|
|||||||
static const char *g_sock_perf = PERF_SOCK;
|
static const char *g_sock_perf = PERF_SOCK;
|
||||||
static const char *g_sock_power = POWER_SOCK;
|
static const char *g_sock_power = POWER_SOCK;
|
||||||
|
|
||||||
|
/* 初始化 TF 卡派生路径(main 开头调用)。env MEOW_TF_ROOT 可覆盖卡根(宿主测试用);
|
||||||
|
* env MEOW_NOTIF_LOG 可覆盖通知转储日志路径(宿主测试用)。默认设备卡路径。 */
|
||||||
|
static void init_tf_paths(void)
|
||||||
|
{
|
||||||
|
const char *env = getenv("MEOW_TF_ROOT");
|
||||||
|
if (env && env[0]) {
|
||||||
|
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", env);
|
||||||
|
/* 去掉末尾 `/`(规范化处统一按无尾斜杠处理) */
|
||||||
|
size_t l = strlen(g_tf_root_buf);
|
||||||
|
while (l > 1 && g_tf_root_buf[l - 1] == '/') g_tf_root_buf[--l] = 0;
|
||||||
|
}
|
||||||
|
snprintf(g_tf_card_buf, sizeof g_tf_card_buf, "%s/MeowOS", TF_ROOT);
|
||||||
|
snprintf(g_apps_dir_buf, sizeof g_apps_dir_buf, "%s/MeowAPPs", TF_ROOT);
|
||||||
|
snprintf(g_logs_dir_buf, sizeof g_logs_dir_buf, "%s/MeowOS/logs", TF_ROOT);
|
||||||
|
snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s/MeowOS/notifications/notifications.log", TF_ROOT);
|
||||||
|
env = getenv("MEOW_NOTIF_LOG");
|
||||||
|
if (env && env[0]) snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s", env);
|
||||||
|
g_tf_card = g_tf_card_buf;
|
||||||
|
g_apps_dir = g_apps_dir_buf;
|
||||||
|
g_logs_dir = g_logs_dir_buf;
|
||||||
|
g_notif_log = g_notif_log_buf;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 有界动态字符串(sbuf) ---------------- */
|
/* ---------------- 有界动态字符串(sbuf) ---------------- */
|
||||||
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
|
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
|
||||||
|
|
||||||
@@ -146,6 +183,16 @@ static void sA_str(sbuf_t *s, const char *v)
|
|||||||
sA(s, "\"");
|
sA(s, "\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 追加一段原始字节到 sbuf(长度明确,绕过 %s 的固定缓冲限制;用于大 JSON 数组复用)。 */
|
||||||
|
static void sA_append(sbuf_t *s, const char *b, size_t n)
|
||||||
|
{
|
||||||
|
if (!b || n == 0) return;
|
||||||
|
if (sb_reserve(s, n) < 0) return;
|
||||||
|
memcpy(s->b + s->n, b, n);
|
||||||
|
s->n += n;
|
||||||
|
s->b[s->n] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
|
static void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
|
||||||
|
|
||||||
/* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */
|
/* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */
|
||||||
@@ -156,6 +203,7 @@ static int send_ok(int sock, const char *data);
|
|||||||
static int send_err(int sock, int status, const char *code, const char *msg);
|
static int send_err(int sock, int status, const char *code, const char *msg);
|
||||||
static int send_json(int sock, int status, const char *data);
|
static int send_json(int sock, int status, const char *data);
|
||||||
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
|
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
|
||||||
|
static void panel_log(const char *fmt, ...);
|
||||||
|
|
||||||
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
|
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
|
||||||
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
|
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
|
||||||
@@ -795,6 +843,7 @@ done:
|
|||||||
/* ---------------- 路径安全 ---------------- */
|
/* ---------------- 路径安全 ---------------- */
|
||||||
static int tf_present(void)
|
static int tf_present(void)
|
||||||
{
|
{
|
||||||
|
if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */
|
||||||
FILE *f = fopen("/proc/mounts", "r");
|
FILE *f = fopen("/proc/mounts", "r");
|
||||||
if (!f) return 0;
|
if (!f) return 0;
|
||||||
char line[512];
|
char line[512];
|
||||||
@@ -836,7 +885,8 @@ static int is_sensitive_name(const char *base)
|
|||||||
static int normalize_path(const char *in, char *buf, size_t buflen)
|
static int normalize_path(const char *in, char *buf, size_t buflen)
|
||||||
{
|
{
|
||||||
size_t rl = strlen(TF_ROOT);
|
size_t rl = strlen(TF_ROOT);
|
||||||
char joined[1024];
|
if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */
|
||||||
|
char joined[2048];
|
||||||
if (in[0] == '/') {
|
if (in[0] == '/') {
|
||||||
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
|
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
|
||||||
snprintf(joined, sizeof joined, "%s", in);
|
snprintf(joined, sizeof joined, "%s", in);
|
||||||
@@ -849,10 +899,13 @@ static int normalize_path(const char *in, char *buf, size_t buflen)
|
|||||||
} else {
|
} else {
|
||||||
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||||||
}
|
}
|
||||||
/* 逐分量规范化:/a/b/../c → /a/c;拒绝逃逸出 root */
|
/* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。
|
||||||
char out[1024];
|
* 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */
|
||||||
|
char out[2048];
|
||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
out[0] = 0;
|
out[0] = '/';
|
||||||
|
out[1] = 0;
|
||||||
|
n = 1;
|
||||||
char *p = joined;
|
char *p = joined;
|
||||||
while (*p) {
|
while (*p) {
|
||||||
while (*p == '/') p++;
|
while (*p == '/') p++;
|
||||||
@@ -863,24 +916,27 @@ static int normalize_path(const char *in, char *buf, size_t buflen)
|
|||||||
seg[k] = 0;
|
seg[k] = 0;
|
||||||
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
|
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
|
||||||
if (!strcmp(seg, "..")) {
|
if (!strcmp(seg, "..")) {
|
||||||
/* 弹出一级;若已在 root 再 .. 则逃逸 */
|
/* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */
|
||||||
if (n == 0) return -1;
|
if (n <= 1) return -1;
|
||||||
while (n > 0 && out[n - 1] != '/') n--;
|
while (n > 1 && out[n - 1] != '/') n--;
|
||||||
if (n > 0) n--; /* 去掉结尾 / */
|
if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */
|
||||||
out[n] = 0;
|
out[n] = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (k > 0) {
|
if (k > 0) {
|
||||||
size_t need = strlen(out) + 1 + k + 1;
|
size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1;
|
||||||
if (need >= sizeof out) return -1;
|
if (need >= sizeof out) return -1;
|
||||||
if (out[0]) strncat(out, "/", sizeof out - strlen(out) - 1);
|
if (n > 1) { out[n++] = '/'; out[n] = 0; }
|
||||||
strncat(out, seg, sizeof out - strlen(out) - 1);
|
memcpy(out + n, seg, (size_t)k);
|
||||||
|
n += (size_t)k;
|
||||||
|
out[n] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* 空 → root */
|
/* 空/根(n==1,仅 "/")→ 卡根 */
|
||||||
if (!out[0]) snprintf(out, sizeof out, "%s", TF_ROOT);
|
if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT);
|
||||||
if (strlen(out) >= buflen) return -1;
|
size_t ol = strlen(out);
|
||||||
snprintf(buf, buflen, "%s", out);
|
if (ol >= buflen) return -1;
|
||||||
|
memcpy(buf, out, ol + 1); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,16 +977,22 @@ static int files_list(int sock, const char *relpath)
|
|||||||
{
|
{
|
||||||
char abs[1024], rel[1024];
|
char abs[1024], rel[1024];
|
||||||
if (normalize_path(relpath, abs, sizeof abs) < 0)
|
if (normalize_path(relpath, abs, sizeof abs) < 0)
|
||||||
return 403;
|
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||||
if (!path_in_whitelist(abs)) return 403;
|
if (!path_in_whitelist(abs))
|
||||||
|
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (stat(abs, &st) < 0) return 404;
|
if (stat(abs, &st) < 0)
|
||||||
if (!S_ISDIR(st.st_mode)) return 404;
|
return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||||||
|
if (!S_ISDIR(st.st_mode))
|
||||||
|
return send_err(sock, 404, "NOT_FOUND", "非目录");
|
||||||
DIR *d = opendir(abs);
|
DIR *d = opendir(abs);
|
||||||
if (!d) return 500;
|
if (!d)
|
||||||
|
return send_err(sock, 500, "INTERNAL", "读目录失败");
|
||||||
make_rel(abs, rel, sizeof rel);
|
make_rel(abs, rel, sizeof rel);
|
||||||
sbuf_t s = {0};
|
sbuf_t s = {0};
|
||||||
sA(&s, "{\"root\":\"" TF_ROOT "\",\"path\":");
|
sA(&s, "{\"root\":");
|
||||||
|
sA_str(&s, TF_ROOT);
|
||||||
|
sA(&s, ",\"path\":");
|
||||||
sA_str(&s, abs);
|
sA_str(&s, abs);
|
||||||
sA(&s, ",\"rel\":");
|
sA(&s, ",\"rel\":");
|
||||||
sA_str(&s, rel);
|
sA_str(&s, rel);
|
||||||
@@ -962,17 +1024,19 @@ static int files_list(int sock, const char *relpath)
|
|||||||
/* send_json 前置声明 */
|
/* send_json 前置声明 */
|
||||||
static int send_json(int sock, int status, const char *data);
|
static int send_json(int sock, int status, const char *data);
|
||||||
|
|
||||||
/* 读文本文件(<1MB),返回 malloc 内容;二进制检测 -2;太大 -3;打不开 -1。 */
|
/* 读文本文件(预览上限 TEXT_PREVIEW_MAX,超出截断返回 truncated):
|
||||||
|
* 返回 malloc 内容(NUL 结尾);二进制检测 -2;打不开 -1。size=文件真实字节数(非截断后)。 */
|
||||||
static char *read_text_file_limited(const char *abs, long long *size, int *truncated)
|
static char *read_text_file_limited(const char *abs, long long *size, int *truncated)
|
||||||
{
|
{
|
||||||
int fd = open_safe_for_read(abs, 0);
|
int fd = open_safe_for_read(abs, 0);
|
||||||
if (fd < 0) return NULL;
|
if (fd < 0) return NULL;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (fstat(fd, &st) < 0 || st.st_size > 1024 * 1024) { close(fd); return NULL; }
|
if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; }
|
||||||
if (st.st_size < 0) { close(fd); return NULL; }
|
long long total = (long long)st.st_size;
|
||||||
char *buf = malloc((size_t)st.st_size + 1);
|
size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total;
|
||||||
|
char *buf = malloc(cap + 1);
|
||||||
if (!buf) { close(fd); return NULL; }
|
if (!buf) { close(fd); return NULL; }
|
||||||
ssize_t n = read(fd, buf, (size_t)st.st_size);
|
ssize_t n = read(fd, buf, cap);
|
||||||
close(fd);
|
close(fd);
|
||||||
if (n < 0) { free(buf); return NULL; }
|
if (n < 0) { free(buf); return NULL; }
|
||||||
buf[n] = 0;
|
buf[n] = 0;
|
||||||
@@ -984,8 +1048,8 @@ static char *read_text_file_limited(const char *abs, long long *size, int *trunc
|
|||||||
if (c < 0x09) ctrl++;
|
if (c < 0x09) ctrl++;
|
||||||
}
|
}
|
||||||
if (ctrl > n / 10) { free(buf); return (char *)-2; }
|
if (ctrl > n / 10) { free(buf); return (char *)-2; }
|
||||||
if (size) *size = (long long)n;
|
if (size) *size = total;
|
||||||
if (truncated) *truncated = 0;
|
if (truncated) *truncated = (total > (long long)cap) ? 1 : 0;
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1286,7 +1350,10 @@ static int read_request(int sock, req_t *r)
|
|||||||
}
|
}
|
||||||
char *body_start = buf + hend + 4; /* body 实际起点 */
|
char *body_start = buf + hend + 4; /* body 实际起点 */
|
||||||
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
|
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
|
||||||
if (clen > MAX_HTTP_BODY) { free(buf); return -1; }
|
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。 */
|
||||||
|
size_t cap = MAX_HTTP_BODY;
|
||||||
|
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = MAX_UPLOAD_BODY;
|
||||||
|
if (clen > cap) { free(buf); return -2; }
|
||||||
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
|
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
|
||||||
char *body = malloc(clen + 1);
|
char *body = malloc(clen + 1);
|
||||||
if (!body) { free(buf); return -1; }
|
if (!body) { free(buf); return -1; }
|
||||||
@@ -1425,7 +1492,28 @@ static int send_file(int sock, int status, const char *ctype, const char *abspat
|
|||||||
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
|
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
|
||||||
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
|
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
|
||||||
*/
|
*/
|
||||||
typedef struct { char path[1400]; char filename[256]; } upfile_t;
|
typedef struct { char path[1400]; char filename[320]; } upfile_t;
|
||||||
|
|
||||||
|
/* 安全文件名:取 basename(去目录分隔符),拒绝空/`.`/`..`/含控制字符或分隔符。
|
||||||
|
* 返回 0=ok 写入 out;-1=拒绝。用于防上传文件名逃逸白名单(如 `../../etc/passwd`)。 */
|
||||||
|
static int safe_filename(const char *name, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
if (!name || !name[0]) return -1;
|
||||||
|
const char *b = name;
|
||||||
|
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
|
||||||
|
const char *slash = strrchr(b, '/');
|
||||||
|
const char *bsl = strrchr(b, '\\');
|
||||||
|
if (bsl && (!slash || bsl > slash)) slash = bsl;
|
||||||
|
if (slash) b = slash + 1;
|
||||||
|
if (!b[0]) return -1;
|
||||||
|
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
|
||||||
|
for (const char *p = b; *p; p++) {
|
||||||
|
unsigned char c = (unsigned char)*p;
|
||||||
|
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
|
||||||
|
}
|
||||||
|
snprintf(out, cap, "%s", b);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
||||||
{
|
{
|
||||||
@@ -1469,10 +1557,12 @@ static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
|||||||
char *l = ln;
|
char *l = ln;
|
||||||
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||||||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||||||
char *nm = strstr(l, "name=\"");
|
/* name 与 filename 常同行(name="file"; filename="up.txt")。
|
||||||
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
|
* 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */
|
||||||
char *fn = strstr(l, "filename=\"");
|
char *fn = strstr(l, "filename=\"");
|
||||||
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
|
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
|
||||||
|
char *nm = strstr(l, "name=\"");
|
||||||
|
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* 数据结束:下一个 "--boundary" 之前 */
|
/* 数据结束:下一个 "--boundary" 之前 */
|
||||||
@@ -1487,18 +1577,25 @@ static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
|||||||
curpath[dl] = 0;
|
curpath[dl] = 0;
|
||||||
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
|
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
|
||||||
size_t dl = (size_t)(data_end - data_start);
|
size_t dl = (size_t)(data_end - data_start);
|
||||||
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", fname);
|
char safe[260];
|
||||||
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", curpath);
|
if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */
|
||||||
/* 写文件 */
|
/* 目标目录规范化 + 白名单校验 */
|
||||||
char abs[1024];
|
char absdir[1024];
|
||||||
if (normalize_path(files[nfiles].path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { nfiles++; continue; }
|
if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; }
|
||||||
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s/%s", abs, fname);
|
/* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */
|
||||||
int fd = open(files[nfiles].path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
char joined[1400];
|
||||||
|
snprintf(joined, sizeof joined, "%s/%s", absdir, safe);
|
||||||
|
char abs[1400];
|
||||||
|
if (normalize_path(joined, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { p = data_end; continue; }
|
||||||
|
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", safe);
|
||||||
|
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", abs);
|
||||||
|
int fd = open(abs, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
if (fd >= 0) {
|
if (fd >= 0) {
|
||||||
write_full(fd, data_start, dl);
|
write_full(fd, data_start, dl);
|
||||||
close(fd);
|
close(fd);
|
||||||
|
nfiles++;
|
||||||
}
|
}
|
||||||
nfiles++;
|
/* fd<0(不可写)→ 不计数,跳过该文件 */
|
||||||
}
|
}
|
||||||
p = data_end;
|
p = data_end;
|
||||||
}
|
}
|
||||||
@@ -2065,7 +2162,9 @@ static int api_logs_list(int sock)
|
|||||||
DIR *d = opendir(LOGS_DIR);
|
DIR *d = opendir(LOGS_DIR);
|
||||||
if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在");
|
if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在");
|
||||||
sbuf_t s = {0};
|
sbuf_t s = {0};
|
||||||
sA(&s, "{\"dir\":\"" LOGS_DIR "\",\"files\":[");
|
sA(&s, "{\"dir\":");
|
||||||
|
sA_str(&s, LOGS_DIR);
|
||||||
|
sA(&s, ",\"files\":[");
|
||||||
int first = 1;
|
int first = 1;
|
||||||
struct dirent *e;
|
struct dirent *e;
|
||||||
while ((e = readdir(d))) {
|
while ((e = readdir(d))) {
|
||||||
@@ -2164,23 +2263,224 @@ static int api_logs_read(int sock, const req_t *r)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 通知(后端跨进程不可读 → 501) ---------------- */
|
/* ---------------- 通知(数据源 = TF 卡通知转储日志) ----------------
|
||||||
/* 注:通知存储为 launcher 进程内存(meow_notif.c g_slots[],NOTIFICATION_SYSTEM §4),
|
* 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务):
|
||||||
* 后端为独立 svcd 子进程,跨进程无法读取/清撤那批内存槽;框架仅在关机前把通知 dump 到
|
* 1) launcher IPC 查询 —— meow_ipc.h 已定义 MSG_NOTIFY_LIST(0x23),但 launcher 侧仅作
|
||||||
* TF 卡 MeowOS/notifications/notifications.log(CSV 追加,非活跃快照)。故本后端无法实现
|
* 守护→App 下发行(util.c:1079「not dispatched here」),**无客户端查询请求类型**,
|
||||||
* 活跃通知列表/清撤 —— 需为通知后端另开 IPC 查询(如 MSG_NOTIFY_LIST 应答)或持久化活跃集,
|
* 后端(svcd 独立进程)无法据此取到活跃清单 → 跳过,回退 ②。详见报告「需 launcher 侧配合项」。
|
||||||
* 见 MEOWPANEL_PROTOCOL §6 待定案。当前一律 501。 */
|
* 2) TF 卡通知转储文件 `<卡根>/MeowOS/notifications/notifications.log`:
|
||||||
|
* launcher 把每条通知归档时追加一行 CSV(meow_notif.c tf_line:CSV 追加 + 10MB 滚动,开机清空)。
|
||||||
|
* 字段顺序:timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,
|
||||||
|
* ongoing,media,dismissed(前 6/7/11/12/13 为裸整数,其余引号包裹 + 内部 " 翻倍)。
|
||||||
|
* 注意:**归档发生在置 dismissed=1 之前(meow_notif_dismiss 先 archive_one 后置 dismissed)**,
|
||||||
|
* 故日志的 dismissed 列恒 ≈0(不可靠),且日志是「会话级归档」,非活跃快照。
|
||||||
|
* 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},**不伪造数据**。
|
||||||
|
* clear(dismiss_all)语义:保留活动中的媒体/进度通知(ongoing||media||progress>=0||category=="media"),
|
||||||
|
* 其余从日志移除。单条删除:按 id 移除该行。 */
|
||||||
|
typedef struct {
|
||||||
|
long long ms; /* created_ms(epoch ms) */
|
||||||
|
unsigned id;
|
||||||
|
char app_id[40];
|
||||||
|
char app_icon[64];
|
||||||
|
char category[40];
|
||||||
|
int urgency;
|
||||||
|
int progress; /* -1=无 */
|
||||||
|
char summary[96];
|
||||||
|
char body[128];
|
||||||
|
char target[64];
|
||||||
|
int ongoing, media, dismissed;
|
||||||
|
} notif_rec_t;
|
||||||
|
|
||||||
|
/* 取一个 CSV 字段(引号包裹 → 解转义;裸字段 → 原样;末后跳过 ',')。返回 0。 */
|
||||||
|
static int notif_csv_field(const char **pp, char *out, int outcap)
|
||||||
|
{
|
||||||
|
const char *p = *pp;
|
||||||
|
int o = 0;
|
||||||
|
if (*p == '"') {
|
||||||
|
p++;
|
||||||
|
while (*p && *p != '"') {
|
||||||
|
if (*p == '"' && p[1] == '"') {
|
||||||
|
if (o < outcap - 1) out[o++] = '"';
|
||||||
|
p += 2;
|
||||||
|
} else {
|
||||||
|
if (o < outcap - 1) out[o++] = *p;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (*p == '"') p++;
|
||||||
|
} else {
|
||||||
|
while (*p && *p != ',' && *p != '\n' && *p != '\r') {
|
||||||
|
if (o < outcap - 1) out[o++] = *p;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (o < outcap) { out[o] = 0; }
|
||||||
|
else if (outcap > 0) { out[outcap - 1] = 0; }
|
||||||
|
if (*p == ',') p++;
|
||||||
|
*pp = p;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int notif_parse_line(const char *line, notif_rec_t *r)
|
||||||
|
{
|
||||||
|
const char *p = line;
|
||||||
|
char f1[32], f2[16], f6[8], f7[16], f11[8], f12[8], f13[8];
|
||||||
|
char app[40], icon[64], cat[40], sum[96], body[128], tgt[64];
|
||||||
|
notif_csv_field(&p, f1, sizeof f1);
|
||||||
|
notif_csv_field(&p, f2, sizeof f2);
|
||||||
|
notif_csv_field(&p, app, sizeof app);
|
||||||
|
notif_csv_field(&p, icon, sizeof icon);
|
||||||
|
notif_csv_field(&p, cat, sizeof cat);
|
||||||
|
notif_csv_field(&p, f6, sizeof f6);
|
||||||
|
notif_csv_field(&p, f7, sizeof f7);
|
||||||
|
notif_csv_field(&p, sum, sizeof sum);
|
||||||
|
notif_csv_field(&p, body, sizeof body);
|
||||||
|
notif_csv_field(&p, tgt, sizeof tgt);
|
||||||
|
notif_csv_field(&p, f11, sizeof f11);
|
||||||
|
notif_csv_field(&p, f12, sizeof f12);
|
||||||
|
notif_csv_field(&p, f13, sizeof f13);
|
||||||
|
r->ms = strtoll(f1, NULL, 10);
|
||||||
|
r->id = (unsigned)strtoul(f2, NULL, 10);
|
||||||
|
r->urgency = atoi(f6);
|
||||||
|
r->progress = atoi(f7);
|
||||||
|
r->ongoing = atoi(f11);
|
||||||
|
r->media = atoi(f12);
|
||||||
|
r->dismissed = atoi(f13);
|
||||||
|
snprintf(r->app_id, sizeof r->app_id, "%s", app);
|
||||||
|
snprintf(r->app_icon, sizeof r->app_icon, "%s", icon);
|
||||||
|
snprintf(r->category, sizeof r->category, "%s", cat);
|
||||||
|
snprintf(r->summary, sizeof r->summary, "%s", sum);
|
||||||
|
snprintf(r->body, sizeof r->body, "%s", body);
|
||||||
|
snprintf(r->target, sizeof r->target, "%s", tgt);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读通知日志,保留最新 maxrec 条(环形缓冲,避免全量进内存)。返回条数;-1=文件不存在/无卡。 */
|
||||||
|
static int notif_load_list(notif_rec_t *recs, int maxrec)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(g_notif_log, "r");
|
||||||
|
if (!f) return -1;
|
||||||
|
int total = 0, head = 0;
|
||||||
|
char line[1400];
|
||||||
|
notif_rec_t r;
|
||||||
|
while (fgets(line, sizeof line, f)) {
|
||||||
|
if (notif_parse_line(line, &r) != 0) continue;
|
||||||
|
recs[head] = r;
|
||||||
|
head = (head + 1) % maxrec;
|
||||||
|
if (total < maxrec) total++;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
if (total < maxrec) return total; /* 未满窗:recs[0..total-1] 即 oldest..newest */
|
||||||
|
/* 满窗:oldest 在 recs[head],重排为 oldest..newest 连续 */
|
||||||
|
notif_rec_t tmp[NOTIF_PREVIEW_MAX];
|
||||||
|
for (int i = 0; i < maxrec; i++) tmp[i] = recs[(head + i) % maxrec];
|
||||||
|
memcpy(recs, tmp, (size_t)maxrec * sizeof recs[0]);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* app_id → 展示名("system" → "系统";其余用 app_id 兜底,前端可回退)。 */
|
||||||
|
static const char *notif_app_name(const char *app_id)
|
||||||
|
{
|
||||||
|
if (!strcmp(app_id, "system")) return "系统";
|
||||||
|
return app_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void notif_emit_item(sbuf_t *out, const notif_rec_t *r)
|
||||||
|
{
|
||||||
|
sA(out, "{\"id\":%u,\"app_id\":", r->id);
|
||||||
|
sA_str(out, r->app_id);
|
||||||
|
sA(out, ",\"app_name\":");
|
||||||
|
sA_str(out, notif_app_name(r->app_id));
|
||||||
|
sA(out, ",\"app_icon\":");
|
||||||
|
sA_str(out, r->app_icon);
|
||||||
|
sA(out, ",\"category\":");
|
||||||
|
sA_str(out, r->category);
|
||||||
|
sA(out, ",\"summary\":");
|
||||||
|
sA_str(out, r->summary);
|
||||||
|
sA(out, ",\"body\":");
|
||||||
|
sA_str(out, r->body);
|
||||||
|
sA(out, ",\"urgency\":%d,\"ts\":%lld,\"progress\":", r->urgency, r->ms > 0 ? r->ms / 1000 : 0);
|
||||||
|
if (r->progress >= 0) sA(out, "%d", r->progress); else sA(out, "null");
|
||||||
|
sA(out, ",\"ongoing\":%s,\"media\":%s,\"target_activity\":",
|
||||||
|
r->ongoing ? "true" : "false", r->media ? "true" : "false");
|
||||||
|
sA_str(out, r->target);
|
||||||
|
sA(out, ",\"dismissed\":%s}", r->dismissed ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
static int api_notifications(int sock)
|
static int api_notifications(int sock)
|
||||||
{
|
{
|
||||||
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知存储为 launcher 进程内存,后端跨进程不可读(需通知后端暴露 IPC 查询/清撤)");
|
notif_rec_t recs[NOTIF_PREVIEW_MAX];
|
||||||
|
int n = notif_load_list(recs, NOTIF_PREVIEW_MAX);
|
||||||
|
if (n < 0) return send_json(sock, 200, "{\"count\":0,\"items\":[],\"notifications\":[],\"source\":\"none\"}");
|
||||||
|
sbuf_t arr = {0};
|
||||||
|
int first = 1;
|
||||||
|
for (int i = n - 1; i >= 0; i--) { /* newest first */
|
||||||
|
if (!first) sA(&arr, ",");
|
||||||
|
first = 0;
|
||||||
|
notif_emit_item(&arr, &recs[i]);
|
||||||
|
}
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"count\":%d,\"source\":\"tf\",\"items\":[", n);
|
||||||
|
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||||||
|
sA(&s, "],\"notifications\":[");
|
||||||
|
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||||||
|
sA(&s, "]}");
|
||||||
|
sbuf_free(&arr);
|
||||||
|
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
static int api_notification_delete(int sock)
|
|
||||||
|
/* 重写通知日志。mode=1:保留活动媒体/进度(ongoing||media||progress>=0||category=="media"),
|
||||||
|
* 其余删除(dismiss_all 语义,对应通知中心既定语义);mode=0:删除指定 id(单条删除)。
|
||||||
|
* 返回删除行数;-1=IO 错;0=无文件(无可删)。写临时文件后 rename(避免与 launcher append 竞写)。 */
|
||||||
|
static int notif_rewrite(int mode, unsigned del_id)
|
||||||
{
|
{
|
||||||
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清撤需 launcher 通知后端 IPC,后端未实现");
|
if (!g_notif_log || !g_notif_log[0]) return -1;
|
||||||
|
char tmppath[1400];
|
||||||
|
snprintf(tmppath, sizeof tmppath, "%s.panel.tmp", g_notif_log);
|
||||||
|
FILE *in = fopen(g_notif_log, "r");
|
||||||
|
if (!in) return 0;
|
||||||
|
FILE *out = fopen(tmppath, "w");
|
||||||
|
if (!out) { fclose(in); return -1; }
|
||||||
|
int removed = 0;
|
||||||
|
char line[1400];
|
||||||
|
while (fgets(line, sizeof line, in)) {
|
||||||
|
notif_rec_t r;
|
||||||
|
int parsed = (notif_parse_line(line, &r) == 0);
|
||||||
|
int drop = 0;
|
||||||
|
if (mode == 1) {
|
||||||
|
if (!parsed) drop = 1;
|
||||||
|
else drop = !(r.ongoing || r.media || r.progress >= 0 || !strcmp(r.category, "media"));
|
||||||
|
} else {
|
||||||
|
if (parsed && r.id == del_id) drop = 1;
|
||||||
|
}
|
||||||
|
if (drop) removed++;
|
||||||
|
else fputs(line, out);
|
||||||
|
}
|
||||||
|
fclose(in);
|
||||||
|
if (fclose(out) != 0) return -1;
|
||||||
|
if (rename(tmppath, g_notif_log) < 0) { unlink(tmppath); return -1; }
|
||||||
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int api_notification_delete(int sock, unsigned id)
|
||||||
|
{
|
||||||
|
int removed = notif_rewrite(0, id);
|
||||||
|
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||||||
|
if (removed == 0) return send_err(sock, 404, "NOT_FOUND", "通知不存在");
|
||||||
|
char d[64];
|
||||||
|
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
|
||||||
|
return send_json(sock, 200, d);
|
||||||
|
}
|
||||||
|
|
||||||
static int api_notification_dismiss_all(int sock)
|
static int api_notification_dismiss_all(int sock)
|
||||||
{
|
{
|
||||||
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清空需 launcher 通知后端 IPC,后端未实现");
|
int removed = notif_rewrite(1, 0);
|
||||||
|
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||||||
|
char d[64];
|
||||||
|
snprintf(d, sizeof d, "{\"dismissed\":%d}", removed);
|
||||||
|
return send_json(sock, 200, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 文件操作 ---------------- */
|
/* ---------------- 文件操作 ---------------- */
|
||||||
@@ -2208,7 +2508,7 @@ static int api_files_read(int sock, const req_t *r)
|
|||||||
sA_str(&s, abs);
|
sA_str(&s, abs);
|
||||||
sA(&s, ",\"content\":");
|
sA(&s, ",\"content\":");
|
||||||
sA_str(&s, txt);
|
sA_str(&s, txt);
|
||||||
sA(&s, ",\"size\":%lld,\"truncated\":false}", size);
|
sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false");
|
||||||
free(txt);
|
free(txt);
|
||||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||||
sbuf_free(&s);
|
sbuf_free(&s);
|
||||||
@@ -2318,8 +2618,16 @@ static int api_files_rename(int sock, const req_t *r)
|
|||||||
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
|
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
|
||||||
char abs[1024], newabs[1400];
|
char abs[1024], newabs[1400];
|
||||||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||||
snprintf(newabs, sizeof newabs, "%s/%s", abs, pnn->str);
|
/* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */
|
||||||
/* newabs 也须在白名单(new_name 不含 /,父目录已校验,无需再 normalize) */
|
char parent[1024];
|
||||||
|
snprintf(parent, sizeof parent, "%s", abs);
|
||||||
|
char *sl = strrchr(parent, '/');
|
||||||
|
if (!sl || sl == parent) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "无法重命名根路径"); }
|
||||||
|
*sl = 0;
|
||||||
|
if (!parent[0]) snprintf(parent, sizeof parent, "%s", TF_ROOT); /* abs 顶层(如 /tmp/mptf/x)→ 父=根 */
|
||||||
|
snprintf(newabs, sizeof newabs, "%s/%s", parent, pnn->str);
|
||||||
|
/* newabs 复校白名单(new_name 无 /,父目录已校验;防御) */
|
||||||
|
if (!path_in_whitelist(newabs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||||
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
|
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
|
||||||
sbuf_t s = {0};
|
sbuf_t s = {0};
|
||||||
sA(&s, "{\"item\":{\"name\":");
|
sA(&s, "{\"item\":{\"name\":");
|
||||||
@@ -2557,7 +2865,9 @@ static int api_apps_export(int sock, const char *pkg)
|
|||||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||||
char tmp[1100];
|
char tmp[1100];
|
||||||
mkdir(TF_CARD_DIR "/tmp", 0777);
|
char card_tmp[1200];
|
||||||
|
snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR);
|
||||||
|
mkdir(card_tmp, 0777);
|
||||||
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
|
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
|
||||||
unlink(tmp);
|
unlink(tmp);
|
||||||
char cmd[1400];
|
char cmd[1400];
|
||||||
@@ -2668,7 +2978,7 @@ static int handle(int sock, req_t *r)
|
|||||||
if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r);
|
if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r);
|
||||||
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
|
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
|
||||||
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
||||||
if (!strcmp(p, "/api/notifications/dismiss_all")) return api_notification_dismiss_all(sock);
|
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
|
||||||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||||||
char seg[256];
|
char seg[256];
|
||||||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||||||
@@ -2721,8 +3031,10 @@ static int handle(int sock, req_t *r)
|
|||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock);
|
||||||
if (!strcmp(r->method, "DELETE")) {
|
if (!strcmp(r->method, "DELETE")) {
|
||||||
char seg[256];
|
char seg[256];
|
||||||
if (path_segment("/api/notifications/", p, seg, sizeof seg))
|
if (path_segment("/api/notifications/", p, seg, sizeof seg)) {
|
||||||
return api_notification_delete(sock);
|
unsigned nid = (unsigned)strtoul(seg, NULL, 10);
|
||||||
|
return api_notification_delete(sock, nid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return send_err(sock, 404, "NOT_FOUND", "端点不存在");
|
return send_err(sock, 404, "NOT_FOUND", "端点不存在");
|
||||||
@@ -2757,7 +3069,12 @@ static void handle_conn(int sock)
|
|||||||
{
|
{
|
||||||
req_t r;
|
req_t r;
|
||||||
memset(&r, 0, sizeof r);
|
memset(&r, 0, sizeof r);
|
||||||
if (read_request(sock, &r) != 0) { close(sock); return; }
|
int rr = read_request(sock, &r);
|
||||||
|
if (rr != 0) {
|
||||||
|
if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)");
|
||||||
|
close(sock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
handle(sock, &r);
|
handle(sock, &r);
|
||||||
req_free(&r);
|
req_free(&r);
|
||||||
close(sock);
|
close(sock);
|
||||||
@@ -2778,6 +3095,7 @@ int main(int argc, char **argv)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */
|
||||||
const char *env = getenv("MEOW_IPC_PERF_SOCK");
|
const char *env = getenv("MEOW_IPC_PERF_SOCK");
|
||||||
if (env && env[0]) g_sock_perf = env;
|
if (env && env[0]) g_sock_perf = env;
|
||||||
env = getenv("MEOW_IPC_POWER_SOCK");
|
env = getenv("MEOW_IPC_POWER_SOCK");
|
||||||
|
|||||||
Binary file not shown.
@@ -339,7 +339,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function doDownload(url, name) {
|
function doDownload(url, name) {
|
||||||
apiDownload(url).then(blobUrl => {
|
return apiDownload(url).then(blobUrl => {
|
||||||
const a = document.createElement('a'); a.href = blobUrl; a.download = name || 'file';
|
const a = document.createElement('a'); a.href = blobUrl; a.download = name || 'file';
|
||||||
document.body.appendChild(a); a.click(); a.remove();
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
|
||||||
@@ -432,7 +432,17 @@
|
|||||||
if (install) { const fi = $('#app-file'); if (fi) { fi.value = ''; fi.click(); } return; }
|
if (install) { const fi = $('#app-file'); if (fi) { fi.value = ''; fi.click(); } return; }
|
||||||
if (refresh) { renderApps(); return; }
|
if (refresh) { renderApps(); return; }
|
||||||
if (info) { appInfo(info.dataset.ainfo); return; }
|
if (info) { appInfo(info.dataset.ainfo); return; }
|
||||||
if (exp) { e.preventDefault(); doDownload('/api/apps/' + exp.dataset.aexport + '/export', exp.dataset.aexport + '.mpk'); return; }
|
if (exp) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (exp.dataset.busy) return; /* 打包期间防连点 */
|
||||||
|
exp.dataset.busy = '1';
|
||||||
|
const oldTxt = exp.textContent;
|
||||||
|
exp.textContent = '打包中…';
|
||||||
|
toast('正在打包导出 ' + exp.dataset.aexport + ',请稍候…');
|
||||||
|
doDownload('/api/apps/' + exp.dataset.aexport + '/export', exp.dataset.aexport + '.mpk')
|
||||||
|
.finally(() => { delete exp.dataset.busy; exp.textContent = oldTxt; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (uninst) { appUninstall(uninst.dataset.auninst); return; }
|
if (uninst) { appUninstall(uninst.dataset.auninst); return; }
|
||||||
if (goPath) { installFromPath($('#app-install-path').value); return; }
|
if (goPath) { installFromPath($('#app-install-path').value); return; }
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user