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

3142 lines
126 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#define _GNU_SOURCE /* gethostname/strcasestr/strnlen 等 GNU/musl 扩展 */
/* meowpanel.c — MeowOS Web 面板后端(MeowPanel)。
*
* 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件
* (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 `MeowOS/services.d/meowpanel.conf`
* 启动、meow_sysui 监管。**不进固件**;无卡时系统核心绝不对它有任何依赖。
*
* 架构(MEOWPANEL_PROTOCOL §1/§2 + SYSTEM_FRAMEWORK_SERVICES §4):
* - 单进程单线程 + poll 串行处理(面板访问量极小);内嵌 HTTP server。
* - 读侧(/api/status,/api/perf,/api/system)经 meow_svc_perf IPC(MSG_PERF_QUERY→RESULT)
* 聚合,**绝不各自爬 /proc**(单一权威源)。
* - 动作侧(/api/system/{reboot,shutdown,ota})经 meow_svc_power IPC(MSG_POWER_ACTION→STATUS),
* 不再直连 poweroff/写 mtd5。
* - 设置镜像(/api/settings*,/api/theme*)直接读写板载 /usr/data/meow/settings.conf——
* **与 meow_ui_vol_read / meow_svc_power settings_get_int_trim 同款解析**('=' 切分 +
* key 去首尾空白精确匹配;写用 "key = value\n")。后端是独立进程,无法链接 launcher 内的
* meow_settings_*(那是 launcher 进程内存表),故直接镜像文件,保持同一存储契约。
* - 文件/应用/日志/通知:白名单根 /usr/data/mnt/sd_0 下 MeowOS/Music/MeowData/MeowAPPs/book。
*
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。
* 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库。设备无 python3/lua。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp/strncasecmp */
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <poll.h>
#include <time.h>
#include <ctype.h>
#include <dirent.h>
#include <limits.h>
#include <math.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/input.h> /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */
#include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */
/* ---------------- 常量 ---------------- */
#define PANEL_VERSION "0.1"
#define DEFAULT_PORT 8080
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
#define SETTINGS_HDR "# meow-settings 1"
/* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。
* TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */
static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
static char g_tf_card_buf[2048];
static char g_apps_dir_buf[2048];
static char g_logs_dir_buf[2048];
static char g_notif_log_buf[2048];
static const char *g_tf_card = NULL; /* 派生:<root>/MeowOS */
static const char *g_apps_dir = NULL; /* 派生:<root>/MeowAPPs */
static const char *g_logs_dir = NULL; /* 派生:<card>/logs */
static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notifications/notifications.log */
#define TF_ROOT g_tf_root_buf
#define TF_CARD_DIR g_tf_card
#define MEOWAPPS_DIR g_apps_dir
#define LOGS_DIR g_logs_dir
#define PERF_SOCK "/usr/data/meow_perf.sock"
#define POWER_SOCK "/usr/data/meow_power.sock"
#define 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, ...);
/* ---------------- 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, "系统" },
};
#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)。
*/
static char *settings_get_value(const char *key)
{
FILE *f = fopen(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)
{
mkdir("/usr/data/meow", 0777); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
FILE *f = fopen(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(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 行替换;其余照抄 */
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++;
if (!strncmp(p, key, klen) && p[klen] == '=') {
fprintf(nf, "%s = %s\n", key, val);
found = 1;
} else {
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)
{
mkdir("/usr/data/meow", 0777);
FILE *nf = fopen(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); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
return 0;
}
/* 展示 rel(相对 root):如 /MeowOS/x。 */
static void make_rel(const char *abs, char *buf, size_t buflen)
{
size_t rl = strlen(TF_ROOT);
if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; }
const char *rest = abs + rl;
if (*rest == 0) snprintf(buf, buflen, "/");
else snprintf(buf, buflen, "%s", rest);
}
/* 安全地打开一个可读文件(白名单内 + 非敏感名)。失败返回 -1。 */
static int open_safe_for_read(const char *abs, int follow_rel)
{
(void)follow_rel;
if (!path_in_whitelist(abs)) return -1;
const char *base = strrchr(abs, '/');
base = base ? base + 1 : abs;
if (is_sensitive_name(base)) return -1;
struct stat st;
if (stat(abs, &st) < 0) return -1;
if (!S_ISREG(st.st_mode)) return -1;
return open(abs, O_RDONLY);
}
/* ---------------- 文件浏览 ---------------- */
static void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime)
{
sA(s, "{\"name\":");
sA_str(s, name);
sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}",
isdir ? "dir" : "file", size, mtime);
}
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\":[");
int first = 1;
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;
int isdir = S_ISDIR(fs.st_mode);
long long size = (long long)fs.st_size;
long long mtime = (long long)fs.st_mtime;
if (isdir) size = 0;
if (!first) sA(&s, ",");
first = 0;
fmt_json_item(&s, e->d_name, isdir, size, mtime);
}
closedir(d);
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(若非空) */
} 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);
}
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;
}
/* ---------------- HTTP 响应写入 ---------------- */
static int write_full(int sock, const void *b, size_t n)
{
const char *p = (const char *)b;
size_t off = 0;
while (off < n) {
ssize_t w = write(sock, p + off, n - off);
if (w <= 0) return -1;
off += (size_t)w;
}
return 0;
}
static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra)
{
char h[512];
const char *sts = "OK";
switch (status) {
case 200: sts = "OK"; break;
case 201: sts = "Created"; break;
case 202: sts = "Accepted"; break;
case 400: sts = "Bad Request"; break;
case 401: sts = "Unauthorized"; break;
case 403: sts = "Forbidden"; break;
case 404: sts = "Not Found"; break;
case 405: sts = "Method Not Allowed"; break;
case 409: sts = "Conflict"; break;
case 413: sts = "Payload Too Large"; break;
case 415: sts = "Unsupported Media Type"; break;
case 422: sts = "Unprocessable Entity"; break;
case 500: sts = "Internal Server Error"; break;
case 501: sts = "Not Implemented"; break;
case 503: sts = "Service Unavailable"; break;
default: sts = "OK"; break;
}
snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n",
status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : "");
return write_full(sock, h, strlen(h));
}
/* 发送原生 bytes(已含 body)。 */
static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen)
{
if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1;
if (bodylen && write_full(sock, body, bodylen) < 0) return -1;
return 0;
}
/* 成功 JSON 信封:{"ok":true,"data":<data>}。
* 前缀 "{"ok":true,"data":" 共 18 字节。用 memcpy 精确构包(sA 的 %s 有 ~511B 单次截断,不能用于大 payload)。 */
static int send_ok(int sock, const char *data)
{
static const char prefix[] = "{\"ok\":true,\"data\":";
size_t pl = sizeof prefix - 1; /* 18 */
size_t dl = strlen(data);
size_t tot = pl + dl + 1;
char *b = malloc(tot + 1);
if (!b) {
const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}";
return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e));
}
memcpy(b, prefix, pl);
memcpy(b + pl, data, dl);
b[pl + dl] = '}';
b[pl + dl + 1] = 0;
int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot);
free(b);
return rc;
}
/* 错误 JSON 信封:{"ok":false,"error":{"code":..,"message":..}} */
static int send_err(int sock, int status, const char *code, const char *msg)
{
sbuf_t s = {0};
sA(&s, "{\"ok\":false,\"error\":{\"code\":");
sA_str(&s, code ? code : "INTERNAL");
sA(&s, ",\"message\":");
sA_str(&s, msg ? msg : "");
sA(&s, "}}");
int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n);
sbuf_free(&s);
return rc;
}
static int send_json(int sock, int status, const char *data)
{
if (status == 200) return send_ok(sock, data);
return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data));
}
/* 流式发送磁盘文件(下载)。 */
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename)
{
int fd = open(abspath, O_RDONLY);
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在");
struct stat st;
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
char extra[256];
if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename);
else extra[0] = 0;
if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; }
char buf[8192];
ssize_t n;
while ((n = read(fd, buf, sizeof buf)) > 0) {
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; }
}
close(fd);
return 0;
}
/* ---------------- multipart/form-data 解析(文件上传) ----------------
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
*/
typedef struct { char path[1400]; char filename[320]; } upfile_t;
/* 安全文件名:取 basename(去目录分隔符),拒绝空/`.`/`..`/含控制字符或分隔符。
* 返回 0=ok 写入 out;-1=拒绝。用于防上传文件名逃逸白名单(如 `../../etc/passwd`)。 */
static int safe_filename(const char *name, char *out, size_t cap)
{
if (!name || !name[0]) return -1;
const char *b = name;
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
const char *slash = strrchr(b, '/');
const char *bsl = strrchr(b, '\\');
if (bsl && (!slash || bsl > slash)) slash = bsl;
if (slash) b = slash + 1;
if (!b[0]) return -1;
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
for (const char *p = b; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
}
snprintf(out, cap, "%s", b);
return 0;
}
static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
{
/* 取 boundary */
const char *ct = r->content_type;
const char *b = strstr(ct, "boundary=");
if (!b) return -1;
b += 9;
if (*b == '"') { b++; }
char boundary[128];
int bi = 0;
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
boundary[bi] = 0;
char dmark[160];
snprintf(dmark, sizeof dmark, "--%s", boundary);
char emark[160];
snprintf(emark, sizeof emark, "--%s--", boundary);
/* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */
const char *p = r->body;
const char *end = r->body + r->body_len;
int nfiles = 0;
char curpath[1024] = "";
while (p < end) {
const char *fstart = 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 {
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 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;
}
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;
}
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/<f> → <f>;/<f> → <f>(meowpanel.css/js 等) */
if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html");
else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8);
else snprintf(rel, sizeof rel, "%s", path + 1);
if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸");
/* 只允许安全文件(防读到二进制/任意文件) */
char full[1024];
snprintf(full, sizeof full, "%s/%s", root, rel);
/* 敏感名拒绝 */
const char *base = strrchr(full, '/');
base = base ? base + 1 : full;
if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") ) 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);
}
/* ---- 性能/状态 ---- */
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;
(void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */
/* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */
if (port != DEFAULT_PORT) g_port = port;
else g_port = read_port_config();
/* 再按 env MEOWPORT 覆盖 conf(-p 最高) */
if (port == DEFAULT_PORT) {
const char *ep = getenv("MEOWPORT");
if (ep && ep[0]) g_port = atoi(ep);
}
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
signal(SIGPIPE, SIG_IGN);
web_root();
panel_log("MeowPanel %s listening on port %d (web root %s)", PANEL_VERSION, g_port, web_root());
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; }
int on = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons((uint16_t)g_port);
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) {
panel_log("bind :%d fail: %s", g_port, strerror(errno));
close(lfd);
return 1;
}
if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; }
for (;;) {
int c = accept(lfd, NULL, NULL);
if (c < 0) { if (errno == EINTR) continue; continue; }
handle_conn(c);
}
return 0;
}