Files
MeowOS-For-R3P2/patch_v2/meowpanel/meowpanel.c
T

5751 lines
249 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp/strncasecmp */
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <poll.h>
#include <time.h>
#include <ctype.h>
#include <dirent.h>
#include <limits.h>
#include <math.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/input.h> /* 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; /* 派生:<root>/MeowOS */
static const char *g_apps_dir = NULL; /* 派生:<root>/MeowAPPs */
static const char *g_logs_dir = NULL; /* 派生:<card>/logs */
static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notifications/notifications.log */
#define TF_ROOT g_tf_root_buf
#define TF_CARD_DIR g_tf_card
#define MEOWAPPS_DIR g_apps_dir
#define LOGS_DIR g_logs_dir
#define PERF_SOCK "/usr/data/meow_perf.sock"
#define 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。 */
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; jv_free(c->child); free(c->str); free(c); 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;
}
/* ---------------- 设置表(权威来源: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 哈希(`<salt_hex>:<sha256_hex(salt||pin)>`,绝不存明文)。
* 与未来 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);
}
/* ---- 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, "}}");
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_<bootts>.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;
}
/* ---------------- 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,返回个数;<maxtoks)。 */
static int txt_tokens(char *buf, char **toks, int maxtoks)
{
int n = 0;
char *p = buf;
while (*p && n < maxtoks) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (!*p) break;
toks[n++] = p;
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
if (*p) *p++ = 0;
}
return n;
}
/* join toks[from..to) 空格连接进 out(exec/name 可能含空格)。 */
static void join_tokens(char **toks, int from, int to, char *out, size_t cap)
{
size_t o = 0;
for (int i = from; i < to; i++) {
size_t need = strlen(toks[i]) + (o ? 1 : 0);
if (o + need >= 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;
}
/* 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 <pkg> <name...> <state> <pid> <launches> <crashes> <last_kind> <last_code>
* 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);
}
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;
}
/* 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},");
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);
if (s.b && (nul_inside || utf8_bad)) { 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 n=%zu strlen=%zu), raw dumped /tmp/overview_bad.json", nul_inside, utf8_bad, 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<buflen,用 memcpy 避免 %s 截断告警 */
return 0;
}
/* 展示 rel(相对 root):如 /MeowOS/x。 */
static void make_rel(const char *abs, char *buf, size_t buflen)
{
size_t rl = strlen(TF_ROOT);
if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; }
const char *rest = abs + rl;
if (*rest == 0) snprintf(buf, buflen, "/");
else snprintf(buf, buflen, "%s", rest);
}
/* 安全地打开一个可读文件(白名单内 + 非敏感名)。失败返回 -1。 */
static int open_safe_for_read(const char *abs, int follow_rel)
{
(void)follow_rel;
if (!path_in_whitelist(abs)) return -1;
const char *base = strrchr(abs, '/');
base = base ? base + 1 : abs;
if (is_sensitive_name(base)) return -1;
struct stat st;
if (stat(abs, &st) < 0) return -1;
if (!S_ISREG(st.st_mode)) return -1;
return open(abs, O_RDONLY);
}
/* ---------------- 文件浏览 ---------------- */
static void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime)
{
sA(s, "{\"name\":");
sA_str(s, name);
sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}",
isdir ? "dir" : "file", size, mtime);
}
struct fent { char name[256]; int isdir; long long size, mtime; };
/* 自然字符串比较:数字段按数值比较(track2 < track10),ASCII 字母大小写不敏感
* (同字母时原始字节作 tie-break),其余按**无符号字节序**(UTF-8 中文等直接按字节序,
* 设备无 locale,避免 strcasecmp 对高位字节的 locale 依赖导致乱序)。
* 实现:逐字节;遇到连续数字则各取整段,比「有效位数→数值→原始长度」;否则比字节。 */
static int nat_strcmp(const char *a, const char *b)
{
const unsigned char *pa = (const unsigned char *)a;
const unsigned char *pb = (const unsigned char *)b;
while (*pa && *pb) {
unsigned char ca = *pa, cb = *pb;
if (ca >= '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 <token>) */
char x_meow_token[256]; /* X-MeowToken 头 */
char cookie[512]; /* Cookie 头(meow_token=<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(格式 `<salt_hex>:<sha256_hex(salt||pin)>`,
* 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_hex>:<sha256_hex(salt || password)>`。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":<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=<path> 字段与 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_<bootts>.log(meow_crash 落盘格式)
* 一行一条:`<epoch_sec> <uptime_sec> <svc|dmesg> <detail>`;svc 的 detail=`<name> exit=<code>`/`<name> signal=<sig>`,
* 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 = `<name> exit=<code>` 或 `<name> signal=<sig>`;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/<family>/<weight>/) ----------------
* 定案(用户 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/<family>/<weight>/;目标已存在则 409(前端确认后带 force=1 重试)。
* - sha256_png/sha256_idx 仅 WARN(含在响应 warning 里);导入后**不触发 launcher 重扫**
* (扫描时机=启动/进设置页,勿加热路径),响应用 hint 提示。
* 导出(GET /api/fonts/export?family=&weight=):
* - `tar -cf - -C <font_dir> font.png font.idx font.json | gzip -c > <tmp>` 打包为
* `<family>-<weight>.font.tar.gz` 下载流(先落临时文件求 Content-Length 再流式)。
* 列表(GET /api/fonts):扫描 fonts/<family>/<weight>/(三件套齐)供导出选择。
*/
#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 <dir> 三件套 | gzip -c > <tmp>,落临时文件求 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/<f> → <f>;/<f> → <f>(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/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);
/* ---- 网络管理(meow_netmgrd 期2 透传:读级任意调用者;flush 写级走 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/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/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/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);
}
/* ---- 设置 ---- */
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);
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」→ 合法 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;
}