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

2816 lines
112 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"
#define TF_ROOT "/usr/data/mnt/sd_0"
#define TF_CARD_DIR "/usr/data/mnt/sd_0/MeowOS"
#define MEOWAPPS_DIR "/usr/data/mnt/sd_0/MeowAPPs"
#define LOGS_DIR "/usr/data/mnt/sd_0/MeowOS/logs"
#define PERF_SOCK "/usr/data/meow_perf.sock"
#define POWER_SOCK "/usr/data/meow_power.sock"
#define NYAI_PATH "/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) /* 上传上限 */
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
#define LOG_MAX_LINES 1000
/* 白名单子目录(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;
/* ---------------- 有界动态字符串(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, "\"");
}
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);
/* ---------------- 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)
{
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);
char joined[1024];
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 */
char out[1024];
size_t n = 0;
out[0] = 0;
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, "..")) {
/* 弹出一级;若已在 root 再 .. 则逃逸 */
if (n == 0) return -1;
while (n > 0 && out[n - 1] != '/') n--;
if (n > 0) n--; /* 去掉结尾 / */
out[n] = 0;
continue;
}
if (k > 0) {
size_t need = strlen(out) + 1 + k + 1;
if (need >= sizeof out) return -1;
if (out[0]) strncat(out, "/", sizeof out - strlen(out) - 1);
strncat(out, seg, sizeof out - strlen(out) - 1);
}
}
/* 空 → root */
if (!out[0]) snprintf(out, sizeof out, "%s", TF_ROOT);
if (strlen(out) >= buflen) return -1;
snprintf(buf, buflen, "%s", out);
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 403;
if (!path_in_whitelist(abs)) return 403;
struct stat st;
if (stat(abs, &st) < 0) return 404;
if (!S_ISDIR(st.st_mode)) return 404;
DIR *d = opendir(abs);
if (!d) return 500;
make_rel(abs, rel, sizeof rel);
sbuf_t s = {0};
sA(&s, "{\"root\":\"" TF_ROOT "\",\"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);
/* 读文本文件(<1MB),返回 malloc 内容;二进制检测 -2;太大 -3;打不开 -1。 */
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 > 1024 * 1024) { close(fd); return NULL; }
if (st.st_size < 0) { close(fd); return NULL; }
char *buf = malloc((size_t)st.st_size + 1);
if (!buf) { close(fd); return NULL; }
ssize_t n = read(fd, buf, (size_t)st.st_size);
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 = (long long)n;
if (truncated) *truncated = 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;
if (clen > MAX_HTTP_BODY) { free(buf); return -1; }
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[256]; } upfile_t;
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) {
char *nm = strstr(l, "name=\"");
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
char *fn = strstr(l, "filename=\"");
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
}
}
/* 数据结束:下一个 "--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);
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", fname);
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", curpath);
/* 写文件 */
char abs[1024];
if (normalize_path(files[nfiles].path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { nfiles++; continue; }
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s/%s", abs, fname);
int fd = open(files[nfiles].path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
write_full(fd, data_start, dl);
close(fd);
}
nfiles++;
}
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\":\"" LOGS_DIR "\",\"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;
}
/* ---------------- 通知(后端跨进程不可读 → 501) ---------------- */
/* 注:通知存储为 launcher 进程内存(meow_notif.c g_slots[],NOTIFICATION_SYSTEM §4),
* 后端为独立 svcd 子进程,跨进程无法读取/清撤那批内存槽;框架仅在关机前把通知 dump 到
* TF 卡 MeowOS/notifications/notifications.log(CSV 追加,非活跃快照)。故本后端无法实现
* 活跃通知列表/清撤 —— 需为通知后端另开 IPC 查询(如 MSG_NOTIFY_LIST 应答)或持久化活跃集,
* 见 MEOWPANEL_PROTOCOL §6 待定案。当前一律 501。 */
static int api_notifications(int sock)
{
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知存储为 launcher 进程内存,后端跨进程不可读(需通知后端暴露 IPC 查询/清撤)");
}
static int api_notification_delete(int sock)
{
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清撤需 launcher 通知后端 IPC,后端未实现");
}
static int api_notification_dismiss_all(int sock)
{
return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清空需 launcher 通知后端 IPC,后端未实现");
}
/* ---------------- 文件操作 ---------------- */
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\":false}", size);
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", "白名单外/路径逃逸"); }
snprintf(newabs, sizeof newabs, "%s/%s", abs, pnn->str);
/* newabs 也须在白名单(new_name 不含 /,父目录已校验,无需再 normalize) */
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 int api_apps_install(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
if (!access(NYAI_PATH, X_OK)) { /* ok */ }
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_PATH, 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];
mkdir(TF_CARD_DIR "/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_PATH, 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_PATH, 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")) 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))
return api_notification_delete(sock);
}
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);
if (read_request(sock, &r) != 0) { 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;
}
}
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;
}