#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 内核头),零第三方库。设备无 python3/lua。 */ #include #include #include #include /* strcasecmp/strncasecmp */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */ #include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */ /* ---------------- 常量 ---------------- */ #define PANEL_VERSION "0.1" #define DEFAULT_PORT 8080 #define SETTINGS_FILE "/usr/data/meow/settings.conf" #define SETTINGS_HDR "# meow-settings 1" /* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;默认设备路径)。 */ static const char *g_settings_file = SETTINGS_FILE; /* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。 * TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */ static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0"; static char g_tf_card_buf[2048]; static char g_apps_dir_buf[2048]; static char g_logs_dir_buf[2048]; static char g_notif_log_buf[2048]; static const char *g_tf_card = NULL; /* 派生:/MeowOS */ static const char *g_apps_dir = NULL; /* 派生:/MeowAPPs */ static const char *g_logs_dir = NULL; /* 派生:/logs */ static const char *g_notif_log = NULL; /* 通知转储日志:/notifications/notifications.log */ #define TF_ROOT g_tf_root_buf #define TF_CARD_DIR g_tf_card #define MEOWAPPS_DIR g_apps_dir #define LOGS_DIR g_logs_dir #define PERF_SOCK "/usr/data/meow_perf.sock" #define POWER_SOCK "/usr/data/meow_power.sock" #define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */ #define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */ #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_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 缓冲安全) */ /* 白名单子目录(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; /* 初始化 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; } /* JSON 字符串转义(version/hostname/文本值;UTF-8 按字节透传,控制字符转 \u00xx) */ static void sA_str(sbuf_t *s, const char *v) { if (!v) v = ""; sA(s, "\""); for (const char *p = v; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); } else if (c < 0x20) { sA(s, "\\u%04x", c); } else sA(s, "%c", (int)c); } sA(s, "\""); } /* 追加一段原始字节到 sbuf(长度明确,绕过 %s 的固定缓冲限制;用于大 JSON 数组复用)。 */ static void sA_append(sbuf_t *s, const char *b, size_t n) { if (!b || n == 0) return; if (sb_reserve(s, n) < 0) return; memcpy(s->b + s->n, b, n); s->n += n; s->b[s->n] = 0; } static void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; } /* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */ static 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 const char *web_root(void); /* 定义在文件后部;此处前向声明供鉴权会话路径使用 */ /* ---------------- 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 自身,可选鉴权) */ { "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; } /* ---------------- 路径安全 ---------------- */ 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); /* 已证明 olisdir != fb->isdir) return fb->isdir - fa->isdir; int c = strcasecmp(fa->name, fb->name); return c ? c : strcmp(fa->name, fb->name); } static int files_list(int sock, const char *relpath) { char abs[1024], rel[1024]; if (normalize_path(relpath, abs, sizeof abs) < 0) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); if (!path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); struct stat st; if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在"); if (!S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "非目录"); DIR *d = opendir(abs); if (!d) return send_err(sock, 500, "INTERNAL", "读目录失败"); make_rel(abs, rel, sizeof rel); sbuf_t s = {0}; sA(&s, "{\"root\":"); sA_str(&s, TF_ROOT); sA(&s, ",\"path\":"); sA_str(&s, abs); sA(&s, ",\"rel\":"); sA_str(&s, rel); sA(&s, ",\"allowed\":["); for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); } sA(&s, "],\"items\":["); /* M2.78:排序后输出(原 readdir 原始顺序混杂难用)——目录优先,同组按名称 * 不区分大小写排序;条目上限 1024(与 files App 一致,防巨目录爆内存)。 */ int cap = 1024, n = 0; struct fent *ents = malloc((size_t)cap * sizeof *ents); struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char full[1400]; snprintf(full, sizeof full, "%s/%s", abs, e->d_name); struct stat fs; if (stat(full, &fs) < 0) continue; if (n >= cap) break; struct fent *en = &ents[n++]; snprintf(en->name, sizeof en->name, "%s", e->d_name); en->isdir = S_ISDIR(fs.st_mode); en->size = en->isdir ? 0 : (long long)fs.st_size; en->mtime = (long long)fs.st_mtime; } closedir(d); qsort(ents, (size_t)n, sizeof *ents, fent_cmp); for (int i = 0; i < n; i++) { if (i) sA(&s, ","); fmt_json_item(&s, ents[i].name, ents[i].isdir, ents[i].size, ents[i].mtime); } free(ents); sA(&s, "]}"); return send_json(sock, 200, s.b ? s.b : ""); } /* ---------------- HTTP 响应(JSON 信封) ---------------- */ /* send_json 前置声明 */ static int send_json(int sock, int status, const char *data); /* 读文本文件(预览上限 TEXT_PREVIEW_MAX,超出截断返回 truncated): * 返回 malloc 内容(NUL 结尾);二进制检测 -2;打不开 -1。size=文件真实字节数(非截断后)。 */ static char *read_text_file_limited(const char *abs, long long *size, int *truncated) { int fd = open_safe_for_read(abs, 0); if (fd < 0) return NULL; struct stat st; if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; } long long total = (long long)st.st_size; size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total; char *buf = malloc(cap + 1); if (!buf) { close(fd); return NULL; } ssize_t n = read(fd, buf, cap); close(fd); if (n < 0) { free(buf); return NULL; } buf[n] = 0; /* 二进制检测:含 NUL 或大量控制字符 */ int ctrl = 0; for (ssize_t i = 0; i < n; i++) { unsigned char c = (unsigned char)buf[i]; if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */ if (c < 0x09) ctrl++; } if (ctrl > n / 10) { free(buf); return (char *)-2; } if (size) *size = total; if (truncated) *truncated = (total > (long long)cap) ? 1 : 0; return buf; } /* ---------------- 按键注入(经 /dev/input/event* 写 EV_KEY;uinput_tap inject 同机制) ---------------- * 设备映射(launcher meow_launcher_internal.h): * /dev/input/event0 = md-gpio-keys: KEY_POWER(116), KEY_PREVIOUSSONG(165) * /dev/input/event2 = jz adc keyboard: KEY_VOLUMEUP(115), KEY_VOLUMEDOWN(114), KEY_NEXTSONG(163), KEY_PLAYPAUSE(164) * 注入 = open(O_WRONLY) 写 EV_KEY press/release + SYN_REPORT(evdev 支持 write 注入,uinput_tap.c inject 先例)。 * 红线(APPS.md 物理键 / 任务约束):只注入音量/电源/播放/上下曲,**禁止映射 返回(158)/主页(172)/确认/exit**。 */ #define PWR_KEYDEV "/dev/input/event0" #define ADC_KEYDEV "/dev/input/event2" static int keycode_to_device(int code, const char **dev) { switch (code) { case KEY_POWER: *dev = PWR_KEYDEV; return KEY_POWER; case KEY_PREVIOUSSONG: *dev = PWR_KEYDEV; return KEY_PREVIOUSSONG; case KEY_VOLUMEUP: *dev = ADC_KEYDEV; return KEY_VOLUMEUP; case KEY_VOLUMEDOWN: *dev = ADC_KEYDEV; return KEY_VOLUMEDOWN; case KEY_NEXTSONG: *dev = ADC_KEYDEV; return KEY_NEXTSONG; case KEY_PLAYPAUSE: *dev = ADC_KEYDEV; return KEY_PLAYPAUSE; default: return -1; } } static int inject_key_event(const char *dev, int code, int down) { int fd = open(dev, O_WRONLY | O_NONBLOCK); if (fd < 0) return -1; struct input_event ev; memset(&ev, 0, sizeof ev); ev.type = EV_KEY; ev.code = code; ev.value = down ? 1 : 0; int ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev); if (ok) { memset(&ev, 0, sizeof ev); ev.type = EV_SYN; ev.code = SYN_REPORT; ev.value = 0; ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev); } close(fd); return ok ? 0 : -1; } /* 注入 tap(按下+抬起)或 long(按住 ms)。返回 0=成功。 */ static int inject_key_release(int code, int press_ms) { const char *dev = NULL; int kc = keycode_to_device(code, &dev); if (kc < 0) return -1; int release_ms = press_ms > 0 ? press_ms : 0; if (inject_key_event(dev, code, 1) < 0) return -1; if (release_ms > 0) usleep((useconds_t)release_ms * 1000); else usleep(80 * 1000); if (inject_key_event(dev, code, 0) < 0) return -1; return 0; } /* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */ static int read_small_file(const char *path, char *buf, int cap) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; int n = 0; while (n < cap - 1) { ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n)); if (r <= 0) break; n += (int)r; } close(fd); buf[n] = 0; return n; } /* 读某应用目录的 app.json(用 jv 解析提取字段,供扫描/详情)。 */ static int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail) { char pj[1024]; snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name); char buf[8192]; if (read_small_file(pj, buf, sizeof buf) <= 0) { /* 解析失败项:invalid */ if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, name); sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); } return -1; } jv_t *root = jv_parse(buf, (int)strlen(buf)); if (!root) { if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, name); sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); } return -1; } jv_t *jpkg = jv_obj_get(root, "package"); jv_t *jname = jv_obj_get(root, "name"); jv_t *jver = jv_obj_get(root, "version"); jv_t *jexec = jv_obj_get(root, "exec"); jv_t *jfw = jv_obj_get(root, "framework"); jv_t *jim = jv_obj_get(root, "immersive"); jv_t *jfull = jv_obj_get(root, "fullscreen"); const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name; const char *nm = (jname && jname->type == J_STR) ? jname->str : ""; const char *ver = (jver && jver->type == J_STR) ? jver->str : ""; const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : ""; /* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */ long long dsize = 0; snprintf(pj, sizeof pj, "%s/%s", appdir, name); DIR *d = opendir(pj); if (d) { struct dirent *e; while ((e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char full[1024]; snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name); struct stat st; if (stat(full, &st) == 0) dsize += (long long)st.st_size; } closedir(d); } if (!want_detail) { sA(out, "{\"package\":"); sA_str(out, pkg); sA(out, ",\"name\":"); sA_str(out, nm[0] ? nm : NULL); sA(out, ",\"version\":"); sA_str(out, ver[0] ? ver : NULL); sA(out, ",\"exec\":"); sA_str(out, exec); sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}", jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL)); } else { sA(out, "{\"package\":"); sA_str(out, pkg); sA(out, ",\"name\":"); sA_str(out, nm[0] ? nm : NULL); sA(out, ",\"version\":"); sA_str(out, ver[0] ? ver : NULL); sA(out, ",\"exec\":"); sA_str(out, exec); sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":", jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize); sA_str(out, pj); sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}"); } jv_free(root); return 0; } /* ---------------- HTTP 请求读取 ---------------- */ typedef struct { char method[8]; char path[512]; /* 解码后的路径(不含 query) */ char query[1024]; /* 原始 query(key=value&…,未解码;取值时用 qparam) */ char *body; size_t body_len; char content_type[128]; /* 请求 Content-Type(若非空) */ char authorization[256]; /* Authorization 头(Bearer ) */ char x_meow_token[256]; /* X-MeowToken 头 */ char cookie[512]; /* Cookie 头(meow_token= 等) */ } req_t; static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; } static int 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)。 */ size_t cap = MAX_HTTP_BODY; if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = MAX_UPLOAD_BODY; if (clen > cap) { free(buf); return -2; } if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; } 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; } /* ================= 可选鉴权(panel.auth_enabled / panel.auth_pass) ================= * 设计(MEOWPANEL_PROTOCOL §1.3 落地): * - 键 `panel.auth_enabled`(默认 0=关)开关;`panel.auth_pass` 存「sha256 带盐哈希」格式 `salt:hex`, * 绝不落明文,也绝不进日志/响应。 * - 启用后,除 /api/auth/{login,logout,status} 外,所有 /api/ 端点需有效会话 * (`Authorization: Bearer ` / `X-MeowToken: ` / `Cookie: meow_token=`)。 * - 会话 token 由 /dev/urandom 生成(32B → 64 hex),存内存 + 卡上文件持久化(防重启失效),带过期。 * - 未启用(默认)时鉴权门禁直接放行,行为与现状完全一致(零回归);危险端点不额外限制。 */ #define AUTH_TOKEN_TTL (12 * 3600) /* 会话有效期:12 小时 */ #define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */ #define AUTH_HASH_LEN 64 /* sha256 hex 长度 */ static char g_auth_session_file[600]; static const char *auth_session_file(void) { if (!g_auth_session_file[0]) snprintf(g_auth_session_file, sizeof g_auth_session_file, "%s/meowpanel.session", web_root()); return g_auth_session_file; } /* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 mp_ 避免符号冲突) ---- */ static const uint32_t mp_sha256_k[64] = { 0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u, 0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u, 0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau, 0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u, 0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u, 0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u, 0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u, 0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u }; #define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n)))) #define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) #define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) #define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22)) #define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25)) #define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3)) #define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10)) typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; size_t buflen; } mp_sha256_ctx; static void mp_sha256_init(mp_sha256_ctx *c) { c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u; c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au; c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu; c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u; c->count = 0; c->buflen = 0; } static void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p) { uint32_t w[64]; int i; for (i = 0; i < 16; i++) w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) | ((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3]; for (i = 16; i < 64; i++) w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16]; uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3]; uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7]; for (i = 0; i < 64; i++) { uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i]; uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc); h = g; g = f; f = e; e = d + t1; d = cc; cc = b; b = a; a = t1 + t2; } c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d; c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h; } static void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len) { c->count += (uint64_t)len; if (c->buflen) { size_t need = 64 - c->buflen; size_t take = len < need ? len : need; memcpy(c->buf + c->buflen, data, take); c->buflen += take; data += take; len -= take; if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; } } while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; } if (len) { memcpy(c->buf, data, len); c->buflen = len; } } static void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32]) { uint64_t bitlen = c->count * 8; uint8_t *b = c->buf; size_t bl = c->buflen; int i; b[bl++] = 0x80; if (bl > 56) { while (bl < 64) b[bl++] = 0; mp_sha256_block(c, b); bl = 0; } while (bl < 56) b[bl++] = 0; for (i = 0; i < 8; i++) b[bl++] = (uint8_t)(bitlen >> (56 - i * 8)); mp_sha256_block(c, b); for (i = 0; i < 8; i++) { out[i*4] = (uint8_t)(c->state[i] >> 24); out[i*4+1] = (uint8_t)(c->state[i] >> 16); out[i*4+2] = (uint8_t)(c->state[i] >> 8); out[i*4+3] = (uint8_t)(c->state[i]); } } #undef MP_ROR #undef MP_CH #undef MP_MAJ #undef MP_BSIG0 #undef MP_BSIG1 #undef MP_SSIG0 #undef MP_SSIG1 /* ---- 鉴权基础 ---- * 密码存储格式:`:`。salt 由 /dev/urandom 生成(16B → 32 hex)。 * 校验时重算并比较,绝不反解、绝不在日志/响应中输出 hash 或明文。 */ static int auth_read_random(unsigned char *buf, size_t n) { int fd = open("/dev/urandom", O_RDONLY); if (fd < 0) return -1; size_t got = 0; while (got < n) { ssize_t rd = read(fd, buf + got, n - got); if (rd <= 0) { close(fd); return -1; } got += (size_t)rd; } close(fd); return 0; } static void auth_hex(const unsigned char *raw, size_t n, char *out) { static const char hex[] = "0123456789abcdef"; for (size_t i = 0; i < n; i++) { out[i*2] = hex[raw[i] >> 4]; out[i*2+1] = hex[raw[i] & 0xf]; } out[n*2] = 0; } static int auth_token_gen(char *out, int outcap) /* 32B → 64 hex */ { unsigned char raw[32]; if (outcap < 65) 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; } static int auth_enabled(void) { return settings_get_int("panel.auth_enabled", 0) != 0; } 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) { char *stored = settings_get_value("panel.auth_pass"); int ok = 0; if (stored && stored[0]) { char *colon = strchr(stored, ':'); if (colon) { *colon = 0; char want[AUTH_HASH_LEN + 1]; if (auth_pass_hash(stored, password, want, sizeof want) == 0 && strlen(colon + 1) == AUTH_HASH_LEN && strcmp(want, colon + 1) == 0) ok = 1; } } free(stored); return ok; } /* ---- 会话(内存 + 卡上持久化) ---- */ 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 sess_save(void) { FILE *f = fopen(auth_session_file(), "w"); if (!f) return; time_t now = time(NULL); for (int i = 0; i < g_sess_n; i++) { if (g_sess[i].expires > 0 && g_sess[i].expires <= now) continue; fprintf(f, "%s %lld\n", g_sess[i].token, (long long)g_sess[i].expires); } fclose(f); } static void sess_load(void) { FILE *f = fopen(auth_session_file(), "r"); if (!f) return; char line[128]; time_t now = time(NULL); g_sess_n = 0; while (g_sess_n < AUTH_MAX_SESS && fgets(line, sizeof line, f)) { char tok[65]; long long exp = 0; if (sscanf(line, "%64s %lld", tok, &exp) >= 1) { if (exp > 0 && exp <= (long long)now) continue; /* 已过期 */ snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", tok); g_sess[g_sess_n].expires = (time_t)exp; g_sess_n++; } } fclose(f); } 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/ 端点需会话 ---- */ static int require_auth(int sock, const req_t *r) { if (!auth_enabled()) return 1; /* 默认关:零回归 */ const char *p = r->path; if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") || !strcmp(p, "/api/auth/status")) 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", "未认证或会话已失效"); } static int api_auth_status(int sock, const req_t *r) { int en = auth_enabled(); int sess = 0; if (en) { char t[65]; if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t); } sbuf_t s = {0}; sA(&s, "{\"auth_enabled\":%s,\"session\":%s}", en ? "true" : "false", sess ? "true" : "false"); int rc = send_json(sock, 200, s.b ? s.b : "{}"); sbuf_free(&s); return rc; } static int api_auth_login(int sock, const req_t *r) { if (!auth_enabled()) return send_err(sock, 400, "BAD_REQUEST", "面板鉴权未启用"); if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少密码"); jv_t *root = jv_parse(r->body, (int)r->body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *pw = jv_obj_get(root, "password"); if (!pw || pw->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); } int ok = auth_verify(pw->str); jv_free(root); if (!ok) return send_err(sock, 401, "UNAUTHORIZED", "密码错误"); char token[65]; if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败"); time_t exp = time(NULL) + AUTH_TOKEN_TTL; sess_add(token, exp); sess_save(); 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; } 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); sess_save(); } return send_json(sock, 200, "{\"logged_out\":true}"); } /* ---------------- HTTP 响应写入 ---------------- */ static int write_full(int sock, const void *b, size_t n) { const char *p = (const char *)b; size_t off = 0; while (off < n) { ssize_t w = write(sock, p + off, n - off); if (w <= 0) return -1; off += (size_t)w; } return 0; } static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra) { char h[512]; const char *sts = "OK"; switch (status) { case 200: sts = "OK"; break; case 201: sts = "Created"; break; case 202: sts = "Accepted"; break; case 400: sts = "Bad Request"; break; case 401: sts = "Unauthorized"; break; case 403: sts = "Forbidden"; break; case 404: sts = "Not Found"; break; case 405: sts = "Method Not Allowed"; break; case 409: sts = "Conflict"; break; case 413: sts = "Payload Too Large"; break; case 415: sts = "Unsupported Media Type"; break; case 422: sts = "Unprocessable Entity"; break; case 500: sts = "Internal Server Error"; break; case 501: sts = "Not Implemented"; break; case 503: sts = "Service Unavailable"; break; default: sts = "OK"; break; } snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n", status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : ""); return write_full(sock, h, strlen(h)); } /* 发送原生 bytes(已含 body)。 */ static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen) { if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1; if (bodylen && write_full(sock, body, bodylen) < 0) return -1; return 0; } /* 成功 JSON 信封:{"ok":true,"data":}。 * 前缀 "{"ok":true,"data":" 共 18 字节。用 memcpy 精确构包(sA 的 %s 有 ~511B 单次截断,不能用于大 payload)。 */ static int send_ok(int sock, const char *data) { static const char prefix[] = "{\"ok\":true,\"data\":"; size_t pl = sizeof prefix - 1; /* 18 */ size_t dl = strlen(data); size_t tot = pl + dl + 1; char *b = malloc(tot + 1); if (!b) { const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}"; return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e)); } memcpy(b, prefix, pl); memcpy(b + pl, data, dl); b[pl + dl] = '}'; b[pl + dl + 1] = 0; int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot); free(b); return rc; } /* 错误 JSON 信封:{"ok":false,"error":{"code":..,"message":..}} */ static int send_err(int sock, int status, const char *code, const char *msg) { sbuf_t s = {0}; sA(&s, "{\"ok\":false,\"error\":{\"code\":"); sA_str(&s, code ? code : "INTERNAL"); sA(&s, ",\"message\":"); sA_str(&s, msg ? msg : ""); sA(&s, "}}"); int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n); sbuf_free(&s); return rc; } static int send_json(int sock, int status, const char *data) { if (status == 200) return send_ok(sock, data); return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data)); } /* 流式发送磁盘文件(下载)。 */ static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename) { int fd = open(abspath, O_RDONLY); if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在"); struct stat st; if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); } char extra[256]; if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename); else extra[0] = 0; if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; } char buf[8192]; ssize_t n; while ((n = read(fd, buf, sizeof buf)) > 0) { if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; } } close(fd); return 0; } /* ---------------- multipart/form-data 解析(文件上传) ---------------- * 解析 body,提取 name= 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。 * 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。 */ typedef struct { char path[1400]; char filename[320]; } upfile_t; /* 安全文件名:取 basename(去目录分隔符),拒绝空/`.`/`..`/含控制字符或分隔符。 * 返回 0=ok 写入 out;-1=拒绝。用于防上传文件名逃逸白名单(如 `../../etc/passwd`)。 */ static int safe_filename(const char *name, char *out, size_t cap) { if (!name || !name[0]) return -1; const char *b = name; /* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */ const char *slash = strrchr(b, '/'); const char *bsl = strrchr(b, '\\'); if (bsl && (!slash || bsl > slash)) slash = bsl; if (slash) b = slash + 1; if (!b[0]) return -1; if (!strcmp(b, ".") || !strcmp(b, "..")) return -1; for (const char *p = b; *p; p++) { unsigned char c = (unsigned char)*p; if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */ } snprintf(out, cap, "%s", b); return 0; } static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles) { /* 取 boundary */ const char *ct = r->content_type; const char *b = strstr(ct, "boundary="); if (!b) return -1; b += 9; if (*b == '"') { b++; } char boundary[128]; int bi = 0; while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; } boundary[bi] = 0; char dmark[160]; snprintf(dmark, sizeof dmark, "--%s", boundary); char emark[160]; snprintf(emark, sizeof emark, "--%s--", boundary); /* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */ const char *p = r->body; const char *end = r->body + r->body_len; int nfiles = 0; char curpath[1024] = ""; while (p < end) { const char *fstart = strstr(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 = strstr(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")) { /* 密码哈希绝不出现在响应里: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.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:启用前必须先设密码 */ 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") && !auth_pass_set()) return send_err(sock, 422, "UNSUPPORTED", "请先设置面板密码"); 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.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:启用前必须先设密码 */ if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !auth_pass_set()) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板密码\"}"); 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) 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"); 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, "},\"network\":"); char netbuf[1024]; if (perf_extract_network(json, netbuf, sizeof netbuf) == 0) { sA(&s, "%s", netbuf); /* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local) */ } 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}}"); jv_free(root); int ret = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return ret; } /* ---------------- 系统动作(经 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; } /* ---------------- 日志 ---------------- */ 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}", (long long)st.st_size, (long long)st.st_mtime); } 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, ","); sbuf_t ln = {0}; for (const char *c = p; c < line_end; c++) { unsigned char cc = (unsigned char)*c; if (cc == '"' || cc == '\\') sA(&ln, "\\%c", (int)cc); else if (cc < 0x20 && cc != '\t') sA(&ln, "\\u%04x", cc); else sA(&ln, "%c", (int)cc); } /* 用 sA_str(任意长度安全);不要 sA("%s")——单次超 511B 会截断坏 JSON */ sA(&s, "\""); for (const char *c = ln.b ? ln.b : ""; *c; c++) sA(&s, "%c", (int)(unsigned char)*c); sA(&s, "\""); sbuf_free(&ln); 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; } /* ---------------- 通知(数据源 = TF 卡通知转储日志) ---------------- * 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务): * 1) launcher IPC 查询 —— meow_ipc.h 已定义 MSG_NOTIFY_LIST(0x23),但 launcher 侧仅作 * 守护→App 下发行(util.c:1079「not dispatched here」),**无客户端查询请求类型**, * 后端(svcd 独立进程)无法据此取到活跃清单 → 跳过,回退 ②。详见报告「需 launcher 侧配合项」。 * 2) TF 卡通知转储文件 `<卡根>/MeowOS/notifications/notifications.log`: * launcher 把每条通知归档时追加一行 CSV(meow_notif.c tf_line:CSV 追加 + 10MB 滚动,开机清空)。 * 字段顺序:timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity, * ongoing,media,dismissed(前 6/7/11/12/13 为裸整数,其余引号包裹 + 内部 " 翻倍)。 * 注意:**归档发生在置 dismissed=1 之前(meow_notif_dismiss 先 archive_one 后置 dismissed)**, * 故日志的 dismissed 列恒 ≈0(不可靠),且日志是「会话级归档」,非活跃快照。 * 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},**不伪造数据**。 * clear(dismiss_all)语义:保留活动中的媒体/进度通知(ongoing||media||progress>=0||category=="media"), * 其余从日志移除。单条删除:按 id 移除该行。 */ typedef struct { long long ms; /* created_ms(epoch ms) */ unsigned id; char app_id[40]; char app_icon[64]; char category[40]; int urgency; int progress; /* -1=无 */ char summary[96]; char body[128]; char target[64]; int ongoing, media, dismissed; } notif_rec_t; /* 取一个 CSV 字段(引号包裹 → 解转义;裸字段 → 原样;末后跳过 ',')。返回 0。 */ static int notif_csv_field(const char **pp, char *out, int outcap) { const char *p = *pp; int o = 0; if (*p == '"') { p++; while (*p && *p != '"') { if (*p == '"' && p[1] == '"') { if (o < outcap - 1) out[o++] = '"'; p += 2; } else { if (o < outcap - 1) out[o++] = *p; p++; } } if (*p == '"') p++; } else { while (*p && *p != ',' && *p != '\n' && *p != '\r') { if (o < outcap - 1) out[o++] = *p; p++; } } if (o < outcap) { out[o] = 0; } else if (outcap > 0) { out[outcap - 1] = 0; } if (*p == ',') p++; *pp = p; return 0; } static int notif_parse_line(const char *line, notif_rec_t *r) { const char *p = line; char f1[32], f2[16], f6[8], f7[16], f11[8], f12[8], f13[8]; char app[40], icon[64], cat[40], sum[96], body[128], tgt[64]; notif_csv_field(&p, f1, sizeof f1); notif_csv_field(&p, f2, sizeof f2); notif_csv_field(&p, app, sizeof app); notif_csv_field(&p, icon, sizeof icon); notif_csv_field(&p, cat, sizeof cat); notif_csv_field(&p, f6, sizeof f6); notif_csv_field(&p, f7, sizeof f7); notif_csv_field(&p, sum, sizeof sum); notif_csv_field(&p, body, sizeof body); notif_csv_field(&p, tgt, sizeof tgt); notif_csv_field(&p, f11, sizeof f11); notif_csv_field(&p, f12, sizeof f12); notif_csv_field(&p, f13, sizeof f13); r->ms = strtoll(f1, NULL, 10); r->id = (unsigned)strtoul(f2, NULL, 10); r->urgency = atoi(f6); r->progress = atoi(f7); r->ongoing = atoi(f11); r->media = atoi(f12); r->dismissed = atoi(f13); snprintf(r->app_id, sizeof r->app_id, "%s", app); snprintf(r->app_icon, sizeof r->app_icon, "%s", icon); snprintf(r->category, sizeof r->category, "%s", cat); snprintf(r->summary, sizeof r->summary, "%s", sum); snprintf(r->body, sizeof r->body, "%s", body); snprintf(r->target, sizeof r->target, "%s", tgt); return 0; } /* 读通知日志,保留最新 maxrec 条(环形缓冲,避免全量进内存)。返回条数;-1=文件不存在/无卡。 */ static int notif_load_list(notif_rec_t *recs, int maxrec) { FILE *f = fopen(g_notif_log, "r"); if (!f) return -1; int total = 0, head = 0; char line[1400]; notif_rec_t r; while (fgets(line, sizeof line, f)) { if (notif_parse_line(line, &r) != 0) continue; recs[head] = r; head = (head + 1) % maxrec; if (total < maxrec) total++; } fclose(f); if (total < maxrec) return total; /* 未满窗:recs[0..total-1] 即 oldest..newest */ /* 满窗:oldest 在 recs[head],重排为 oldest..newest 连续 */ notif_rec_t tmp[NOTIF_PREVIEW_MAX]; for (int i = 0; i < maxrec; i++) tmp[i] = recs[(head + i) % maxrec]; memcpy(recs, tmp, (size_t)maxrec * sizeof recs[0]); return total; } /* app_id → 展示名("system" → "系统";其余用 app_id 兜底,前端可回退)。 */ static const char *notif_app_name(const char *app_id) { if (!strcmp(app_id, "system")) return "系统"; return app_id; } static void notif_emit_item(sbuf_t *out, const notif_rec_t *r) { sA(out, "{\"id\":%u,\"app_id\":", r->id); sA_str(out, r->app_id); sA(out, ",\"app_name\":"); sA_str(out, notif_app_name(r->app_id)); sA(out, ",\"app_icon\":"); sA_str(out, r->app_icon); sA(out, ",\"category\":"); sA_str(out, r->category); sA(out, ",\"summary\":"); sA_str(out, r->summary); sA(out, ",\"body\":"); sA_str(out, r->body); sA(out, ",\"urgency\":%d,\"ts\":%lld,\"progress\":", r->urgency, r->ms > 0 ? r->ms / 1000 : 0); if (r->progress >= 0) sA(out, "%d", r->progress); else sA(out, "null"); sA(out, ",\"ongoing\":%s,\"media\":%s,\"target_activity\":", r->ongoing ? "true" : "false", r->media ? "true" : "false"); sA_str(out, r->target); sA(out, ",\"dismissed\":%s}", r->dismissed ? "true" : "false"); } static int api_notifications(int sock) { notif_rec_t recs[NOTIF_PREVIEW_MAX]; int n = notif_load_list(recs, NOTIF_PREVIEW_MAX); if (n < 0) return send_json(sock, 200, "{\"count\":0,\"items\":[],\"notifications\":[],\"source\":\"none\"}"); sbuf_t arr = {0}; int first = 1; for (int i = n - 1; i >= 0; i--) { /* newest first */ if (!first) sA(&arr, ","); first = 0; notif_emit_item(&arr, &recs[i]); } sbuf_t s = {0}; sA(&s, "{\"count\":%d,\"source\":\"tf\",\"items\":[", n); if (arr.n) sA_append(&s, arr.b, arr.n); sA(&s, "],\"notifications\":["); if (arr.n) sA_append(&s, arr.b, arr.n); sA(&s, "]}"); sbuf_free(&arr); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* 重写通知日志。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) { 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 removed = notif_rewrite(1, 0); if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败"); char d[64]; snprintf(d, sizeof d, "{\"dismissed\":%d}", removed); return send_json(sock, 200, d); } /* ---------------- 文件操作 ---------------- */ 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}"); } /* ---------------- 应用(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 = strstr(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; } /* ---------------- 静态文件托管(web_root = 二进制所在目录) ---------------- */ static char g_webroot[512]; static const char *web_root(void) { if (g_webroot[0]) return g_webroot; char exe[512]; ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1); if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; } exe[n] = 0; char *slash = strrchr(exe, '/'); if (slash) *slash = 0; snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : "."); return g_webroot; } static const char *mime_for(const char *path) { const char *ext = strrchr(path, '.'); if (!ext) return "application/octet-stream"; if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8"; if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8"; if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8"; if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8"; if (!strcasecmp(ext, ".png")) return "image/png"; if (!strcasecmp(ext, ".ico")) return "image/x-icon"; if (!strcasecmp(ext, ".svg")) return "image/svg+xml"; return "application/octet-stream"; } static int serve_static(int sock, const char *path) { const char *root = web_root(); char rel[512]; /* / → index.html;/assets/ → ;/ → (meowpanel.css/js 等) */ if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html"); else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8); else snprintf(rel, sizeof rel, "%s", path + 1); if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸"); /* 只允许安全文件(防读到二进制/任意文件) */ char full[1024]; snprintf(full, sizeof full, "%s/%s", root, rel); /* 敏感名拒绝 */ const char *base = strrchr(full, '/'); base = base ? base + 1 : full; if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") || !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件"); FILE *f = fopen(full, "rb"); if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在"); fclose(f); return send_file(sock, 200, mime_for(full), full, NULL); } /* ---------------- 路由分发 ---------------- */ static int handle(int sock, req_t *r) { const char *p = r->path; if (strncmp(p, "/api/", 5) != 0) { if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0) return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET"); return serve_static(sock, p); } /* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */ if (require_auth(sock, r) != 1) return 0; /* ---- 鉴权端点 ---- */ if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r); if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/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")) { 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/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/key")) return api_key_batch(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/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); } int main(int argc, char **argv) { /* 参数:-p PORT(可选)。默认端口 8080。 */ int port = DEFAULT_PORT; for (int i = 1; i < argc; i++) { 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") && 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]\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_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(); sess_load(); /* 加载卡上持久化的会话(防重启失效) */ 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; }