#define _GNU_SOURCE /* gethostname/strcasestr/strnlen 等 GNU/musl 扩展 */ /* meowpanel.c — MeowOS Web 面板后端(MeowPanel)。 * * 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件 * (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 `MeowOS/services.d/meowpanel.conf` * 启动、meow_sysui 监管。**不进固件**;无卡时系统核心绝不对它有任何依赖。 * * 架构(MEOWPANEL_PROTOCOL §1/§2 + SYSTEM_FRAMEWORK_SERVICES §4): * - 单进程单线程 + poll 串行处理(面板访问量极小);内嵌 HTTP server。 * - 读侧(/api/status,/api/perf,/api/system)经 meow_svc_perf IPC(MSG_PERF_QUERY→RESULT) * 聚合,**绝不各自爬 /proc**(单一权威源)。 * - 动作侧(/api/system/{reboot,shutdown,ota})经 meow_svc_power IPC(MSG_POWER_ACTION→STATUS), * 不再直连 poweroff/写 mtd5。 * - 设置镜像(/api/settings*,/api/theme*)直接读写板载 /usr/data/meow/settings.conf—— * **与 meow_ui_vol_read / meow_svc_power settings_get_int_trim 同款解析**('=' 切分 + * key 去首尾空白精确匹配;写用 "key = value\n")。后端是独立进程,无法链接 launcher 内的 * meow_settings_*(那是 launcher 进程内存表),故直接镜像文件,保持同一存储契约。 * - 文件/应用/日志/通知:白名单根 /usr/data/mnt/sd_0 下 MeowOS/Music/MeowData/MeowAPPs/book。 * * 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。 * 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库(mp_jpeg.c 自研最小 JPEG; * 字体库导入/导出走 busybox `gzip -dc | tar` 管道——与 nyai .mpk 同构,设备必带 tar/gzip)。 * 设备无 python3/lua。 */ #include #include #include #include /* strcasecmp/strncasecmp */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */ #include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */ #include "mp_jpeg.h" /* 实时画面:RGB565 → 最小 baseline JPEG(自包含,零依赖) */ /* 实时画面 / 触摸注入(阶段 C)。fb ioctl 常量按 launcher fb_compositor 同款手工定义 * (避免 libc/kernel struct ABI 差异,读入裸字节缓冲再按偏移取字段)。 */ #ifndef FBIOGET_VSCREENINFO #define FBIOGET_VSCREENINFO 0x4600 #define FBIOGET_FSCREENINFO 0x4602 #endif #ifndef FB_DEV #define FB_DEV "/dev/fb0" /* 16bpp RGB565,480x720,双层虚拟缓冲 */ #endif #ifndef TOUCH_DEV #define TOUCH_DEV "/dev/input/event1" /* goodix-ts:接收 write() 注入(本机实测可投递) */ #endif /* ---------------- 常量 ---------------- */ #define PANEL_VERSION "0.1" #define DEFAULT_PORT 8080 #define SETTINGS_FILE "/usr/data/meow/settings.conf" #define SETTINGS_HDR "# meow-settings 1" /* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;默认设备路径)。 */ static const char *g_settings_file = SETTINGS_FILE; /* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。 * TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */ static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0"; 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; /* 派生:/MeowOS */ static const char *g_apps_dir = NULL; /* 派生:/MeowAPPs */ static const char *g_logs_dir = NULL; /* 派生:/logs */ static const char *g_notif_log = NULL; /* 通知转储日志:/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 POWER_SOCK "/usr/data/meow_power.sock" #define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */ #define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */ #define NET_SOCK "/usr/data/meow_net.sock" /* meow_netmgrd 网络管理(MSG_NET_*,期2 观测层/DNS 代理) */ #define PM_SOCK "/usr/data/meow_pm.sock" /* meow_powermgr 功耗管理(MSG_PM_QUERY→RESULT) */ #define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */ #define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */ #define AMS_BIN "/usr/bin/am" /* 固件内置 amsd 用户侧 CLI(权威) */ #define AMS_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/am" /* 旧卡上路径(回退) */ #define SVCD_BIN "/usr/bin/meow_svcd" /* 固件内置 svcd 服务表 CLI(权威) */ #define SVCD_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/meow_svcd" /* 旧卡上路径(回退) */ #define BOOT_DEFAULT_FILE "/data/meow_boot_default" #define VERSION_FILE "/etc/meowos_version" #define HOSTNAME_FILE "" #define IPC_TIMEOUT_MS 2000 #define MAX_HTTP_HEADER (64 * 1024) #define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */ #define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */ #define MAX_FONT_UPLOAD (32 * 1024 * 1024) /* 字体库 tar.gz 上传上限(gbk 字库 png+idx 可能 10-20MB) */ #define MAX_JSON_IN 65536 /* JSON body 解析上限 */ #define MAX_STATIC_FREEBUF (2 * 1024 * 1024) #define LOG_MAX_LINES 1000 #define NOTIF_PREVIEW_MAX 200 /* 通知列表返回上限(每条约 0.2KB,总 <50KB) */ #define TEXT_PREVIEW_MAX (1024 * 1024) /* 文本预览上限(超出截断,truncated=true;14MB 约束下 1MB 缓冲安全) */ #define PANEL_VOL_MAX_DEFAULT 100 /* 音频音量上限默认(对齐 meow_launcher MEOW_VOL_MAX_DEFAULT;设备可经 settings.conf audio.volume_max 覆盖) */ /* 白名单子目录(TF 卡根下) */ static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" }; #define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0])) /* 日志敏感文件黑名单(相对 LOGS_DIR 或文件名) */ static const char *g_sensitive_names[] = { "settings.conf", "setting.bak", ".env", "passwd", "shadow", "meow.conf", "user.ini", "config.json", }; #define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0])) static const char *g_sock_perf = PERF_SOCK; static const char *g_sock_power = POWER_SOCK; static const char *g_sock_launcher = LAUNCHER_SOCK; static const char *g_sock_audio = AUDIO_SOCK; static const char *g_sock_net = NET_SOCK; /* meow_netmgrd(--sock-net / env MEOW_NET_SOCK 覆盖) */ static const char *g_sock_pm = PM_SOCK; /* meow_powermgr(--sock-pm / env MEOW_PM_SOCK 覆盖) */ /* ---------------- 增量帧缓存(GET /api/screen.jpeg?delta=1,阶段 C 增强) ---------------- * 服务端缓存上一帧紧凑 RGB565,按 8×8 块比对,只编码变化块(外接 bbox 子图)并以响应头回传偏移。 * 单线程 accept 循环持有,跨请求持久(见 §5.10)。 */ #define SCREEN_BLOCK 8 /* 比对块边长(像素) */ static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565(w*2 行距) */ static int g_screen_pw = 0, g_screen_ph = 0; /* 初始化 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) ---------------- */ typedef struct { char *b; size_t n; size_t cap; } sbuf_t; static int sb_reserve(sbuf_t *s, size_t extra) { if (s->n + extra + 1 <= s->cap) return 0; size_t ncap = s->cap ? s->cap : 256; while (ncap < s->n + extra + 1) ncap *= 2; char *nb = realloc(s->b, ncap); if (!nb) return -1; s->b = nb; s->cap = ncap; return 0; } static void sA(sbuf_t *s, const char *fmt, ...) { char tmp[512]; va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap); int w = vsnprintf(tmp, sizeof tmp, fmt, ap); va_end(ap); if (w < 0) { va_end(ap2); return; } if ((size_t)w < sizeof tmp) { if (sb_reserve(s, (size_t)w) < 0) { va_end(ap2); return; } memcpy(s->b + s->n, tmp, (size_t)w); s->n += (size_t)w; } else { /* 超长 %s 等:格式化结果超出固定缓冲,动态分配后再追加(防截断坏 JSON) */ char *big = malloc((size_t)w + 1); if (!big) { va_end(ap2); return; } int w2 = vsnprintf(big, (size_t)w + 1, fmt, ap2); va_end(ap2); if (w2 < 0) { free(big); return; } if (sb_reserve(s, (size_t)w2) < 0) { free(big); return; } memcpy(s->b + s->n, big, (size_t)w2); s->n += (size_t)w2; free(big); } s->b[s->n] = 0; } /* ---------------- UTF-8 消毒(JSON 字符串收口) ---------------- * 面板把外部文本(amsd/svcd/audiod/net/app.json/字体/日志…)嵌进 JSON 字符串,任一上游产出非法 * UTF-8(如 amsd 曾把「流媒体客户端」截成 16B,腰斩「端」字)都会让浏览器 JSON.parse 拒收整页。 * 统一在 sA_str 内逐字节校验:合法序列透传,非法/截断序列逐字节替换为 '?'(保持字节长度)。 */ static int utf8_seq_len(unsigned char lead) { if (lead >= 0xC2 && lead <= 0xDF) return 2; /* C2+:规避 overlong C0/C1 */ if (lead >= 0xE0 && lead <= 0xEF) return 3; /* 3 字节(含代理区,由 seq_ok 剔除) */ if (lead >= 0xF0 && lead <= 0xF4) return 4; /* F5+ 超 U+10FFFF,剔除 */ return 0; } /* 校验从 p 开始的 len 字节序列是否构成合法 UTF-8 字符(len 由 utf8_seq_len 给出)。 * 调用方须保证 p+len 在可读边界内。检查续字节/overlong/代理对/超 U+10FFFF。 */ /* 严格 JSON 平衡检查(字符串态+括号深度;要求恰好收完,防尾随垃圾/多括号)。 * jv_parse 只解析首个完整值、容忍尾随内容——浏览器 JSON.parse 不容忍,故自校验必须用本函数。 */ static int json_strict_balanced(const char *b, size_t n) { int depth = 0, instr = 0, esc = 0; for (size_t i = 0; i < n; i++) { char c = b[i]; if (instr) { if (esc) esc = 0; else if (c == '\\') esc = 1; else if (c == '"') instr = 0; continue; } if (c == '"') { instr = 1; continue; } if (c == '{' || c == '[') depth++; else if (c == '}' || c == ']') { if (--depth < 0) return 0; } } return depth == 0 && !instr; } static int utf8_seq_ok(const char *p, int len) { unsigned char c0 = (unsigned char)p[0]; for (int k = 1; k < len; k++) { unsigned char ck = (unsigned char)p[k]; if (ck == 0 || (ck & 0xC0) != 0x80) return 0; /* 截断/非续字节 */ } unsigned long cp; if (len == 2) cp = ((unsigned long)(c0 & 0x1F) << 6) | ((unsigned char)p[1] & 0x3F); else if (len == 3) cp = ((unsigned long)(c0 & 0x0F) << 12) | (((unsigned char)p[1] & 0x3F) << 6) | ((unsigned char)p[2] & 0x3F); else cp = ((unsigned long)(c0 & 0x07) << 18) | (((unsigned char)p[1] & 0x3F) << 12) | (((unsigned char)p[2] & 0x3F) << 6) | ((unsigned char)p[3] & 0x3F); if (len == 2 && cp < 0x80) return 0; /* overlong(C0/C1 已由 seq_len 拒,双保险) */ if (len == 3 && cp < 0x800) return 0; /* overlong E0 80.. */ if (len == 4 && cp < 0x10000) return 0; /* overlong F0 80.. */ if (cp >= 0xD800 && cp <= 0xDFFF) return 0; /* 代理对(JS 字符串不允许孤立) */ if (cp > 0x10FFFF) return 0; return 1; } /* 扫描 [s, s+n) 是否为合法 UTF-8:返回 1 合法(纯 ASCII 亦属),0 存在非法/截断字节。 */ static int utf8_valid_n(const char *s, size_t n) { size_t i = 0; while (i < n) { unsigned char c = (unsigned char)s[i]; if (c < 0x80) { i++; continue; } int len = utf8_seq_len(c); if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(s + i, len)) return 0; i += (size_t)len; } return 1; } /* UTF-8 消毒:将 src 前 n 字节拷贝到 dst(dst 容量 ≥ n+1,恒 NUL 结尾),非法/截断序列 * 逐字节替换为 '?'(保持字节长度)。返回 1 = 发生过替换,0 = 原样合法。无依赖,纯指针运算。 */ static int utf8_sanitize(char *dst, const char *src, size_t n) { size_t i = 0, j = 0; int changed = 0; while (i < n) { unsigned char c = (unsigned char)src[i]; if (c < 0x80) { dst[j++] = (char)c; i++; continue; } int len = utf8_seq_len(c); if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(src + i, len)) { dst[j++] = '?'; changed = 1; i++; continue; /* 逐字节替换,前进 1 重对齐 */ } memcpy(dst + j, src + i, (size_t)len); j += (size_t)len; i += (size_t)len; } dst[j] = 0; return changed; } /* JSON 字符串转义 + UTF-8 消毒(非 NUL 终止、长度明确版;控制字符转 \u00xx,非法 UTF-8 逐字节转 '?')。 */ static void sA_str_n(sbuf_t *s, const char *v, size_t n) { sA(s, "\""); size_t i = 0; while (i < n) { unsigned char c = (unsigned char)v[i]; if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); i++; } else if (c < 0x20) { sA(s, "\\u%04x", c); i++; } else if (c < 0x80) { sA(s, "%c", (int)c); i++; } else { int len = utf8_seq_len(c); if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(v + i, len)) { sA(s, "?"); i++; /* 非法/截断序列 → 逐字节替换为 '?',前进 1 重对齐 */ } else { for (int k = 0; k < len; k++) sA(s, "%c", (int)(unsigned char)v[i + k]); i += (size_t)len; } } } sA(s, "\""); } /* NUL 结尾字符串的 JSON 嵌入(统一收口:所有外部文本嵌 JSON 字符串都走这里)。 */ static void sA_str(sbuf_t *s, const char *v) { if (!v) v = ""; sA_str_n(s, v, strlen(v)); } /* 追加一段原始字节到 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 int write_full(int sock, const void *b, size_t n); static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra); static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen); 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_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 void panel_log(const char *fmt, ...); static int qparam(const char *query, const char *key, char *out, int outcap); /* 定义在文件后部;网络连接表 query 取值 */ static int tf_present(void); /* 定义在文件后部;net 日志卡根判定 */ static const char *web_root(void); /* 定义在文件后部;此处前向声明供鉴权会话路径使用 */ static int run_cmd(const char *cmd, char *out, int outcap); /* 定义在文件后部;/api/ams、/api/svcd、/api/overview 用(有界收割) */ /* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */ static const char *csv_at(const char *s, int idx, char *buf, int buflen); static int csv_count(const char *s); /* ---------------- 最简 JSON 解析器(无第三方库) ---------------- * 只覆盖 MeowPanel 实际用到的:对象 / 数组 / 字符串(含转义) / 数字 / bool / null。 * value 树:J_OBJ 的 child 为键值对链(str=key,child=value,next=下个 pair); * J_ARR 的 child 为元素链(next=下个元素)。 */ enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ }; typedef struct jv { int type; int bval; /* J_BOOL */ long long inum; /* J_NUM 整数值 */ double dnum; /* J_NUM 浮点值 */ char *str; /* J_STR: 值; J_OBJ: key */ struct jv *child; /* J_ARR: 头; J_OBJ: 头 pair */ struct jv *next; /* 链 */ } jv_t; typedef struct { const char *p; const char *end; int err; } jvp_t; static jv_t *jv_alloc(int type) { jv_t *v = calloc(1, sizeof *v); if (v) v->type = type; return v; } static void jv_free(jv_t *v) { if (!v) return; jv_t *c = v->child; while (c) { jv_t *nx = c->next; if (v->type == J_ARR) { jv_free(c); } /* 数组元素是完整值(可为对象/嵌套数组),须整体递归释放 */ else { jv_free(c->child); free(c->str); free(c); } /* 对象:pair 链 → 释放 value + key + pair 节点 */ c = nx; } free(v->str); free(v); } static void jvp_skip_ws(jvp_t *t) { while (t->p < t->end && (*t->p == ' ' || *t->p == '\t' || *t->p == '\n' || *t->p == '\r')) t->p++; } static char *jv_parse_string(jvp_t *t) { if (t->p >= t->end || *t->p != '"') { t->err = 1; return NULL; } t->p++; sbuf_t s = {0}; while (t->p < t->end && *t->p != '"') { unsigned char c = (unsigned char)*t->p++; if (c == '\\') { if (t->p >= t->end) { t->err = 1; break; } char e = *t->p++; switch (e) { case 'n': sA(&s, "\n"); break; case 't': sA(&s, "\t"); break; case 'r': sA(&s, "\r"); break; case 'b': sA(&s, "\b"); break; case 'f': sA(&s, "\f"); break; case 'u': { if (t->end - t->p < 4) { t->err = 1; sbuf_free(&s); return NULL; } unsigned int cp = 0; for (int i = 0; i < 4; i++) { char h = *t->p++; cp <<= 4; if (h>='0'&&h<='9') cp|=h-'0'; else if (h>='a'&&h<='f') cp|=h-'a'+10; else if (h>='A'&&h<='F') cp|=h-'A'+10; else { t->err=1; sbuf_free(&s); return NULL; } } /* 只支持 BMP 常见字符;代理对按 UTF-8 重算(简化:cp<0x80 单字节,否则保守转发) */ if (cp < 0x80) sA(&s, "%c", (int)cp); else if (cp < 0x800) { sA(&s, "%c", 0xc0|(cp>>6)); sA(&s, "%c", 0x80|(cp&0x3f)); } else { sA(&s, "%c", 0xe0|(cp>>12)); sA(&s, "%c", 0x80|((cp>>6)&0x3f)); sA(&s, "%c", 0x80|(cp&0x3f)); } break; } default: sA(&s, "%c", (int)e); break; } } else if (c < 0x20) { t->err = 1; break; } else sA(&s, "%c", (int)c); } if (t->p >= t->end || *t->p != '"') { t->err = 1; sbuf_free(&s); return NULL; } t->p++; if (!s.b) { s.b = calloc(1, 1); } return s.b; } static jv_t *jv_parse_value(jvp_t *t); static jv_t *jv_parse_object(jvp_t *t) { t->p++; jv_t *obj = jv_alloc(J_OBJ); if (!obj) { t->err = 1; return NULL; } jv_t **tail = &obj->child; jvp_skip_ws(t); if (t->p < t->end && *t->p == '}') { t->p++; return obj; } for (;;) { jvp_skip_ws(t); if (t->p >= t->end) { t->err = 1; jv_free(obj); return NULL; } char *key = jv_parse_string(t); if (!key) { jv_free(obj); return NULL; } jvp_skip_ws(t); if (t->p >= t->end || *t->p != ':') { t->err = 1; free(key); jv_free(obj); return NULL; } t->p++; jv_t *val = jv_parse_value(t); if (!val) { t->err = 1; free(key); jv_free(obj); return NULL; } jv_t *pair = calloc(1, sizeof *pair); if (!pair) { t->err = 1; free(key); jv_free(val); jv_free(obj); return NULL; } pair->type = J_OBJ; pair->str = key; pair->child = val; *tail = pair; tail = &pair->next; jvp_skip_ws(t); if (t->p < t->end && *t->p == ',') { t->p++; continue; } if (t->p < t->end && *t->p == '}') { t->p++; return obj; } t->err = 1; jv_free(obj); return NULL; } } static jv_t *jv_parse_array(jvp_t *t) { t->p++; jv_t *arr = jv_alloc(J_ARR); if (!arr) { t->err = 1; return NULL; } jv_t **tail = &arr->child; jvp_skip_ws(t); if (t->p < t->end && *t->p == ']') { t->p++; return arr; } for (;;) { jv_t *v = jv_parse_value(t); if (!v) { jv_free(arr); return NULL; } *tail = v; tail = &v->next; jvp_skip_ws(t); if (t->p < t->end && *t->p == ',') { t->p++; continue; } if (t->p < t->end && *t->p == ']') { t->p++; return arr; } t->err = 1; jv_free(arr); return NULL; } } static jv_t *jv_parse_value(jvp_t *t) { jvp_skip_ws(t); if (t->p >= t->end) { t->err = 1; return NULL; } char c = *t->p; if (c == '{') return jv_parse_object(t); if (c == '[') return jv_parse_array(t); if (c == '"') { char *s = jv_parse_string(t); if (!s) return NULL; jv_t *v = jv_alloc(J_STR); if (!v) { free(s); t->err = 1; return NULL; } v->str = s; return v; } if (!strncmp(t->p, "true", 4)) { t->p += 4; jv_t *v = jv_alloc(J_BOOL); if (v) v->bval = 1; return v; } if (!strncmp(t->p, "false", 5)) { t->p += 5; return jv_alloc(J_BOOL); } if (!strncmp(t->p, "null", 4)) { t->p += 4; return jv_alloc(J_NULL); } /* number */ const char *q = t->p; int isint = 1; while (q < t->end && (*q == '-' || *q == '+' || *q == '.' || *q == 'e' || *q == 'E' || isdigit((unsigned char)*q))) { if (*q == '.' || *q == 'e' || *q == 'E') isint = 0; q++; } if (q == t->p) { t->err = 1; return NULL; } char tmp[64]; size_t dlen = (size_t)(q - t->p); if (dlen >= sizeof tmp) dlen = sizeof tmp - 1; memcpy(tmp, t->p, dlen); tmp[dlen] = 0; t->p = q; jv_t *v = jv_alloc(J_NUM); if (!v) { t->err = 1; return NULL; } v->dnum = strtod(tmp, NULL); v->inum = (long long)strtoll(tmp, NULL, 10); (void)isint; return v; } static jv_t *jv_parse(const char *text, int len) { jvp_t t; t.p = text; t.end = text + len; t.err = 0; jv_t *v = jv_parse_value(&t); if (!v || t.err) { if (v) jv_free(v); return NULL; } return v; } static jv_t *jv_obj_get(const jv_t *obj, const char *key) { if (!obj || obj->type != J_OBJ) return NULL; for (const jv_t *p = obj->child; p; p = p->next) if (p->str && !strcmp(p->str, key)) return p->child; return NULL; } /* 将 jv 树序列化回 JSON 文本(追加到 sbuf)。覆盖解析器支持的 6 类型;字符串经 sA_str 转义+UTF-8 消毒。 * 注意 J_OBJ 的 child 链是「键值对」节点(str=key、child=value),此处按键值对展开,不递归其自身。 */ static void jv_to_json(sbuf_t *s, const jv_t *v) { if (!v) { sA(s, "null"); return; } switch (v->type) { case J_NULL: sA(s, "null"); break; case J_BOOL: sA(s, v->bval ? "true" : "false"); break; case J_NUM: if (v->dnum == (double)v->inum) sA(s, "%lld", v->inum); else sA(s, "%g", v->dnum); break; case J_STR: sA_str(s, v->str ? v->str : ""); break; case J_ARR: sA(s, "["); { int f = 1; for (const jv_t *e = v->child; e; e = e->next) { if (!f) sA(s, ","); f = 0; jv_to_json(s, e); } } sA(s, "]"); break; case J_OBJ: sA(s, "{"); { int f2 = 1; for (const jv_t *p = v->child; p; p = p->next) { if (!f2) { sA(s, ","); } f2 = 0; sA_str(s, p->str); sA(s, ":"); jv_to_json(s, p->child); } } sA(s, "}"); break; } } /* ---------------- 设置表(权威来源:meow_settings.c g_defs,逐键一致) ---------------- * 特别说明(与 MEOWPANEL_PROTOCOL/实现出入记录): * - ui.style 值域源码为 "md3,miui",但 SYSTEM_SETTINGS §1 定案 + MEOWPANEL_PROTOCOL §5 * 一致行为 **miuix**(对应 MEOW_STYLE_MIUIX)。前端 data-style 也用 miuix。故本表对齐为 * "md3,miuix" / "Material You,MIUIx"(实现优先,回改见 README)。 * - display.brightness 的 g_defs 为 min=0 max=255 def=101,但语义 clamp 到 [brightness_min(默认1),100] * (防 101 闪屏)。本表对元数据给出 min=1 max=100(协议 §3.2 期望),实际 set 走 brightness clamp。 */ enum { ST_INT, ST_STR, ST_ENUM, ST_BOOL }; typedef struct setdef { const char *key; int type; const char *def; int min, max; /* ST_INT 取值域;max<=0 表示无上界 */ const char *enum_names; /* ST_ENUM 逗号名 */ const char *enum_labels; /* 对应中文 */ const char *group; /* 分组标签(协议 groups 用) */ } setdef_t; static const setdef_t g_sets[] = { /* 显示 */ { "display.brightness", ST_INT, "101", 1, 100, NULL, NULL, "显示" }, { "display.brightness_min", ST_INT, "1", 0, 255, NULL, NULL, "显示" }, { "display.screen_off_mode", ST_ENUM, "panel",0,0, "panel,black", "面板下电,背光全黑", "显示" }, { "display.screen_off_timeout", ST_INT, "30", 0, 86400,NULL, NULL, "显示" }, { "display.refresh_rate", ST_ENUM, "60", 0,0, "60,30,15", "60Hz,30Hz,15Hz", "显示" }, { "display.refresh_always_max", ST_BOOL, "0", 0,0, NULL, NULL, "显示" }, { "display.theme", ST_ENUM, "dark", 0,0, "dark,light", "深色,浅色", "显示" }, { "display.theme_mode", ST_ENUM, "manual",0,0, "manual,auto", "手动,时段自动", "显示" }, { "display.theme_dark_start", ST_INT, "0", 0, 1439, NULL, NULL, "显示" }, { "display.theme_dark_end", ST_INT, "0", 0, 1439, NULL, NULL, "显示" }, { "display.background", ST_STR, "black",0,0, NULL, NULL, "显示" }, { "display.multi_font", ST_INT, "0", 0, 1, NULL, NULL, "显示" }, /* 电源 */ { "power.longpress_menu_ms", ST_INT, "3000", 0,0, NULL, NULL, "电源" }, { "power.forceoff_ms", ST_INT, "8000", 0,0, NULL, NULL, "电源" }, { "power.defeat_hotkey", ST_STR, "power_voldown",0,0, NULL, NULL, "电源" }, /* 开机 */ { "boot.default", ST_ENUM, "meow", 0,0, "meow,hiby", "MeowOS,HiByOS", "开机" }, { "boot.menu_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "开机" }, { "boot.timeout_ms", ST_INT, "5000", 0,0, NULL, NULL, "开机" }, /* 输入 */ { "input.hotkey_seq", ST_STR, "volup,volup,volup",0,0, NULL, NULL, "输入" }, { "input.hotkey_window_ms", ST_INT, "2500", 0,0, NULL, NULL, "输入" }, { "input.touch_arm_delay_ms", ST_INT, "700", 0,0, NULL, NULL, "输入" }, { "input.swipe_invert", ST_BOOL, "0", 0,0, NULL, NULL, "输入" }, /* 字体/主题 */ { "font.file", ST_STR, "MeowOS/fonts/default/regular/font.png",0,0, NULL, NULL, "字体主题" }, { "font.class_cjk", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, { "font.class_cjk_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, { "font.class_num", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, { "font.class_latin", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, { "font.class_latin_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, { "font.class_other", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, { "font.load_full", ST_INT, "0", 0,1, NULL, NULL, "字体主题" }, { "font.size.title", ST_INT, "28", 0,0, NULL, NULL, "字体主题" }, { "font.size.body", ST_INT, "20", 0,0, NULL, NULL, "字体主题" }, { "ui.style", ST_ENUM, "md3", 0,0, "md3,miuix", "Material You,MIUIx", "字体主题" }, /* 通知 */ { "notify.center_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "通知" }, { "notify.app_quota", ST_INT, "16", 0,0, NULL, NULL, "通知" }, { "notify.categories", ST_INT, "4", 1,4, NULL, NULL, "通知" }, { "notify.category_quota", ST_INT, "4", 1,16, NULL, NULL, "通知" }, { "notify.media_quota", ST_INT, "1", 1,1, NULL, NULL, "通知" }, { "notify.dnd", ST_BOOL, "0", 0,0, NULL, NULL, "通知" }, /* 日志 */ { "log.level", ST_ENUM, "info", 0,0, "error,warn,info,debug", "错误,警告,信息,调试", "日志" }, { "log.to_card", ST_BOOL, "1", 0,0, NULL, NULL, "日志" }, /* 音频 */ { "audio.volume", ST_INT, "60", 0,100, NULL, NULL, "音频" }, { "audio.volume_max", ST_INT, "100", 0,100, NULL, NULL, "音频" }, { "audio.volume_step", ST_INT, "1", 1,5, NULL, NULL, "音频" }, { "audio.output", ST_STR, "hw:0,0",0,0, NULL, NULL, "音频" }, /* 网络 */ { "network.wifi_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "网络" }, { "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" }, /* 系统 */ { "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" }, /* 面板(MeowPanel 自身,访问级 PIN 鉴权) * panel.pin = 4 位数字 PIN 的 sha256+salt 哈希(`:`,绝不存明文)。 * 与未来 MeowOS 锁屏 PIN 同源:锁屏键 `system.pin` 预留——本后端只读面板键 panel.pin;将来锁屏启用时 * 以 system.pin 为准并做同源迁移(panel.pin 视为其缓存/别名),此处仅留兼容注记,不实现。 * panel.auth_enabled/panel.auth_pass 为旧密码机制,保留兼容迁移(见鉴权段注释),新代码以 PIN 为准。 */ { "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" }, { "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" }, { "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" }, }; #define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0])) static const setdef_t *setdef_find(const char *key) { for (int i = 0; i < SETS_COUNT; i++) if (!strcmp(g_sets[i].key, key)) return &g_sets[i]; return NULL; } /* ---------------- settings.conf 解析(照 meow_ui_vol_read / settings_get_int_trim) ---------------- * '=' 切分,key 去首尾空白精确匹配(防 audio.volume 误配 audio.volume_max)。 * 路径取 g_settings_file(env MEOW_SETTINGS_FILE 可覆盖,宿主测试用)。 */ static void settings_mkdir_p(void) { /* 逐级创建 settings.conf 所在目录(mkdir -p 语义,容忍已存在)。 */ char tmp[1024]; snprintf(tmp, sizeof tmp, "%s", g_settings_file); char *slash = strrchr(tmp, '/'); if (!slash || slash == tmp) return; /* 无目录或根部 */ *slash = 0; if (!tmp[0]) return; char buf[1024]; buf[0] = 0; size_t i = 0; if (tmp[0] == '/') buf[i++] = '/'; const char *s = tmp + (tmp[0] == '/' ? 1 : 0); while (*s) { const char *tom = strchr(s, '/'); size_t l = tom ? (size_t)(tom - s) : strlen(s); if (l) { if (i && buf[i - 1] != '/') buf[i++] = '/'; if (i + l >= sizeof buf) break; memcpy(buf + i, s, l); i += l; buf[i] = 0; mkdir(buf, 0777); } if (!tom) break; s = tom + 1; } } static char *settings_get_value(const char *key) { FILE *f = fopen(g_settings_file, "r"); if (!f) return NULL; size_t kl = strlen(key); char line[256]; char *found = NULL; while (fgets(line, sizeof line, f)) { char *eq = strchr(line, '='); if (!eq) continue; char *k = line; while (*k == ' ' || *k == '\t') k++; long n = (long)(eq - k); while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue; found = eq + 1; break; } fclose(f); if (!found) return NULL; while (*found == ' ' || *found == '\t') found++; char *e = found + strlen(found); while (e > found && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0; return strdup(found); } static int settings_get_int(const char *key, int defv) { char *v = settings_get_value(key); if (!v) return defv; int r = atoi(v); free(v); return r; } /* 亮度 clamp:下限取 display.brightness_min(默认 1),上限 100(防 101 闪屏)。 * 与 meow_settings.c clamp_brightness 同口径。 */ static int brightness_clamp(int v) { int mn = settings_get_int("display.brightness_min", 1); if (mn < 1) mn = 1; if (mn > 100) mn = 100; if (v < mn) v = mn; if (v > 100) v = 100; return v; } /* 重写 settings.conf:把 key 写为 "key = value"。保留头与其它行;缺 key 则追加。 * O_TRUNC 重写。成功返回 0。 */ static int settings_write_one(const char *key, const char *val) { settings_mkdir_p(); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */ FILE *f = fopen(g_settings_file, "r"); char tmp[2048]; size_t n = 0; int c; if (f) { while (n < sizeof tmp - 1 && (c = fgetc(f)) != EOF) tmp[n++] = (char)c; fclose(f); } tmp[n] = 0; FILE *nf = fopen(g_settings_file, "w"); if (!nf) return -1; size_t klen = strlen(key); int found = 0; if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR); /* 逐行重写:头照抄;命中 key 行替换;其余照抄。匹配容忍 `key = value` 两侧空格(与 settings_get_value 同口径)。 */ char *save = NULL; for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) { char *p = ln; while (*p == ' ' || *p == '\t') p++; char *eq = strchr(p, '='); if (eq) { char *k = p; size_t km = (size_t)(eq - k); while (km > 0 && (k[km - 1] == ' ' || k[km - 1] == '\t')) km--; if (km == klen && strncmp(k, key, klen) == 0) { fprintf(nf, "%s = %s\n", key, val); found = 1; continue; } } fprintf(nf, "%s\n", ln); } if (!found) fprintf(nf, "%s = %s\n", key, val); fclose(nf); return 0; } /* 设定值→落盘规范字符串(clamp/校验集中在此)。返回 -1=非法(422),0=成功。 */ static int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap) { switch (d->type) { case ST_INT: { int v = atoi(input); if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v); else { if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; } else if (v < d->min) v = d->min; } snprintf(out, outcap, "%d", v); return 0; } case ST_BOOL: { char *q = (char *)input; while (*q == ' ' || *q == '\t') q++; int b = 0; if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1; else { long v = strtol(q, NULL, 10); if (q[0] && v != 0) b = 1; } snprintf(out, outcap, "%d", b ? 1 : 0); return 0; } case ST_ENUM: { const char *names = d->enum_names; if (!names || !names[0]) { snprintf(out, outcap, "%s", input); return 0; } int cnt = csv_count(names); char tok[128]; /* 接受名字或索引(向后兼容) */ int idx = -1; for (int i = 0; i < cnt; i++) { csv_at(names, i, tok, sizeof tok); if (!strcmp(tok, input)) { idx = i; break; } } if (idx < 0) { int n = atoi(input); if (n >= 0 && n < cnt) idx = n; } if (idx < 0) return -1; /* 非法枚举 */ csv_at(names, idx, tok, sizeof tok); snprintf(out, outcap, "%s", tok); return 0; } case ST_STR: strncpy(out, input, outcap - 1); out[outcap - 1] = 0; return 0; } return -1; } /* 恢复全部默认:重写整个文件为 头 + 所有键 def。 */ static int settings_reset_all(void) { settings_mkdir_p(); FILE *nf = fopen(g_settings_file, "w"); if (!nf) return -1; fprintf(nf, "%s\n", SETTINGS_HDR); for (int i = 0; i < SETS_COUNT; i++) fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def); fclose(nf); return 0; } /* ---------------- IPC 帧读写(同 meow_svc_perf/power 客户端约定,全小端 LE) ---------------- */ static int x_read_full(int fd, uint8_t *buf, size_t n) { size_t off = 0; while (off < n) { ssize_t r = read(fd, buf + off, n - off); if (r <= 0) return -1; off += (size_t)r; } return 0; } static int x_write_full(int fd, const uint8_t *buf, size_t n) { size_t off = 0; while (off < n) { ssize_t w = write(fd, buf + off, n - off); if (w <= 0) return -1; off += (size_t)w; } return 0; } static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len) { uint8_t hdr[MEOW_IPC_HDR_LEN]; uint32_t magic = MEOW_IPC_MAGIC, rl = len; uint16_t ver = MEOW_IPC_VERSION, t = type; memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2); memcpy(hdr + 8, &req_id, 4); memcpy(hdr + 12, &rl, 4); if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1; if (len && payload && x_write_full(fd, payload, len) < 0) return -1; return 0; } /* 读一帧:返回 payload 长度(<=cap 时复制到 buf),-1 失败;type/req_id 回传。 */ static int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap) { uint8_t hdr[MEOW_IPC_HDR_LEN]; if (x_read_full(fd, hdr, sizeof hdr) < 0) return -1; uint32_t magic, rlen, rid; uint16_t ver, t; memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(&t, hdr + 6, 2); memcpy(&rid, hdr + 8, 4); memcpy(&rlen, hdr + 12, 4); if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1; if (rlen > MEOW_IPC_MAX) return -1; if (type) *type = t; if (req_id) *req_id = rid; uint32_t n = (rlen < cap) ? rlen : cap; if (n && x_read_full(fd, buf, n) < 0) return -1; /* 若实际更长,丢弃剩余 */ for (uint32_t i = n; i < rlen; i++) { uint8_t c; if (x_read_full(fd, &c, 1) < 0) return -1; } return (int)n; } /* 带超时连 AF_UNIX 流 socket:非阻塞 connect + poll(POLLOUT)。 */ static int ipc_connect_tmo(const char *sock, int tmo_ms) { int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return -1; struct sockaddr_un a; memset(&a, 0, sizeof a); a.sun_family = AF_UNIX; strncpy(a.sun_path, sock, sizeof a.sun_path - 1); int fl = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, fl | O_NONBLOCK); int rc = connect(fd, (struct sockaddr *)&a, sizeof a); if (rc < 0 && errno == EINPROGRESS) { struct pollfd pf = { fd, POLLOUT, 0 }; int pr = poll(&pf, 1, tmo_ms); if (pr <= 0) { close(fd); return -1; } int soerr = 0; socklen_t sl = sizeof soerr; getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl); if (soerr != 0) { close(fd); return -1; } } else if (rc < 0) { close(fd); return -1; } fcntl(fd, F_SETFL, fl); /* 读/写超时兜底(正常服务应即时响应) */ struct timeval tv = { tmo_ms / 1000, (tmo_ms % 1000) * 1000 }; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); return fd; } /* 服务端握手(MSG_HELLO → MSG_ACK),app_id 用系统面板身份 os.meow.panel。 */ static int ipc_hello(int fd) { m_hello_t h; memset(&h, 0, sizeof h); strncpy(h.app_id, "os.meow.panel", sizeof h.app_id - 1); h.version = 1; if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) < 0) return -1; uint8_t buf[64]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < (int)sizeof(m_ack_t)) return -1; if (type != MSG_ACK) return -1; m_ack_t a; memcpy(&a, buf, sizeof a); return (a.code == MEOW_OK) ? 0 : -1; } /* ---------------- 读侧:meow_svc_perf IPC ---------------- * 返回 0=成功(json 已填入 out/cap),否则负错误码,errmsg 填中文说明。 */ static const char *perf_code_str(int code) { switch (code) { case MEOW_OK: return ""; /* 不应使用 */ case MEOW_ERR_INVAL: return "参数非法"; case MEOW_ERR_IO: return "读取失败"; case MEOW_ERR_NODEV: return "设备不存在"; default: return "服务端错误"; } } static int perf_subset_for_path(const char *path) { if (!strcmp(path, "/api/perf")) return PERF_SUBSET_PERF; if (!strcmp(path, "/api/system")) return 0; /* system 需 uptime/storage,取全量 */ return 0; /* /api/status */ } static int ipc_perf_query(int subset, char *out, int cap) { int fd = ipc_connect_tmo(g_sock_perf, IPC_TIMEOUT_MS); if (fd < 0) return MEOW_ERR_NODEV; int rc = MEOW_ERR_INTERNAL; char msg[128] = ""; if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; snprintf(msg, sizeof msg, "握手中断:性能服务未就绪"); goto done; } m_perf_query_t q; memset(&q, 0, sizeof q); q.subset = (uint32_t)subset; if (send_frame(fd, MSG_PERF_QUERY, 2, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "发送查询失败"); goto done; } uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < (int)sizeof(m_perf_result_t)) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "应答帧过短"); goto done; } if (type != MSG_PERF_RESULT) { rc = MEOW_ERR_INVAL; snprintf(msg, sizeof msg, "非预期应答类型 0x%04x", type); goto done; } m_perf_result_t r; memcpy(&r, buf, sizeof r); if (r.code != MEOW_OK) { rc = (int)r.code; snprintf(msg, sizeof msg, "性能服务错误: %s", perf_code_str((int)r.code)); goto done; } uint16_t jl = r.json_len; if (jl > sizeof r.json - 1) jl = (uint16_t)(sizeof r.json - 1); if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1); memcpy(out, r.json, jl); out[jl] = 0; rc = MEOW_OK; done: close(fd); /* 服务不可达/非 OK 由调用方据此返回错误;绝不静默失败、绝不降级直连 /proc。 */ return rc; } /* ---------------- 动作侧:meow_svc_power IPC ---------------- * 返回 0=受理;否则负错误码(PERM/INVAL/NODEV/…),msg 填服务端说明。 */ static const char *power_code_str(int code) { switch (code) { case MEOW_OK: return ""; case MEOW_ERR_PERM: return "无权限(需 root)"; case MEOW_ERR_INVAL: return "未知命令"; case MEOW_ERR_NODEV: return "设备/能力不支持"; case MEOW_ERR_NOENT: return "缺少升级文件"; case MEOW_ERR_IO: return "读写失败"; default: return "电源服务错误"; } } static int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st) { int fd = ipc_connect_tmo(g_sock_power, IPC_TIMEOUT_MS); if (fd < 0) return MEOW_ERR_NODEV; int rc = MEOW_ERR_INTERNAL; if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; } m_power_action_t a; memset(&a, 0, sizeof a); a.cmd = cmd; a.flag = flag; if (arg) snprintf(a.arg, sizeof a.arg, "%s", arg); if (send_frame(fd, MSG_POWER_ACTION, 3, &a, sizeof a) < 0) { rc = MEOW_ERR_IO; goto done; } uint8_t buf[256]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < (int)sizeof(m_power_status_t)) { rc = MEOW_ERR_IO; goto done; } if (type != MSG_POWER_STATUS) { rc = MEOW_ERR_INVAL; goto done; } m_power_status_t st; memcpy(&st, buf, sizeof st); if (out_st) *out_st = st; rc = (int)st.code; done: close(fd); return rc; } /* 从 svc_power 状态文本取 `k=v` 的 v(如 "coldboot=reboot" → "reboot";key 须精确匹配带 '=')。 * 读级命令(POWER_REBOOT_REASON/UPGRADE_STATUS)的 st.msg 即此形态。 */ static void svc_msg_kv(const char *msg, const char *key, char *out, size_t cap) { out[0] = 0; if (!msg || !key) return; size_t kl = strlen(key); if (strncmp(msg, key, kl) == 0 && msg[kl] == '=' && msg[kl + 1]) snprintf(out, cap, "%s", msg + kl + 1); } /* ---------------- 音频状态:meow_audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE) ---------------- * audiod 无 MSG_HELLO 握手(见 patch_v2/meow_audiod/meow_audio.c CLI:直连发空负载 QUERY, * 收 STATE 文本帧——"k=v" 空格分隔、NUL 结尾)。audiod 不在/超时/应答异常一律回 * {"available":false}(HTTP 200,不报错)——面板音频页据此显示「audiod 不可用」。 * socket 可被 env MEOW_AUDIO_SOCK / --sock-audio 覆盖(宿主/qemu 测试用,与 audiod CLI 同 env)。 */ static const char *audio_kv(const char *txt, const char *key, char *out, size_t cap) { size_t kl = strlen(key); const char *p = txt; while (p && *p) { if (!strncmp(p, key, kl) && p[kl] == '=') { p += kl + 1; size_t n = strcspn(p, " \t\n"); if (n >= cap) n = cap - 1; memcpy(out, p, n); out[n] = 0; return out; } p = strchr(p, ' '); if (p) p++; } out[0] = 0; return NULL; } static long long audio_kv_ll(const char *txt, const char *key, long long defv) { char b[64]; if (!audio_kv(txt, key, b, sizeof b) || !b[0]) return defv; return strtoll(b, NULL, 10); } /* 音频音量上限:读 settings.conf 的 audio.volume_max(权威键,与 launcher/watchdog/svc_power/sdk 同用); * 缺省回 PANEL_VOL_MAX_DEFAULT(=MEOW_VOL_MAX_DEFAULT 100)。面板写音量时据此 clamp(防炸耳红线)。 */ static int audio_vol_max(void) { int v = settings_get_int("audio.volume_max", PANEL_VOL_MAX_DEFAULT); if (v < 0) v = 0; if (v > 100) v = 100; return v; } /* 音频快照填充:把 `{"available":...}` 追加进 sbuf(供 /api/audio 单发与 /api/overview 聚合共用)。 * audiod 不在/超时/应答异常一律 `{"available":false}`(HTTP 200,不报错)——面板/首页据此降级,绝不假数据。 */ static void audio_fill(sbuf_t *s) { int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS); if (fd < 0) { sA(s, "{\"available\":false}"); return; } uint8_t buf[MEOW_IPC_MAX]; uint16_t type = 0; uint32_t rid = 0; int n = -1; if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0) n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1); close(fd); if (n <= 0 || type != MSG_AUDIO_STATE) { sA(s, "{\"available\":false}"); return; } buf[n] = 0; const char *txt = (const char *)buf; char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16]; audio_kv(txt, "app", app, sizeof app); audio_kv(txt, "fmt", fmt, sizeof fmt); audio_kv(txt, "route_mode", rmode, sizeof rmode); sA(s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0)); sA_str(s, app); sA(s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":", audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0)); sA_str(s, fmt); sA(s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":", audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0), audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0)); sA_str(s, rmode); sA(s, ",\"port\":%lld,\"vol\":%lld", audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1)); /* 电平监控(261):rms/peak deci-dB(-990=-inf),lvlseq=快照代际;透传时别漏(ABI 追加键)。 */ sA(s, ",\"rms\":%lld,\"peak\":%lld,\"lvlseq\":%lld,\"vol_max\":%d}", audio_kv_ll(txt, "rms", -990), audio_kv_ll(txt, "peak", -990), audio_kv_ll(txt, "lvlseq", 0), audio_vol_max()); } static int api_audio(int sock) { sbuf_t s = {0}; audio_fill(&s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } /* ---- 音频写命令(写级,PIN 已护):route/volume 直连 audiod(无 MSG_HELLO 握手),发帧→收 MSG_ACK。 * audiod 对非法 port 也回 MEOW_OK(内部仅记录不应用,见 meow_audiod.c MSG_AUDIO_ROUTE), * 故面板在**发出前**自行校验,绝不放 1(lineout)/4(SPDIF)/5(audiod 内部强制重烧值)。 */ static int audio_write_cmd(uint16_t type, const void *pl, uint32_t len, m_ack_t *out) { int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS); if (fd < 0) return -1; uint8_t buf[64]; uint16_t rtype = 0; uint32_t rid = 0; int n = -1; if (send_frame(fd, type, 0x22, pl, len) == 0) n = recv_frame(fd, &rtype, &rid, buf, sizeof buf); close(fd); if (n < (int)sizeof(m_ack_t) || rtype != MSG_ACK) return -1; if (out) memcpy(out, buf, sizeof *out); return 0; } /* POST /api/audio/route → 手动切输出路由。仅接受 0=auto/2=3.5/3=4.4; * **绝不接受 1/4/5**(4=SPDIF、5=audiod 内部强制重烧 FPGA 值)。写级:前端切换前确认弹窗(选错口→无声)。 */ static int api_audio_route(int sock, const char *body, size_t body_len) { if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(body, (int)body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jp = jv_obj_get(root, "port"); if (!jp || jp->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 port 数字"); } long port = (long)jp->inum; jv_free(root); if (port != 0 && port != 2 && port != 3) return send_err(sock, 422, "UNSUPPORTED", "非法路由口(仅允许 0=auto/2=3.5/3=4.4;4=SPDIF、5=audiod 内部值禁止)"); m_audio_route_t a; a.port = (uint32_t)port; m_ack_t st; if (audio_write_cmd(MSG_AUDIO_ROUTE, &a, sizeof a, &st) < 0) return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)"); sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"port\":%ld}", port); int out = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return out; } /* POST /api/audio/volume → 写音量。**必须 clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`, * 缺省 100;设备当前按 plan 为 50)。audiod 内部仅 clamp 0-100、无 vol_max 防护——面板此处是唯一把守方 * (防炸耳红线:平方锥度下靠近 100 极响,M2.96 曾有 dac 写 0=满幅事故)。 */ static int api_audio_volume(int sock, const char *body, size_t body_len) { if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(body, (int)body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jv = jv_obj_get(root, "value"); if (!jv || jv->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 value 数字"); } long v = (long)jv->inum; jv_free(root); int vmax = audio_vol_max(); if (v < 0) v = 0; if (v > vmax) v = vmax; /* 关键 clamp 红线 */ m_audio_volume_t a; a.pct = (int32_t)v; m_ack_t st; if (audio_write_cmd(MSG_AUDIO_VOLUME, &a, sizeof a, &st) < 0) return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)"); sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"value\":%ld,\"applied\":%u,\"vol_max\":%d}", v, st.status, vmax); int out = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return out; } /* ---------------- 功耗管理:meow_powermgr IPC(MSG_PM_QUERY→MSG_PM_RESULT) ---------------- * 客户端序列同 perf/power:connect → MSG_HELLO → 读 ACK → MSG_PM_QUERY(subset) → MSG_PM_RESULT(JSON)。 * powermgr 不在/超时/应答异常 → 负错误码(调用方转 503 POWER_UNAVAILABLE;overview 里转 available:false)。 */ static int ipc_pm_query(uint32_t subset, char *out, int cap) { int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS); if (fd < 0) return MEOW_ERR_NODEV; int rc = MEOW_ERR_INTERNAL; if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; } m_pm_query_t q; memset(&q, 0, sizeof q); q.subset = subset; if (send_frame(fd, MSG_PM_QUERY, 0x31, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; goto done; } uint8_t buf[MEOW_IPC_MAX]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */ if (type != MSG_PM_RESULT) { rc = MEOW_ERR_INVAL; goto done; } uint32_t c; memcpy(&c, buf, 4); if (c != MEOW_OK) { rc = (int)c; goto done; } /* powermgr 发**紧凑**负载:json 从偏移 12 起(json_len 在偏移 8);同 m_pm_result_t 布局,兼容全量/紧凑。 */ uint16_t jl; memcpy(&jl, buf + 8, 2); if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12); if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1); memcpy(out, buf + 12, jl); out[jl] = 0; rc = MEOW_OK; done: close(fd); return rc; } /* GET /api/power → powermgr 聚合 JSON(battery/estimate/breakdown,confidence 原样透传)。 */ static int api_power(int sock) { char js[MEOW_IPC_MAX]; int rc = ipc_pm_query(0, js, sizeof js); if (rc != MEOW_OK || !utf8_valid_n(js, strlen(js))) /* 上游坏字节 → 不返回,降级 */ return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用"); return send_json(sock, 200, js); } /* ---------------- powermgr 期2/期3 扩展消息号(POWER_MANAGER_DESIGN §6.1;本文件局部定,同 meow_ipc.h 号段)---------------- * meow_ipc.h 只在注释里预留 0x00D2-0x00D7,未定义宏;与 meow_powermgr.c 本地 `#ifndef` 同值对齐。 */ #ifndef MSG_PM_POLICY_GET #define MSG_PM_POLICY_GET 0x00D2 /* ↑ 策略读(空负载 → MSG_PM_RESULT policy JSON;读级任意调用者) */ #define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份) */ #define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count,0=默认) */ #define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON */ #define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count,0=默认) */ #define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON */ #define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(动作级;本期面板不暴露,预留) */ #define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(动作级;预留) */ #endif /* 期 3 · 策略写负载(item="profile"/"low-pct"/"crit-pct"/"bri-cap"/"scr-cap")。 */ typedef struct m_pm_policy_set { char item[16]; char val[32]; } m_pm_policy_set_t; /* 48 B */ /* 通用 powermgr IPC:发 req_type + 可选 payload,收任意响应帧。返回值: * >0 = json 字节长度(out 已填,*rtype 置响应帧类型;帧为 m_pm_result_t 布局 code+subset+json_len+json); * <=0 = 负 MEOW_ERR_*(含连不上 NODEV / MSG_ERR 的 code),*rtype 置收到的帧类型(MSG_ERR=0x0003)。 * 兼容服务端「紧凑负载」(json 从偏移 12 起,json_len 在偏移 8)。 */ static int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen, uint16_t *rtype, char *out, int cap) { int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS); if (fd < 0) return MEOW_ERR_NODEV; int rc = MEOW_ERR_INTERNAL; if (rtype) *rtype = 0; if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; } if (send_frame(fd, req_type, 0x41, pl, plen) < 0) { rc = MEOW_ERR_IO; goto done; } uint8_t buf[MEOW_IPC_MAX]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n <= 0) { rc = MEOW_ERR_IO; goto done; } if (rtype) *rtype = type; if (type == MSG_ERR) { m_err_t e; memcpy(&e, buf, sizeof e); rc = (int)e.code; goto done; } if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */ uint32_t c; memcpy(&c, buf, 4); if (c != MEOW_OK) { rc = (int)c; goto done; } uint16_t jl; memcpy(&jl, buf + 8, 2); if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12); if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1); memcpy(out, buf + 12, jl); out[jl] = 0; rc = (int)jl; done: close(fd); return rc; } /* GET/POST /api/power/policy → powermgr 期3 策略档位(GET 读级;POST 动作级,前端需二次确认)。 * GET:MSG_PM_POLICY_GET → policy JSON(profile 四档/低电阈值/亮度息屏上限,默认全关只透传)。 */ static int api_power_policy_get(int sock) { char js[MEOW_IPC_MAX]; uint16_t rt = 0; int rc = ipc_pm_send(MSG_PM_POLICY_GET, NULL, 0, &rt, js, sizeof js); if (rc <= 0) return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)"); if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js))) return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节"); return send_json(sock, 200, js); } /* POST /api/power/policy:body {"item":"profile|low-pct|crit-pct|bri-cap|scr-cap","value":<枚举|数字>}。 * 经 MSG_PM_POLICY_SET 写 settings.conf power.* 键(powermgr 只写键不直接动系统)。 * 服务端对非法值回 200 + {"kind":"policy","ok":false,"why":..};权限不足回 MSG_ERR(MEOW_ERR_PERM)。 */ static int api_power_policy_set(int sock, const char *body, size_t body_len) { if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(body, (int)body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value"); char item[20] = "", val[40] = ""; if (ji && ji->type == J_STR && ji->str) snprintf(item, sizeof item, "%s", ji->str); if (jv && jv->type == J_STR && jv->str) snprintf(val, sizeof val, "%s", jv->str); else if (jv && jv->type == J_NUM) snprintf(val, sizeof val, "%lld", jv->inum); jv_free(root); if (!item[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 item(profile/low-pct/crit-pct/bri-cap/scr-cap)"); if (!val[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 value"); m_pm_policy_set_t s; memset(&s, 0, sizeof s); snprintf(s.item, sizeof s.item, "%.15s", item); /* item[16] 留一个 NUL;防越界截断告警 */ snprintf(s.val, sizeof s.val, "%.31s", val); /* val[32] 同理 */ char js[MEOW_IPC_MAX]; uint16_t rt = 0; int rc = ipc_pm_send(MSG_PM_POLICY_SET, &s, sizeof s, &rt, js, sizeof js); if (rc <= 0) { if (rt == MSG_ERR) /* 权限不足:powermgr 拒绝(面板 root 仅当被白名单才放行;如实回传) */ return send_err(sock, 403, "FORBIDDEN", rc == MEOW_ERR_PERM ? "设备身份不允许写电源策略" : "电源策略写入被拒绝"); return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)"); } if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js))) return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节"); /* 服务端对非法值回 ok:false(HTTP 层 200,code 仍 OK);按其 why 映射 422。 */ if (strstr(js, "\"ok\":false")) { char why[96] = ""; jv_t *jr = jv_parse(js, (int)strlen(js)); if (jr) { jv_t *w = jv_obj_get(jr, "why"); if (w && w->type == J_STR && w->str) snprintf(why, sizeof why, "%s", w->str); jv_free(jr); } return send_err(sock, 422, "UNSUPPORTED", why[0] ? why : "非法电源策略值"); } return send_json(sock, 200, js); } /* GET /api/power/history → MSG_PM_HISTORY 采样环(30s 环形 4h,不落盘)。 * GET /api/power/events → MSG_PM_EVENTS 异常耗电/充放电/亮灭屏/背光跳变事件(内存环形)。 * 两者语义对齐 powermgr 期 2;count=0(默认)由服务端选合理上限,读级任意调用者。 */ static int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail) { uint32_t count = 0; char js[MEOW_IPC_MAX]; uint16_t rt = 0; int rc = ipc_pm_send(req_type, &count, sizeof count, &rt, js, sizeof js); if (rc <= 0) return send_err(sock, 503, "POWER_UNAVAILABLE", unavail); if (rt != want_type || !utf8_valid_n(js, strlen(js))) return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节"); return send_json(sock, 200, js); } static int api_power_history(int sock) { return api_power_window(sock, MSG_PM_HISTORY, MSG_PM_HISTORY_RESULT, "电源历史采样不可用(powermgr 未运行)"); } static int api_power_events(int sock) { return api_power_window(sock, MSG_PM_EVENTS, MSG_PM_EVENTS_RESULT, "电源事件不可用(powermgr 未运行)"); } /* ---- am/meow_svcd 二进制解析(popen 薄壳,复用有界收割 run_cmd) ---- * env MEOW_AMS_BIN / MEOW_SVCD_BIN 可覆盖(宿主/qemu 测试用;设备默认 /usr/bin,回退卡上路径)。 */ static const char *ams_bin(void) { const char *e = getenv("MEOW_AMS_BIN"); if (e && e[0]) return e; if (!access(AMS_BIN, X_OK)) return AMS_BIN; return AMS_BIN_CARD; } static const char *svcd_bin(void) { const char *e = getenv("MEOW_SVCD_BIN"); if (e && e[0]) return e; if (!access(SVCD_BIN, X_OK)) return SVCD_BIN; return SVCD_BIN_CARD; } /* ---------------- 网络管理:meow_netmgrd IPC(MSG_NET_*,期2 观测层/DNS 代理) ---------------- * 收口于独立 socket /usr/data/meow_net.sock(--sock-net / env MEOW_NET_SOCK 覆盖,宿主测试用)。 * 读级(STATUS/CONNS/DNS status|stats)netmgrd 放行任意调用者;flush 写级——HTTP 层沿用面板 * 鉴权门禁(require_auth),IPC 层身份由 netmgrd 依 SO_PEERCRED 判定(面板路径非系统身份时 * netmgrd 会拒 flush,面板如实回传其错误码/文本,不掩盖)。 * netmgrd 不在/超时/应答异常 → 503 降级 JSON({"ok":false,...}),面板其它页不受影响。 */ static int net_ipc(uint16_t req_type, const void *req, uint32_t req_len, uint16_t *ret_type, uint32_t *code, uint16_t *meta, char *out, size_t cap) { int fd = ipc_connect_tmo(g_sock_net, IPC_TIMEOUT_MS); if (fd < 0) return -1; uint8_t buf[MEOW_IPC_MAX]; uint16_t type = 0; uint32_t rid = 0; int rlen = -1; if (send_frame(fd, req_type, 0x1020, req, req_len) == 0) rlen = recv_frame(fd, &type, &rid, buf, sizeof buf); close(fd); if (rlen <= 0) return -1; if (ret_type) *ret_type = type; /* m_net_*_result 统一布局:code(u32)|len(u16)|meta/pad(u16)|content[len],content 偏移 8 */ uint32_t c = 0; uint16_t ln = 0, mv = 0; memcpy(&c, buf, 4); memcpy(&ln, buf + 4, 2); memcpy(&mv, buf + 6, 2); if (code) *code = c; if (meta) *meta = mv; if ((size_t)ln >= cap) ln = (uint16_t)(cap - 1); if ((size_t)(8 + ln) < sizeof buf) buf[8 + ln] = 0; memcpy(out, buf + 8, ln); out[ln] = 0; return (int)ln; } /* GET /api/net/status → 透传 0xE0 概览 JSON(接口表 + WiFi/BT 摘要 + 接口速率)。 */ static int api_net_status(int sock) { uint16_t t; uint32_t code; char json[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &t, &code, NULL, json, sizeof json); if (n <= 0 || t != MSG_NET_STATUS_RESULT || code != MEOW_OK || !json[0]) return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); if (!utf8_valid_n(json, strlen(json))) /* 上游坏字节(含 ssid)→ 不返回,降级 */ return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务应答包含非法字节"); return send_json(sock, 200, json); } /* 连接表一行:entity|lip:lport|rip:rport|state|age_s|domain(netmgrd build_conns_text)。 * 把每条构造成 `{...}` 对象、逗号分隔(不含外层 [])写入 sbuf,返回条数。 */ static int net_conns_parse_body(sbuf_t *s, const char *txt) { const char *p = txt; int first = 1, n = 0; while (p && *p) { const char *nl = strchr(p, '\n'); size_t ll = nl ? (size_t)(nl - p) : strlen(p); if (ll == 0) { p = nl ? nl + 1 : p + 1; continue; } char line[384]; if (ll >= sizeof line) ll = sizeof line - 1; memcpy(line, p, ll); line[ll] = 0; char *f[6]; int fn = 0; char *cur = line; while (fn < 6) { f[fn++] = cur; char *bar = strchr(cur, '|'); if (!bar) break; *bar = 0; cur = bar + 1; } if (fn >= 6) { if (!first) sA(s, ","); first = 0; sA(s, "{\"entity\":"); sA_str(s, f[0]); sA(s, ",\"local\":"); sA_str(s, f[1]); sA(s, ",\"remote\":"); sA_str(s, f[2]); sA(s, ",\"state\":"); sA_str(s, f[3]); sA(s, ",\"age\":%lld,\"domain\":", atoll(f[4])); sA_str(s, strcmp(f[5], "-") == 0 ? "" : f[5]); sA(s, "}"); n++; } p = nl ? nl + 1 : p + ll; } return n; } /* GET /api/net/conns?entity=<可选> → 0xE8 → 连接表 JSON(可实体前缀过滤)。 */ static int api_net_conns(int sock, const char *query) { m_net_conns_req_t q; memset(&q, 0, sizeof q); char ent[128]; if (qparam(query, "entity", ent, sizeof ent)) strncpy(q.entity, ent, sizeof q.entity - 1); uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_CONNS_REQ, &q, sizeof q, &t, &code, &meta, txt, sizeof txt); if (n <= 0 || t != MSG_NET_CONNS_RESULT || code != MEOW_OK) return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); sbuf_t a = {0}; int count = net_conns_parse_body(&a, txt); sbuf_t s = {0}; sA(&s, "{\"truncated\":%s,\"count\":%d,\"conns\":[", meta ? "true" : "false", count); if (a.n) sA_append(&s, a.b, a.n); sA(&s, "]}"); sbuf_free(&a); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* DNS 代理统计对象 JSON 到 s(即 `{...}` 本体,不含外层 "dns" 键)。 * 返回 0=成功;-1=传输不可达(netmgrd 不在);-2=code!=OK(DNS 代理不可用)。 * 供 /api/net/dns 单发与 /api/overview 的 net 块复用(首页严格单请求)。 */ static int net_dns_obj_fill(sbuf_t *s) { m_net_dns_ctl_t q; memset(&q, 0, sizeof q); strcpy(q.cmd, "status"); uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt); if (n <= 0 || t != MSG_NET_DNS_RESULT) return -1; if (code != MEOW_OK) return -2; /* txt: `dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d` */ char mode[32] = "", upstream[64] = ""; int enabled = 0, ch = 0, ct = 0, rmod = 0; unsigned long long qq = 0, deny = 0, first = 0; (void)sscanf(txt, "dns mode=%31s enabled=%d upstream=%63s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d", mode, &enabled, upstream, &ch, &ct, &qq, &deny, &first, &rmod); sA(s, "{\"mode\":"); sA_str(s, mode); sA(s, ",\"enabled\":%s,\"upstream\":", enabled ? "true" : "false"); sA_str(s, upstream); sA(s, ",\"cache_hits\":%d,\"cache_total\":%d,\"queries\":%llu,\"denies\":%llu,\"first_seen\":%llu,\"resolv_modified\":%s}", ch, ct, qq, deny, first, rmod ? "true" : "false"); return 0; } /* GET /api/net/dns → 0xEC status(netmgrd 对 status/stats 同输出)→ DNS 代理统计 JSON。 */ static int api_net_dns(int sock) { sbuf_t s = {0}; int rc = net_dns_obj_fill(&s); if (rc == -1) { sbuf_free(&s); return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); } if (rc == -2) { sbuf_free(&s); return send_err(sock, 503, "NET_NODEV", "DNS 代理不可用"); } int out = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return out; } /* overview 用的 net 块:netmgrd 状态 JSON + 本面板 DNS 统计(并入 "dns" 子键),首页可严格单请求。 * 返回 0=成功(已追加 `,"net":{...}`);-1=netmgrd 状态不可用(调用方回 available:false,绝不假数据)。 */ static int net_block_fill(sbuf_t *s) { uint16_t nt; uint32_t ncode; char njson[MEOW_IPC_MAX]; int nn = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &nt, &ncode, NULL, njson, sizeof njson); if (nn <= 0 || nt != MSG_NET_STATUS_RESULT || ncode != MEOW_OK || !njson[0]) return -1; /* 状态 JSON 为对象:剥外层大括号,拼入 DNS 子键(DNS 不可用则省略该键,仍回 net 状态,不谎报)。 */ size_t l = strlen(njson); while (l > 0 && (njson[l - 1] == ' ' || njson[l - 1] == '\t' || njson[l - 1] == '\n' || njson[l - 1] == '\r')) njson[--l] = 0; if (l < 2 || njson[0] != '{' || njson[l - 1] != '}') return -1; if (!utf8_valid_n(njson, l)) return -1; /* 任一上游(netmgrd 含 ssid)产出非法 UTF-8 → 整块降级,不拖垮整页 */ njson[l - 1] = 0; /* 去尾部 } */ sbuf_t dns = {0}; int drc = net_dns_obj_fill(&dns); sA(s, ",\"net\":{"); sA(s, "%s", njson + 1); /* 去首 { 的状态内层 */ if (drc == 0 && dns.b) sA(s, ",\"dns\":%s", dns.b); sA(s, "}"); /* net 块只开了一个 {,一个 } 收尾(批2 曾写 }} 多一括号,浏览器 Extra data 破页 2026-09-02) */ sbuf_free(&dns); return 0; } /* POST /api/net/dns/flush → 0xEC flush(写级:HTTP 层走 require_auth;IPC 层由 netmgrd 依身份判定)。 */ static int api_net_dns_flush(int sock) { m_net_dns_ctl_t q; memset(&q, 0, sizeof q); strcpy(q.cmd, "flush"); uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt); if (n <= 0 || t != MSG_NET_DNS_RESULT) return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); if (code == (uint32_t)MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "DNS flush 需系统身份"); if (code != MEOW_OK) return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS flush 失败"); /* txt: `dns cache flushed (N entries)` */ int entries = 0; (void)sscanf(txt, "dns cache flushed (%d entries)", &entries); sbuf_t s = {0}; sA(&s, "{\"flushed\":true,\"entries\":%d}", entries); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } #define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限(内存约束:不整读 64MB 滚动文件) */ /* GET /api/net/log → 读 TF MeowOS/logs/ 最新的 net_.log 尾部(同日志页模式)。 */ static int api_net_log(int sock) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); DIR *d = opendir(LOGS_DIR); if (!d) return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); char best[512] = ""; long best_mt = -1; struct dirent *e; while ((e = readdir(d))) { if (strncmp(e->d_name, "net_", 4) != 0) continue; if (e->d_name[0] == '.') continue; char full[1024]; snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name); struct stat st; if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue; if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); } } closedir(d); if (!best[0]) return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); char full[1024]; snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best); int fd = open(full, O_RDONLY); struct stat st; if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { if (fd >= 0) close(fd); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); } off_t off = st.st_size > NET_LOG_TAIL_MAX ? st.st_size - NET_LOG_TAIL_MAX : 0; size_t buflen = (size_t)(st.st_size - off); if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); } if (off) (void)lseek(fd, off, SEEK_SET); char *buf = malloc(buflen + 1); if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); } ssize_t n = read(fd, buf, buflen); close(fd); if (n <= 0) { free(buf); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); } sbuf_t s = {0}; sA(&s, "{\"file\":"); sA_str(&s, best); sA(&s, ",\"lines\":["); const char *p = buf, *end = buf + n; int linecnt = 0; for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++; if (linecnt > 0 && end[-1] != '\n') linecnt++; int maxlines = LOG_MAX_LINES; int startrow = linecnt - maxlines; if (startrow < 0) startrow = 0; int truncated = (off > 0 || startrow > 0) ? 1 : 0; int rowidx = 0, gotlines = 0; p = buf; while (p < end) { const char *nl = memchr(p, '\n', (size_t)(end - p)); const char *line_end = nl ? nl : end; if (rowidx >= startrow) { if (gotlines) sA(&s, ","); /* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */ sA_str_n(&s, p, (size_t)(line_end - p)); gotlines++; } rowidx++; if (!nl) break; p = nl + 1; } free(buf); sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* ---------------- netmgrd 期2/期3:接口列表 + 管控策略(只读展示 / 写级经 require_auth) ---------------- * GET /api/net/ifaces → 0xE2(name 空=全部)接口数组 + 0xE0 状态里的 rssi(WiFi 信号)+ rfkill + dns。 * 上游任一源坏字节 → available:false(绝不伪造)。netmgrd 不在 → available:false(HTTP 200,不报错)。 * GET /api/net/policy → 0xEA SHOW(只读)→ 策略文本 parse 成 JSON(enforce=dns_only 如实透传)。 * POST /api/net/policy → 0xEA 写级(SET_NET/SET_DOMAIN/DEL);HTTP 层已过 require_auth,IPC 层由 netmgrd * 依 SO_PEERCRED 判系统身份(面板精确登记 os.meow.panel),拒绝时如实回传错误码,绝不掩盖。 */ static int api_net_ifaces(int sock) { m_net_iface_req_t q; memset(&q, 0, sizeof q); uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr); if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0]) return send_json(sock, 200, "{\"available\":false}"); /* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */ if (!utf8_valid_n(arr, strlen(arr))) return send_json(sock, 200, "{\"available\":false}"); /* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */ uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX]; int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj); long long rssi = -9999; int got_rssi = 0; sbuf_t rfkill = {0}, dns = {0}; if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) { jv_t *jr = jv_parse(sj, (int)strlen(sj)); if (jr) { jv_t *w = jv_obj_get(jr, "wifi"); jv_t *r = w ? jv_obj_get(w, "rssi") : NULL; if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; } jv_t *rf = jv_obj_get(jr, "rfkill"); if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf); jv_t *bt = jv_obj_get(jr, "bt"); jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL; if (dn && dn->type == J_ARR) jv_to_json(&dns, dn); jv_free(jr); } } sbuf_t s = {0}; sA(&s, "{\"available\":true,\"rssi\":"); if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null"); sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr); sbuf_free(&rfkill); sbuf_free(&dns); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* 策略 SHOW 文本 → JSON。netmgrd 首行:`# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(...)`; * 其后每实体一行:` net=block domain=+h1,-h2` 或 `(allow)`(无规则)。 */ static void net_policy_parse(const char *txt, sbuf_t *s) { char lvl[24] = "", enf[24] = "dns_only"; const char *p = txt; sA(s, "{\"available\":true,\"enforce\":"); /* 首行找 enforce=... */ /* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */ const char *en = strstr(p, "enforce="); if (en) { en += 8; int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; } enf[i] = 0; } const char *lg = strstr(p, "log_level="); if (lg) { lg += 10; int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; } lvl[i] = 0; } sA_str(s, enf); sA(s, ",\"log_level\":"); sA_str(s, lvl); sA(s, ",\"rules\":["); int first = 1; const char *ln = txt; while (ln && *ln) { const char *nl = strchr(ln, '\n'); size_t ll = nl ? (size_t)(nl - ln) : strlen(ln); if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; } /* 跳注释行(以 # 开头)与空;数据行为 ` net=... domain=...`(entity 含冒号无空格) */ const char *c = ln; while (c < ln + ll && (*c == ' ' || *c == '\t')) c++; int is_comment = (c < ln + ll && *c == '#'); int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0); if (!is_comment && !is_none && c < ln + ll) { /* entity = 首个空白符之前 */ char entity[64] = ""; size_t ei = 0; const char *e = c; while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; } entity[ei] = 0; /* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */ char net[8] = "allow"; int has_net = 0; char allow_h[512] = "", deny_h[512] = ""; const char *k = e; if (k < ln + ll) { const char *seg = k; while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++; const char *nh = strstr(seg, "net="); if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; } const char *dh = strstr(seg, "domain="); if (dh) { dh += 7; /* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */ size_t ao = 0, do2 = 0; const char *h = dh; while (*h && *h != ' ' && *h != '\n' && *h != '\r') { if (*h == '+') { h++; if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ','; while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++; } else if (*h == '-') { h++; if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ','; while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++; } else h++; if (*h == ',') h++; } allow_h[ao] = 0; deny_h[do2] = 0; } if (!first) sA(s, ","); first = 0; sA(s, "{\"entity\":"); sA_str(s, entity); sA(s, ",\"net\":"); sA_str(s, has_net ? net : ""); sA(s, ",\"has_net\":%s", has_net ? "true" : "false"); sA(s, ",\"allow_hosts\":["); { const char *hp = allow_h; int hf = 1; while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp); if (!hf) { sA(s, ","); } hf = 0; sA_str_n(s, hp, hl); hp = comma ? comma + 1 : hp + hl; } } sA(s, "],\"deny_hosts\":["); { const char *hp = deny_h; int hf = 1; while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp); if (!hf) { sA(s, ","); } hf = 0; sA_str_n(s, hp, hl); hp = comma ? comma + 1 : hp + hl; } } sA(s, "]}"); } } ln = nl ? nl + 1 : ln + ll; } sA(s, "]}"); } /* GET /api/net/policy(只读):0xEA SHOW → JSON(rules + enforce=dns_only 如实)。 */ static int api_net_policy_get(int sock) { m_net_policy_t q; memset(&q, 0, sizeof q); q.op = NET_POLICY_SHOW; uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt); if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK) return send_json(sock, 200, "{\"available\":false}"); if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */ return send_json(sock, 200, "{\"available\":false}"); sbuf_t s = {0}; net_policy_parse(txt, &s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } /* POST /api/net/policy:body {"op":"set_net|set_domain|del","entity":"","arg":""}。 * op 缺失默认 set_net;op 非法 → 422。arg 校验交给 netmgrd(防自锁硬拒在 netmgrd 服务端)。 */ static int api_net_policy_set(int sock, const char *body, size_t body_len) { if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(body, (int)body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg"); char op[16] = "set_net", entity[64] = "", arg[64] = ""; if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str); if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str); if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str); jv_free(root); if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:/svc:/proc:)"); if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法"); int nop = 0; if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET; else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN; else if (!strcmp(op, "del")) nop = NET_POLICY_DEL; else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del"); if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg"); m_net_policy_t q; memset(&q, 0, sizeof q); q.op = (uint32_t)nop; snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */ snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */ uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt); if (n <= 0 || t != MSG_NET_POLICY_RESULT) return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); if (code == (uint32_t)MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?"); if (code == (uint32_t)MEOW_ERR_INVAL) return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法"); if (code != MEOW_OK) return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败"); sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"message\":"); sA_str(&s, txt); sA(&s, "}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* ---------------- amsd / svcd 服务表 + 首页聚合(批1 只读) ---------------- */ /* 就地去掉 ANSI 转义序列(\033[...final);svcd status all 走管道时 CLI 会去色,此处双保险)。 */ static void panel_strip_ansi(char *s) { char *r = s, *w = s; while (*r) { if (*r == '\033' && r[1] == '[') { r += 2; while (*r && !(*r >= '@' && *r <= '~')) r++; if (*r) r++; } else { *w++ = *r++; } } *w = 0; } /* 切分成空白分隔 token(就地改 buf,返回个数;= cap) { out[cap - 1] = 0; return; } if (o) out[o++] = ' '; memcpy(out + o, toks[i], strlen(toks[i])); o += strlen(toks[i]); } out[o] = 0; } static void ams_fg_fill(sbuf_t *s); /* 定义于下;ams_fill 内引用其前台摘要(前向声明) */ /* GET /api/ams:`am list`(注册表+状态)parse → App 表。amsd 不在 → available:false(绝无假数据)。 */ static void ams_fill(sbuf_t *s) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin()); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; } panel_strip_ansi(out); char *toks[256]; sbuf_t apps = {0}; int first = 1, count = 0; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "APP ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 9) { /* 布局:APP * tok[0]="APP" 前缀;自右向左固定:state=tok[n-6], pid=n-5, launches=n-4, crashes=n-3, * last_kind=n-2, last_code=n-1;pkg=tok[1],name=join(tok[2..n-6))。 */ char nm[128]; join_tokens(toks, 2, n - 6, nm, sizeof nm); if (!first) sA(&apps, ","); first = 0; count++; sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]); sA(&apps, ",\"name\":"); sA_str(&apps, nm); sA(&apps, ",\"state\":"); sA_str(&apps, toks[n-6]); sA(&apps, ",\"pid\":%d", atoi(toks[n-5])); sA(&apps, ",\"launches\":%d", atoi(toks[n-4])); sA(&apps, ",\"crashes\":%d", atoi(toks[n-3])); sA(&apps, ",\"last_kind\":"); sA_str(&apps, toks[n-2]); sA(&apps, ",\"last_code\":%d}", atoi(toks[n-1])); } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : ""); sbuf_free(&apps); ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */ sA(s, "}"); } /* `am status` → `,"fg":{...},"running":[...]`(供 /api/ams 与 overview 的 ams 块复用;仅前台摘要,轻)。 * am status 行:`RUN %s %s %ld %llu %s\n` = pkg state pid dur activity(可含空格)。 * activity=join(tok[5..n));fg=首个 state=="fg"。amsd 不在 → fg:null,running:[](绝无假数据)。 */ static void ams_fg_fill(sbuf_t *s) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin()); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; } panel_strip_ansi(out); char *toks[256]; char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0; sbuf_t run = {0}; int first = 1; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "RUN ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 6) { /* RUN pkg state pid dur activity... */ char act[160]; join_tokens(toks, 5, n, act, sizeof act); const char *st = toks[2]; if (!first) sA(&run, ","); first = 0; sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]); sA(&run, ",\"state\":"); sA_str(&run, st); sA(&run, ",\"pid\":%d", atoi(toks[3])); sA(&run, ",\"activity\":"); sA_str(&run, act); sA(&run, "}"); if (strcmp(st, "fg") == 0 && !fg_pkg[0]) { snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]); snprintf(fg_act, sizeof fg_act, "%s", act); fg_pid = atoi(toks[3]); } } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, ",\"fg\":"); if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); } else sA(s, "null"); sA(s, ",\"running\":[%s]", run.b ? run.b : ""); sbuf_free(&run); } static int api_ams(int sock) { sbuf_t s = {0}; ams_fill(&s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } /* 包名合法性校验(ams history/start/kill 共用):反向域名命名字符集,拒 /、: 与空白。 */ static int ams_pkg_ok(const char *pkg) { if (!pkg || !pkg[0]) return 0; if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0; for (const char *p = pkg; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0; if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */ } return 1; } /* GET /api/ams/:pkg → `am history `(amsd 现成:incident 登记 + 崩溃根因 detail)。 * 输出行:`EVT `(detail 可含空格,为崩溃根因/事件文本)。 * 上游 text 含崩溃 detail,须 utf8 校验坏字节降级 available:false(绝不发非法 JSON)。 * 解析部分独立成 ams_history_parse(供 selftest 喂截断 detail 断言合法 JSON)。 */ static void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */ { if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */ panel_strip_ansi(out); char *toks[256]; sbuf_t ev = {0}; int first = 1, count = 0; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (strncmp(ln, "EVT ", 4) == 0) { int n = txt_tokens(ln, toks, 256); if (n >= 5) { /* EVT epoch up kind pkg detail... */ char det[256]; join_tokens(toks, 5, n, det, sizeof det); if (!first) sA(&ev, ","); first = 0; count++; sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2])); sA_str(&ev, toks[3]); sA(&ev, ",\"detail\":"); sA_str(&ev, det); sA(&ev, "}"); } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg); sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : ""); sbuf_free(&ev); } static void ams_history_fill(sbuf_t *s, const char *pkg) { char out[16384]; char cmd[320]; snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg); int code = run_cmd(cmd, out, sizeof out); if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; } ams_history_parse(out, s, pkg); } static int api_ams_history(int sock, const char *pkg) { if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); sbuf_t s = {0}; ams_history_fill(&s, pkg); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } /* POST /api/ams/:pkg/start → `am start `(amsd→launcher MSG_AMS_START 拉起;launcher 离线回 ERR)。 * POST /api/ams/:pkg/kill → `am kill `(SIGTERM 语义,前台需前端二次确认)。均 require_auth 之后。 */ static int api_ams_ctl(int sock, const char *pkg, int kill) { if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char out[256]; char cmd[320]; snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg); int code = run_cmd(cmd, out, sizeof out); if (code == 0 && strstr(out, "OK ")) { sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start"); sA_str(&s, pkg); sA(&s, ",\"message\":"); sA_str(&s, out); sA(&s, "}"); int rc2 = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc2; } /* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */ const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败"); if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail); if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail); if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail); if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达"); return send_err(sock, 500, "INTERNAL", detail); } /* GET /api/svcd:`meow_svcd status all` parse → 服务表(state/pid/restart 计数/exec/来源)。 * svcd 未运行(exit!=0)→ available:false(绝无假数据)。 */ static void svcd_fill(sbuf_t *s) { char out[16384]; char cmd[300]; snprintf(cmd, sizeof cmd, "%s status all 2>/dev/null", svcd_bin()); int code = run_cmd(cmd, out, sizeof out); if (code != 0) { sA(s, "{\"available\":false}"); return; } panel_strip_ansi(out); char *toks[256]; sbuf_t svcs = {0}; int first = 1, count = 0; char *ln = out; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; int n = txt_tokens(ln, toks, 256); /* 数据行:NAME STATE PID RESTART BACKOFF TRIGGER SRC EXIT EXIT_TIME EXEC... * state=tok[1] 须为合法状态;exec=join(tok[9..n))。 */ if (n >= 10) { const char *st = toks[1]; int is_known = (!strcmp(st,"running") || !strcmp(st,"stopped") || !strcmp(st,"stopping") || !strcmp(st,"disabled") || !strcmp(st,"failed")); if (is_known) { char exec[512]; join_tokens(toks, 9, n, exec, sizeof exec); int restarts = 0; { const char *slash = strchr(toks[3], '/'); (void)slash; restarts = atoi(toks[3]); } if (!first) sA(&svcs, ","); first = 0; count++; sA(&svcs, "{\"name\":"); sA_str(&svcs, toks[0]); sA(&svcs, ",\"state\":"); sA_str(&svcs, toks[1]); sA(&svcs, ",\"pid\":%d", atoi(toks[2])); sA(&svcs, ",\"restart\":%d", restarts); sA(&svcs, ",\"restart_str\":"); sA_str(&svcs, toks[3]); sA(&svcs, ",\"backoff_ms\":%d", atoi(toks[4])); sA(&svcs, ",\"trigger\":"); sA_str(&svcs, toks[5]); sA(&svcs, ",\"src\":"); sA_str(&svcs, toks[6]); sA(&svcs, ",\"exit\":"); sA_str(&svcs, toks[7]); sA(&svcs, ",\"exit_time\":"); sA_str(&svcs, toks[8]); sA(&svcs, ",\"exec\":"); sA_str(&svcs, exec); sA(&svcs, "}"); } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; } sA(s, "{\"available\":true,\"count\":%d,\"services\":[%s]}", count, svcs.b ? svcs.b : ""); sbuf_free(&svcs); } static int api_svcd(int sock) { sbuf_t s = {0}; svcd_fill(&s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } /* GET /api/overview:首页生命线。**单线程后端只暴露聚合**(不放六个单测端点给首页轮询)。 * 后端顺序 fan-out perf/power/audio/net/amsd/svcd 六源合成一份 JSON;任一源失败 → 该子块 `available:false`, * 绝不假数据(对齐 netmgrd 503 降级语义)。 */ static int api_overview(int sock) { sbuf_t s = {0}; sA(&s, "{\"ts\":%lld,", (long long)time(NULL)); char pb[4096]; if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK && utf8_valid_n(pb, strlen(pb))) sA(&s, "\"perf\":%s,", pb); else sA(&s, "\"perf\":{\"available\":false},"); char pw[MEOW_IPC_MAX]; if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw))) sA(&s, "\"power\":%s,", pw); else sA(&s, "\"power\":{\"available\":false},"); /* 最近异常耗电/充放电/亮灭屏/背光跳变事件(摘要 top2 进 overview,完整列表走 /api/power/events)。 */ { uint32_t ecnt = 2; char ej[MEOW_IPC_MAX]; uint16_t ert = 0; int ert_rc = ipc_pm_send(MSG_PM_EVENTS, &ecnt, sizeof ecnt, &ert, ej, sizeof ej); if (ert_rc > 0 && ert == MSG_PM_EVENTS_RESULT && utf8_valid_n(ej, strlen(ej))) sA(&s, "\"events\":%s,", ej); else sA(&s, "\"events\":{\"available\":false},"); } sA(&s, "\"audio\":"); audio_fill(&s); if (net_block_fill(&s) == 0) ; /* net 块已追加(状态 + 合并的 dns 子键) */ else sA(&s, ",\"net\":{\"available\":false}"); sA(&s, ",\"ams\":"); ams_fill(&s); sA(&s, ",\"svcd\":"); svcd_fill(&s); sA(&s, "}"); /* 服务端自校验:发出前 jv_parse 自查,坏了留证+回 500(防「非 JSON 响应」破页,2026-09-02 间歇性事故) * 加严:内嵌 NUL(strlen 截断浏览器收残体)/非法 UTF-8(浏览器 JSON.parse 拒收,宽松解析器放行)*/ jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2)); size_t slen = s.b ? strlen(s.b) : 0; int nul_inside = s.b && (slen != s.n); /* 内嵌 NUL:strlen < s.n */ int utf8_bad = s.b && !utf8_valid_n(s.b, s.n); int unbalanced = s.b && !json_strict_balanced(s.b, s.n); /* 尾随垃圾/多括号(jv_parse 容忍,浏览器不容忍) */ if (s.b && (nul_inside || utf8_bad || unbalanced)) { chk = NULL; } if (!chk && s.b) { FILE *df = fopen("/tmp/overview_bad.json", "w"); if (df) { fwrite(s.b, 1, s.n, df); fclose(df); } panel_log("overview: SELF-CHECK fail (nul=%d utf8_bad=%d unbalanced=%d n=%zu strlen=%zu), raw dumped /tmp/overview_bad.json", nul_inside, utf8_bad, unbalanced, s.n, slen); sbuf_free(&s); return send_err(sock, 500, "INTERNAL", "overview 聚合输出未通过 JSON 自校验(原始数据已留 /tmp/overview_bad.json)"); } if (chk) jv_free(chk); int rc = send_json(sock, 200, s.b ? s.b : "{}"); sbuf_free(&s); return rc; } /* ---------------- 路径安全 ---------------- */ static int tf_present(void) { if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */ FILE *f = fopen("/proc/mounts", "r"); if (!f) return 0; char line[512]; int present = 0; while (fgets(line, sizeof line, f)) { if (strstr(line, TF_ROOT)) { present = 1; break; } } fclose(f); return present; } /* 判断某个规范化路径是否在卡根白名单内(==TF_ROOT 或属于某个白名单一级子目录)。 */ static int path_in_whitelist(const char *abs) { size_t rl = strlen(TF_ROOT); if (strncmp(abs, TF_ROOT, rl) != 0) return 0; if (abs[rl] == 0) return 1; /* 根自身 */ if (abs[rl] != '/') return 0; const char *rest = abs + rl + 1; for (int i = 0; i < ALLOWED_COUNT; i++) { size_t dl = strlen(g_allowed_dirs[i]); if (strncmp(rest, g_allowed_dirs[i], dl) == 0) { if (rest[dl] == 0 || rest[dl] == '/') return 1; } } return 0; } static int is_sensitive_name(const char *base) { for (int i = 0; i < SENSITIVE_COUNT; i++) if (!strcmp(base, g_sensitive_names[i])) return 1; return 0; } /* 规范化输入路径(去除 . / ..;绝对/相对容错)。 * 输入可能是绝对(`/usr/data/mnt/sd_0/MeowOS/x`,前端下载/删除等用)或相对 root(`/MeowOS/x`)。 * 输出(buf)为规范化后的绝对路径;若 escape 出卡根,返回 -1。 */ static int normalize_path(const char *in, char *buf, size_t buflen) { size_t rl = strlen(TF_ROOT); if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */ char joined[2048]; if (in[0] == '/') { if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) { snprintf(joined, sizeof joined, "%s", in); } else { /* 相对 root 的展示路径,如 /MeowOS/x → 拼到 root */ if (in[0] == '/' && in[1] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in + 1); else if (in[0] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in); else snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in); } } else { snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in); } /* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。 * 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */ char out[2048]; size_t n = 0; out[0] = '/'; out[1] = 0; n = 1; char *p = joined; while (*p) { while (*p == '/') p++; if (!*p) break; char seg[256]; int k = 0; while (*p && *p != '/') { if (k < 255) seg[k++] = *p; p++; } seg[k] = 0; if (!strcmp(seg, ".") || !strcmp(seg, "")) continue; if (!strcmp(seg, "..")) { /* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */ if (n <= 1) return -1; while (n > 1 && out[n - 1] != '/') n--; if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */ out[n] = 0; continue; } if (k > 0) { size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1; if (need >= sizeof out) return -1; if (n > 1) { out[n++] = '/'; out[n] = 0; } memcpy(out + n, seg, (size_t)k); n += (size_t)k; out[n] = 0; } } /* 空/根(n==1,仅 "/")→ 卡根 */ if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT); size_t ol = strlen(out); if (ol >= buflen) return -1; memcpy(buf, out, ol + 1); /* 已证明 ol= '0' && ca <= '9' && cb >= '0' && cb <= '9') { const unsigned char *na = pa, *nb = pb; while (*na == '0') na++; while (*nb == '0') nb++; const unsigned char *da = na, *db = nb; while (*da >= '0' && *da <= '9') da++; while (*db >= '0' && *db <= '9') db++; int la = (int)(da - na), lb = (int)(db - nb); /* 有效数字位数(去前导0) */ if (la != lb) return la - lb; /* 位数小者数小 */ int c = memcmp(na, nb, (size_t)la); if (c) return c; int ta = (int)(da - pa), tb = (int)(db - pb); /* 原始数字串长(含前导0) */ if (ta != tb) return ta - tb; pa = da; pb = db; continue; /* 数字段完全相同,继续 */ } unsigned char la = (ca >= 'A' && ca <= 'Z') ? (unsigned char)(ca + 32) : ca; unsigned char lbc = (cb >= 'A' && cb <= 'Z') ? (unsigned char)(cb + 32) : cb; if (la != lbc) return (int)la - (int)lbc; if (ca != cb) return (int)ca - (int)cb; /* 大小写 tie-break:大写在前 */ pa++; pb++; } return (int)*pa - (int)*pb; } /* 目录优先 + 名称自然排序(数字感知 + 无符号字节序) */ static int fent_cmp(const void *a, const void *b) { const struct fent *fa = a, *fb = b; if (fa->isdir != fb->isdir) return fb->isdir - fa->isdir; return nat_strcmp(fa->name, fb->name); } static int files_list(int sock, const char *relpath) { char abs[1024], rel[1024]; if (normalize_path(relpath, abs, sizeof abs) < 0) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); if (!path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); struct stat st; if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在"); if (!S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "非目录"); DIR *d = opendir(abs); if (!d) return send_err(sock, 500, "INTERNAL", "读目录失败"); make_rel(abs, rel, sizeof rel); sbuf_t s = {0}; sA(&s, "{\"root\":"); sA_str(&s, TF_ROOT); sA(&s, ",\"path\":"); sA_str(&s, abs); sA(&s, ",\"rel\":"); sA_str(&s, rel); sA(&s, ",\"allowed\":["); for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); } sA(&s, "],\"items\":["); /* M2.78:排序后输出(原 readdir 原始顺序混杂难用)——目录优先,同组按名称 * 不区分大小写排序;条目上限 1024(与 files App 一致,防巨目录爆内存)。 */ int cap = 1024, n = 0; struct fent *ents = malloc((size_t)cap * sizeof *ents); struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char full[1400]; snprintf(full, sizeof full, "%s/%s", abs, e->d_name); struct stat fs; if (stat(full, &fs) < 0) continue; if (n >= cap) break; struct fent *en = &ents[n++]; snprintf(en->name, sizeof en->name, "%s", e->d_name); en->isdir = S_ISDIR(fs.st_mode); en->size = en->isdir ? 0 : (long long)fs.st_size; en->mtime = (long long)fs.st_mtime; } closedir(d); qsort(ents, (size_t)n, sizeof *ents, fent_cmp); for (int i = 0; i < n; i++) { if (i) sA(&s, ","); fmt_json_item(&s, ents[i].name, ents[i].isdir, ents[i].size, ents[i].mtime); } free(ents); sA(&s, "]}"); return send_json(sock, 200, s.b ? s.b : ""); } /* ---------------- HTTP 响应(JSON 信封) ---------------- */ /* send_json 前置声明 */ static int send_json(int sock, int status, const char *data); /* 读文本文件(预览上限 TEXT_PREVIEW_MAX,超出截断返回 truncated): * 返回 malloc 内容(NUL 结尾);二进制检测 -2;打不开 -1。size=文件真实字节数(非截断后)。 */ static char *read_text_file_limited(const char *abs, long long *size, int *truncated) { int fd = open_safe_for_read(abs, 0); if (fd < 0) return NULL; struct stat st; if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; } long long total = (long long)st.st_size; 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; } ssize_t n = read(fd, buf, cap); close(fd); if (n < 0) { free(buf); return NULL; } buf[n] = 0; /* 二进制检测:含 NUL 或大量控制字符 */ int ctrl = 0; for (ssize_t i = 0; i < n; i++) { unsigned char c = (unsigned char)buf[i]; if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */ if (c < 0x09) ctrl++; } if (ctrl > n / 10) { free(buf); return (char *)-2; } if (size) *size = total; if (truncated) *truncated = (total > (long long)cap) ? 1 : 0; return buf; } /* ---------------- 按键注入(经 /dev/input/event* 写 EV_KEY;uinput_tap inject 同机制) ---------------- * 设备映射(launcher meow_launcher_internal.h): * /dev/input/event0 = md-gpio-keys: KEY_POWER(116), KEY_PREVIOUSSONG(165) * /dev/input/event2 = jz adc keyboard: KEY_VOLUMEUP(115), KEY_VOLUMEDOWN(114), KEY_NEXTSONG(163), KEY_PLAYPAUSE(164) * 注入 = open(O_WRONLY) 写 EV_KEY press/release + SYN_REPORT(evdev 支持 write 注入,uinput_tap.c inject 先例)。 * 红线(APPS.md 物理键 / 任务约束):只注入音量/电源/播放/上下曲,**禁止映射 返回(158)/主页(172)/确认/exit**。 */ #define PWR_KEYDEV "/dev/input/event0" #define ADC_KEYDEV "/dev/input/event2" static int keycode_to_device(int code, const char **dev) { switch (code) { case KEY_POWER: *dev = PWR_KEYDEV; return KEY_POWER; case KEY_PREVIOUSSONG: *dev = PWR_KEYDEV; return KEY_PREVIOUSSONG; case KEY_VOLUMEUP: *dev = ADC_KEYDEV; return KEY_VOLUMEUP; case KEY_VOLUMEDOWN: *dev = ADC_KEYDEV; return KEY_VOLUMEDOWN; case KEY_NEXTSONG: *dev = ADC_KEYDEV; return KEY_NEXTSONG; case KEY_PLAYPAUSE: *dev = ADC_KEYDEV; return KEY_PLAYPAUSE; default: return -1; } } static int inject_key_event(const char *dev, int code, int down) { int fd = open(dev, O_WRONLY | O_NONBLOCK); if (fd < 0) return -1; struct input_event ev; memset(&ev, 0, sizeof ev); ev.type = EV_KEY; ev.code = code; ev.value = down ? 1 : 0; int ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev); if (ok) { memset(&ev, 0, sizeof ev); ev.type = EV_SYN; ev.code = SYN_REPORT; ev.value = 0; ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev); } close(fd); return ok ? 0 : -1; } /* 注入 tap(按下+抬起)或 long(按住 ms)。返回 0=成功。 */ static int inject_key_release(int code, int press_ms) { const char *dev = NULL; int kc = keycode_to_device(code, &dev); if (kc < 0) return -1; int release_ms = press_ms > 0 ? press_ms : 0; if (inject_key_event(dev, code, 1) < 0) return -1; if (release_ms > 0) usleep((useconds_t)release_ms * 1000); else usleep(80 * 1000); if (inject_key_event(dev, code, 0) < 0) return -1; return 0; } /* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */ static int read_small_file(const char *path, char *buf, int cap) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; int n = 0; while (n < cap - 1) { ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n)); if (r <= 0) break; n += (int)r; } close(fd); buf[n] = 0; return n; } /* 读某应用目录的 app.json(用 jv 解析提取字段,供扫描/详情)。 */ static int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail) { char pj[1024]; snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name); char buf[8192]; if (read_small_file(pj, buf, sizeof buf) <= 0) { /* 解析失败项:invalid */ if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, name); sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); } return -1; } jv_t *root = jv_parse(buf, (int)strlen(buf)); if (!root) { if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, name); sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); } return -1; } jv_t *jpkg = jv_obj_get(root, "package"); jv_t *jname = jv_obj_get(root, "name"); jv_t *jver = jv_obj_get(root, "version"); jv_t *jexec = jv_obj_get(root, "exec"); jv_t *jfw = jv_obj_get(root, "framework"); jv_t *jim = jv_obj_get(root, "immersive"); jv_t *jfull = jv_obj_get(root, "fullscreen"); const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name; const char *nm = (jname && jname->type == J_STR) ? jname->str : ""; const char *ver = (jver && jver->type == J_STR) ? jver->str : ""; const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : ""; /* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */ long long dsize = 0; snprintf(pj, sizeof pj, "%s/%s", appdir, name); DIR *d = opendir(pj); if (d) { struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name); struct stat st; if (stat(full, &st) == 0) dsize += (long long)st.st_size; } closedir(d); } if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, pkg); sA(out, ",\"name\":"); sA_str(out, nm[0] ? nm : NULL); sA(out, ",\"version\":"); sA_str(out, ver[0] ? ver : NULL); sA(out, ",\"exec\":"); sA_str(out, exec); sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}", jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL)); } else { sA(out, "{\"package\":"); sA_str(out, pkg); sA(out, ",\"name\":"); sA_str(out, nm[0] ? nm : NULL); sA(out, ",\"version\":"); sA_str(out, ver[0] ? ver : NULL); sA(out, ",\"exec\":"); sA_str(out, exec); sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":", jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize); sA_str(out, pj); sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}"); } jv_free(root); return 0; } /* ---------------- HTTP 请求读取 ---------------- */ typedef struct { char method[8]; char path[512]; /* 解码后的路径(不含 query) */ char query[1024]; /* 原始 query(key=value&…,未解码;取值时用 qparam) */ char *body; size_t body_len; char content_type[128]; /* 请求 Content-Type(若非空) */ char authorization[256]; /* Authorization 头(Bearer ) */ char x_meow_token[256]; /* X-MeowToken 头 */ char cookie[512]; /* Cookie 头(meow_token= 等) */ } req_t; static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; } static int multipart_extract_file(const req_t *r, const char *outpath); /* 定义在文件后部;字体导入用(写 multipart file 到临时文件) */ static int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return c - 'A' + 10; } /* URL 解码到 buf。 */ static void url_decode(const char *in, char *buf, int buflen) { int o = 0; for (int i = 0; in[i] && o < buflen - 1; i++) { if (in[i] == '%' && is_hex(in[i + 1]) && is_hex(in[i + 2])) { buf[o++] = (char)((hexval(in[i + 1]) << 4) | hexval(in[i + 2])); i += 2; } else if (in[i] == '+') buf[o++] = ' '; else buf[o++] = in[i]; } buf[o] = 0; } /* 从 query(key=value&…)取第一个值(url 解码后写入 out),未找到返回 0。 */ static int qparam(const char *query, const char *key, char *out, int outcap) { size_t kl = strlen(key); const char *p = query; while (p && *p) { const char *amp = strchr(p, '&'); size_t seg = amp ? (size_t)(amp - p) : strlen(p); if (seg >= kl + 1 && strncmp(p, key, kl) == 0 && p[kl] == '=') { char tmp[1024]; size_t vlen = seg - kl - 1; if (vlen >= sizeof tmp) vlen = sizeof tmp - 1; memcpy(tmp, p + kl + 1, vlen); tmp[vlen] = 0; url_decode(tmp, out, outcap); return 1; } if (!amp) break; p = amp + 1; } return 0; } /* 从请求 path 中取动态段(如 /api/settings/:key)。返回是否匹配。 */ static int path_segment(const char *prefix, const char *full, char *out, int outcap) { size_t pl = strlen(prefix); if (strncmp(full, prefix, pl) != 0) return 0; const char *rest = full + pl; if (*rest == '/') rest++; if (!*rest) return 0; size_t l = strlen(rest); if (l >= (size_t)outcap) l = (size_t)outcap - 1; memcpy(out, rest, l); out[l] = 0; /* 去掉后续 / 段(取第一段) */ char *slash = strchr(out, '/'); if (slash) *slash = 0; return 1; } /* 从 "GET /path?query HTTP/1.1" 的 path 部分切出 path 与 query。 */ static void split_path_query(const char *src, char *path, int plc, char *query, int qlc) { const char *q = strchr(src, '?'); if (q) { size_t pl = (size_t)(q - src); if (pl >= (size_t)plc) pl = (size_t)plc - 1; memcpy(path, src, pl); path[pl] = 0; snprintf(query, qlc, "%s", q + 1); } else { snprintf(path, plc, "%s", src); query[0] = 0; } } /* 在 buf[0..n) 中找首个 "\r\n\r\n",返回其起始下标;找不到返回 -1。 */ static ptrdiff_t find_header_end(const char *buf, size_t n) { if (n < 4) return -1; for (size_t i = 0; i + 3 < n; i++) { if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n') return (ptrdiff_t)i; } return -1; } /* 读请求:先读头(到 \r\n\r\n),解析 Content-Length/Content-Type,再精确读 body。 * 处理「头+body 同一 TCP 段到达」(小 POST 常见):单次 read 可能把 body 一并读入, * 故在缓冲区内**搜索** \r\n\r\n 定位头边界,body 从边界后搬移。成功 0;超时/过大/失败 -1。 */ static int read_request(int sock, req_t *r) { char *buf = malloc(MAX_HTTP_HEADER); if (!buf) return -1; size_t hn = 0; int deadline_ms = 5000; ptrdiff_t hend = -1; for (;;) { struct pollfd pf = { sock, POLLIN, 0 }; int pr = poll(&pf, 1, deadline_ms); if (pr <= 0) { free(buf); return -1; } ssize_t rd = read(sock, buf + hn, MAX_HTTP_HEADER - 1 - hn); if (rd <= 0) { free(buf); return -1; } hn += (size_t)rd; buf[hn] = 0; hend = find_header_end(buf, hn); if (hend >= 0) break; if (hn >= MAX_HTTP_HEADER - 2) { free(buf); return -1; } } /* 头结束在 hend,body 起点在 hend+4 */ char *hdr = buf; char *line_end = strstr(hdr, "\r\n"); if (!line_end) { free(buf); return -1; } *line_end = 0; char method[8], pathraw[512], proto[16]; if (sscanf(hdr, "%7s %511s %15s", method, pathraw, proto) < 2) { free(buf); return -1; } snprintf(r->method, sizeof r->method, "%s", method); split_path_query(pathraw, r->path, sizeof r->path, r->query, sizeof r->query); size_t clen = 0; char *hp = line_end + 2; while (hp < buf + hend && strncmp(hp, "\r\n", 2)) { char *he = strstr(hp, "\r\n"); if (!he || he > buf + hend) break; *he = 0; if (!strncasecmp(hp, "Content-Length:", 15)) clen = (size_t)strtoul(hp + 15, NULL, 10); else if (!strncasecmp(hp, "Content-Type:", 13)) { char *v = hp + 13; while (*v == ' ') v++; snprintf(r->content_type, sizeof r->content_type, "%s", v); } else if (!strncasecmp(hp, "Authorization:", 14)) { char *v = hp + 14; while (*v == ' ') v++; snprintf(r->authorization, sizeof r->authorization, "%s", v); } else if (!strncasecmp(hp, "X-MeowToken:", 12)) { char *v = hp + 12; while (*v == ' ') v++; snprintf(r->x_meow_token, sizeof r->x_meow_token, "%s", v); } else if (!strncasecmp(hp, "Cookie:", 7)) { char *v = hp + 7; while (*v == ' ') v++; snprintf(r->cookie, sizeof r->cookie, "%s", v); } hp = he + 2; } char *body_start = buf + hend + 4; /* body 实际起点 */ size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0; /* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。 * 字体库 tar.gz 端点例外:gbk/full 字库 png+idx 可能 10-20MB,放宽到 MAX_FONT_UPLOAD(32MB)。 */ size_t cap = MAX_HTTP_BODY; if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = (!strcmp(r->path, "/api/fonts/import")) ? MAX_FONT_UPLOAD : MAX_UPLOAD_BODY; if (clen > cap) { free(buf); return -2; } if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; } char *body = malloc(clen + 1); if (!body) { free(buf); return -1; } size_t got = 0; if (buffered > 0) { size_t take = buffered < clen ? buffered : clen; memcpy(body, body_start, take); got = take; } free(buf); while (got < clen) { struct pollfd pf = { sock, POLLIN, 0 }; int pr = poll(&pf, 1, 5000); if (pr <= 0) { free(body); return -1; } ssize_t rd = read(sock, body + got, clen - got); if (rd <= 0) { free(body); return -1; } got += (size_t)rd; } body[clen] = 0; r->body = body; r->body_len = clen; return 0; } /* ================= 访问级 PIN 鉴权(PANEL_ENHANCE_PLAN 用户定案 2026-09-01) ================= * 模型(用户二次定案,覆盖 282「默认无鉴权」前提): * - **访问面板即需登录**:未认证请求一律 401(静态登录页自身 + css/js 放行——serve_static 先于本门禁)。 * - 4 位数字 PIN,设置键 `panel.pin`:sha256+salt 存 settings.conf(格式 `:`, * auth_make_hash 生成)。与未来 MeowOS 锁屏 PIN 同源:设置键 `system.pin` 预留(本后端只读面板键,见 g_sets 注记)。 * - token 随机 **128bit**(/dev/urandom 16B → 32 hex),**只存面板进程内存**(`g_sess[]`,重启自然失效,无卡上持久化)。 * - 会话有效期 **3 天**(AUTH_TOKEN_TTL,**固定不滑动续期**)或设备重启(内存清、先到为准);多会话并存。 * - **未设 PIN**:首次访问走 `POST /api/auth/setup` 引导设置;未设 PIN 前无法建立任何会话,故全部 * `/api/` 路径被拦(读端点也按「访问级」全拦——定案即全拦,登录/设置流程除外),写端点自然一律拒绝。 * - **错误锁定**:连续 PIN_MAX_FAILS=5 次错 PIN 锁 PIN_LOCK_SEC=300s(内存态,重启清)。 * - **旧密码机制兼容迁移**(`panel.auth_enabled`/`panel.auth_pass`):旧密码存在且未设 PIN 时, * 视为「已设访问屏障」的同等物——登录暂可用旧密码(body `password` 字段),status 透出 `legacy:true` * 提示改设 4 位 PIN;一旦设置 `panel.pin`,**新代码以 PIN 为准**(登录只认 PIN,旧密码不再作为凭据)。 */ #define AUTH_TOKEN_BYTES 16 /* 128bit → 32 hex */ #define AUTH_TOKEN_TTL (3 * 86400) /* 3 天(固定,不滑动续期) */ #define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */ #define AUTH_HASH_LEN 64 /* sha256 hex 长度 */ #define PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */ #define PIN_LOCK_SEC 300 /* 锁定 5 分钟 */ static time_t g_auth_ttl = AUTH_TOKEN_TTL; /* 会话 TTL(env MEOW_AUTH_TTL 可覆盖,宿主测试用) */ /* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 mp_ 避免符号冲突) ---- */ static const uint32_t mp_sha256_k[64] = { 0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u, 0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u, 0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau, 0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u, 0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u, 0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u, 0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u, 0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u }; #define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n)))) #define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) #define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) #define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22)) #define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25)) #define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3)) #define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10)) typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; size_t buflen; } mp_sha256_ctx; static void mp_sha256_init(mp_sha256_ctx *c) { c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u; c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au; c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu; c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u; c->count = 0; c->buflen = 0; } static void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p) { uint32_t w[64]; int i; for (i = 0; i < 16; i++) w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) | ((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3]; for (i = 16; i < 64; i++) w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16]; uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3]; uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7]; for (i = 0; i < 64; i++) { uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i]; uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc); h = g; g = f; f = e; e = d + t1; d = cc; cc = b; b = a; a = t1 + t2; } c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d; c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h; } static void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len) { c->count += (uint64_t)len; if (c->buflen) { size_t need = 64 - c->buflen; size_t take = len < need ? len : need; memcpy(c->buf + c->buflen, data, take); c->buflen += take; data += take; len -= take; if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; } } while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; } if (len) { memcpy(c->buf, data, len); c->buflen = len; } } static void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32]) { uint64_t bitlen = c->count * 8; uint8_t *b = c->buf; size_t bl = c->buflen; int i; b[bl++] = 0x80; if (bl > 56) { while (bl < 64) b[bl++] = 0; mp_sha256_block(c, b); bl = 0; } while (bl < 56) b[bl++] = 0; for (i = 0; i < 8; i++) b[bl++] = (uint8_t)(bitlen >> (56 - i * 8)); mp_sha256_block(c, b); for (i = 0; i < 8; i++) { out[i*4] = (uint8_t)(c->state[i] >> 24); out[i*4+1] = (uint8_t)(c->state[i] >> 16); out[i*4+2] = (uint8_t)(c->state[i] >> 8); out[i*4+3] = (uint8_t)(c->state[i]); } } #undef MP_ROR #undef MP_CH #undef MP_MAJ #undef MP_BSIG0 #undef MP_BSIG1 #undef MP_SSIG0 #undef MP_SSIG1 /* ---- 鉴权基础 ---- * 密码存储格式:`:`。salt 由 /dev/urandom 生成(16B → 32 hex)。 * 校验时重算并比较,绝不反解、绝不在日志/响应中输出 hash 或明文。 */ static int auth_read_random(unsigned char *buf, size_t n) { int fd = open("/dev/urandom", O_RDONLY); if (fd < 0) return -1; size_t got = 0; while (got < n) { ssize_t rd = read(fd, buf + got, n - got); if (rd <= 0) { close(fd); return -1; } got += (size_t)rd; } close(fd); return 0; } static void auth_hex(const unsigned char *raw, size_t n, char *out) { static const char hex[] = "0123456789abcdef"; for (size_t i = 0; i < n; i++) { out[i*2] = hex[raw[i] >> 4]; out[i*2+1] = hex[raw[i] & 0xf]; } out[n*2] = 0; } static int auth_token_gen(char *out, int outcap) /* 16B → 32 hex(128bit) */ { unsigned char raw[AUTH_TOKEN_BYTES]; if (outcap < AUTH_TOKEN_BYTES * 2 + 1) return -1; if (auth_read_random(raw, sizeof raw) < 0) return -1; auth_hex(raw, sizeof raw, out); return 0; } static int auth_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */ { unsigned char raw[16]; if (outcap < 33) return -1; if (auth_read_random(raw, sizeof raw) < 0) return -1; auth_hex(raw, sizeof raw, out); return 0; } static int auth_pass_hash(const char *salt, const char *password, char *out, int outcap) { mp_sha256_ctx c; uint8_t digest[32]; if (outcap < AUTH_HASH_LEN + 1) return -1; mp_sha256_init(&c); mp_sha256_update(&c, (const uint8_t *)salt, strlen(salt)); mp_sha256_update(&c, (const uint8_t *)password, strlen(password)); mp_sha256_final(&c, digest); auth_hex(digest, sizeof digest, out); return 0; } static int auth_make_hash(const char *password, char *out, int outcap) /* salt:hex */ { char salt[33], h[AUTH_HASH_LEN + 1]; if (auth_make_salt(salt, sizeof salt) < 0) return -1; if (auth_pass_hash(salt, password, h, sizeof h) < 0) return -1; snprintf(out, outcap, "%s:%s", salt, h); return 0; } /* ---- PIN / 旧密码 凭据校验(都走「salt:hex」哈希校验,恒定比较) ---- */ /* 校验 `salt:hex` 哈希是否匹配明文(恒定比较,不反解)。stored 为 `salt:hex` 完整串。 */ static int hash_matches(const char *stored, const char *password) { if (!stored || !stored[0] || !password) return 0; char tmp[160]; snprintf(tmp, sizeof tmp, "%s", stored); char *colon = strchr(tmp, ':'); if (!colon) return 0; *colon = 0; char want[AUTH_HASH_LEN + 1]; if (auth_pass_hash(tmp, password, want, sizeof want) != 0) return 0; if (strlen(colon + 1) != AUTH_HASH_LEN) return 0; return memcmp(want, colon + 1, AUTH_HASH_LEN) == 0; } static int pin_set(void) /* panel.pin 是否已设(访问屏障开启) */ { char *v = settings_get_value("panel.pin"); int s = (v && v[0]) ? 1 : 0; free(v); return s; } static int auth_pass_set(void) /* 旧密码是否已设(兼容迁移用) */ { char *v = settings_get_value("panel.auth_pass"); int s = (v && v[0]) ? 1 : 0; free(v); return s; } static int auth_verify(const char *password) /* 旧密码校验(迁移期;见 login) */ { char *stored = settings_get_value("panel.auth_pass"); int ok = hash_matches(stored, password); free(stored); return ok; } static int pin_verify(const char *pin) /* PIN 校验 */ { char *stored = settings_get_value("panel.pin"); int ok = hash_matches(stored, pin); free(stored); return ok; } /* PIN 合法:恰好 4 位数字(0-9)。 */ static int pin_validate(const char *pin) { if (!pin) return 0; size_t l = strlen(pin); if (l != 4) return 0; for (size_t i = 0; i < l; i++) if (pin[i] < '0' || pin[i] > '9') return 0; return 1; } /* ---- 错误锁定(内存态,重启清;4 位 PIN 防爆破必备) ---- */ static int g_pin_fails = 0; static time_t g_pin_locked_until = 0; static long long pin_lock_remaining(void) /* 剩余锁定秒;0=未锁 */ { time_t now = time(NULL); if (g_pin_locked_until <= now) return 0; return (long long)(g_pin_locked_until - now); } static void pin_lock_reset(void) { g_pin_fails = 0; g_pin_locked_until = 0; } static void pin_record_fail(void) { if (++g_pin_fails >= PIN_MAX_FAILS) { g_pin_locked_until = time(NULL) + PIN_LOCK_SEC; g_pin_fails = 0; } } /* ---- 会话(**纯内存**:重启自然失效,无卡上持久化) ---- */ typedef struct { char token[65]; time_t expires; } auth_sess_t; static auth_sess_t g_sess[AUTH_MAX_SESS]; static int g_sess_n = 0; static int sess_find(const char *token) { for (int i = 0; i < g_sess_n; i++) if (!strcmp(g_sess[i].token, token)) return i; return -1; } static int sess_valid(const char *token) { int i = sess_find(token); if (i < 0) return 0; if (g_sess[i].expires > 0 && time(NULL) >= g_sess[i].expires) return 0; return 1; } static void sess_add(const char *token, time_t expires) { int i = sess_find(token); if (i >= 0) { g_sess[i].expires = expires; return; } if (g_sess_n >= AUTH_MAX_SESS) { /* 满:淘汰最早过期的一条 */ int e = 0; for (int k = 1; k < g_sess_n; k++) if (g_sess[k].expires < g_sess[e].expires) e = k; if (e < g_sess_n - 1) memmove(&g_sess[e], &g_sess[e + 1], (size_t)(g_sess_n - 1 - e) * sizeof(auth_sess_t)); g_sess_n--; } snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", token); g_sess[g_sess_n].expires = expires; g_sess_n++; } static void sess_remove(const char *token) { int i = sess_find(token); if (i < 0) return; if (i < g_sess_n - 1) memmove(&g_sess[i], &g_sess[i + 1], (size_t)(g_sess_n - 1 - i) * sizeof(auth_sess_t)); g_sess_n--; } static void auth_cp(char *dst, int dcap, const char *src) /* 有界拷贝(防 -Wformat-truncation) */ { size_t l = strlen(src); if (l >= (size_t)dcap) l = (size_t)dcap - 1; memcpy(dst, src, l); dst[l] = 0; } /* ---- 从请求提取 token(X-MeowToken → Authorization: Bearer → Cookie meow_token) ---- */ static int extract_token(const req_t *r, char *out, int outcap) { out[0] = 0; if (r->x_meow_token[0]) { auth_cp(out, outcap, r->x_meow_token); return out[0] ? 1 : 0; } if (!strncasecmp(r->authorization, "Bearer ", 7)) { auth_cp(out, outcap, r->authorization + 7); return out[0] ? 1 : 0; } if (r->cookie[0]) { const char *p = strstr(r->cookie, "meow_token="); if (p) { p += 11; const char *e = p; while (*e && *e != ';' && *e != ' ' && *e != '\t') e++; size_t l = (size_t)(e - p); if (l >= (size_t)outcap) l = (size_t)outcap - 1; memcpy(out, p, l); out[l] = 0; return out[0] ? 1 : 0; } } return 0; } /* ---- 鉴权门禁:**访问面板即需登录**——除 /api/auth 系列外,全部 /api/ 端点需有效会话。 * 未设 PIN 时无法建立会话 → 读端点也一律 401(「全拦」定案),写端点自然一律拒绝;登录/设置流程除外。 ---- */ static int require_auth(int sock, const req_t *r) { const char *p = r->path; if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") || !strcmp(p, "/api/auth/status") || !strcmp(p, "/api/auth/setup")) return 1; char t[65]; if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1; return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效"); } /* GET /api/auth/status(公开):访问屏障状态 + 当前会话 + 锁定信息。 */ static int api_auth_status(int sock, const req_t *r) { int ps = pin_set(); int lg = auth_pass_set(); int sess = 0; char t[65]; if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t); long long rem = pin_lock_remaining(); sbuf_t s = {0}; sA(&s, "{\"auth_enabled\":true,\"session\":%s,\"pin_set\":%s,\"legacy\":%s,\"locked\":%s", sess ? "true" : "false", ps ? "true" : "false", lg ? "true" : "false", rem > 0 ? "true" : "false"); if (rem > 0) sA(&s, ",\"lock_remaining_ms\":%lld", rem * 1000LL); sA(&s, "}"); int rc = send_json(sock, 200, s.b ? s.b : "{}"); sbuf_free(&s); return rc; } /* 登录统一入口:PIN(panel.pin 已设,权威)或旧密码(仅未设 PIN 时;迁移期)。 * 连续 PIN_MAX_FAILS 次错 → 锁 PIN_LOCK_SEC(429 LOCKED)。 */ static int api_auth_login(int sock, const req_t *r) { if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jpin = jv_obj_get(root, "pin"); jv_t *jpw = jv_obj_get(root, "password"); char pin[8], pw[256]; if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0; if (jpw && jpw->type == J_STR && jpw->str) snprintf(pw, sizeof pw, "%s", jpw->str); else pw[0] = 0; jv_free(root); if (pin_lock_remaining() > 0) { /* 已在锁定期 */ sbuf_t e = {0}; sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"已锁定,请稍后重试\"},\"lock_remaining_ms\":%lld}", pin_lock_remaining() * 1000LL); int rc = send_json(sock, 429, e.b ? e.b : ""); sbuf_free(&e); return rc; } int use_legacy = 0; const char *cred = NULL; if (pin_set()) { cred = pin; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 pin 字段"); } else if (auth_pass_set()) { use_legacy = 1; cred = pw; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); } else return send_err(sock, 400, "BAD_REQUEST", "请先设置面板 PIN(/api/auth/setup)"); int ok = use_legacy ? auth_verify(cred) : pin_verify(cred); if (!ok) { pin_record_fail(); if (pin_lock_remaining() > 0) { /* 本错已触发锁定 */ sbuf_t e = {0}; sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"连续错误次数过多,已锁定 5 分钟\"},\"lock_remaining_ms\":%lld}", pin_lock_remaining() * 1000LL); int rc = send_json(sock, 429, e.b ? e.b : ""); sbuf_free(&e); return rc; } return send_err(sock, 401, "UNAUTHORIZED", "PIN 错误"); } pin_lock_reset(); char token[65]; if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败"); time_t exp = time(NULL) + g_auth_ttl; /* 固定 3 天(MEOW_AUTH_TTL 可覆盖测试用),不滑动续期 */ sess_add(token, exp); sbuf_t s = {0}; sA(&s, "{\"token\":"); sA_str(&s, token); sA(&s, ",\"expires_at\":%lld,\"auth_enabled\":true}", (long long)exp); int rc = send_json(sock, 200, s.b ? s.b : "{}"); sbuf_free(&s); return rc; } /* POST /api/auth/setup(公开):首次设置 4 位 PIN(仅未设 PIN 时可用;设置后走 login)。 * 未设 PIN 前写端点一律拒绝;本端点即「设置流程」例外,用于引导建立首个访问屏障。 */ static int api_auth_setup(int sock, const req_t *r) { if (pin_set()) return send_err(sock, 409, "CONFLICT", "面板 PIN 已设置,请直接登录"); if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jpin = jv_obj_get(root, "pin"); char pin[8]; if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0; jv_free(root); if (!pin_validate(pin)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字"); char h[200]; if (auth_make_hash(pin, h, sizeof h) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败"); if (settings_write_one("panel.pin", h) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败"); pin_lock_reset(); return send_json(sock, 200, "{\"pin_set\":true}"); } static int api_auth_logout(int sock, const req_t *r) { char t[65]; if (extract_token(r, t, sizeof t) && t[0]) sess_remove(t); return send_json(sock, 200, "{\"logged_out\":true}"); } /* ---------------- HTTP 响应写入 ---------------- */ static int write_full(int sock, const void *b, size_t n) { const char *p = (const char *)b; size_t off = 0; while (off < n) { ssize_t w = write(sock, p + off, n - off); if (w <= 0) return -1; off += (size_t)w; } return 0; } static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra) { char h[512]; const char *sts = "OK"; switch (status) { case 200: sts = "OK"; break; case 201: sts = "Created"; break; case 202: sts = "Accepted"; break; case 400: sts = "Bad Request"; break; case 401: sts = "Unauthorized"; break; case 403: sts = "Forbidden"; break; case 404: sts = "Not Found"; break; case 405: sts = "Method Not Allowed"; break; case 409: sts = "Conflict"; break; case 413: sts = "Payload Too Large"; break; case 415: sts = "Unsupported Media Type"; break; case 422: sts = "Unprocessable Entity"; break; case 500: sts = "Internal Server Error"; break; case 501: sts = "Not Implemented"; break; case 503: sts = "Service Unavailable"; break; default: sts = "OK"; break; } snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n", status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : ""); return write_full(sock, h, strlen(h)); } /* 发送原生 bytes(已含 body)。 */ static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen) { if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1; if (bodylen && write_full(sock, body, bodylen) < 0) return -1; return 0; } /* 成功 JSON 信封:{"ok":true,"data":}。 * 前缀 "{"ok":true,"data":" 共 18 字节。用 memcpy 精确构包(sA 的 %s 有 ~511B 单次截断,不能用于大 payload)。 */ static int send_ok(int sock, const char *data) { static const char prefix[] = "{\"ok\":true,\"data\":"; size_t pl = sizeof prefix - 1; /* 18 */ size_t dl = strlen(data); size_t tot = pl + dl + 1; char *b = malloc(tot + 1); if (!b) { const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}"; return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e)); } memcpy(b, prefix, pl); memcpy(b + pl, data, dl); b[pl + dl] = '}'; b[pl + dl + 1] = 0; int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot); free(b); return rc; } /* 错误 JSON 信封:{"ok":false,"error":{"code":..,"message":..}} */ static int send_err(int sock, int status, const char *code, const char *msg) { sbuf_t s = {0}; sA(&s, "{\"ok\":false,\"error\":{\"code\":"); sA_str(&s, code ? code : "INTERNAL"); sA(&s, ",\"message\":"); sA_str(&s, msg ? msg : ""); sA(&s, "}}"); int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n); sbuf_free(&s); return rc; } static int send_json(int sock, int status, const char *data) { if (status == 200) return send_ok(sock, data); return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data)); } /* 流式发送磁盘文件(下载)。 */ static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename) { int fd = open(abspath, O_RDONLY); if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在"); struct stat st; if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); } char extra[256]; if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename); else extra[0] = 0; if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; } char buf[8192]; ssize_t n; while ((n = read(fd, buf, sizeof buf)) > 0) { if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; } } close(fd); return 0; } /* 二进制安全子串查找:strstr 会因数据中的 NUL 提前停止,二进制(zip/mpk/图)必须用 memmem。 * 返回 needle 在 hay[0..hlen) 中首次出现的指针;找不到 NULL。 * 仅当 mot 出现,用 memmem(_GNU_SOURCE 已定义,glibc/musl 均提供)。 * 若无 memmem(极端环境),退回手写字节扫描,保证可移植。 */ static const char *mem_find(const void *hay, size_t hlen, const char *needle) { size_t nl = strlen(needle); if (!nl || hlen < nl) return NULL; const char *h = (const char *)hay; for (size_t i = 0; i + nl <= hlen; i++) { if (h[i] == needle[0] && memcmp(h + i, needle, nl) == 0) return h + i; } return NULL; } /* ---------------- multipart/form-data 解析(文件上传) ---------------- * 解析 body,提取 name= 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。 * 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。 */ 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) { /* 取 boundary */ const char *ct = r->content_type; const char *b = strstr(ct, "boundary="); if (!b) return -1; b += 9; if (*b == '"') { b++; } char boundary[128]; int bi = 0; while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; } boundary[bi] = 0; char dmark[160]; snprintf(dmark, sizeof dmark, "--%s", boundary); char emark[160]; snprintf(emark, sizeof emark, "--%s--", boundary); /* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */ const char *p = r->body; const char *end = r->body + r->body_len; int nfiles = 0; char curpath[1024] = ""; while (p < end) { const char *fstart = mem_find(p, (size_t)(end - p), dmark); if (!fstart) break; fstart += strlen(dmark); if (*fstart == '-') break; /* 结束标记 --boundary-- */ if (*fstart == '\r' || *fstart == '\n') fstart++; /* 解析该 part 的头部(直至空行) */ const char *headers_end = strstr(fstart, "\r\n\r\n"); if (!headers_end) break; char hdr[512]; size_t hl = (size_t)(headers_end - fstart); if (hl >= sizeof hdr) hl = sizeof hdr - 1; memcpy(hdr, fstart, hl); hdr[hl] = 0; const char *data_start = headers_end + 4; const char *name = NULL, *fname = NULL; char *save = NULL; for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) { char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++; if (strncasecmp(l, "Content-Disposition:", 20) == 0) { /* name 与 filename 常同行(name="file"; filename="up.txt")。 * 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */ char *fn = strstr(l, "filename=\""); 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" 之前(二进制安全) */ const char *data_end = mem_find(data_start, (size_t)(end - data_start), dmark); if (!data_end) data_end = end; /* 去掉数据尾部 \r\n */ while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--; if (name && !strcmp(name, "path")) { size_t dl = (size_t)(data_end - data_start); if (dl >= sizeof curpath) dl = sizeof curpath - 1; memcpy(curpath, data_start, dl); curpath[dl] = 0; } else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) { size_t dl = (size_t)(data_end - data_start); char safe[260]; if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */ /* 目标目录规范化 + 白名单校验 */ char absdir[1024]; if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; } /* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */ 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) { write_full(fd, data_start, dl); close(fd); nfiles++; } /* fd<0(不可写)→ 不计数,跳过该文件 */ } p = data_end; } return nfiles; /* >=0:已保存文件数;-1 边界缺失/解析失败 */ } /* ---------------- 日志(stderr;svcd supervisor 捕获) ---------------- */ static void panel_log(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(stderr, "[MeowPanel] "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } /* ---------------- 设置项 JSON 序列化 ---------------- */ static int str_truthy(const char *v) { char q[32]; snprintf(q, sizeof q, "%s", v); char *p = q; while (*p == ' ' || *p == '\t') p++; if (!strcasecmp(p, "true") || !strcasecmp(p, "yes") || !strcasecmp(p, "on")) return 1; long n = strtol(p, NULL, 10); return (p[0] && n != 0); } static int int_meta_max(const setdef_t *d) { if (d->max > 0) return d->max; int dn = atoi(d->def); if (dn > 0) return dn * 2 + 100; return 100; } /* 非改写的 CSV 辅助:取第 idx 个项(抄入 buf,NUL 结尾),未命中返回空串。 * 用于 enum 名/标签 —— 不再 strtok_r(那会在只读字面量上写 NUL SEGV)。 */ static const char *csv_at(const char *s, int idx, char *buf, int buflen) { buf[0] = 0; if (!s || !s[0]) return buf; const char *p = s; int i = 0; for (;;) { const char *start = p; while (*p && *p != ',') p++; if (i == idx) { size_t k = (size_t)(p - start); if (k >= (size_t)buflen) k = (size_t)buflen - 1; memcpy(buf, start, k); buf[k] = 0; break; } if (!*p) break; p++; i++; } return buf; } static int csv_count(const char *s) { if (!s || !s[0]) return 0; int n = 1; for (const char *p = s; *p; p++) if (*p == ',') n++; return n; } static void enum_options_json(sbuf_t *s, const char *names, const char *labels) { sA(s, "["); int cnt = csv_count(names); for (int i = 0; i < cnt; i++) { char nb[128], lb[128]; csv_at(names, i, nb, sizeof nb); csv_at(labels, i, lb, sizeof lb); if (i) sA(s, ","); sA(s, "{\"name\":"); sA_str(s, nb); sA(s, ",\"label\":"); sA_str(s, lb[0] ? lb : nb); sA(s, "}"); } sA(s, "]"); } static void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val) { const char *tn = "str"; if (d->type == ST_INT) tn = "int"; else if (d->type == ST_BOOL) tn = "bool"; else if (d->type == ST_ENUM) tn = "enum"; sA(s, "{\"key\":"); sA_str(s, d->key); sA(s, ",\"type\":\"%s\",\"value\":", tn); if (d->type == ST_INT) { int v = atoi(val); if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v); else if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; } else if (v < d->min) v = d->min; sA(s, "%d", v); sA(s, ",\"min\":%d,\"max\":%d", d->min, int_meta_max(d)); } else if (d->type == ST_BOOL) { sA(s, "%s", str_truthy(val) ? "true" : "false"); } else { if (!strcmp(d->key, "panel.auth_pass") || !strcmp(d->key, "panel.pin")) { /* 密码/PIN 哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设) */ sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false"); } else { sA_str(s, val); } } sA(s, ",\"def\":"); sA_str(s, d->def); if (d->type == ST_ENUM) { sA(s, ",\"options\":"); enum_options_json(s, d->enum_names, d->enum_labels); sA(s, ",\"default_name\":"); sA_str(s, d->def); } sA(s, "}"); } /* 读某键当前值(无则用默认;str 用默认)。返回 malloc 字符串。 */ static char *settings_current_value(const setdef_t *d) { char *v = settings_get_value(d->key); if (v) return v; return strdup(d->def); } /* ---------------- 设置端点 ---------------- */ static int api_settings_get_any(int sock, const char *key) { const setdef_t *d = setdef_find(key); if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项"); char *val = settings_current_value(d); sbuf_t s = {0}; settings_item_json(&s, d, val); int rc = send_json(sock, 200, s.b ? s.b : ""); free(val); sbuf_free(&s); return rc; } static int api_settings_get_all(int sock) { sbuf_t s = {0}; sA(&s, "{\"items\":["); char groups[16][32]; int ngroups = 0; for (int i = 0; i < SETS_COUNT; i++) { const setdef_t *d = &g_sets[i]; char *val = settings_current_value(d); if (i) sA(&s, ","); settings_item_json(&s, d, val); free(val); if (d->group && d->group[0]) { int found = 0; for (int g = 0; g < ngroups; g++) if (!strcmp(groups[g], d->group)) { found = 1; break; } if (!found && ngroups < 16) snprintf(groups[ngroups++], 32, "%s", d->group); } } sA(&s, "],\"count\":%d,\"groups\":[", SETS_COUNT); for (int g = 0; g < ngroups; g++) { if (g) sA(&s, ","); sA_str(&s, groups[g]); } sA(&s, "]}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_settings_put(int sock, const char *key, const req_t *r) { const setdef_t *d = setdef_find(key); if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项"); if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *val = jv_obj_get(root, "value"); if (!val) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 value 字段"); } char raw[256]; switch (val->type) { case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break; case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break; case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break; default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持"); } jv_free(root); char hashbuf[200]; /* panel.pin:4 位数字 PIN,只落带盐哈希;空串=清除(回未设状态);非法(非 4 位数字)→ 422 */ if (!strcmp(key, "panel.pin")) { if (raw[0]) { if (!pin_validate(raw)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字"); if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败"); } else { hashbuf[0] = 0; /* 清空 → 回未设状态 */ } if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败"); pin_lock_reset(); sbuf_t s = {0}; settings_item_json(&s, d, hashbuf); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */ if (!strcmp(key, "panel.auth_pass")) { if (raw[0]) { if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "生成密码哈希失败"); } else { hashbuf[0] = 0; /* 清空 → 禁用密码 */ } if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败"); sbuf_t s = {0}; settings_item_json(&s, d, hashbuf); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* panel.auth_enabled(旧机制,兼容迁移):置 1 前须已有访问屏障(PIN 或旧密码) */ if (!strcmp(key, "panel.auth_enabled")) { char coerced[256]; if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法"); if (!strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) return send_err(sock, 422, "UNSUPPORTED", "请先设置面板 PIN 或密码"); if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败"); sbuf_t s = {0}; settings_item_json(&s, d, coerced); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } char coerced[256]; if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法"); } if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败"); /* boot.default 同步权威文件(同 meow_settings_save) */ if (!strcmp(key, "boot.default")) { int v = (!strcmp(coerced, "hiby")) ? 1 : 0; FILE *bf = fopen(BOOT_DEFAULT_FILE, "w"); if (bf) { fprintf(bf, "%d\n", v); fclose(bf); } } sbuf_t s = {0}; settings_item_json(&s, d, coerced); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_settings_batch(int sock, const req_t *r) { if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root || root->type != J_OBJ) { if (root) jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON 对象"); } sbuf_t ok = {0}, fail = {0}; int nok = 0, nfail = 0; for (const jv_t *p = root->child; p; p = p->next) { const setdef_t *d = setdef_find(p->str); if (!d) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"NOT_FOUND\",\"message\":\"未知设置项\"}"); nfail++; continue; } jv_t *val = p->child; char raw[256]; switch (val->type) { case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break; case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break; case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break; default: if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"BAD_REQUEST\",\"message\":\"非法值类型\"}"); nfail++; continue; } /* panel.pin:只落带盐哈希(空=清除);非法(非 4 位数字)→ 422 */ if (!strcmp(p->str, "panel.pin")) { char hashbuf[200]; if (raw[0]) { if (!pin_validate(raw)) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"PIN 必须为 4 位数字\"}"); nfail++; continue; } if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成 PIN 哈希失败\"}"); nfail++; continue; } } else hashbuf[0] = 0; if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; } else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; } continue; } /* panel.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */ if (!strcmp(p->str, "panel.auth_pass")) { char hashbuf[200]; if (raw[0]) { if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成密码哈希失败\"}"); nfail++; continue; } } else hashbuf[0] = 0; if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; } else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; } continue; } char coerced[256]; if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"值域/枚举非法\"}"); nfail++; continue; } /* panel.auth_enabled(迁移):启用前须已有访问屏障(PIN 或旧密码) */ if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板 PIN 或密码\"}"); nfail++; continue; } if (settings_write_one(p->str, coerced) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; } else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; } } jv_free(root); sbuf_t s = {0}; sA(&s, "{\"updated\":["); if (ok.b) sA(&s, "%s", ok.b); sA(&s, "],\"failed\":["); if (fail.b) sA(&s, "%s", fail.b); sA(&s, "]}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); sbuf_free(&ok); sbuf_free(&fail); return rc; } static int api_settings_restore(int sock) { if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败"); return send_json(sock, 200, "{\"reset\":true}"); } /* ---------------- 主题端点 ---------------- */ static void theme_json(sbuf_t *s) { const setdef_t *dst = setdef_find("ui.style"); const setdef_t *dth = setdef_find("display.theme"); const setdef_t *dbg = setdef_find("display.background"); char *style = settings_current_value(dst); char *theme = settings_current_value(dth); char *bg = settings_current_value(dbg); /* 当前值 → 中文标签(csv 里同下标查 label) */ char slabel[128] = "", tlabel[128] = ""; for (int i = 0; i < csv_count(dst->enum_names); i++) { char nb[128], lb[128]; csv_at(dst->enum_names, i, nb, sizeof nb); csv_at(dst->enum_labels, i, lb, sizeof lb); if (!strcmp(nb, style)) { snprintf(slabel, sizeof slabel, "%s", lb[0] ? lb : nb); break; } } for (int i = 0; i < csv_count(dth->enum_names); i++) { char nb[128], lb[128]; csv_at(dth->enum_names, i, nb, sizeof nb); csv_at(dth->enum_labels, i, lb, sizeof lb); if (!strcmp(nb, theme)) { snprintf(tlabel, sizeof tlabel, "%s", lb[0] ? lb : nb); break; } } sA(s, "{\"style\":"); sA_str(s, style); sA(s, ",\"theme\":"); sA_str(s, theme); sA(s, ",\"background\":"); sA_str(s, bg); sA(s, ",\"style_label\":"); sA_str(s, slabel); sA(s, ",\"theme_label\":"); sA_str(s, tlabel); sA(s, ",\"style_options\":"); enum_options_json(s, dst->enum_names, dst->enum_labels); sA(s, ",\"theme_options\":"); enum_options_json(s, dth->enum_names, dth->enum_labels); sA(s, ",\"background_options\":[{\"name\":\"black\",\"label\":\"黑\"},{\"name\":\"blue\",\"label\":\"深蓝\"},{\"name\":\"green\",\"label\":\"墨绿\"}]}"); free(style); free(theme); free(bg); } static int api_theme_get(int sock) { sbuf_t s = {0}; theme_json(&s); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_theme_put(int sock, const req_t *r) { if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); /* 允许 style/theme/background 任意子集 */ const char *keys[3] = { "style", "theme", "background" }; const char *setkeys[3] = { "ui.style", "display.theme", "display.background" }; for (int i = 0; i < 3; i++) { jv_t *v = jv_obj_get(root, keys[i]); if (!v) continue; const setdef_t *d = setdef_find(setkeys[i]); if (!d) continue; char raw[256]; if (v->type == J_STR) snprintf(raw, sizeof raw, "%s", v->str); else if (v->type == J_NUM) snprintf(raw, sizeof raw, "%lld", v->inum); else continue; char coerced[256]; if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "非法值"); } settings_write_one(setkeys[i], coerced); } jv_free(root); return api_theme_get(sock); } /* ---------------- 系统信息 / 性能 ---------------- */ static int api_perf(int sock, const char *path) { char json[4096]; int rc = ipc_perf_query(perf_subset_for_path(path), json, sizeof json); if (rc != MEOW_OK || !utf8_valid_n(json, strlen(json))) /* 上游坏字节 → 不返回,降级 */ return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)"); return send_json(sock, 200, json); } /* 从 meow_svc_perf 聚合 JSON 提取顶层 "network":{...} 段原始文本(含外层 {}), * 供 /api/system 透传(单一权威源,后端不再各自采集)。找不到顶层 network 返回 -1。 */ static int perf_extract_network(const char *json, char *out, int cap) { const char *p = json; for (;;) { p = strstr(p, "\"network\""); if (!p) return -1; const char *q = p + 9; /* strlen("\"network\"") */ while (*q == ' ' || *q == '\t') q++; if (*q == ':') { p = q + 1; break; } /* 顶层 network 键 */ p += 9; /* 嵌套同名键跳过 */ } while (*p == ' ' || *p == '\t') p++; if (*p != '{') return -1; int depth = 0, in_str = 0, esc = 0; const char *q; for (q = p; *q; q++) { char ch = *q; if (in_str) { if (esc) esc = 0; else if (ch == '\\') esc = 1; else if (ch == '"') in_str = 0; continue; } if (ch == '"') { in_str = 1; continue; } if (ch == '{') depth++; else if (ch == '}') { depth--; if (depth == 0) { q++; break; } } } if (depth != 0) return -1; size_t len = (size_t)(q - p); if (len == 0) return -1; if (len >= (size_t)cap) len = (size_t)cap - 1; memcpy(out, p, len); out[len] = 0; return 0; } /* /api/system:性能服务 SYSTEM 子集 + boot/ota 局部补全。 * network 段由 meow_svc_perf 聚合(SYSTEM_FRAMEWORK_SERVICES §4.1 已定案),此处**透传 perf 的 network**, * 后端不再各自采集 wifi/蓝牙;perf 若缺 network 段才回退 hostname_local(保持不崩)。 */ static int api_system(int sock) { char json[4096]; int rc = ipc_perf_query(0, json, sizeof json); if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)"); jv_t *root = jv_parse(json, (int)strlen(json)); if (!root) return send_err(sock, 500, "INTERNAL", "性能服务 JSON 解析失败"); jv_t *jsys = jv_obj_get(root, "system"); jv_t *jupt = jv_obj_get(root, "uptime_sec"); jv_t *jst = jv_obj_get(root, "storage"); jv_t *jtf = jv_obj_get(root, "tfcard"); const char *ver = "-", *host = "-", *ker = "-", *arch = "-"; if (jsys && jsys->type == J_OBJ) { jv_t *v = jv_obj_get(jsys, "version"); if (v && v->type == J_STR) ver = v->str; v = jv_obj_get(jsys, "hostname"); if (v && v->type == J_STR) host = v->str; v = jv_obj_get(jsys, "kernel"); if (v && v->type == J_STR) ker = v->str; v = jv_obj_get(jsys, "arch"); if (v && v->type == J_STR) arch = v->str; } long long upt = jupt ? (long long)jupt->dnum : 0; /* boot default:读权威 /data/meow_boot_default(0=meow 默认, 1=hiby),失败回退 meow */ int bdf = -1; { char b[16]; FILE *f = fopen(BOOT_DEFAULT_FILE, "r"); if (f) { if (fgets(b, sizeof b, f)) bdf = atoi(b); fclose(f); } } const char *bootname = bdf < 0 ? "meow" : (bdf ? "hiby" : "meow"); /* 当前运行模式:读 /tmp/meow_boot_mode(meow/hiby),缺失回退 default(PANEL_ENHANCE_PLAN 批1 前端读 boot.mode) */ char modebuf[16] = {0}; { FILE *f = fopen("/tmp/meow_boot_mode", "r"); if (f) { if (fgets(modebuf, sizeof modebuf, f)) { char *p = modebuf; while (*p) { if (*p == '\n' || *p == '\r' || *p == ' ') { *p = 0; break; } p++; } } fclose(f); } } const char *bootmode = (modebuf[0] == 'm' || modebuf[0] == 'h') ? modebuf : bootname; char host_local[80]; snprintf(host_local, sizeof host_local, "%s.local", host); sbuf_t s = {0}; sA(&s, "{\"version\":"); sA_str(&s, ver); sA(&s, ",\"hostname\":"); sA_str(&s, host); sA(&s, ",\"kernel\":"); sA_str(&s, ker); sA(&s, ",\"arch\":"); sA_str(&s, arch); sA(&s, ",\"uptime_sec\":%lld,\"boot\":{\"default\":", upt); sA_str(&s, bootname); sA(&s, ",\"mode\":"); sA_str(&s, bootmode); sA(&s, "},\"network\":"); char netbuf[1024]; /* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local);含非法 UTF-8(如坏 ssid)→ 回退,不嵌坏字节 */ if (perf_extract_network(json, netbuf, sizeof netbuf) == 0 && utf8_valid_n(netbuf, strlen(netbuf))) { sA(&s, "%s", netbuf); } else { /* 回退:perf 未含 network 段 → 仅 hostname_local(保持原语义,不崩) */ sA(&s, "{\"hostname_local\":"); sA_str(&s, host_local); sA(&s, "}"); } sA(&s, ",\"storage\":{"); if (jst && jst->type == J_OBJ) { jv_t *t = jv_obj_get(jst, "total"); long long tot = t ? (long long)t->dnum : 0; jv_t *u = jv_obj_get(jst, "used"); long long usd = u ? (long long)u->dnum : 0; jv_t *a = jv_obj_get(jst, "avail"); long long ava = a ? (long long)a->dnum : 0; sA(&s, "\"root\":{\"total\":%lld,\"used\":%lld,\"avail\":%lld}", tot, usd, ava); } else { sA(&s, "\"root\":{\"total\":0,\"used\":0,\"avail\":0}"); } sA(&s, ",\"tfcard\":{"); if (jtf && jtf->type == J_OBJ) { jv_t *pr = jv_obj_get(jtf, "present"); int present = pr && pr->bval; long long tot = 0, usd = 0, ava = 0; jv_t *t = jv_obj_get(jtf, "total"); if (t) tot = (long long)t->dnum; jv_t *u = jv_obj_get(jtf, "used"); if (u) usd = (long long)u->dnum; jv_t *a = jv_obj_get(jtf, "avail"); if (a) ava = (long long)a->dnum; sA(&s, "\"present\":%s,\"total\":%lld,\"used\":%lld,\"avail\":%lld}", present ? "true" : "false", tot, usd, ava); } else { sA(&s, "\"present\":false,\"total\":0,\"used\":0,\"avail\":0}"); } sA(&s, "},\"ota\":{\"current\":"); sA_str(&s, ver); sA(&s, ",\"latest\":"); sA_str(&s, ver); sA(&s, ",\"available\":false,\"url\":null}"); /* 重启原因 / 升级状态:svc_power 读级查询(POWER_REBOOT_REASON(8)/POWER_UPGRADE_STATUS(9),只读、 * 任意调用者)。st.msg 分别为 "coldboot=reboot|shutdown|unknown" 与 "upgrade=yes|no"。 * 前端批1-B 已留位(读 d.reboot_reason / d.upgrade_status),此处点亮。查询失败保持空串(不谎报)。 */ char rreason[48] = "", ustatus[32] = ""; { m_power_status_t st; memset(&st, 0, sizeof st); if (ipc_power_action(POWER_REBOOT_REASON, 0, NULL, &st) == MEOW_OK) svc_msg_kv(st.msg, "coldboot", rreason, sizeof rreason); memset(&st, 0, sizeof st); if (ipc_power_action(POWER_UPGRADE_STATUS, 0, NULL, &st) == MEOW_OK) svc_msg_kv(st.msg, "upgrade", ustatus, sizeof ustatus); } sA(&s, ",\"reboot_reason\":"); sA_str(&s, rreason); sA(&s, ",\"upgrade_status\":"); sA_str(&s, ustatus); sA(&s, "}"); jv_free(root); int ret = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return ret; } /* ---------------- 锁屏一次性解锁(写 /tmp/meow_pin_unlock_once,launcher 锁屏页消费) ---------------- */ static int api_pin_unlock_once(int sock) { const char *path = "/tmp/meow_pin_unlock_once"; FILE *f = fopen(path, "w"); if (!f) return send_err(sock, 500, "INTERNAL", "无法写入一次性解锁标志"); fputs("1\n", f); fclose(f); return send_json(sock, 200, "{\"unlock_once\":true}"); } /* ---------------- 系统动作(经 meow_svc_power IPC) ---------------- */ static int api_system_action(int sock, const char *which, const req_t *r) { (void)r; uint32_t cmd; if (!strcmp(which, "reboot")) cmd = POWER_REBOOT; else if (!strcmp(which, "shutdown")) cmd = POWER_SHUTDOWN; else if (!strcmp(which, "ota")) cmd = POWER_UPGRADE; else return send_err(sock, 404, "NOT_FOUND", "未知系统动作"); m_power_status_t st; memset(&st, 0, sizeof st); int rc = ipc_power_action(cmd, 0, NULL, &st); if (rc == MEOW_OK) return send_json(sock, 200, "{\"accepted\":true}"); /* 系统动作类服务端通常「先应答受理再执行」;此处若返回非 OK 则说明服务未受理 */ const char *why = st.msg[0] ? st.msg : power_code_str(rc); if (rc == MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", "电源服务拒绝(非 root 调用)"); if (rc == MEOW_ERR_NODEV) return send_err(sock, 503, "NO_CARD", "升级文件缺失或服务不可达"); if (rc == MEOW_ERR_NOENT || rc == MEOW_ERR_IO) return send_err(sock, 503, "NO_CARD", why); return send_err(sock, 500, "INTERNAL", why); } /* ---------------- 远程控制(按键注入) ---------------- */ static int api_key_single(int sock, int code, const req_t *r) { int press_ms = 0; char pressbuf[16] = "tap"; const char *press = pressbuf; if (r->body && r->body[0]) { jv_t *root = jv_parse(r->body, (int)r->body_len); if (root) { jv_t *p = jv_obj_get(root, "press"); if (p && p->type == J_STR) snprintf(pressbuf, sizeof pressbuf, "%s", p->str); jv_t *m = jv_obj_get(root, "ms"); if (m && m->type == J_NUM) press_ms = (int)m->inum; jv_free(root); } } if (!strcmp(press, "hold")) return send_err(sock, 501, "NOT_IMPLEMENTED", "hold 持续注入需常驻通道(阶段 D),后端未实现"); if (strcmp(press, "tap") && strcmp(press, "long")) return send_err(sock, 400, "BAD_REQUEST", "press 非法"); if (press_ms <= 0) { if (!strcmp(press, "long")) press_ms = 3000; } /* 物理键红线:禁止映射 返回(158)/主页(172)/确认/exit(APPS.md)。 */ if (code == 158 || code == 172 || code == 32 || code == 28 || code == 96) return send_err(sock, 403, "FORBIDDEN", "该键码(返回/主页/确认)按物理键红线禁止映射"); const char *tmpdev = NULL; int dispatchable = (keycode_to_device(code, &tmpdev) >= 0); if (!dispatchable) return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/电源/播放/上下曲)"); if (inject_key_release(code, press_ms) < 0) return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)"); sbuf_t s = {0}; sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press); int ret = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return ret; } static int api_key_batch(int sock, const req_t *r) { if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *keys = jv_obj_get(root, "keys"); if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); } sbuf_t s = {0}; int first = 1; for (const jv_t *k = keys->child; k; k = k->next) { if (k->type != J_OBJ) continue; jv_t *c = jv_obj_get(k, "code"); jv_t *p = jv_obj_get(k, "press"); jv_t *m = jv_obj_get(k, "ms"); if (!c || c->type != J_NUM) continue; int code = (int)c->inum; if (code == 158 || code == 172) continue; /* 红线跳过 */ int press_ms = (m && m->type == J_NUM) ? (int)m->inum : 0; if (p && p->type == J_STR && !strcmp(p->str, "long") && press_ms <= 0) press_ms = 3000; const char *tmpdev = NULL; if (keycode_to_device(code, &tmpdev) < 0) continue; if (inject_key_release(code, press_ms) < 0) continue; if (!first) sA(&s, ","); first = 0; sA(&s, "%d", code); } jv_free(root); sbuf_t o = {0}; if (first) sA(&o, "{\"dispatched\":[]}"); else sA(&o, "{\"dispatched\":[%s]}", s.b); int ret = send_json(sock, 200, o.b ? o.b : ""); sbuf_free(&s); sbuf_free(&o); return ret; } /* ---------------- 日志 ---------------- */ /* 日志来源分组(批2 崩溃/日志中心):按文件名前缀归组,供前端分组展示 + 前缀过滤。 * meow_* → system(MeowOS 服务); crash_* → crash; ams_* → ams; audio_monitor_* → audio_monitor; * net_* → net; power_* → power; 其余 → app(App/stdout 兜底)。 */ static const char *log_group(const char *name) { if (!strncmp(name, "crash_", 6)) return "crash"; if (!strncmp(name, "ams_", 4)) return "ams"; if (!strncmp(name, "audio_monitor_", 14)) return "audio_monitor"; if (!strncmp(name, "net_", 4)) return "net"; if (!strncmp(name, "power_", 6)) return "power"; if (!strncmp(name, "meow_", 5)) return "system"; return "app"; } static int api_logs_list(int sock) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); DIR *d = opendir(LOGS_DIR); if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在"); sbuf_t s = {0}; sA(&s, "{\"dir\":"); sA_str(&s, LOGS_DIR); sA(&s, ",\"files\":["); int first = 1; struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; if (is_sensitive_name(e->d_name)) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name); struct stat st; if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue; if (!first) sA(&s, ","); first = 0; sA(&s, "{\"name\":"); sA_str(&s, e->d_name); sA(&s, ",\"size\":%lld,\"mtime\":%lld,\"group\":", (long long)st.st_size, (long long)st.st_mtime); sA_str(&s, log_group(e->d_name)); sA(&s, "}"); } closedir(d); sA(&s, "]}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_logs_read(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char file[512], lines[16]; if (!qparam(r->query, "file", file, sizeof file)) return send_err(sock, 400, "BAD_REQUEST", "缺 file 参数"); int maxlines = 200; if (qparam(r->query, "lines", lines, sizeof lines)) { int n = atoi(lines); if (n > 0) maxlines = n; if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES; } /* 防逃逸:file 相对 LOGS_DIR,禁止 .. 与绝对路径 */ if (file[0] == '/' || strstr(file, "..")) return send_err(sock, 403, "FORBIDDEN", "文件路径逃逸"); if (is_sensitive_name(strrchr(file, '/') ? strrchr(file, '/') + 1 : file)) return send_err(sock, 403, "FORBIDDEN", "敏感文件"); char full[1024]; snprintf(full, sizeof full, "%s/%s", LOGS_DIR, file); int fd = open(full, O_RDONLY); if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "日志文件不存在"); struct stat st; if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); } /* 逐行读,保留末尾 maxlines 行 */ const char *buf = malloc((size_t)st.st_size + 1); if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); } ssize_t n = read(fd, (char *)buf, (size_t)st.st_size); close(fd); if (n < 0) { free((void *)buf); return send_err(sock, 500, "INTERNAL", "读日志失败"); } sbuf_t s = {0}; sA(&s, "{\"file\":"); sA_str(&s, file); sA(&s, ",\"lines\":["); /* 直接扫一遍,保留末尾 maxlines 行文本 */ const char *p = buf; const char *end = buf + n; int total = 0, truncated = 0; int gotlines = 0; int linecnt = 0; for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++; if (linecnt > 0 && end[-1] != '\n') linecnt++; int startrow = linecnt - maxlines; if (startrow < 0) startrow = 0; if (startrow > 0) truncated = 1; int rowidx = 0; p = buf; while (p < end) { const char *nl = memchr(p, '\n', (size_t)(end - p)); const char *line_end = nl ? nl : end; if (rowidx >= startrow) { if (gotlines) sA(&s, ","); /* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */ sA_str_n(&s, p, (size_t)(line_end - p)); gotlines++; } rowidx++; if (!nl) break; p = nl + 1; } total = linecnt; free((void *)buf); sA(&s, "],\"total\":%d,\"truncated\":%s}", total, truncated ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* ---- 崩溃汇总(批2 崩溃/日志中心):解析最新 crash_.log(meow_crash 落盘格式) * 一行一条:` `;svc 的 detail=` exit=`/` signal=`, * dmesg 的 detail=内核行原文。聚合出「每实体崩溃数 + 最近崩溃时间/信号」。只读、无副作用。 */ #define CRASH_TAIL_MAX (256u * 1024u) /* 崩溃日志单次读取上限(内存约束,不整读大文件) */ #define CRASH_ENT_MAX 64 /* 实体汇总上限 */ #define CRASH_DMESG_TAIL 5 /* dmesg 保留最近行数 */ static int api_logs_crash(int sock) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); /* 找最新 crash_*.log(按 mtime);无 → 空汇总(不 404,不谎报)。 */ DIR *d = opendir(LOGS_DIR); if (!d) return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}"); char best[512] = ""; long best_mt = -1; struct dirent *e; while ((e = readdir(d))) { if (strncmp(e->d_name, "crash_", 6) != 0 || e->d_name[0] == '.') continue; char full[1024]; snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name); struct stat st; if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue; if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); } } closedir(d); if (!best[0]) return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}"); char full[1024]; snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best); int fd = open(full, O_RDONLY); struct stat st; if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { if (fd >= 0) close(fd); return send_err(sock, 500, "INTERNAL", "崩溃日志不可读"); } off_t off = st.st_size > (off_t)CRASH_TAIL_MAX ? st.st_size - (off_t)CRASH_TAIL_MAX : 0; size_t buflen = (size_t)(st.st_size - off); if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}"); } if (off) (void)lseek(fd, off, SEEK_SET); char *buf = malloc(buflen + 1); if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); } ssize_t n = read(fd, buf, buflen); close(fd); if (n < 0) { free(buf); return send_err(sock, 500, "INTERNAL", "读崩溃日志失败"); } buf[n] = 0; /* 每实体聚合(svc 行);dmesg 单独计数 + 留最近几行。 * 有界缓冲(det ≤ 127 字符)保证 snprintf 到 name/detail 不越界且 -Wformat-truncation 零告警。 */ struct { char name[128]; int count; long long last_epoch; char kind[8]; int code; char detail[160]; } ents[CRASH_ENT_MAX]; int entn = 0, svc_count = 0, dmesg_count = 0; char dmesg_tail[CRASH_DMESG_TAIL][128]; int dmesg_tail_n = 0; char *ln = buf; while (*ln && (*ln == '\n')) ln++; while (*ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; long long epoch = 0, up = 0; char src[16] = "", det[128] = ""; int f = sscanf(ln, "%lld %lld %15s %127[^\n]", &epoch, &up, src, det); if (f >= 3) { if (!strcmp(src, "svc")) { svc_count++; /* det = ` exit=` 或 ` signal=`;name 为第一 token(就地切分) */ char name[128] = "", kind[8] = ""; int code = 0, k2 = 0; char *sp = strchr(det, ' '); if (sp) { *sp = 0; snprintf(name, sizeof name, "%s", det); /* det ≤127 → name[128] 不截断 */ char *kp = sp + 1; if (!strncmp(kp, "exit=", 5)) { snprintf(kind, sizeof kind, "exit"); code = atoi(kp + 5); k2 = 1; } else if (!strncmp(kp, "signal=", 7)) { snprintf(kind, sizeof kind, "sig"); code = atoi(kp + 7); k2 = 1; } else { snprintf(kind, sizeof kind, "crash"); k2 = 0; } } else { snprintf(name, sizeof name, "%s", det); snprintf(kind, sizeof kind, "crash"); } /* 聚合到 ents(按 name 匹配;超限则丢弃剩余陌生实体,仅计入总数) */ int found = -1; for (int i = 0; i < entn; i++) if (!strcmp(ents[i].name, name)) { found = i; break; } if (found < 0 && entn < CRASH_ENT_MAX) { found = entn++; snprintf(ents[found].name, sizeof ents[found].name, "%s", name); ents[found].count = 0; ents[found].last_epoch = 0; ents[found].kind[0] = 0; ents[found].code = 0; ents[found].detail[0] = 0; } if (found >= 0) { ents[found].count++; if (epoch >= ents[found].last_epoch) { ents[found].last_epoch = epoch; snprintf(ents[found].kind, sizeof ents[found].kind, "%s", k2 ? kind : "crash"); ents[found].code = code; snprintf(ents[found].detail, sizeof ents[found].detail, "%s", det); } } } else if (!strcmp(src, "dmesg")) { dmesg_count++; if (dmesg_tail_n < CRASH_DMESG_TAIL) snprintf(dmesg_tail[dmesg_tail_n++], sizeof dmesg_tail[0], "%s", det); else { /* 前移一行,末尾放最新 */ for (int i = 0; i < dmesg_tail_n - 1; i++) memcpy(dmesg_tail[i], dmesg_tail[i + 1], sizeof dmesg_tail[0]); snprintf(dmesg_tail[dmesg_tail_n - 1], sizeof dmesg_tail[0], "%s", det); } } } if (nl) { *nl = '\n'; ln = nl + 1; } else break; while (*ln == '\n') ln++; } free(buf); sbuf_t s = {0}; sA(&s, "{\"file\":"); sA_str(&s, best); sA(&s, ",\"count\":%d,\"dmesg_count\":%d,\"dmesg_tail\":[", svc_count, dmesg_count); for (int i = 0; i < dmesg_tail_n; i++) { if (i) sA(&s, ","); sA_str(&s, dmesg_tail[i]); } sA(&s, "],\"entities\":["); int first = 1; for (int i = 0; i < entn; i++) { if (!ents[i].count) continue; if (!first) sA(&s, ","); first = 0; sA(&s, "{\"name\":"); sA_str(&s, ents[i].name); sA(&s, ",\"source\":\"svc\",\"count\":%d,\"last_epoch\":%lld,\"last_kind\":", ents[i].count, ents[i].last_epoch); sA_str(&s, ents[i].kind[0] ? ents[i].kind : "crash"); sA(&s, ",\"last_code\":%d,\"last_detail\":", ents[i].code); sA_str(&s, ents[i].detail); /* 最近崩溃时间(epoch → 可读);strftime 本地时区,失败回退 epoch */ char tbuf[32] = ""; time_t t = (time_t)ents[i].last_epoch; struct tm tmv; if (ents[i].last_epoch > 0 && localtime_r(&t, &tmv)) strftime(tbuf, sizeof tbuf, "%Y-%m-%d %H:%M:%S", &tmv); sA(&s, ",\"last_time\":"); sA_str(&s, tbuf); sA(&s, "}"); } sA(&s, "]}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* ---------------- 通知(数据源 = 优先 launcher 内存活跃集,回退 TF 归档) ---------------- * 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务): * 1) launcher IPC 内存活跃集(实时)—— `MSG_NOTIFY_QUERY`(0x25)→`MSG_NOTIFY_QUERY_RESULT`(0x27) * (launcher `util.c` `case MSG_NOTIFY_QUERY`,**主循环单写者**序列化;agent-199 已落地)。 * 2) IPC 不可达/超时/应答失败 → 回退 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"},**不伪造数据**。 * 撤/清(delete / dismiss_all)同样**优先走 IPC**联动 launcher 内存(launcher 撤下时自动归档留档), * IPC 不可达时回退重写 TF 日志。clear(dismiss_all)语义:保留活动中的媒体/进度通知 * (ongoing||media||progress>=0||category=="media"),其余移除。 */ 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"); } /* ---------------- 通知实时化:launcher IPC 活跃集 ---------------- * 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务): * 1) launcher IPC 内存活跃集(实时)—— `MSG_NOTIFY_QUERY`(0x25)→`MSG_NOTIFY_QUERY_RESULT`(0x27), * JSON 字段同 TF 归档(id/app_id/category/summary/body/urgency/ts(ms)/progress/ongoing/media/ * target_activity/app_icon)。launcher 在**主循环单写者**序列化(绝不把通知存储读到 IPC 线程)。 * 2) IPC 不可达/超时/应答失败 → 回退 TF 归档 `notifications.log`(保留旧行为,见 §5.8)。 * 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},不伪造数据。 * 撤/清(delete / dismiss_all)同样**优先走 IPC**联动 launcher 内存(launcher 撤下时自动归档留档), * IPC 不可达时回退重写 TF 日志。 */ /* 发起 MSG_NOTIFY_QUERY(空 payload=全部 app),把 launcher 返回的 JSON 数组解析为 recs[]。 * 返回条数(>=0,按 ms 升序 oldest..newest,与 TF 路径一致);-1=IPC 不可达/应答失败/解析失败。 * 注:launcher 按固定 slot 表顺序序列化,非时间序,故此处按 ms 升序重排后再返回。 */ static int notif_ipc_query(notif_rec_t *recs, int maxrec) { int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS); if (fd < 0) return -1; int cnt = -1; if (ipc_hello(fd) < 0) goto done; if (send_frame(fd, MSG_NOTIFY_QUERY, 8, NULL, 0) < 0) goto done; uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < (int)sizeof(m_notify_query_result_t)) goto done; if (type != MSG_NOTIFY_QUERY_RESULT) goto done; m_notify_query_result_t qr; memcpy(&qr, buf, sizeof qr); if (qr.code != MEOW_OK) goto done; uint16_t tl = qr.text_len; if (tl >= sizeof qr.text) tl = (uint16_t)(sizeof qr.text - 1); if (tl == 0) { cnt = 0; goto done; } /* 无活跃通知 */ jv_t *arr = jv_parse(qr.text, tl); if (!arr) goto done; if (arr->type != J_ARR) { jv_free(arr); goto done; } int c = 0; for (jv_t *e = arr->child; e && c < maxrec; e = e->next) { if (e->type != J_OBJ) continue; notif_rec_t r; memset(&r, 0, sizeof r); r.progress = -1; r.urgency = 0; jv_t *v; if ((v = jv_obj_get(e, "id")) && v->type == J_NUM) r.id = (unsigned)v->inum; if ((v = jv_obj_get(e, "app_id")) && v->type == J_STR) snprintf(r.app_id, sizeof r.app_id, "%s", v->str); if ((v = jv_obj_get(e, "app_icon")) && v->type == J_STR) snprintf(r.app_icon, sizeof r.app_icon, "%s", v->str); if ((v = jv_obj_get(e, "category")) && v->type == J_STR) snprintf(r.category, sizeof r.category, "%s", v->str); if ((v = jv_obj_get(e, "summary")) && v->type == J_STR) snprintf(r.summary, sizeof r.summary, "%s", v->str); if ((v = jv_obj_get(e, "body")) && v->type == J_STR) snprintf(r.body, sizeof r.body, "%s", v->str); if ((v = jv_obj_get(e, "target_activity")) && v->type == J_STR) snprintf(r.target, sizeof r.target, "%s", v->str); if ((v = jv_obj_get(e, "urgency")) && v->type == J_NUM) r.urgency = (int)v->inum; /* ts 为 epoch 毫秒 → 存入 r.ms(emit 时 /1000 出秒,与 TF 同口径) */ if ((v = jv_obj_get(e, "ts")) && v->type == J_NUM) r.ms = (long long)v->inum; if ((v = jv_obj_get(e, "progress")) && v->type == J_NUM) r.progress = (int)v->inum; /* <0=无;json 无该键则 -1 */ if ((v = jv_obj_get(e, "ongoing")) && v->type == J_NUM) r.ongoing = (int)v->inum; if ((v = jv_obj_get(e, "media")) && v->type == J_NUM) r.media = (int)v->inum; recs[c++] = r; } jv_free(arr); /* 按 ms 升序(oldest..newest),与 TF 路径一致(emit 时倒序遍历= newest first)。 */ for (int i = 1; i < c; i++) { notif_rec_t key = recs[i]; int j = i - 1; while (j >= 0 && recs[j].ms > key.ms) { recs[j + 1] = recs[j]; j--; } recs[j + 1] = key; } cnt = c; done: close(fd); return cnt; } /* 撤下单条通知 → MSG_NOTIFY_DISMISS(0x22),期望 MSG_ACK。返回 0=受理;-1=IPC 不可达/应答失败/未受理。 */ static int notif_ipc_dismiss(unsigned id) { int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS); if (fd < 0) return -1; int rc = -1; if (ipc_hello(fd) < 0) goto done; uint32_t nid = (uint32_t)id; if (send_frame(fd, MSG_NOTIFY_DISMISS, 9, &nid, sizeof nid) < 0) goto done; uint8_t buf[64]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < (int)sizeof(m_ack_t)) goto done; if (type != MSG_ACK) goto done; m_ack_t a; memcpy(&a, buf, sizeof a); rc = (a.code == MEOW_OK) ? 0 : -1; done: close(fd); return rc; } /* 一键清除 → MSG_NOTIFY_DISMISS_ALL(0x26),期望 MSG_ACK(status=清除数,保留 ongoing/media/progress)。 * 返回 0=受理(*out_count 填清除数);-1=IPC 不可达/应答失败。 */ static int notif_ipc_dismiss_all(int *out_count) { int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS); if (fd < 0) return -1; int rc = -1; if (ipc_hello(fd) < 0) goto done; if (send_frame(fd, MSG_NOTIFY_DISMISS_ALL, 10, NULL, 0) < 0) goto done; uint8_t buf[64]; uint16_t type; uint32_t rid; int n = recv_frame(fd, &type, &rid, buf, sizeof buf); if (n < (int)sizeof(m_ack_t)) goto done; if (type != MSG_ACK) goto done; m_ack_t a; memcpy(&a, buf, sizeof a); if (a.code == MEOW_OK) { if (out_count) *out_count = (int)a.status; rc = 0; } done: close(fd); return rc; } static int api_notifications(int sock) { notif_rec_t recs[NOTIF_PREVIEW_MAX]; const char *source = "ipc"; int n = notif_ipc_query(recs, NOTIF_PREVIEW_MAX); if (n < 0) { /* IPC 不可达 → 回退 TF 归档 */ n = notif_load_list(recs, NOTIF_PREVIEW_MAX); source = (n < 0) ? "none" : "tf"; if (n < 0) n = 0; } 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\":\"%s\",\"items\":[", n, source); 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; } /* 重写通知日志。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) { 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) { /* 优先 IPC 联动 launcher 内存(launcher 撤下时自动归档留档);IPC 不可达/未受理回退 TF 重写。 */ if (notif_ipc_dismiss(id) == 0) { char d[64]; snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id); return send_json(sock, 200, d); } 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) { int cleared = 0; if (notif_ipc_dismiss_all(&cleared) == 0) { /* 联动 launcher 内存,返回清除数 */ char d[64]; snprintf(d, sizeof d, "{\"dismissed\":%d}", cleared); return send_json(sock, 200, d); } int removed = notif_rewrite(1, 0); /* IPC 不可达 → 回退重写 TF 日志 */ 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); } /* ---------------- 文件操作 ---------------- */ static int api_files_list(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char path[1024] = "/"; if (r->query[0]) qparam(r->query, "path", path, sizeof path); return files_list(sock, path); } static int api_files_read(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char path[1024]; if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path"); char abs[1024]; if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); long long size; int trunc; char *txt = read_text_file_limited(abs, &size, &trunc); if (txt == (char *)-2) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "二进制文件不支持文本读取"); if (!txt) return send_err(sock, 404, "NOT_FOUND", "文件不存在或过大"); sbuf_t s = {0}; sA(&s, "{\"path\":"); sA_str(&s, abs); sA(&s, ",\"content\":"); sA_str(&s, txt); sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false"); free(txt); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_files_download(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char path[1024]; if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path"); char abs[1024]; if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); const char *base = strrchr(abs, '/'); base = base ? base + 1 : abs; if (is_sensitive_name(base)) return send_err(sock, 403, "FORBIDDEN", "敏感文件"); return send_file(sock, 200, "application/octet-stream", abs, base); } static int api_files_upload(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "需 multipart/form-data"); upfile_t files[16]; memset(files, 0, sizeof files); int n = parse_multipart(r, files, 16); if (n < 0) return send_err(sock, 400, "BAD_REQUEST", "multipart 解析失败"); sbuf_t s = {0}; sA(&s, "{\"item\":["); int first = 1; for (int i = 0; i < n && i < 16; i++) { if (!files[i].filename[0]) continue; if (!strstr(files[i].path, "/")) continue; /* 无目标目录的跳过 */ struct stat st; if (stat(files[i].path, &st) < 0) continue; const char *base = strrchr(files[i].path, '/'); base = base ? base + 1 : files[i].path; if (!first) sA(&s, ","); first = 0; sA(&s, "{\"name\":"); sA_str(&s, base); sA(&s, ",\"type\":\"file\",\"size\":%lld,\"mtime\":%lld}", (long long)st.st_size, (long long)st.st_mtime); } sA(&s, "]}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* 递归删除目录/文件。 */ static int rm_recursive(const char *path) { struct stat st; if (lstat(path, &st) < 0) return -1; if (S_ISDIR(st.st_mode)) { DIR *d = opendir(path); if (!d) return -1; struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s", path, e->d_name); rm_recursive(full); } closedir(d); return rmdir(path); } return unlink(path); } static int api_files_mkdir(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *pj = jv_obj_get(root, "path"); if (!pj || pj->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path"); } char abs[1024]; if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); } struct stat st; if (stat(abs, &st) == 0) { jv_free(root); return send_err(sock, 409, "CONFLICT", "目录已存在"); } if (mkdir(abs, 0755) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "创建目录失败"); } const char *base = strrchr(abs, '/'); const char *b = base ? base + 1 : abs; sbuf_t s = {0}; sA(&s, "{\"item\":{\"name\":"); sA_str(&s, b); sA(&s, ",\"type\":\"dir\",\"size\":0,\"mtime\":%lld}}", (long long)time(NULL)); jv_free(root); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_files_rename(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *pj = jv_obj_get(root, "path"); jv_t *pnn = jv_obj_get(root, "new_name"); if (!pj || pj->type != J_STR || !pnn || pnn->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path/new_name"); } if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); } 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", "白名单外/路径逃逸"); } /* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */ 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", "重命名失败"); } sbuf_t s = {0}; sA(&s, "{\"item\":{\"name\":"); sA_str(&s, pnn->str); sA(&s, ",\"type\":\""); struct stat st; if (stat(newabs, &st) == 0 && S_ISDIR(st.st_mode)) sA(&s, "dir"); else sA(&s, "file"); sA(&s, "\",\"size\":%lld,\"mtime\":%lld}}", (long long)st.st_size, (long long)st.st_mtime); jv_free(root); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_files_delete(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char path[1024]; if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path"); char recursive[8] = "0"; qparam(r->query, "recursive", recursive, sizeof recursive); char abs[1024]; if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); if (!strcmp(abs, TF_ROOT)) return send_err(sock, 403, "FORBIDDEN", "禁止删除卡根"); struct stat st; if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在"); if (S_ISDIR(st.st_mode) && atoi(recursive) != 1) { DIR *d = opendir(abs); int nonempty = 0; if (d) { struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; nonempty = 1; break; } closedir(d); } if (nonempty) return send_err(sock, 409, "CONFLICT", "目录非空(需 recursive=1)"); } if (rm_recursive(abs) < 0) return send_err(sock, 500, "INTERNAL", "删除失败"); return send_json(sock, 200, "{\"removed\":true}"); } /* ---------------- 字体库 tar.gz 导入 / 导出(MeowOS/fonts///) ---------------- * 定案(用户 2026-08-31):**与 .mpk 同构,放弃 zip**——设备 busybox tar/gzip 必定存在, * 后端全部走 `gzip -dc | tar` 管道(nyai .mpk 同款),零新增依赖、不再自实现 zip 读写/miniz。 * 导入(POST /api/fonts/import,multipart file=<.tar.gz> [&force=1]): * - 上传 tar.gz(上限 MAX_FONT_UPLOAD=32MB,gbk/full 字库 png+idx 可能 10-20MB)→ * `gzip -dc | tar -t` 预校验成员名(sanitize_member:拒绝对路径/`..`/shell 危险字符,含三个以上条目疑拒); * 解压到临时目录 → opendir/lstat 逐个校验:恰为 font.png/font.idx/font.json 三个**普通文件** * (拒软链/目录/设备,拒解压后超大)→ 缺件/坏 json 硬失败。 * - family/weight 取 font.json 的 family(缺省回退 name)/weight(缺省 regular),safe_dir_segment 消毒。 * - rename 到 MeowOS/fonts///;目标已存在则 409(前端确认后带 force=1 重试)。 * - sha256_png/sha256_idx 仅 WARN(含在响应 warning 里);导入后**不触发 launcher 重扫** * (扫描时机=启动/进设置页,勿加热路径),响应用 hint 提示。 * 导出(GET /api/fonts/export?family=&weight=): * - `tar -cf - -C font.png font.idx font.json | gzip -c > ` 打包为 * `-.font.tar.gz` 下载流(先落临时文件求 Content-Length 再流式)。 * 列表(GET /api/fonts):扫描 fonts///(三件套齐)供导出选择。 */ #define FONT_EXTRACT_MAX (32u * 1024u * 1024u) /* 三件套解压后总大小上限(防 gzip 炸弹;gbk/full 亦在界内) */ static void fonts_root(char *buf, size_t cap) { snprintf(buf, cap, "%s/MeowOS/fonts", TF_ROOT); } static int safe_dir_segment(const char *s, char *out, size_t cap) { if (!s || !s[0]) return -1; if (!strcmp(s, ".") || !strcmp(s, "..")) return -1; for (const char *p = s; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; } if (strlen(s) + 1 > cap) return -1; snprintf(out, cap, "%s", s); return 0; } /* ---- tar 成员 sanitize(沿用 nyai 语义:拒绝对路径/`..` 组件/shell 危险字符) ---- */ static int unsafe_path(const char *s) { return s && strpbrk(s, " ;`$'\"&|<>") != NULL; } static int has_dotdot_comp(const char *p) { const char *s = p; while (*s) { if (s[0] == '.' && s[1] == '.' && (s == p || s[-1] == '/') && (s[2] == 0 || s[2] == '/')) return 1; s++; } return 0; } /* 校验并相对化为平铺成员名:去前导 '/' 与重复 './',禁绝对/`..`/危险字符。返回 0=ok 写 out。 */ static int sanitize_member(const char *in, char *out, int n) { if (!in || !*in) return -1; const char *p = in; while (*p == '/' || (p[0] == '.' && p[1] == '/')) p++; if (*p == '/' || !*p) return -1; if (has_dotdot_comp(p)) return -1; if (unsafe_path(p)) return -1; size_t l = strlen(p); if (l + 1 > (size_t)n) return -1; memcpy(out, p, l + 1); /* memcpy 而非 snprintf:p 可能来自长行缓冲,防 -Wformat-truncation 误报 */ return 0; } /* 递归建目录(mkdir 系统调用;已存在 EEXIST 忽略)。 */ static int mkdir_p(const char *path) { char buf[1600]; snprintf(buf, sizeof buf, "%s", path); for (char *p = buf + 1; *p; p++) { if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; } } return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1; } /* 逐一手写从 font.json 取字段(family/weight/name/tier/glyph_count/sha256_png/sha256_idx)。 * 返回 0=ok;*pf 指向解析出的 J_STR(供复制)。 */ static const jv_t *font_json_str(const jv_t *root, const char *key) { const jv_t *v = jv_obj_get(root, key); if (v && v->type == J_STR && v->str) return v; return NULL; } /* 得到已装载字体库(family/weight 三级扫描 + 三件套齐)JSON 数组。 */ static int api_fonts_list(int sock) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char root[1100]; fonts_root(root, sizeof root); DIR *fd = opendir(root); if (!fd) return send_json(sock, 200, "{\"fonts\":[]}"); sbuf_t s = {0}; sA(&s, "{\"fonts\":["); int first = 1; struct dirent *fe; while ((fe = readdir(fd))) { if (fe->d_name[0] == '.') continue; char famdir[1700]; snprintf(famdir, sizeof famdir, "%s/%s", root, fe->d_name); struct stat fst; if (stat(famdir, &fst) < 0 || !S_ISDIR(fst.st_mode)) continue; DIR *wd = opendir(famdir); if (!wd) continue; struct dirent *we; while ((we = readdir(wd))) { if (we->d_name[0] == '.') continue; char wdir[2200]; snprintf(wdir, sizeof wdir, "%s/%s", famdir, we->d_name); struct stat wst; if (stat(wdir, &wst) < 0 || !S_ISDIR(wst.st_mode)) continue; char pp[2560], pi[2560], pj[2560]; snprintf(pp, sizeof pp, "%s/font.png", wdir); snprintf(pi, sizeof pi, "%s/font.idx", wdir); snprintf(pj, sizeof pj, "%s/font.json", wdir); if (access(pp, R_OK) || access(pi, R_OK) || access(pj, R_OK)) continue; /* 解析 font.json(可选增强字段) */ char name[256] = "", tier[64] = ""; long long gc = 0; char jbuf[4096]; if (read_small_file(pj, jbuf, sizeof jbuf) >= 0) { jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf)); if (jr) { const jv_t *v = font_json_str(jr, "name"); if (v) snprintf(name, sizeof name, "%s", v->str); v = font_json_str(jr, "tier"); if (v) snprintf(tier, sizeof tier, "%s", v->str); else { v = font_json_str(jr, "charset"); if (v) snprintf(tier, sizeof tier, "%s", v->str); } v = jv_obj_get(jr, "glyph_count"); if (v && v->type == J_NUM) gc = v->inum; jv_free(jr); } } if (!name[0]) snprintf(name, sizeof name, "%s", fe->d_name); if (!first) sA(&s, ","); sA(&s, "{\"family\":"); sA_str(&s, fe->d_name); sA(&s, ",\"weight\":"); sA_str(&s, we->d_name); sA(&s, ",\"name\":"); sA_str(&s, name); sA(&s, ",\"tier\":"); sA_str(&s, tier); sA(&s, ",\"glyph_count\":%lld,\"mtime\":%lld}", gc, (long long)wst.st_mtime); first = 0; } closedir(wd); } closedir(fd); sA(&s, "]}"); return send_json(sock, 200, s.b ? s.b : "{\"fonts\":[]}"); } /* 在 multipart body 中定位 name="file" 字段数据区间(r->body 内)。 */ /* 对文件算 sha256 hex(64 字符,流式,不整读进内存)。返回 0=ok;-1=打开/读失败。 */ static int sha256_file_hex(const char *path, char out[65]) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; mp_sha256_ctx c; mp_sha256_init(&c); uint8_t buf[8192]; ssize_t n; while ((n = read(fd, buf, sizeof buf)) > 0) mp_sha256_update(&c, buf, (size_t)n); close(fd); if (n < 0) return -1; uint8_t h[32]; mp_sha256_final(&c, h); auth_hex(h, sizeof h, out); out[64] = 0; return 0; } static int api_fonts_import(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char force[8] = "0"; qparam(r->query, "force", force, sizeof force); long uni = (long)time(NULL); char tmpdir[1500], tmpgz[1500], stg[1500]; snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR); if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败"); snprintf(tmpgz, sizeof tmpgz, "%s/tmp/mp_font_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni); snprintf(stg, sizeof stg, "%s/tmp/mp_font_stg_%d_%ld", TF_CARD_DIR, (int)getpid(), uni); /* ① 写 multipart file 到临时 tar.gz */ if (multipart_extract_file(r, tmpgz) < 0) return send_err(sock, 400, "BAD_REQUEST", "缺字体库 tar.gz(multipart 字段 file)"); /* ② 成员名预校验:gzip -dc | tar -t → 每名 sanitize,且恰为三件套平铺(拒绝对/.. / 多出条目) */ { char cmd[3200]; snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null", tmpgz); FILE *pf = popen(cmd, "r"); if (!pf) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "无法列 tar 成员"); } int bad = 0, have_png = 0, have_idx = 0, have_json = 0, nent = 0; char line[512]; while (fgets(line, sizeof line, pf)) { char *e = strchr(line, '\n'); if (e) *e = 0; if (!line[0]) continue; char san[128]; if (sanitize_member(line, san, sizeof san) < 0) { bad = 1; break; } if (!strcmp(san, "font.png")) have_png = 1; else if (!strcmp(san, "font.idx")) have_idx = 1; else if (!strcmp(san, "font.json")) have_json = 1; else { bad = 1; break; } nent++; } pclose(pf); if (bad || nent != 3 || !have_png || !have_idx || !have_json) { unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 缺三件套或含非法成员(拒绝绝对路径/../多出条目)"); } } /* ③ 解压到临时目录 */ rm_recursive(stg); if (mkdir_p(stg) != 0) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "建临时目录失败"); } { char cmd[3200]; snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null", tmpgz, stg); if (system(cmd) != 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 解压失败(非 gzip/tar?)"); } } /* ④ 目录锁:恰为 3 个普通文件(lstat 拒软链/目录/设备),总大小 ≤ FONT_EXTRACT_MAX(防炸弹) */ { static const char *names[3] = { "font.png", "font.idx", "font.json" }; int seen[3] = {0,0,0}, cnt = 0, ok = 1; DIR *d = opendir(stg); if (!d) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "读临时目录失败"); } struct dirent *de; while ((de = readdir(d))) { if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue; int idx = -1; for (int i = 0; i < 3; i++) if (!strcmp(de->d_name, names[i])) { idx = i; break; } if (idx < 0) { ok = 0; break; } char fp[1800]; snprintf(fp, sizeof fp, "%s/%s", stg, de->d_name); struct stat st; if (lstat(fp, &st) != 0 || !S_ISREG(st.st_mode)) { ok = 0; break; } /* 拒软链/目录/设备 */ seen[idx] = 1; cnt++; } closedir(d); long long tot = 0; for (int i = 0; i < 3; i++) { if (!seen[i]) { ok = 0; break; } char fp[1600]; snprintf(fp, sizeof fp, "%s/%s", stg, names[i]); struct stat st; if (stat(fp, &st) != 0) { ok = 0; break; } tot += (long long)st.st_size; } if (!ok || cnt != 3 || tot > (long long)FONT_EXTRACT_MAX) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 内容非法(缺件/软链/多出条目/解压后超大)"); } } /* ⑤ 读 font.json → family/weight(可解析硬失败) */ char jpath[1600]; snprintf(jpath, sizeof jpath, "%s/font.json", stg); char jbuf[8192]; if (read_small_file(jpath, jbuf, sizeof jbuf) < 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 缺失"); } jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf)); if (!jr) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 解析失败"); } char family[128] = "font", weight[64] = "regular"; const jv_t *v = font_json_str(jr, "family"); if (v) snprintf(family, sizeof family, "%s", v->str); else { v = font_json_str(jr, "name"); if (v) snprintf(family, sizeof family, "%s", v->str); } v = font_json_str(jr, "weight"); if (v) snprintf(weight, sizeof weight, "%s", v->str); char fam_s[128], wt_s[64]; if (safe_dir_segment(family, fam_s, sizeof fam_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json family 非法"); } if (safe_dir_segment(weight, wt_s, sizeof wt_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json weight 非法"); } snprintf(family, sizeof family, "%s", fam_s); snprintf(weight, sizeof weight, "%s", wt_s); /* ⑥ 目标目录 + 覆盖冲突(409,除非 force=1) */ char root[1100], target[1500]; fonts_root(root, sizeof root); snprintf(target, sizeof target, "%s/%s/%s", root, family, weight); if (strcmp(force, "1") != 0) { static const char *names3[] = { "font.png", "font.idx", "font.json" }; char tp[1600]; int conflicted = 0; for (int i = 0; i < 3; i++) { snprintf(tp, sizeof tp, "%s/%s", target, names3[i]); if (access(tp, F_OK) == 0) { conflicted = 1; break; } } if (conflicted) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 409, "CONFLICT", "目标字库已存在,请确认覆盖"); } } /* ⑦ sha256 仅 WARN(对齐 font.json 约定,不硬失败) */ char warn[256] = ""; { const jv_t *wp = font_json_str(jr, "sha256_png"); const jv_t *wi = font_json_str(jr, "sha256_idx"); char need[65], got[65]; if (wp && wp->str[0]) { snprintf(need, sizeof need, "%s", wp->str); char fp[1600]; snprintf(fp, sizeof fp, "%s/font.png", stg); if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0) snprintf(warn, sizeof warn, "sha256_png 不匹配(源库校验失败,但已导入)"); } if (wi && wi->str[0] && !warn[0]) { snprintf(need, sizeof need, "%s", wi->str); char fp[1600]; snprintf(fp, sizeof fp, "%s/font.idx", stg); if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0) snprintf(warn, sizeof warn, "sha256_idx 不匹配(源库校验失败,但已导入)"); } } /* ⑧ 建目标目录 + rename 三件套(原子替换) */ { char p1[1400]; snprintf(p1, sizeof p1, "%s/%s", root, family); mkdir(p1, 0755); mkdir(target, 0755); } { static const char *names3[] = { "font.png", "font.idx", "font.json" }; int ok = 1; for (int i = 0; i < 3; i++) { char sp[1600], tp[1600]; snprintf(sp, sizeof sp, "%s/%s", stg, names3[i]); snprintf(tp, sizeof tp, "%s/%s", target, names3[i]); if (rename(sp, tp) < 0) ok = 0; } if (!ok) { rm_recursive(stg); unlink(tmpgz); jv_free(jr); return send_err(sock, 500, "INTERNAL", "写入字体文件失败(磁盘满?)"); } } rm_recursive(stg); unlink(tmpgz); sbuf_t s = {0}; sA(&s, "{\"family\":"); sA_str(&s, family); sA(&s, ",\"weight\":"); sA_str(&s, weight); sA(&s, ",\"imported\":true,\"hint\":\"字体已导入。请进设备「设置 → 字体」页,字体列表在进入时自动重扫生效。\",\"warning\":"); sA_str(&s, warn[0] ? warn : ""); sA(&s, "}"); jv_free(jr); return send_json(sock, 200, s.b ? s.b : ""); } static int api_fonts_export(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); char family[128], weight[64]; if (!qparam(r->query, "family", family, sizeof family)) family[0] = 0; if (!qparam(r->query, "weight", weight, sizeof weight)) weight[0] = 0; char f[128], w[64]; if (safe_dir_segment(family, f, sizeof f) < 0) return send_err(sock, 400, "BAD_REQUEST", "family 非法"); if (!weight[0]) snprintf(w, sizeof w, "%s", "regular"); else if (safe_dir_segment(weight, w, sizeof w) < 0) return send_err(sock, 400, "BAD_REQUEST", "weight 非法"); char root[1100], dir[1500]; fonts_root(root, sizeof root); snprintf(dir, sizeof dir, "%s/%s/%s", root, f, w); static const char *names3[] = { "font.png", "font.idx", "font.json" }; for (int i = 0; i < 3; i++) { char ap[1600]; snprintf(ap, sizeof ap, "%s/%s", dir, names3[i]); struct stat st; if (stat(ap, &st) < 0) { sbuf_t s = {0}; sA(&s, "font."); sA_str(&s, names3[i]); sA(&s, " 缺失(字库 <"); sA_str(&s, f); sA(&s, "/"); sA_str(&s, w); sA(&s, "> 不完整)"); int rc = send_err(sock, 404, "NOT_FOUND", s.b ? s.b : "字体三件套缺失"); sbuf_free(&s); return rc; } } /* 打包:tar -cf - -C 三件套 | gzip -c > ,落临时文件求 Content-Length 再流式下发 */ long uni = (long)time(NULL); char tmpdir[1500], tmp[1500]; snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR); if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败"); snprintf(tmp, sizeof tmp, "%s/tmp/mp_font_exp_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni); char cmd[3600]; snprintf(cmd, sizeof cmd, "tar -cf - -C '%s' font.png font.idx font.json 2>/dev/null | gzip -c > '%s' 2>/dev/null", dir, tmp); if (system(cmd) != 0) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包失败"); } struct stat st; if (stat(tmp, &st) < 0 || !S_ISREG(st.st_mode)) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包文件不可读"); } char fname[220]; snprintf(fname, sizeof fname, "%s-%s.font.tar.gz", f, w); char extra[300]; snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", fname); if (send_head(sock, 200, "application/gzip", (long long)st.st_size, extra) < 0) { unlink(tmp); return -1; } int fd = open(tmp, O_RDONLY); if (fd < 0) { unlink(tmp); return -1; } char buf[8192]; ssize_t n; while ((n = read(fd, buf, sizeof buf)) > 0) if (write_full(sock, buf, (size_t)n) < 0) { close(fd); unlink(tmp); return -1; } close(fd); unlink(tmp); return 0; } /* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */ /* 执行命令(sh -c),捕获 stdout(上限 cap-1),返回退出状态(-1=无法启动)。 */ static int run_cmd(const char *cmd, char *out, int outcap) { FILE *f = popen(cmd, "r"); if (!f) return -1; size_t off = 0; while (off < (size_t)outcap - 1) { size_t rd = fread(out + off, 1, (size_t)outcap - 1 - off, f); if (rd == 0) break; off += rd; } out[off] = 0; int st = pclose(f); int code = -1; if (WIFEXITED(st)) code = WEXITSTATUS(st); return code; } static int api_apps_list(int sock) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); DIR *d = opendir(MEOWAPPS_DIR); if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在"); sbuf_t s = {0}; sA(&s, "{\"count\":0,\"apps\":["); int first = 1, count = 0; struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue; if (strstr(e->d_name, ".bak")) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name); struct stat st; if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue; if (!first) sA(&s, ","); first = 0; apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0); count++; } closedir(d); sA(&s, "]}"); /* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */ /* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */ int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); (void)count; return rc; } static int api_apps_info(int sock, const char *pkg) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char appdir[1024]; snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg); struct stat st; if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装"); sbuf_t s = {0}; if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0) return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* multipart 提取第一个 file part 并写入 outpath。返回 0=成功。 */ static int multipart_extract_file(const req_t *r, const char *outpath) { const char *b = strstr(r->content_type, "boundary="); if (!b) return -1; b += 9; if (*b == '"') b++; char boundary[128]; int bi = 0; while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; } boundary[bi] = 0; char dmark[160]; snprintf(dmark, sizeof dmark, "--%s", boundary); const char *p = r->body; while (p < r->body + r->body_len) { const char *fs = strstr(p, dmark); if (!fs) break; fs += strlen(dmark); if (*fs == '-') break; if (*fs == '\r' || *fs == '\n') fs++; const char *he = strstr(fs, "\r\n\r\n"); if (!he) break; char hdr[512]; size_t hl = (size_t)(he - fs); if (hl >= sizeof hdr) hl = sizeof hdr - 1; memcpy(hdr, fs, hl); hdr[hl] = 0; int isfile = 0; char *save = NULL; for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) { char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++; if (strncasecmp(l, "Content-Disposition:", 20) == 0) { if (strstr(l, "name=\"file\"")) isfile = 1; } } const char *ds = he + 4; const char *de = mem_find(ds, (size_t)((r->body + r->body_len) - ds), dmark); if (!de) de = r->body + r->body_len; while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--; if (isfile) { int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) return -1; write_full(fd, ds, (size_t)(de - ds)); close(fd); return 0; } p = de; } return -1; } static const char *nyai_bin(void) { /* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */ if (!access(NYAI_PATH, X_OK)) return NYAI_PATH; return NYAI_PATH_CARD; } static int api_apps_install(int sock, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (access(nyai_bin(), X_OK) < 0) return send_err(sock, 500, "INTERNAL", "nyai 不可用(固件与卡上均无)"); char mpkpath[1024]; int ispath = 0; if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) { /* JSON {path} */ if (r->body && r->body[0]) { jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *pj = jv_obj_get(root, "path"); if (pj && pj->type == J_STR) { snprintf(mpkpath, sizeof mpkpath, "%s", pj->str); ispath = 1; } jv_free(root); } if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file"); } else { /* 保存上传 file 到临时文件 */ snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR); mkdir(mpkpath, 0777); char tmp[1024]; snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR); if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败"); snprintf(mpkpath, sizeof mpkpath, "%s", tmp); } if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在"); char cmd[1400]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' install '%s' 2>&1", nyai_bin(), mpkpath); char out[4096]; int code = run_cmd(cmd, out, sizeof out); if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败"); /* 包名 = 文件名茎;版本从 app.json 读 */ char stem[256]; const char *base = strrchr(mpkpath, '/'); base = base ? base + 1 : mpkpath; snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base); char *dot = strrchr(stem, '.'); if (dot) *dot = 0; char appdir[1024], pj[1100]; snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem); jv_t *approot = NULL; char version[16] = ""; int upgraded = 0; snprintf(pj, sizeof pj, "%s/app.json", appdir); char jbuf[8192]; if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0) approot = jv_parse(jbuf, (int)strlen(jbuf)); if (approot) { jv_t *v = jv_obj_get(approot, "version"); if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str); jv_free(approot); } if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1; sbuf_t s = {0}; sA(&s, "{\"package\":"); sA_str(&s, stem); sA(&s, ",\"version\":"); sA_str(&s, version[0] ? version : NULL); sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } static int api_apps_export(int sock, const char *pkg) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char tmp[1100]; 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); unlink(tmp); char cmd[1400]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' export '%s' '%s' 2>&1", nyai_bin(), pkg, tmp); char out[1024]; int code = run_cmd(cmd, out, sizeof out); if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败"); char dn[256]; snprintf(dn, sizeof dn, "%s.mpk", pkg); return send_file(sock, 200, "application/octet-stream", tmp, dn); } static int api_apps_remove(int sock, const char *pkg, const req_t *r) { if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); char purgebuf[8] = "0"; if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf); int use_purge = atoi(purgebuf); char cmd[1400]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' remove '%s'%s 2>&1", nyai_bin(), pkg, use_purge ? " --purge" : ""); char out[1024]; int code = run_cmd(cmd, out, sizeof out); if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败"); sbuf_t s = {0}; sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* ---------------- 实时画面(GET /api/screen.jpeg) ---------------- */ /* fb0 读当前显示帧(只读,不影响渲染):按 yoffset 读活动缓冲,16bpp RGB565。 */ static int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h) { int fd = open(dev, O_RDONLY); if (fd < 0) return -1; unsigned char vinfo[160], finfo[68]; memset(vinfo, 0, sizeof vinfo); memset(finfo, 0, sizeof finfo); if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 || ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { close(fd); return -1; } uint32_t xres = *(uint32_t *)(vinfo + 0); uint32_t yres = *(uint32_t *)(vinfo + 4); uint32_t yoff = *(uint32_t *)(vinfo + 20); uint32_t bpp = *(uint32_t *)(vinfo + 24); uint32_t line = *(uint32_t *)(finfo + 44); if (!line) line = xres * (bpp / 8); if (bpp != 16 || xres == 0 || yres == 0 || line < xres * 2) { close(fd); return -1; } size_t framelen = (size_t)line * (size_t)yres; uint8_t *frame = (uint8_t *)malloc(framelen); if (!frame) { close(fd); return -1; } ssize_t got = pread(fd, frame, framelen, (off_t)line * (off_t)yoff); close(fd); if (got != (ssize_t)framelen) { free(frame); return -1; } /* 若 line != xres*2(行尾填充),压缩为紧凑 RGB565(xres*yres*2) */ if (line != xres * 2) { size_t cplen = (size_t)xres * 2; for (uint32_t y = 0; y < yres; y++) memmove(frame + (size_t)y * cplen, frame + (size_t)y * line, cplen); } *out = frame; *w = (int)xres; *h = (int)yres; return 0; } /* 仅读 fb 几何(不抓帧),用于坐标 clamp 上限。 */ static int fb_dims(const char *dev, int *w, int *h) { int fd = open(dev, O_RDONLY); if (fd < 0) return -1; unsigned char vinfo[160]; memset(vinfo, 0, sizeof vinfo); if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0) { close(fd); return -1; } uint32_t xres = *(uint32_t *)(vinfo + 0); uint32_t yres = *(uint32_t *)(vinfo + 4); close(fd); if (!xres || !yres) return -1; *w = (int)xres; *h = (int)yres; return 0; } /* 把压缩帧缓存为「上一帧」基线(几何变化时重分配)。供 delta 模式 8×8 块比对。 */ static void screen_prev_store(const uint8_t *frame, int w, int h) { size_t need = (size_t)w * (size_t)h * 2; if (w != g_screen_pw || h != g_screen_ph) { free(g_screen_prev); g_screen_prev = NULL; g_screen_pw = w; g_screen_ph = h; g_screen_prev = (uint8_t *)malloc(need); } if (g_screen_prev) memcpy(g_screen_prev, frame, need); } /* 8×8 块比对:返回变化块数;bbox 为块坐标(bx/by=min 块,bw/bh=块数),像素原点=bx*8/by*8。 * changed==0 时 bbox 无效。*total_blocks 返回总块数。 */ static int screen_diff_blocks(const uint8_t *cur, const uint8_t *prev, int w, int h, int *bx, int *by, int *bw, int *bh, int *total_blocks) { const int B = SCREEN_BLOCK; int nbx = (w + B - 1) / B, nby = (h + B - 1) / B; if (total_blocks) *total_blocks = nbx * nby; int changed = 0; int minx = nbx, miny = nby, maxx = -1, maxy = -1; for (int byi = 0; byi < nby; byi++) { for (int bxi = 0; bxi < nbx; bxi++) { int x0 = bxi * B, y0 = byi * B; int wdt = B, hgt = B; if (x0 + wdt > w) wdt = w - x0; if (y0 + hgt > h) hgt = h - y0; int diff = 0; for (int y = 0; y < hgt && !diff; y++) { const uint8_t *a = cur + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2; const uint8_t *b = prev + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2; if (memcmp(a, b, (size_t)wdt * 2) != 0) diff = 1; } if (diff) { changed++; if (bxi < minx) minx = bxi; if (byi < miny) miny = byi; if (bxi > maxx) maxx = bxi; if (byi > maxy) maxy = byi; } } } if (changed) { *bx = minx; *by = miny; *bw = maxx - minx + 1; *bh = maxy - miny + 1; } return changed; } static int api_screen_jpeg(int sock, const req_t *r) { int q = 70; /* 默认质量(体积/清晰折中) */ char qv[8]; if (qparam(r->query, "q", qv, sizeof qv)) { int t = atoi(qv); if (t >= 1 && t <= 100) q = t; } char dv[8]; int delta = (qparam(r->query, "delta", dv, sizeof dv) && atoi(dv) == 1); const char *dev = getenv("MEOW_FB_DEV"); if (!dev || !dev[0]) dev = FB_DEV; uint8_t *frame = NULL; int w = 0, h = 0; if (screen_read_frame(dev, &frame, &w, &h) != 0) return send_err(sock, 500, "INTERNAL", "读取帧缓冲失败(/dev/fb0)"); if (!delta) { /* 默认整帧(不破坏原行为) */ uint8_t *jpeg = NULL; size_t jlen = 0; int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen); if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); } screen_prev_store(frame, w, h); /* 维护 delta 基线 */ int ret = send_raw(sock, 200, "image/jpeg", jpeg, jlen); free(jpeg); free(frame); return ret; } /* ---- delta 模式 ---- */ const int B = SCREEN_BLOCK; int changed = 0, bx = 0, by = 0, bw = 0, bh = 0, total = 0; int force_full = (!g_screen_prev || g_screen_pw != w || g_screen_ph != h); if (!force_full) { changed = screen_diff_blocks(frame, g_screen_prev, w, h, &bx, &by, &bw, &bh, &total); if (bw * B >= w && bh * B >= h) force_full = 1; /* 全屏变化 → 直接发整帧 */ } else { total = ((w + B - 1) / B) * ((h + B - 1) / B); } screen_prev_store(frame, w, h); /* 基线更新为当前帧 */ char hdr[224]; int ret = -1; if (changed == 0 && !force_full) { /* 无变化:空 body,前端跳过叠加 */ snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n" "X-Meow-Changed: 0\r\nX-Meow-Blocks: %d", w, h, total); ret = send_head(sock, 200, "image/jpeg", 0, hdr); free(frame); return ret; } if (force_full) { /* 冷藏起/几何变化/全屏变化 → 整帧 */ uint8_t *jpeg = NULL; size_t jlen = 0; int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen); if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); } snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 1\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n" "X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, changed ? changed : total, total); ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr); if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1; free(jpeg); free(frame); return ret; } /* 子图 = 变化块 bbox 外接矩形(8 对齐,钳到屏内),重编码为独立 JPEG 并以响应头回传偏移。 */ int x0 = bx * B, y0 = by * B; int sw = bw * B, sh = bh * B; if (x0 + sw > w) sw = w - x0; if (y0 + sh > h) sh = h - y0; uint8_t *sub = (uint8_t *)malloc((size_t)sw * (size_t)sh * 2); if (!sub) { free(frame); return send_err(sock, 500, "INTERNAL", "内存不足"); } for (int y = 0; y < sh; y++) memcpy(sub + (size_t)y * (size_t)sw * 2, frame + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2, (size_t)sw * 2); uint8_t *jpeg = NULL; size_t jlen = 0; int rc = mp_jpeg_encode_rgb565(sub, sw, sh, q, &jpeg, &jlen); free(sub); if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); } snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n" "X-Meow-Sub-X: %d\r\nX-Meow-Sub-Y: %d\r\nX-Meow-Sub-W: %d\r\nX-Meow-Sub-H: %d\r\n" "X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, x0, y0, sw, sh, changed, total); ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr); if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1; free(jpeg); free(frame); return ret; } /* ---------------- 触摸注入(POST /api/input/tap) ---------------- * 本机厂商内核 write 进 goodix(事件1) 会投递给读者(实测:写 tap 打开 App 成功),故复用 * 「写 evdev」注入(同按键注入机制,但用 MT 协议 B + legacy ABS_X/Y + BTN_TOUCH,等于真实 * goodix 事件序列)。仲裁:本机 event1 最近活动后 5s 内忽略注入(本机优先打断)。 */ #define TOUCH_ACTIVE_MS 5000 static int64_t now_mono_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } static int64_t g_last_self_inject_ms = 0; /* 本进程上次注入时刻(用于剔除自注入事件) */ /* 排空触摸设备,返回「最近一次本机真实触摸」的单调毫秒时间(0=无)。剔除自注入事件。 * MIPS 的 struct input_event 无 time 成员(ABI),按 launcher 同款读裸 16 字节、 * 取偏移 0(int32 tv_sec)/4(int32 tv_usec)。 */ static int64_t touch_last_local_ms(const char *dev) { int fd = open(dev, O_RDONLY | O_NONBLOCK); if (fd < 0) return 0; int64_t last = 0; unsigned char ev[16]; int64_t inject_deadline = g_last_self_inject_ms + 200; /* 注入事件时间 ≈ 注入时刻 */ while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { int64_t sec = (int64_t)(*(int32_t *)(ev + 0)); int64_t usec = (int64_t)(*(int32_t *)(ev + 4)); int64_t t = sec * 1000 + usec / 1000; if (t <= inject_deadline) continue; /* 自注入,跳过 */ if (t > last) last = t; } close(fd); return last; } static int inject_tap(const char *dev, int x, int y) { int fd = open(dev, O_WRONLY); if (fd < 0) return -1; struct input_event ev; #define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \ ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0) /* 事件顺序:先 BTN_TOUCH=1(按下边沿),后 ABS 坐标 —— launcher/meow_touch 在按下边沿 * 会重置坐标 (-1),只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)。若先发 * ABS 再发 BTN_TOUCH,原点永不锁定 → 抬手表被判为无效 tap(只高亮不启动)。 */ __WE(EV_KEY, BTN_TOUCH, 1); __WE(EV_ABS, ABS_MT_SLOT, 0); __WE(EV_ABS, ABS_MT_TRACKING_ID, 7); __WE(EV_ABS, ABS_MT_POSITION_X, x); __WE(EV_ABS, ABS_MT_POSITION_Y, y); __WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6); __WE(EV_ABS, ABS_X, x); __WE(EV_ABS, ABS_Y, y); __WE(EV_SYN, SYN_REPORT, 0); usleep(100000); /* 按住 100ms 再抬起,抬起到按下的边沿被 launcher 视为一次 tap */ __WE(EV_KEY, BTN_TOUCH, 0); __WE(EV_ABS, ABS_MT_TRACKING_ID, -1); __WE(EV_SYN, SYN_REPORT, 0); #undef __WE close(fd); return 0; } static int api_input_tap(int sock, const req_t *r) { if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jx = jv_obj_get(root, "x"); jv_t *jy = jv_obj_get(root, "y"); if (!jx || jx->type != J_NUM || !jy || jy->type != J_NUM) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 x/y(数值)"); } int x = (int)jx->inum, y = (int)jy->inum; jv_free(root); const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV; /* 坐标 clamp 到屏幕范围(前端已按 480x720 映射,此处再兜底) */ int sw = 480, sh = 720; { int fx = 0, fy = 0; const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV; if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } } if (x < 0) x = 0; if (x >= sw) x = sw - 1; if (y < 0) y = 0; if (y >= sh) y = sh - 1; /* 仲裁:本机最近触摸后 5s 内忽略注入 */ int64_t now = now_mono_ms(); int64_t last = touch_last_local_ms(dev); if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) { sbuf_t s = {0}; sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}", (long long)(TOUCH_ACTIVE_MS - (now - last))); int ret = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return ret; } g_last_self_inject_ms = now_mono_ms(); if (inject_tap(dev, x, y) < 0) return send_err(sock, 500, "INTERNAL", "触摸注入失败(无法打开触摸设备)"); sbuf_t s = {0}; sA(&s, "{\"dispatched\":true,\"x\":%d,\"y\":%d}", x, y); int ret = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return ret; } /* ---------------- 静态文件托管(web_root = 二进制所在目录) ---------------- */ static char g_webroot[512]; static const char *web_root(void) { if (g_webroot[0]) return g_webroot; char exe[512]; ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1); if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; } exe[n] = 0; char *slash = strrchr(exe, '/'); if (slash) *slash = 0; snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : "."); return g_webroot; } static const char *mime_for(const char *path) { const char *ext = strrchr(path, '.'); if (!ext) return "application/octet-stream"; if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8"; if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8"; if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8"; if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8"; if (!strcasecmp(ext, ".png")) return "image/png"; if (!strcasecmp(ext, ".ico")) return "image/x-icon"; if (!strcasecmp(ext, ".svg")) return "image/svg+xml"; return "application/octet-stream"; } static int serve_static(int sock, const char *path) { const char *root = web_root(); char rel[512]; /* / → index.html;/assets/ → ;/ → (meowpanel.css/js 等) */ if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html"); else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8); else snprintf(rel, sizeof rel, "%s", path + 1); if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸"); /* 只允许安全文件(防读到二进制/任意文件) */ char full[1024]; snprintf(full, sizeof full, "%s/%s", root, rel); /* 敏感名拒绝 */ const char *base = strrchr(full, '/'); base = base ? base + 1 : full; if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") || !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件"); FILE *f = fopen(full, "rb"); if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在"); fclose(f); return send_file(sock, 200, mime_for(full), full, NULL); } /* ---------------- 路由分发 ---------------- */ static int handle(int sock, req_t *r) { const char *p = r->path; if (strncmp(p, "/api/", 5) != 0) { if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0) return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET"); return serve_static(sock, p); } /* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */ if (require_auth(sock, r) != 1) return 0; /* ---- 鉴权端点(登录/设置/登出/状态:公开——访问面板即需登录,须先经此建立会话) ---- */ if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r); if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/setup")) return api_auth_setup(sock, r); if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/logout")) return api_auth_logout(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/auth/status")) return api_auth_status(sock, r); /* ---- 性能/状态 ---- */ if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf"))) return api_perf(sock, p); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system")) return api_system(sock); /* ---- 字体库(导入/导出/列表) ---- */ if (!strcmp(r->method, "POST") && !strcmp(p, "/api/fonts/import")) return api_fonts_import(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r); /* ---- 音频状态(meow_audiod 快照,只读;含 rms/peak/lvlseq 电平键) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock); /* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock); if (!strcmp(r->method, "GET")) { char seg[256]; if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0]) return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */ } /* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock); /* ---- 实时画面(阶段 C) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg")) return api_screen_jpeg(sock, r); /* ---- 系统动作 ---- */ if (!strcmp(r->method, "POST")) { if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r); if (!strcmp(p, "/api/system/shutdown")) return api_system_action(sock, "shutdown", r); if (!strcmp(p, "/api/system/ota")) return api_system_action(sock, "ota", r); if (!strcmp(p, "/api/system/pin-unlock-once")) return api_pin_unlock_once(sock); if (!strcmp(p, "/api/debug/report_bad_json")) { /* 前端 NOT_JSON 自动回传浏览器收到的原始字节(抓间歇性破包) */ FILE *df = fopen("/tmp/overview_client_bad.txt", "w"); if (df && r->body && r->body_len) fwrite(r->body, 1, r->body_len, df); if (df) fclose(df); panel_log("debug: client bad json captured %zu bytes -> /tmp/overview_client_bad.txt", (size_t)r->body_len); return send_json(sock, 200, "{\"captured\":true}"); } if (!strcmp(p, "/api/settings")) return api_settings_batch(sock, r); if (!strcmp(p, "/api/settings/restore")) return api_settings_restore(sock); if (!strcmp(p, "/api/theme")) return api_theme_put(sock, r); if (!strcmp(p, "/api/files/upload")) return api_files_upload(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/apps/install")) return api_apps_install(sock, r); if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock); if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock); if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len); if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len); if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len); if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len); if (!strcmp(p, "/api/key")) return api_key_batch(sock, r); if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r); char seg[256]; if (path_segment("/api/key/", p, seg, sizeof seg)) return api_key_single(sock, atoi(seg), r); /* POST /api/ams//start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */ if (!strncmp(p, "/api/ams/", 9)) { size_t pl = strlen(p); if (pl > 5 && !strcmp(p + pl - 6, "/start")) { char pkg[256]; if (path_segment("/api/ams/", p, pkg, sizeof pkg)) return api_ams_ctl(sock, pkg, 0); } else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) { char pkg[256]; if (path_segment("/api/ams/", p, pkg, sizeof pkg)) return api_ams_ctl(sock, pkg, 1); } } } /* ---- 设置 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/settings")) return api_settings_get_all(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/theme")) return api_theme_get(sock); if (!strcmp(r->method, "PUT")) { char seg[256]; if (path_segment("/api/settings/", p, seg, sizeof seg)) return api_settings_put(sock, seg, r); } if (!strcmp(r->method, "GET")) { char seg[256]; if (path_segment("/api/settings/", p, seg, sizeof seg)) return api_settings_get_any(sock, seg); } /* ---- 文件 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r); if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r); /* ---- 应用 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/apps")) return api_apps_list(sock); if (!strcmp(r->method, "GET")) { char seg[256]; if (path_segment("/api/apps/", p, seg, sizeof seg)) { /* :pkg/export 结尾(path_segment 已把 seg 截为 pkg,这里检查原 path 尾缀) */ size_t pl = strlen(p); int is_export = (pl >= 7 && !strcmp(p + pl - 7, "/export")); if (is_export) return api_apps_export(sock, seg); return api_apps_info(sock, seg); } } if (!strcmp(r->method, "DELETE")) { char seg[256]; if (path_segment("/api/apps/", p, seg, sizeof seg)) return api_apps_remove(sock, seg, r); } /* ---- 日志 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs")) return api_logs_list(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/crash")) return api_logs_crash(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/read")) return api_logs_read(sock, r); /* ---- 通知 ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock); if (!strcmp(r->method, "DELETE")) { char seg[256]; if (path_segment("/api/notifications/", p, seg, sizeof seg)) { unsigned nid = (unsigned)strtoul(seg, NULL, 10); return api_notification_delete(sock, nid); } } return send_err(sock, 404, "NOT_FOUND", "端点不存在"); } /* ---------------- 主循环 ---------------- */ static int g_port = DEFAULT_PORT; static int read_port_config(void) { /* 配置文件 meowpanel.conf(web 根目录),格式 key=value,取 port */ char path[1024]; snprintf(path, sizeof path, "%s/meowpanel.conf", web_root()); FILE *f = fopen(path, "r"); if (f) { char line[160]; while (fgets(line, sizeof line, f)) { char *eq = strchr(line, '='); if (!eq) continue; char *k = line; while (*k == ' ' || *k == '\t') k++; if (!strncmp(k, "port", 4) && k[4] == '=') g_port = atoi(eq + 1); } fclose(f); } const char *env = getenv("MEOWPORT"); if (env && env[0]) g_port = atoi(env); if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT; return g_port; } static void handle_conn(int sock) { req_t r; memset(&r, 0, sizeof r); 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); req_free(&r); close(sock); } /* ---------------- 宿主自测(`meowpanel --selftest`) ---------------- * 复现 2026-09-02 事故:amsd 的 am list 把 App 显示名「流媒体客户端」(18B)截成 16B, * 腰斩「端」字(U+7AEF = E7 AB AF → 只剩首字节 E7)。面板必须把这类非法 UTF-8 消毒成合法字符, * JSON 才不会被浏览器拒收。直接喂截断串给 utf8_sanitize / sA_str / ams_fill 桩,断言: * 1) utf8_sanitize 改写截断串、保持字节长度、产物为合法 UTF-8,且不误改完整合法名; * 2) sA_str 嵌入 JSON 字符串后整串为合法 UTF-8、jv_parse 可完整解析; * 3) ams_fill 桩(APP 行解析 + sA_str)产物同理可解析。 * 仅宿主 `meowpanel --selftest` 进入;设备服务循环不触发。 */ static int panel_selftest(void) { const char *trunc = "流媒体客户\xE7"; /* 事故里被腰斩的显示名(16B,末字节为「端」首字节) */ const char *oknat = "流媒体客户端"; /* 完整合法名(18B,回归确认不误伤) */ size_t n = strlen(trunc); /* 1a) utf8_sanitize:截断串必被改写,长度保持,产物合法 UTF-8 */ char san[64]; if (!utf8_sanitize(san, trunc, n)) { printf("SELFTEST FAIL: utf8_sanitize 未改写截断串\n"); return 1; } if (strlen(san) != n) { printf("SELFTEST FAIL: utf8_sanitize 长度变化 %zu->%zu\n", n, strlen(san)); return 1; } if (!utf8_valid_n(san, strlen(san))) { printf("SELFTEST FAIL: utf8_sanitize 产物含非法字节\n"); return 1; } /* 1b) 完整合法名不得被改写(误伤检测) */ char san2[64]; if (utf8_sanitize(san2, oknat, strlen(oknat)) != 0 || strcmp(san2, oknat) != 0) { printf("SELFTEST FAIL: utf8_sanitize 误改合法 UTF-8\n"); return 1; } /* 2) sA_str 嵌入:产物合法 UTF-8 且 jv_parse 完整解析 */ sbuf_t s = {0}; sA(&s, "{\"pkg\":"); sA_str(&s, "com.example.trunc"); sA(&s, ",\"name\":"); sA_str(&s, trunc); sA(&s, ",\"state\":"); sA_str(&s, "running"); sA(&s, "}"); if (!s.b) { printf("SELFTEST FAIL: sA_str 输出空\n"); return 1; } if (!utf8_valid_n(s.b, s.n)) { printf("SELFTEST FAIL: sA_str 产物含非法字节\n"); sbuf_free(&s); return 1; } jv_t *r = jv_parse(s.b, (int)s.n); if (!r) { printf("SELFTEST FAIL: sA_str 产物 JSON 无法解析\n"); sbuf_free(&s); return 1; } jv_t *nm = jv_obj_get(r, "name"); if (!nm || nm->type != J_STR) { printf("SELFTEST FAIL: sA_str 产物缺 name\n"); jv_free(r); sbuf_free(&s); return 1; } jv_free(r); sbuf_free(&s); /* 3) ams_fill 桩:模拟 APP 行 parse 出的截断名(join_tokens 后)→ apps 数组可解析 */ sbuf_t a = {0}; sA(&a, "{\"available\":true,\"count\":1,\"apps\":[{\"pkg\":"); sA_str(&a, "com.example.trunc"); sA(&a, ",\"name\":"); sA_str(&a, trunc); sA(&a, ",\"state\":"); sA_str(&a, "running"); sA(&a, ",\"pid\":123}]}"); if (!a.b || !utf8_valid_n(a.b, a.n)) { printf("SELFTEST FAIL: ams_fill 桩产物含非法字节/为空\n"); sbuf_free(&a); return 1; } jv_t *ar = jv_parse(a.b, (int)a.n); if (!ar) { printf("SELFTEST FAIL: ams_fill 桩产物 JSON 无法解析\n"); sbuf_free(&a); return 1; } jv_free(ar); sbuf_free(&a); /* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。 * 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */ { static const char ptext[] = "# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n" "app:com.example.trunc net=block domain=+good.example,-bad\xE7\n" "svc:meowpanel net=allow domain=+svc.local\n"; sbuf_t p = {0}; net_policy_parse(ptext, &p); if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; } jv_t *pr = jv_parse(p.b, (int)p.n); if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; } jv_t *rules = jv_obj_get(pr, "rules"); if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; } jv_t *enf = jv_obj_get(pr, "enforce"); if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; } jv_free(pr); sbuf_free(&p); } /* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。 * detail 合法时 → 合法 JSON 可解析。 */ { char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n"; sbuf_t h = {0}; ams_history_parse(bad, &h, "com.example.trunc"); if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; } sbuf_free(&h); char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n"; sbuf_t h2 = {0}; ams_history_parse(good, &h2, "com.example.trunc"); if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; } sbuf_free(&h2); } printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n"); return 0; } int main(int argc, char **argv) { /* 参数:-p PORT(可选)。默认端口 8080。 */ int port = DEFAULT_PORT; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--selftest")) return panel_selftest(); if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]); else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i]; else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i]; else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i]; else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i]; else if (!strcmp(argv[i], "--sock-net") && i + 1 < argc) g_sock_net = argv[++i]; else if (!strcmp(argv[i], "--sock-pm") && i + 1 < argc) g_sock_pm = argv[++i]; else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { printf("MeowPanel backend %s\n", PANEL_VERSION); printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P] [--sock-net P] [--sock-pm P]\n"); return 0; } } init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */ const char *env = getenv("MEOW_IPC_PERF_SOCK"); if (env && env[0]) g_sock_perf = env; env = getenv("MEOW_IPC_POWER_SOCK"); if (env && env[0]) g_sock_power = env; env = getenv("MEOW_IPC_LAUNCHER_SOCK"); if (env && env[0]) g_sock_launcher = env; env = getenv("MEOW_AUDIO_SOCK"); if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_NET_SOCK"); if (env && env[0]) g_sock_net = env; /* 与 netmgrd/meow_net 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_PM_SOCK"); if (env && env[0]) g_sock_pm = env; /* 与 powermgr/meow_power 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_AUTH_TTL"); if (env && env[0]) g_auth_ttl = (time_t)atol(env); /* 会话 TTL 覆盖(宿主测试用) */ env = getenv("MEOW_SETTINGS_FILE"); if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */ (void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */ /* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */ if (port != DEFAULT_PORT) g_port = port; else g_port = read_port_config(); /* 再按 env MEOWPORT 覆盖 conf(-p 最高) */ if (port == DEFAULT_PORT) { const char *ep = getenv("MEOWPORT"); if (ep && ep[0]) g_port = atoi(ep); } if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT; signal(SIGPIPE, SIG_IGN); web_root(); panel_log("MeowPanel %s listening on port %d (web root %s)", PANEL_VERSION, g_port, web_root()); int lfd = socket(AF_INET, SOCK_STREAM, 0); if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; } int on = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on); struct sockaddr_in addr; memset(&addr, 0, sizeof addr); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons((uint16_t)g_port); if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) { panel_log("bind :%d fail: %s", g_port, strerror(errno)); close(lfd); return 1; } if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; } for (;;) { int c = accept(lfd, NULL, NULL); if (c < 0) { if (errno == EINTR) continue; continue; } handle_conn(c); } return 0; }