- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘) - OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复 - ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640 - 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫) - 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
6362 lines
282 KiB
C
6362 lines
282 KiB
C
#define _GNU_SOURCE /* gethostname/strcasestr/strnlen 等 GNU/musl 扩展 */
|
||
|
||
/* meowpanel.c — MeowOS Web 面板后端(MeowPanel)。
|
||
*
|
||
* 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件
|
||
* (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 `MeowOS/services.d/meowpanel.conf`
|
||
* 启动、meow_sysui 监管。**不进固件**;无卡时系统核心绝不对它有任何依赖。
|
||
*
|
||
* 架构(MEOWPANEL_PROTOCOL §1/§2 + SYSTEM_FRAMEWORK_SERVICES §4):
|
||
* - 单进程单线程 + poll 串行处理(面板访问量极小);内嵌 HTTP server。
|
||
* - 读侧(/api/status,/api/perf,/api/system)经 meow_svc_perf IPC(MSG_PERF_QUERY→RESULT)
|
||
* 聚合,**绝不各自爬 /proc**(单一权威源)。
|
||
* - 动作侧(/api/system/{reboot,shutdown,ota})经 meow_svc_power IPC(MSG_POWER_ACTION→STATUS),
|
||
* 不再直连 poweroff/写 mtd5。
|
||
* - 设置镜像(/api/settings*,/api/theme*)直接读写板载 /usr/data/meow/settings.conf——
|
||
* **与 meow_ui_vol_read / meow_svc_power settings_get_int_trim 同款解析**('=' 切分 +
|
||
* key 去首尾空白精确匹配;写用 "key = value\n")。后端是独立进程,无法链接 launcher 内的
|
||
* meow_settings_*(那是 launcher 进程内存表),故直接镜像文件,保持同一存储契约。
|
||
* - 文件/应用/日志/通知:白名单根 /usr/data/mnt/sd_0 下 MeowOS/Music/MeowData/MeowAPPs/book。
|
||
*
|
||
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。
|
||
* 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库(mp_jpeg.c 自研最小 JPEG;
|
||
* 字体库导入/导出走 busybox `gzip -dc | tar` 管道——与 nyai .mpk 同构,设备必带 tar/gzip)。
|
||
* 设备无 python3/lua。
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <strings.h> /* strcasecmp/strncasecmp */
|
||
#include <stdarg.h>
|
||
#include <stdint.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <signal.h>
|
||
#include <poll.h>
|
||
#include <time.h>
|
||
#include <ctype.h>
|
||
#include <dirent.h>
|
||
#include <limits.h>
|
||
#include <math.h>
|
||
#include <sys/types.h>
|
||
#include <sys/socket.h>
|
||
#include <sys/un.h>
|
||
#include <sys/time.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/wait.h>
|
||
#include <sys/utsname.h>
|
||
#include <sys/ioctl.h>
|
||
#include <netinet/in.h>
|
||
#include <arpa/inet.h>
|
||
#include <linux/input.h> /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */
|
||
|
||
#include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */
|
||
#include "mp_jpeg.h" /* 实时画面:RGB565 → 最小 baseline JPEG(自包含,零依赖) */
|
||
|
||
/* 实时画面 / 触摸注入(阶段 C)。fb ioctl 常量按 launcher fb_compositor 同款手工定义
|
||
* (避免 libc/kernel struct ABI 差异,读入裸字节缓冲再按偏移取字段)。 */
|
||
#ifndef FBIOGET_VSCREENINFO
|
||
#define FBIOGET_VSCREENINFO 0x4600
|
||
#define FBIOGET_FSCREENINFO 0x4602
|
||
#endif
|
||
#ifndef FB_DEV
|
||
#define FB_DEV "/dev/fb0" /* 16bpp RGB565,480x720,双层虚拟缓冲 */
|
||
#endif
|
||
#ifndef TOUCH_DEV
|
||
#define TOUCH_DEV "/dev/input/event1" /* goodix-ts:接收 write() 注入(本机实测可投递) */
|
||
#endif
|
||
|
||
/* ---------------- 常量 ---------------- */
|
||
#define PANEL_VERSION "0.1"
|
||
#define DEFAULT_PORT 8080
|
||
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||
#define SETTINGS_HDR "# meow-settings 1"
|
||
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;默认设备路径)。 */
|
||
static const char *g_settings_file = SETTINGS_FILE;
|
||
/* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。
|
||
* TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */
|
||
static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
|
||
static char g_tf_card_buf[2048];
|
||
static char g_apps_dir_buf[2048];
|
||
static char g_logs_dir_buf[2048];
|
||
static char g_notif_log_buf[2048];
|
||
static const char *g_tf_card = NULL; /* 派生:<root>/MeowOS */
|
||
static const char *g_apps_dir = NULL; /* 派生:<root>/MeowAPPs */
|
||
static const char *g_logs_dir = NULL; /* 派生:<card>/logs */
|
||
static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notifications/notifications.log */
|
||
#define TF_ROOT g_tf_root_buf
|
||
#define TF_CARD_DIR g_tf_card
|
||
#define MEOWAPPS_DIR g_apps_dir
|
||
#define LOGS_DIR g_logs_dir
|
||
#define PERF_SOCK "/usr/data/meow_perf.sock"
|
||
#define POWER_SOCK "/usr/data/meow_power.sock"
|
||
#define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */
|
||
#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */
|
||
#define NET_SOCK "/usr/data/meow_net.sock" /* meow_netmgrd 网络管理(MSG_NET_*,期2 观测层/DNS 代理) */
|
||
#define PM_SOCK "/usr/data/meow_pm.sock" /* meow_powermgr 功耗管理(MSG_PM_QUERY→RESULT) */
|
||
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
|
||
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
|
||
#define AMS_BIN "/usr/bin/am" /* 固件内置 amsd 用户侧 CLI(权威) */
|
||
#define AMS_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/am" /* 旧卡上路径(回退) */
|
||
#define SVCD_BIN "/usr/bin/meow_svcd" /* 固件内置 svcd 服务表 CLI(权威) */
|
||
#define SVCD_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/meow_svcd" /* 旧卡上路径(回退) */
|
||
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
|
||
#define VERSION_FILE "/etc/meowos_version"
|
||
#define HOSTNAME_FILE ""
|
||
|
||
#define IPC_TIMEOUT_MS 2000
|
||
#define MAX_HTTP_HEADER (64 * 1024)
|
||
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */
|
||
#define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */
|
||
#define MAX_FONT_UPLOAD (32 * 1024 * 1024) /* 字体库 tar.gz 上传上限(gbk 字库 png+idx 可能 10-20MB) */
|
||
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
|
||
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
|
||
#define LOG_MAX_LINES 1000
|
||
#define NOTIF_PREVIEW_MAX 200 /* 通知列表返回上限(每条约 0.2KB,总 <50KB) */
|
||
#define TEXT_PREVIEW_MAX (1024 * 1024) /* 文本预览上限(超出截断,truncated=true;14MB 约束下 1MB 缓冲安全) */
|
||
#define PANEL_VOL_MAX_DEFAULT 100 /* 音频音量上限默认(对齐 meow_launcher MEOW_VOL_MAX_DEFAULT;设备可经 settings.conf audio.volume_max 覆盖) */
|
||
|
||
/* 白名单子目录(TF 卡根下) */
|
||
static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
|
||
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
|
||
|
||
/* 日志敏感文件黑名单(相对 LOGS_DIR 或文件名) */
|
||
static const char *g_sensitive_names[] = {
|
||
"settings.conf", "setting.bak", ".env", "passwd", "shadow",
|
||
"meow.conf", "user.ini", "config.json",
|
||
};
|
||
#define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0]))
|
||
|
||
static const char *g_sock_perf = PERF_SOCK;
|
||
static const char *g_sock_power = POWER_SOCK;
|
||
static const char *g_sock_launcher = LAUNCHER_SOCK;
|
||
static const char *g_sock_audio = AUDIO_SOCK;
|
||
static const char *g_sock_net = NET_SOCK; /* meow_netmgrd(--sock-net / env MEOW_NET_SOCK 覆盖) */
|
||
static const char *g_sock_pm = PM_SOCK; /* meow_powermgr(--sock-pm / env MEOW_PM_SOCK 覆盖) */
|
||
|
||
/* ---------------- 增量帧缓存(GET /api/screen.jpeg?delta=1,阶段 C 增强) ----------------
|
||
* 服务端缓存上一帧紧凑 RGB565,按 8×8 块比对,只编码变化块(外接 bbox 子图)并以响应头回传偏移。
|
||
* 单线程 accept 循环持有,跨请求持久(见 §5.10)。 */
|
||
#define SCREEN_BLOCK 8 /* 比对块边长(像素) */
|
||
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565(w*2 行距) */
|
||
static int g_screen_pw = 0, g_screen_ph = 0;
|
||
|
||
/* 初始化 TF 卡派生路径(main 开头调用)。env MEOW_TF_ROOT 可覆盖卡根(宿主测试用);
|
||
* env MEOW_NOTIF_LOG 可覆盖通知转储日志路径(宿主测试用)。默认设备卡路径。 */
|
||
static void init_tf_paths(void)
|
||
{
|
||
const char *env = getenv("MEOW_TF_ROOT");
|
||
if (env && env[0]) {
|
||
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", env);
|
||
/* 去掉末尾 `/`(规范化处统一按无尾斜杠处理) */
|
||
size_t l = strlen(g_tf_root_buf);
|
||
while (l > 1 && g_tf_root_buf[l - 1] == '/') g_tf_root_buf[--l] = 0;
|
||
}
|
||
snprintf(g_tf_card_buf, sizeof g_tf_card_buf, "%s/MeowOS", TF_ROOT);
|
||
snprintf(g_apps_dir_buf, sizeof g_apps_dir_buf, "%s/MeowAPPs", TF_ROOT);
|
||
snprintf(g_logs_dir_buf, sizeof g_logs_dir_buf, "%s/MeowOS/logs", TF_ROOT);
|
||
snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s/MeowOS/notifications/notifications.log", TF_ROOT);
|
||
env = getenv("MEOW_NOTIF_LOG");
|
||
if (env && env[0]) snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s", env);
|
||
g_tf_card = g_tf_card_buf;
|
||
g_apps_dir = g_apps_dir_buf;
|
||
g_logs_dir = g_logs_dir_buf;
|
||
g_notif_log = g_notif_log_buf;
|
||
}
|
||
|
||
/* ---------------- 有界动态字符串(sbuf) ---------------- */
|
||
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
|
||
|
||
static int sb_reserve(sbuf_t *s, size_t extra)
|
||
{
|
||
if (s->n + extra + 1 <= s->cap) return 0;
|
||
size_t ncap = s->cap ? s->cap : 256;
|
||
while (ncap < s->n + extra + 1) ncap *= 2;
|
||
char *nb = realloc(s->b, ncap);
|
||
if (!nb) return -1;
|
||
s->b = nb;
|
||
s->cap = ncap;
|
||
return 0;
|
||
}
|
||
|
||
static void sA(sbuf_t *s, const char *fmt, ...)
|
||
{
|
||
char tmp[512];
|
||
va_list ap, ap2;
|
||
va_start(ap, fmt);
|
||
va_copy(ap2, ap);
|
||
int w = vsnprintf(tmp, sizeof tmp, fmt, ap);
|
||
va_end(ap);
|
||
if (w < 0) { va_end(ap2); return; }
|
||
if ((size_t)w < sizeof tmp) {
|
||
if (sb_reserve(s, (size_t)w) < 0) { va_end(ap2); return; }
|
||
memcpy(s->b + s->n, tmp, (size_t)w);
|
||
s->n += (size_t)w;
|
||
} else {
|
||
/* 超长 %s 等:格式化结果超出固定缓冲,动态分配后再追加(防截断坏 JSON) */
|
||
char *big = malloc((size_t)w + 1);
|
||
if (!big) { va_end(ap2); return; }
|
||
int w2 = vsnprintf(big, (size_t)w + 1, fmt, ap2);
|
||
va_end(ap2);
|
||
if (w2 < 0) { free(big); return; }
|
||
if (sb_reserve(s, (size_t)w2) < 0) { free(big); return; }
|
||
memcpy(s->b + s->n, big, (size_t)w2);
|
||
s->n += (size_t)w2;
|
||
free(big);
|
||
}
|
||
s->b[s->n] = 0;
|
||
}
|
||
|
||
/* ---------------- UTF-8 消毒(JSON 字符串收口) ----------------
|
||
* 面板把外部文本(amsd/svcd/audiod/net/app.json/字体/日志…)嵌进 JSON 字符串,任一上游产出非法
|
||
* UTF-8(如 amsd 曾把「流媒体客户端」截成 16B,腰斩「端」字)都会让浏览器 JSON.parse 拒收整页。
|
||
* 统一在 sA_str 内逐字节校验:合法序列透传,非法/截断序列逐字节替换为 '?'(保持字节长度)。 */
|
||
static int utf8_seq_len(unsigned char lead)
|
||
{
|
||
if (lead >= 0xC2 && lead <= 0xDF) return 2; /* C2+:规避 overlong C0/C1 */
|
||
if (lead >= 0xE0 && lead <= 0xEF) return 3; /* 3 字节(含代理区,由 seq_ok 剔除) */
|
||
if (lead >= 0xF0 && lead <= 0xF4) return 4; /* F5+ 超 U+10FFFF,剔除 */
|
||
return 0;
|
||
}
|
||
|
||
/* 校验从 p 开始的 len 字节序列是否构成合法 UTF-8 字符(len 由 utf8_seq_len 给出)。
|
||
* 调用方须保证 p+len 在可读边界内。检查续字节/overlong/代理对/超 U+10FFFF。 */
|
||
/* 严格 JSON 平衡检查(字符串态+括号深度;要求恰好收完,防尾随垃圾/多括号)。
|
||
* jv_parse 只解析首个完整值、容忍尾随内容——浏览器 JSON.parse 不容忍,故自校验必须用本函数。 */
|
||
static int json_strict_balanced(const char *b, size_t n)
|
||
{
|
||
int depth = 0, instr = 0, esc = 0;
|
||
for (size_t i = 0; i < n; i++) {
|
||
char c = b[i];
|
||
if (instr) {
|
||
if (esc) esc = 0;
|
||
else if (c == '\\') esc = 1;
|
||
else if (c == '"') instr = 0;
|
||
continue;
|
||
}
|
||
if (c == '"') { instr = 1; continue; }
|
||
if (c == '{' || c == '[') depth++;
|
||
else if (c == '}' || c == ']') { if (--depth < 0) return 0; }
|
||
}
|
||
return depth == 0 && !instr;
|
||
}
|
||
|
||
static int utf8_seq_ok(const char *p, int len)
|
||
{
|
||
unsigned char c0 = (unsigned char)p[0];
|
||
for (int k = 1; k < len; k++) {
|
||
unsigned char ck = (unsigned char)p[k];
|
||
if (ck == 0 || (ck & 0xC0) != 0x80) return 0; /* 截断/非续字节 */
|
||
}
|
||
unsigned long cp;
|
||
if (len == 2) cp = ((unsigned long)(c0 & 0x1F) << 6) | ((unsigned char)p[1] & 0x3F);
|
||
else if (len == 3) cp = ((unsigned long)(c0 & 0x0F) << 12) | (((unsigned char)p[1] & 0x3F) << 6) | ((unsigned char)p[2] & 0x3F);
|
||
else cp = ((unsigned long)(c0 & 0x07) << 18) | (((unsigned char)p[1] & 0x3F) << 12) | (((unsigned char)p[2] & 0x3F) << 6) | ((unsigned char)p[3] & 0x3F);
|
||
if (len == 2 && cp < 0x80) return 0; /* overlong(C0/C1 已由 seq_len 拒,双保险) */
|
||
if (len == 3 && cp < 0x800) return 0; /* overlong E0 80.. */
|
||
if (len == 4 && cp < 0x10000) return 0; /* overlong F0 80.. */
|
||
if (cp >= 0xD800 && cp <= 0xDFFF) return 0; /* 代理对(JS 字符串不允许孤立) */
|
||
if (cp > 0x10FFFF) return 0;
|
||
return 1;
|
||
}
|
||
|
||
/* 扫描 [s, s+n) 是否为合法 UTF-8:返回 1 合法(纯 ASCII 亦属),0 存在非法/截断字节。 */
|
||
static int utf8_valid_n(const char *s, size_t n)
|
||
{
|
||
size_t i = 0;
|
||
while (i < n) {
|
||
unsigned char c = (unsigned char)s[i];
|
||
if (c < 0x80) { i++; continue; }
|
||
int len = utf8_seq_len(c);
|
||
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(s + i, len)) return 0;
|
||
i += (size_t)len;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* UTF-8 消毒:将 src 前 n 字节拷贝到 dst(dst 容量 ≥ n+1,恒 NUL 结尾),非法/截断序列
|
||
* 逐字节替换为 '?'(保持字节长度)。返回 1 = 发生过替换,0 = 原样合法。无依赖,纯指针运算。 */
|
||
static int utf8_sanitize(char *dst, const char *src, size_t n)
|
||
{
|
||
size_t i = 0, j = 0;
|
||
int changed = 0;
|
||
while (i < n) {
|
||
unsigned char c = (unsigned char)src[i];
|
||
if (c < 0x80) { dst[j++] = (char)c; i++; continue; }
|
||
int len = utf8_seq_len(c);
|
||
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(src + i, len)) {
|
||
dst[j++] = '?'; changed = 1; i++; continue; /* 逐字节替换,前进 1 重对齐 */
|
||
}
|
||
memcpy(dst + j, src + i, (size_t)len);
|
||
j += (size_t)len; i += (size_t)len;
|
||
}
|
||
dst[j] = 0;
|
||
return changed;
|
||
}
|
||
|
||
/* JSON 字符串转义 + UTF-8 消毒(非 NUL 终止、长度明确版;控制字符转 \u00xx,非法 UTF-8 逐字节转 '?')。 */
|
||
static void sA_str_n(sbuf_t *s, const char *v, size_t n)
|
||
{
|
||
sA(s, "\"");
|
||
size_t i = 0;
|
||
while (i < n) {
|
||
unsigned char c = (unsigned char)v[i];
|
||
if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); i++; }
|
||
else if (c < 0x20) { sA(s, "\\u%04x", c); i++; }
|
||
else if (c < 0x80) { sA(s, "%c", (int)c); i++; }
|
||
else {
|
||
int len = utf8_seq_len(c);
|
||
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(v + i, len)) {
|
||
sA(s, "?"); i++; /* 非法/截断序列 → 逐字节替换为 '?',前进 1 重对齐 */
|
||
} else {
|
||
for (int k = 0; k < len; k++) sA(s, "%c", (int)(unsigned char)v[i + k]);
|
||
i += (size_t)len;
|
||
}
|
||
}
|
||
}
|
||
sA(s, "\"");
|
||
}
|
||
|
||
/* NUL 结尾字符串的 JSON 嵌入(统一收口:所有外部文本嵌 JSON 字符串都走这里)。 */
|
||
static void sA_str(sbuf_t *s, const char *v)
|
||
{
|
||
if (!v) v = "";
|
||
sA_str_n(s, v, strlen(v));
|
||
}
|
||
|
||
/* 追加一段原始字节到 sbuf(长度明确,绕过 %s 的固定缓冲限制;用于大 JSON 数组复用)。 */
|
||
static void sA_append(sbuf_t *s, const char *b, size_t n)
|
||
{
|
||
if (!b || n == 0) return;
|
||
if (sb_reserve(s, n) < 0) return;
|
||
memcpy(s->b + s->n, b, n);
|
||
s->n += n;
|
||
s->b[s->n] = 0;
|
||
}
|
||
|
||
static void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
|
||
|
||
/* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */
|
||
static int write_full(int sock, const void *b, size_t n);
|
||
static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra);
|
||
static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen);
|
||
static int send_ok(int sock, const char *data);
|
||
static int send_err(int sock, int status, const char *code, const char *msg);
|
||
static int send_json(int sock, int status, const char *data);
|
||
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
|
||
static void panel_log(const char *fmt, ...);
|
||
static int qparam(const char *query, const char *key, char *out, int outcap); /* 定义在文件后部;网络连接表 query 取值 */
|
||
static int tf_present(void); /* 定义在文件后部;net 日志卡根判定 */
|
||
static const char *web_root(void); /* 定义在文件后部;此处前向声明供鉴权会话路径使用 */
|
||
static int run_cmd(const char *cmd, char *out, int outcap); /* 定义在文件后部;/api/ams、/api/svcd、/api/overview 用(有界收割) */
|
||
|
||
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
|
||
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
|
||
static int csv_count(const char *s);
|
||
|
||
/* ---------------- 最简 JSON 解析器(无第三方库) ----------------
|
||
* 只覆盖 MeowPanel 实际用到的:对象 / 数组 / 字符串(含转义) / 数字 / bool / null。
|
||
* value 树:J_OBJ 的 child 为键值对链(str=key,child=value,next=下个 pair);
|
||
* J_ARR 的 child 为元素链(next=下个元素)。
|
||
*/
|
||
enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ };
|
||
|
||
typedef struct jv {
|
||
int type;
|
||
int bval; /* J_BOOL */
|
||
long long inum; /* J_NUM 整数值 */
|
||
double dnum; /* J_NUM 浮点值 */
|
||
char *str; /* J_STR: 值; J_OBJ: key */
|
||
struct jv *child; /* J_ARR: 头; J_OBJ: 头 pair */
|
||
struct jv *next; /* 链 */
|
||
} jv_t;
|
||
|
||
typedef struct { const char *p; const char *end; int err; } jvp_t;
|
||
|
||
static jv_t *jv_alloc(int type)
|
||
{
|
||
jv_t *v = calloc(1, sizeof *v);
|
||
if (v) v->type = type;
|
||
return v;
|
||
}
|
||
|
||
static void jv_free(jv_t *v)
|
||
{
|
||
if (!v) return;
|
||
jv_t *c = v->child;
|
||
while (c) {
|
||
jv_t *nx = c->next;
|
||
if (v->type == J_ARR) { jv_free(c); } /* 数组元素是完整值(可为对象/嵌套数组),须整体递归释放 */
|
||
else { jv_free(c->child); free(c->str); free(c); } /* 对象:pair 链 → 释放 value + key + pair 节点 */
|
||
c = nx;
|
||
}
|
||
free(v->str);
|
||
free(v);
|
||
}
|
||
|
||
static void jvp_skip_ws(jvp_t *t)
|
||
{
|
||
while (t->p < t->end && (*t->p == ' ' || *t->p == '\t' || *t->p == '\n' || *t->p == '\r')) t->p++;
|
||
}
|
||
|
||
static char *jv_parse_string(jvp_t *t)
|
||
{
|
||
if (t->p >= t->end || *t->p != '"') { t->err = 1; return NULL; }
|
||
t->p++;
|
||
sbuf_t s = {0};
|
||
while (t->p < t->end && *t->p != '"') {
|
||
unsigned char c = (unsigned char)*t->p++;
|
||
if (c == '\\') {
|
||
if (t->p >= t->end) { t->err = 1; break; }
|
||
char e = *t->p++;
|
||
switch (e) {
|
||
case 'n': sA(&s, "\n"); break;
|
||
case 't': sA(&s, "\t"); break;
|
||
case 'r': sA(&s, "\r"); break;
|
||
case 'b': sA(&s, "\b"); break;
|
||
case 'f': sA(&s, "\f"); break;
|
||
case 'u': {
|
||
if (t->end - t->p < 4) { t->err = 1; sbuf_free(&s); return NULL; }
|
||
unsigned int cp = 0;
|
||
for (int i = 0; i < 4; i++) { char h = *t->p++; cp <<= 4; if (h>='0'&&h<='9') cp|=h-'0'; else if (h>='a'&&h<='f') cp|=h-'a'+10; else if (h>='A'&&h<='F') cp|=h-'A'+10; else { t->err=1; sbuf_free(&s); return NULL; } }
|
||
/* 只支持 BMP 常见字符;代理对按 UTF-8 重算(简化:cp<0x80 单字节,否则保守转发) */
|
||
if (cp < 0x80) sA(&s, "%c", (int)cp);
|
||
else if (cp < 0x800) { sA(&s, "%c", 0xc0|(cp>>6)); sA(&s, "%c", 0x80|(cp&0x3f)); }
|
||
else { sA(&s, "%c", 0xe0|(cp>>12)); sA(&s, "%c", 0x80|((cp>>6)&0x3f)); sA(&s, "%c", 0x80|(cp&0x3f)); }
|
||
break; }
|
||
default: sA(&s, "%c", (int)e); break;
|
||
}
|
||
} else if (c < 0x20) { t->err = 1; break; }
|
||
else sA(&s, "%c", (int)c);
|
||
}
|
||
if (t->p >= t->end || *t->p != '"') { t->err = 1; sbuf_free(&s); return NULL; }
|
||
t->p++;
|
||
if (!s.b) { s.b = calloc(1, 1); }
|
||
return s.b;
|
||
}
|
||
|
||
static jv_t *jv_parse_value(jvp_t *t);
|
||
|
||
static jv_t *jv_parse_object(jvp_t *t)
|
||
{
|
||
t->p++;
|
||
jv_t *obj = jv_alloc(J_OBJ);
|
||
if (!obj) { t->err = 1; return NULL; }
|
||
jv_t **tail = &obj->child;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == '}') { t->p++; return obj; }
|
||
for (;;) {
|
||
jvp_skip_ws(t);
|
||
if (t->p >= t->end) { t->err = 1; jv_free(obj); return NULL; }
|
||
char *key = jv_parse_string(t);
|
||
if (!key) { jv_free(obj); return NULL; }
|
||
jvp_skip_ws(t);
|
||
if (t->p >= t->end || *t->p != ':') { t->err = 1; free(key); jv_free(obj); return NULL; }
|
||
t->p++;
|
||
jv_t *val = jv_parse_value(t);
|
||
if (!val) { t->err = 1; free(key); jv_free(obj); return NULL; }
|
||
jv_t *pair = calloc(1, sizeof *pair);
|
||
if (!pair) { t->err = 1; free(key); jv_free(val); jv_free(obj); return NULL; }
|
||
pair->type = J_OBJ;
|
||
pair->str = key;
|
||
pair->child = val;
|
||
*tail = pair;
|
||
tail = &pair->next;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == ',') { t->p++; continue; }
|
||
if (t->p < t->end && *t->p == '}') { t->p++; return obj; }
|
||
t->err = 1;
|
||
jv_free(obj);
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
static jv_t *jv_parse_array(jvp_t *t)
|
||
{
|
||
t->p++;
|
||
jv_t *arr = jv_alloc(J_ARR);
|
||
if (!arr) { t->err = 1; return NULL; }
|
||
jv_t **tail = &arr->child;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == ']') { t->p++; return arr; }
|
||
for (;;) {
|
||
jv_t *v = jv_parse_value(t);
|
||
if (!v) { jv_free(arr); return NULL; }
|
||
*tail = v;
|
||
tail = &v->next;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == ',') { t->p++; continue; }
|
||
if (t->p < t->end && *t->p == ']') { t->p++; return arr; }
|
||
t->err = 1;
|
||
jv_free(arr);
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
static jv_t *jv_parse_value(jvp_t *t)
|
||
{
|
||
jvp_skip_ws(t);
|
||
if (t->p >= t->end) { t->err = 1; return NULL; }
|
||
char c = *t->p;
|
||
if (c == '{') return jv_parse_object(t);
|
||
if (c == '[') return jv_parse_array(t);
|
||
if (c == '"') {
|
||
char *s = jv_parse_string(t);
|
||
if (!s) return NULL;
|
||
jv_t *v = jv_alloc(J_STR);
|
||
if (!v) { free(s); t->err = 1; return NULL; }
|
||
v->str = s;
|
||
return v;
|
||
}
|
||
if (!strncmp(t->p, "true", 4)) { t->p += 4; jv_t *v = jv_alloc(J_BOOL); if (v) v->bval = 1; return v; }
|
||
if (!strncmp(t->p, "false", 5)) { t->p += 5; return jv_alloc(J_BOOL); }
|
||
if (!strncmp(t->p, "null", 4)) { t->p += 4; return jv_alloc(J_NULL); }
|
||
/* number */
|
||
const char *q = t->p;
|
||
int isint = 1;
|
||
while (q < t->end && (*q == '-' || *q == '+' || *q == '.' || *q == 'e' || *q == 'E' || isdigit((unsigned char)*q))) {
|
||
if (*q == '.' || *q == 'e' || *q == 'E') isint = 0;
|
||
q++;
|
||
}
|
||
if (q == t->p) { t->err = 1; return NULL; }
|
||
char tmp[64];
|
||
size_t dlen = (size_t)(q - t->p);
|
||
if (dlen >= sizeof tmp) dlen = sizeof tmp - 1;
|
||
memcpy(tmp, t->p, dlen);
|
||
tmp[dlen] = 0;
|
||
t->p = q;
|
||
jv_t *v = jv_alloc(J_NUM);
|
||
if (!v) { t->err = 1; return NULL; }
|
||
v->dnum = strtod(tmp, NULL);
|
||
v->inum = (long long)strtoll(tmp, NULL, 10);
|
||
(void)isint;
|
||
return v;
|
||
}
|
||
|
||
static jv_t *jv_parse(const char *text, int len)
|
||
{
|
||
jvp_t t;
|
||
t.p = text;
|
||
t.end = text + len;
|
||
t.err = 0;
|
||
jv_t *v = jv_parse_value(&t);
|
||
if (!v || t.err) { if (v) jv_free(v); return NULL; }
|
||
return v;
|
||
}
|
||
|
||
static jv_t *jv_obj_get(const jv_t *obj, const char *key)
|
||
{
|
||
if (!obj || obj->type != J_OBJ) return NULL;
|
||
for (const jv_t *p = obj->child; p; p = p->next)
|
||
if (p->str && !strcmp(p->str, key)) return p->child;
|
||
return NULL;
|
||
}
|
||
|
||
/* 将 jv 树序列化回 JSON 文本(追加到 sbuf)。覆盖解析器支持的 6 类型;字符串经 sA_str 转义+UTF-8 消毒。
|
||
* 注意 J_OBJ 的 child 链是「键值对」节点(str=key、child=value),此处按键值对展开,不递归其自身。 */
|
||
static void jv_to_json(sbuf_t *s, const jv_t *v)
|
||
{
|
||
if (!v) { sA(s, "null"); return; }
|
||
switch (v->type) {
|
||
case J_NULL: sA(s, "null"); break;
|
||
case J_BOOL: sA(s, v->bval ? "true" : "false"); break;
|
||
case J_NUM:
|
||
if (v->dnum == (double)v->inum) sA(s, "%lld", v->inum);
|
||
else sA(s, "%g", v->dnum);
|
||
break;
|
||
case J_STR: sA_str(s, v->str ? v->str : ""); break;
|
||
case J_ARR:
|
||
sA(s, "[");
|
||
{ int f = 1; for (const jv_t *e = v->child; e; e = e->next) { if (!f) sA(s, ","); f = 0; jv_to_json(s, e); } }
|
||
sA(s, "]");
|
||
break;
|
||
case J_OBJ:
|
||
sA(s, "{");
|
||
{ int f2 = 1;
|
||
for (const jv_t *p = v->child; p; p = p->next) {
|
||
if (!f2) { sA(s, ","); }
|
||
f2 = 0;
|
||
sA_str(s, p->str); sA(s, ":"); jv_to_json(s, p->child);
|
||
} }
|
||
sA(s, "}");
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* ---------------- 设置表(权威来源:meow_settings.c g_defs,逐键一致) ----------------
|
||
* 特别说明(与 MEOWPANEL_PROTOCOL/实现出入记录):
|
||
* - ui.style 值域源码为 "md3,miui",但 SYSTEM_SETTINGS §1 定案 + MEOWPANEL_PROTOCOL §5
|
||
* 一致行为 **miuix**(对应 MEOW_STYLE_MIUIX)。前端 data-style 也用 miuix。故本表对齐为
|
||
* "md3,miuix" / "Material You,MIUIx"(实现优先,回改见 README)。
|
||
* - display.brightness 的 g_defs 为 min=0 max=255 def=101,但语义 clamp 到 [brightness_min(默认1),100]
|
||
* (防 101 闪屏)。本表对元数据给出 min=1 max=100(协议 §3.2 期望),实际 set 走 brightness clamp。
|
||
*/
|
||
enum { ST_INT, ST_STR, ST_ENUM, ST_BOOL };
|
||
|
||
typedef struct setdef {
|
||
const char *key;
|
||
int type;
|
||
const char *def;
|
||
int min, max; /* ST_INT 取值域;max<=0 表示无上界 */
|
||
const char *enum_names; /* ST_ENUM 逗号名 */
|
||
const char *enum_labels; /* 对应中文 */
|
||
const char *group; /* 分组标签(协议 groups 用) */
|
||
} setdef_t;
|
||
|
||
static const setdef_t g_sets[] = {
|
||
/* 显示 */
|
||
{ "display.brightness", ST_INT, "101", 1, 100, NULL, NULL, "显示" },
|
||
{ "display.brightness_min", ST_INT, "1", 0, 255, NULL, NULL, "显示" },
|
||
{ "display.screen_off_mode", ST_ENUM, "panel",0,0, "panel,black", "面板下电,背光全黑", "显示" },
|
||
{ "display.screen_off_timeout", ST_INT, "30", 0, 86400,NULL, NULL, "显示" },
|
||
{ "display.refresh_rate", ST_ENUM, "60", 0,0, "60,30,15", "60Hz,30Hz,15Hz", "显示" },
|
||
{ "display.refresh_always_max", ST_BOOL, "0", 0,0, NULL, NULL, "显示" },
|
||
{ "display.theme", ST_ENUM, "dark", 0,0, "dark,light", "深色,浅色", "显示" },
|
||
{ "display.theme_mode", ST_ENUM, "manual",0,0, "manual,auto", "手动,时段自动", "显示" },
|
||
{ "display.theme_dark_start", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
|
||
{ "display.theme_dark_end", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
|
||
{ "display.background", ST_STR, "black",0,0, NULL, NULL, "显示" },
|
||
{ "display.multi_font", ST_INT, "0", 0, 1, NULL, NULL, "显示" },
|
||
/* 电源 */
|
||
{ "power.longpress_menu_ms", ST_INT, "3000", 0,0, NULL, NULL, "电源" },
|
||
{ "power.forceoff_ms", ST_INT, "8000", 0,0, NULL, NULL, "电源" },
|
||
{ "power.defeat_hotkey", ST_STR, "power_voldown",0,0, NULL, NULL, "电源" },
|
||
/* 开机 */
|
||
{ "boot.default", ST_ENUM, "meow", 0,0, "meow,hiby", "MeowOS,HiByOS", "开机" },
|
||
{ "boot.menu_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "开机" },
|
||
{ "boot.timeout_ms", ST_INT, "5000", 0,0, NULL, NULL, "开机" },
|
||
/* 输入 */
|
||
{ "input.hotkey_seq", ST_STR, "volup,volup,volup",0,0, NULL, NULL, "输入" },
|
||
{ "input.hotkey_window_ms", ST_INT, "2500", 0,0, NULL, NULL, "输入" },
|
||
{ "input.touch_arm_delay_ms", ST_INT, "700", 0,0, NULL, NULL, "输入" },
|
||
{ "input.swipe_invert", ST_BOOL, "0", 0,0, NULL, NULL, "输入" },
|
||
/* 字体/主题 */
|
||
{ "font.file", ST_STR, "MeowOS/fonts/default/regular/font.png",0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_cjk", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_cjk_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_num", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_latin", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_latin_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_other", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.load_full", ST_INT, "0", 0,1, NULL, NULL, "字体主题" },
|
||
{ "font.size.title", ST_INT, "28", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.size.body", ST_INT, "20", 0,0, NULL, NULL, "字体主题" },
|
||
{ "ui.style", ST_ENUM, "md3", 0,0, "md3,miuix", "Material You,MIUIx", "字体主题" },
|
||
/* 通知 */
|
||
{ "notify.center_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "通知" },
|
||
{ "notify.app_quota", ST_INT, "16", 0,0, NULL, NULL, "通知" },
|
||
{ "notify.categories", ST_INT, "4", 1,4, NULL, NULL, "通知" },
|
||
{ "notify.category_quota", ST_INT, "4", 1,16, NULL, NULL, "通知" },
|
||
{ "notify.media_quota", ST_INT, "1", 1,1, NULL, NULL, "通知" },
|
||
{ "notify.dnd", ST_BOOL, "0", 0,0, NULL, NULL, "通知" },
|
||
/* 日志 */
|
||
{ "log.level", ST_ENUM, "info", 0,0, "error,warn,info,debug", "错误,警告,信息,调试", "日志" },
|
||
{ "log.to_card", ST_BOOL, "1", 0,0, NULL, NULL, "日志" },
|
||
/* 音频 */
|
||
{ "audio.volume", ST_INT, "60", 0,100, NULL, NULL, "音频" },
|
||
{ "audio.volume_max", ST_INT, "100", 0,100, NULL, NULL, "音频" },
|
||
{ "audio.volume_step", ST_INT, "1", 1,5, NULL, NULL, "音频" },
|
||
{ "audio.output", ST_STR, "hw:0,0",0,0, NULL, NULL, "音频" },
|
||
/* 网络 */
|
||
{ "network.wifi_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "网络" },
|
||
{ "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" },
|
||
/* 系统 */
|
||
{ "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" },
|
||
/* 面板(MeowPanel 自身,访问级 PIN 鉴权)
|
||
* panel.pin = 4 位数字 PIN 的 sha256+salt 哈希(`<salt_hex>:<sha256_hex(salt||pin)>`,绝不存明文)。
|
||
* 与未来 MeowOS 锁屏 PIN 同源:锁屏键 `system.pin` 预留——本后端只读面板键 panel.pin;将来锁屏启用时
|
||
* 以 system.pin 为准并做同源迁移(panel.pin 视为其缓存/别名),此处仅留兼容注记,不实现。
|
||
* panel.auth_enabled/panel.auth_pass 为旧密码机制,保留兼容迁移(见鉴权段注释),新代码以 PIN 为准。 */
|
||
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
||
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||
};
|
||
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
|
||
|
||
static const setdef_t *setdef_find(const char *key) {
|
||
for (int i = 0; i < SETS_COUNT; i++)
|
||
if (!strcmp(g_sets[i].key, key)) return &g_sets[i];
|
||
return NULL;
|
||
}
|
||
|
||
/* ---------------- settings.conf 解析(照 meow_ui_vol_read / settings_get_int_trim) ----------------
|
||
* '=' 切分,key 去首尾空白精确匹配(防 audio.volume 误配 audio.volume_max)。
|
||
* 路径取 g_settings_file(env MEOW_SETTINGS_FILE 可覆盖,宿主测试用)。
|
||
*/
|
||
static void settings_mkdir_p(void)
|
||
{
|
||
/* 逐级创建 settings.conf 所在目录(mkdir -p 语义,容忍已存在)。 */
|
||
char tmp[1024];
|
||
snprintf(tmp, sizeof tmp, "%s", g_settings_file);
|
||
char *slash = strrchr(tmp, '/');
|
||
if (!slash || slash == tmp) return; /* 无目录或根部 */
|
||
*slash = 0;
|
||
if (!tmp[0]) return;
|
||
char buf[1024]; buf[0] = 0;
|
||
size_t i = 0;
|
||
if (tmp[0] == '/') buf[i++] = '/';
|
||
const char *s = tmp + (tmp[0] == '/' ? 1 : 0);
|
||
while (*s) {
|
||
const char *tom = strchr(s, '/');
|
||
size_t l = tom ? (size_t)(tom - s) : strlen(s);
|
||
if (l) {
|
||
if (i && buf[i - 1] != '/') buf[i++] = '/';
|
||
if (i + l >= sizeof buf) break;
|
||
memcpy(buf + i, s, l);
|
||
i += l;
|
||
buf[i] = 0;
|
||
mkdir(buf, 0777);
|
||
}
|
||
if (!tom) break;
|
||
s = tom + 1;
|
||
}
|
||
}
|
||
|
||
static char *settings_get_value(const char *key)
|
||
{
|
||
FILE *f = fopen(g_settings_file, "r");
|
||
if (!f) return NULL;
|
||
size_t kl = strlen(key);
|
||
char line[256];
|
||
char *found = NULL;
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *eq = strchr(line, '=');
|
||
if (!eq) continue;
|
||
char *k = line;
|
||
while (*k == ' ' || *k == '\t') k++;
|
||
long n = (long)(eq - k);
|
||
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--;
|
||
if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue;
|
||
found = eq + 1;
|
||
break;
|
||
}
|
||
fclose(f);
|
||
if (!found) return NULL;
|
||
while (*found == ' ' || *found == '\t') found++;
|
||
char *e = found + strlen(found);
|
||
while (e > found && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
|
||
return strdup(found);
|
||
}
|
||
|
||
static int settings_get_int(const char *key, int defv)
|
||
{
|
||
char *v = settings_get_value(key);
|
||
if (!v) return defv;
|
||
int r = atoi(v);
|
||
free(v);
|
||
return r;
|
||
}
|
||
|
||
/* 亮度 clamp:下限取 display.brightness_min(默认 1),上限 100(防 101 闪屏)。
|
||
* 与 meow_settings.c clamp_brightness 同口径。 */
|
||
static int brightness_clamp(int v)
|
||
{
|
||
int mn = settings_get_int("display.brightness_min", 1);
|
||
if (mn < 1) mn = 1;
|
||
if (mn > 100) mn = 100;
|
||
if (v < mn) v = mn;
|
||
if (v > 100) v = 100;
|
||
return v;
|
||
}
|
||
|
||
/* 重写 settings.conf:把 key 写为 "key = value"。保留头与其它行;缺 key 则追加。
|
||
* O_TRUNC 重写。成功返回 0。 */
|
||
static int settings_write_one(const char *key, const char *val)
|
||
{
|
||
settings_mkdir_p(); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
|
||
FILE *f = fopen(g_settings_file, "r");
|
||
char tmp[2048];
|
||
size_t n = 0;
|
||
int c;
|
||
if (f) {
|
||
while (n < sizeof tmp - 1 && (c = fgetc(f)) != EOF) tmp[n++] = (char)c;
|
||
fclose(f);
|
||
}
|
||
tmp[n] = 0;
|
||
FILE *nf = fopen(g_settings_file, "w");
|
||
if (!nf) return -1;
|
||
size_t klen = strlen(key);
|
||
int found = 0;
|
||
if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR);
|
||
/* 逐行重写:头照抄;命中 key 行替换;其余照抄。匹配容忍 `key = value` 两侧空格(与 settings_get_value 同口径)。 */
|
||
char *save = NULL;
|
||
for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||
char *p = ln;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
char *eq = strchr(p, '=');
|
||
if (eq) {
|
||
char *k = p;
|
||
size_t km = (size_t)(eq - k);
|
||
while (km > 0 && (k[km - 1] == ' ' || k[km - 1] == '\t')) km--;
|
||
if (km == klen && strncmp(k, key, klen) == 0) {
|
||
fprintf(nf, "%s = %s\n", key, val);
|
||
found = 1;
|
||
continue;
|
||
}
|
||
}
|
||
fprintf(nf, "%s\n", ln);
|
||
}
|
||
if (!found) fprintf(nf, "%s = %s\n", key, val);
|
||
fclose(nf);
|
||
return 0;
|
||
}
|
||
|
||
/* 设定值→落盘规范字符串(clamp/校验集中在此)。返回 -1=非法(422),0=成功。 */
|
||
static int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap)
|
||
{
|
||
switch (d->type) {
|
||
case ST_INT: {
|
||
int v = atoi(input);
|
||
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
|
||
else {
|
||
if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
|
||
else if (v < d->min) v = d->min;
|
||
}
|
||
snprintf(out, outcap, "%d", v);
|
||
return 0;
|
||
}
|
||
case ST_BOOL: {
|
||
char *q = (char *)input;
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
int b = 0;
|
||
if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1;
|
||
else { long v = strtol(q, NULL, 10); if (q[0] && v != 0) b = 1; }
|
||
snprintf(out, outcap, "%d", b ? 1 : 0);
|
||
return 0;
|
||
}
|
||
case ST_ENUM: {
|
||
const char *names = d->enum_names;
|
||
if (!names || !names[0]) { snprintf(out, outcap, "%s", input); return 0; }
|
||
int cnt = csv_count(names);
|
||
char tok[128];
|
||
/* 接受名字或索引(向后兼容) */
|
||
int idx = -1;
|
||
for (int i = 0; i < cnt; i++) {
|
||
csv_at(names, i, tok, sizeof tok);
|
||
if (!strcmp(tok, input)) { idx = i; break; }
|
||
}
|
||
if (idx < 0) {
|
||
int n = atoi(input);
|
||
if (n >= 0 && n < cnt) idx = n;
|
||
}
|
||
if (idx < 0) return -1; /* 非法枚举 */
|
||
csv_at(names, idx, tok, sizeof tok);
|
||
snprintf(out, outcap, "%s", tok);
|
||
return 0;
|
||
}
|
||
case ST_STR:
|
||
strncpy(out, input, outcap - 1);
|
||
out[outcap - 1] = 0;
|
||
return 0;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* 恢复全部默认:重写整个文件为 头 + 所有键 def。 */
|
||
static int settings_reset_all(void)
|
||
{
|
||
settings_mkdir_p();
|
||
FILE *nf = fopen(g_settings_file, "w");
|
||
if (!nf) return -1;
|
||
fprintf(nf, "%s\n", SETTINGS_HDR);
|
||
for (int i = 0; i < SETS_COUNT; i++)
|
||
fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def);
|
||
fclose(nf);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- IPC 帧读写(同 meow_svc_perf/power 客户端约定,全小端 LE) ---------------- */
|
||
static int x_read_full(int fd, uint8_t *buf, size_t n)
|
||
{
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t r = read(fd, buf + off, n - off);
|
||
if (r <= 0) return -1;
|
||
off += (size_t)r;
|
||
}
|
||
return 0;
|
||
}
|
||
static int x_write_full(int fd, const uint8_t *buf, size_t n)
|
||
{
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t w = write(fd, buf + off, n - off);
|
||
if (w <= 0) return -1;
|
||
off += (size_t)w;
|
||
}
|
||
return 0;
|
||
}
|
||
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
|
||
{
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
uint32_t magic = MEOW_IPC_MAGIC, rl = len;
|
||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||
memcpy(hdr + 0, &magic, 4);
|
||
memcpy(hdr + 4, &ver, 2);
|
||
memcpy(hdr + 6, &t, 2);
|
||
memcpy(hdr + 8, &req_id, 4);
|
||
memcpy(hdr + 12, &rl, 4);
|
||
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
|
||
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* 读一帧:返回 payload 长度(<=cap 时复制到 buf),-1 失败;type/req_id 回传。 */
|
||
static int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap)
|
||
{
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
if (x_read_full(fd, hdr, sizeof hdr) < 0) return -1;
|
||
uint32_t magic, rlen, rid;
|
||
uint16_t ver, t;
|
||
memcpy(&magic, hdr, 4);
|
||
memcpy(&ver, hdr + 4, 2);
|
||
memcpy(&t, hdr + 6, 2);
|
||
memcpy(&rid, hdr + 8, 4);
|
||
memcpy(&rlen, hdr + 12, 4);
|
||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||
if (rlen > MEOW_IPC_MAX) return -1;
|
||
if (type) *type = t;
|
||
if (req_id) *req_id = rid;
|
||
uint32_t n = (rlen < cap) ? rlen : cap;
|
||
if (n && x_read_full(fd, buf, n) < 0) return -1;
|
||
/* 若实际更长,丢弃剩余 */
|
||
for (uint32_t i = n; i < rlen; i++) { uint8_t c; if (x_read_full(fd, &c, 1) < 0) return -1; }
|
||
return (int)n;
|
||
}
|
||
|
||
/* 带超时连 AF_UNIX 流 socket:非阻塞 connect + poll(POLLOUT)。 */
|
||
static int ipc_connect_tmo(const char *sock, int tmo_ms)
|
||
{
|
||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||
if (fd < 0) return -1;
|
||
struct sockaddr_un a;
|
||
memset(&a, 0, sizeof a);
|
||
a.sun_family = AF_UNIX;
|
||
strncpy(a.sun_path, sock, sizeof a.sun_path - 1);
|
||
int fl = fcntl(fd, F_GETFL, 0);
|
||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||
int rc = connect(fd, (struct sockaddr *)&a, sizeof a);
|
||
if (rc < 0 && errno == EINPROGRESS) {
|
||
struct pollfd pf = { fd, POLLOUT, 0 };
|
||
int pr = poll(&pf, 1, tmo_ms);
|
||
if (pr <= 0) { close(fd); return -1; }
|
||
int soerr = 0;
|
||
socklen_t sl = sizeof soerr;
|
||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl);
|
||
if (soerr != 0) { close(fd); return -1; }
|
||
} else if (rc < 0) { close(fd); return -1; }
|
||
fcntl(fd, F_SETFL, fl);
|
||
/* 读/写超时兜底(正常服务应即时响应) */
|
||
struct timeval tv = { tmo_ms / 1000, (tmo_ms % 1000) * 1000 };
|
||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||
return fd;
|
||
}
|
||
|
||
/* 服务端握手(MSG_HELLO → MSG_ACK),app_id 用系统面板身份 os.meow.panel。 */
|
||
static int ipc_hello(int fd)
|
||
{
|
||
m_hello_t h;
|
||
memset(&h, 0, sizeof h);
|
||
strncpy(h.app_id, "os.meow.panel", sizeof h.app_id - 1);
|
||
h.version = 1;
|
||
if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) < 0) return -1;
|
||
uint8_t buf[64];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_ack_t)) return -1;
|
||
if (type != MSG_ACK) return -1;
|
||
m_ack_t a;
|
||
memcpy(&a, buf, sizeof a);
|
||
return (a.code == MEOW_OK) ? 0 : -1;
|
||
}
|
||
|
||
/* ---------------- 读侧:meow_svc_perf IPC ----------------
|
||
* 返回 0=成功(json 已填入 out/cap),否则负错误码,errmsg 填中文说明。
|
||
*/
|
||
static const char *perf_code_str(int code)
|
||
{
|
||
switch (code) {
|
||
case MEOW_OK: return ""; /* 不应使用 */
|
||
case MEOW_ERR_INVAL: return "参数非法";
|
||
case MEOW_ERR_IO: return "读取失败";
|
||
case MEOW_ERR_NODEV: return "设备不存在";
|
||
default: return "服务端错误";
|
||
}
|
||
}
|
||
|
||
static int perf_subset_for_path(const char *path)
|
||
{
|
||
if (!strcmp(path, "/api/perf")) return PERF_SUBSET_PERF;
|
||
if (!strcmp(path, "/api/system")) return 0; /* system 需 uptime/storage,取全量 */
|
||
return 0; /* /api/status */
|
||
}
|
||
|
||
static int ipc_perf_query(int subset, char *out, int cap)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_perf, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return MEOW_ERR_NODEV;
|
||
int rc = MEOW_ERR_INTERNAL;
|
||
char msg[128] = "";
|
||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; snprintf(msg, sizeof msg, "握手中断:性能服务未就绪"); goto done; }
|
||
m_perf_query_t q;
|
||
memset(&q, 0, sizeof q);
|
||
q.subset = (uint32_t)subset;
|
||
if (send_frame(fd, MSG_PERF_QUERY, 2, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "发送查询失败"); goto done; }
|
||
uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_perf_result_t)) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "应答帧过短"); goto done; }
|
||
if (type != MSG_PERF_RESULT) { rc = MEOW_ERR_INVAL; snprintf(msg, sizeof msg, "非预期应答类型 0x%04x", type); goto done; }
|
||
m_perf_result_t r;
|
||
memcpy(&r, buf, sizeof r);
|
||
if (r.code != MEOW_OK) { rc = (int)r.code; snprintf(msg, sizeof msg, "性能服务错误: %s", perf_code_str((int)r.code)); goto done; }
|
||
uint16_t jl = r.json_len;
|
||
if (jl > sizeof r.json - 1) jl = (uint16_t)(sizeof r.json - 1);
|
||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||
memcpy(out, r.json, jl);
|
||
out[jl] = 0;
|
||
rc = MEOW_OK;
|
||
done:
|
||
close(fd);
|
||
/* 服务不可达/非 OK 由调用方据此返回错误;绝不静默失败、绝不降级直连 /proc。 */
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 动作侧:meow_svc_power IPC ----------------
|
||
* 返回 0=受理;否则负错误码(PERM/INVAL/NODEV/…),msg 填服务端说明。
|
||
*/
|
||
static const char *power_code_str(int code)
|
||
{
|
||
switch (code) {
|
||
case MEOW_OK: return "";
|
||
case MEOW_ERR_PERM: return "无权限(需 root)";
|
||
case MEOW_ERR_INVAL: return "未知命令";
|
||
case MEOW_ERR_NODEV: return "设备/能力不支持";
|
||
case MEOW_ERR_NOENT: return "缺少升级文件";
|
||
case MEOW_ERR_IO: return "读写失败";
|
||
default: return "电源服务错误";
|
||
}
|
||
}
|
||
|
||
static int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_power, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return MEOW_ERR_NODEV;
|
||
int rc = MEOW_ERR_INTERNAL;
|
||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||
m_power_action_t a;
|
||
memset(&a, 0, sizeof a);
|
||
a.cmd = cmd;
|
||
a.flag = flag;
|
||
if (arg) snprintf(a.arg, sizeof a.arg, "%s", arg);
|
||
if (send_frame(fd, MSG_POWER_ACTION, 3, &a, sizeof a) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||
uint8_t buf[256];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_power_status_t)) { rc = MEOW_ERR_IO; goto done; }
|
||
if (type != MSG_POWER_STATUS) { rc = MEOW_ERR_INVAL; goto done; }
|
||
m_power_status_t st;
|
||
memcpy(&st, buf, sizeof st);
|
||
if (out_st) *out_st = st;
|
||
rc = (int)st.code;
|
||
done:
|
||
close(fd);
|
||
return rc;
|
||
}
|
||
|
||
/* 从 svc_power 状态文本取 `k=v` 的 v(如 "coldboot=reboot" → "reboot";key 须精确匹配带 '=')。
|
||
* 读级命令(POWER_REBOOT_REASON/UPGRADE_STATUS)的 st.msg 即此形态。 */
|
||
static void svc_msg_kv(const char *msg, const char *key, char *out, size_t cap)
|
||
{
|
||
out[0] = 0;
|
||
if (!msg || !key) return;
|
||
size_t kl = strlen(key);
|
||
if (strncmp(msg, key, kl) == 0 && msg[kl] == '=' && msg[kl + 1])
|
||
snprintf(out, cap, "%s", msg + kl + 1);
|
||
}
|
||
|
||
/* ---------------- 音频状态:meow_audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE) ----------------
|
||
* audiod 无 MSG_HELLO 握手(见 patch_v2/meow_audiod/meow_audio.c CLI:直连发空负载 QUERY,
|
||
* 收 STATE 文本帧——"k=v" 空格分隔、NUL 结尾)。audiod 不在/超时/应答异常一律回
|
||
* {"available":false}(HTTP 200,不报错)——面板音频页据此显示「audiod 不可用」。
|
||
* socket 可被 env MEOW_AUDIO_SOCK / --sock-audio 覆盖(宿主/qemu 测试用,与 audiod CLI 同 env)。 */
|
||
static const char *audio_kv(const char *txt, const char *key, char *out, size_t cap)
|
||
{
|
||
size_t kl = strlen(key);
|
||
const char *p = txt;
|
||
while (p && *p) {
|
||
if (!strncmp(p, key, kl) && p[kl] == '=') {
|
||
p += kl + 1;
|
||
size_t n = strcspn(p, " \t\n");
|
||
if (n >= cap) n = cap - 1;
|
||
memcpy(out, p, n);
|
||
out[n] = 0;
|
||
return out;
|
||
}
|
||
p = strchr(p, ' ');
|
||
if (p) p++;
|
||
}
|
||
out[0] = 0;
|
||
return NULL;
|
||
}
|
||
static long long audio_kv_ll(const char *txt, const char *key, long long defv)
|
||
{
|
||
char b[64];
|
||
if (!audio_kv(txt, key, b, sizeof b) || !b[0]) return defv;
|
||
return strtoll(b, NULL, 10);
|
||
}
|
||
|
||
/* 音频音量上限:读 settings.conf 的 audio.volume_max(权威键,与 launcher/watchdog/svc_power/sdk 同用);
|
||
* 缺省回 PANEL_VOL_MAX_DEFAULT(=MEOW_VOL_MAX_DEFAULT 100)。面板写音量时据此 clamp(防炸耳红线)。 */
|
||
static int audio_vol_max(void)
|
||
{
|
||
int v = settings_get_int("audio.volume_max", PANEL_VOL_MAX_DEFAULT);
|
||
if (v < 0) v = 0;
|
||
if (v > 100) v = 100;
|
||
return v;
|
||
}
|
||
|
||
/* 音频快照填充:把 `{"available":...}` 追加进 sbuf(供 /api/audio 单发与 /api/overview 聚合共用)。
|
||
* audiod 不在/超时/应答异常一律 `{"available":false}`(HTTP 200,不报错)——面板/首页据此降级,绝不假数据。 */
|
||
static void audio_fill(sbuf_t *s)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
|
||
if (fd < 0) { sA(s, "{\"available\":false}"); return; }
|
||
uint8_t buf[MEOW_IPC_MAX];
|
||
uint16_t type = 0; uint32_t rid = 0;
|
||
int n = -1;
|
||
if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0)
|
||
n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (n <= 0 || type != MSG_AUDIO_STATE) { sA(s, "{\"available\":false}"); return; }
|
||
buf[n] = 0;
|
||
const char *txt = (const char *)buf;
|
||
char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16];
|
||
audio_kv(txt, "app", app, sizeof app);
|
||
audio_kv(txt, "fmt", fmt, sizeof fmt);
|
||
audio_kv(txt, "route_mode", rmode, sizeof rmode);
|
||
sA(s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0));
|
||
sA_str(s, app);
|
||
sA(s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":",
|
||
audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0));
|
||
sA_str(s, fmt);
|
||
sA(s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":",
|
||
audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0),
|
||
audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0));
|
||
sA_str(s, rmode);
|
||
sA(s, ",\"port\":%lld,\"vol\":%lld",
|
||
audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1));
|
||
/* 电平监控(261):rms/peak deci-dB(-990=-inf),lvlseq=快照代际;透传时别漏(ABI 追加键)。 */
|
||
sA(s, ",\"rms\":%lld,\"peak\":%lld,\"lvlseq\":%lld,\"vol_max\":%d}",
|
||
audio_kv_ll(txt, "rms", -990), audio_kv_ll(txt, "peak", -990), audio_kv_ll(txt, "lvlseq", 0), audio_vol_max());
|
||
}
|
||
|
||
static int api_audio(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
audio_fill(&s);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---- 音频写命令(写级,PIN 已护):route/volume 直连 audiod(无 MSG_HELLO 握手),发帧→收 MSG_ACK。
|
||
* audiod 对非法 port 也回 MEOW_OK(内部仅记录不应用,见 meow_audiod.c MSG_AUDIO_ROUTE),
|
||
* 故面板在**发出前**自行校验,绝不放 1(lineout)/4(SPDIF)/5(audiod 内部强制重烧值)。 */
|
||
static int audio_write_cmd(uint16_t type, const void *pl, uint32_t len, m_ack_t *out)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return -1;
|
||
uint8_t buf[64];
|
||
uint16_t rtype = 0; uint32_t rid = 0;
|
||
int n = -1;
|
||
if (send_frame(fd, type, 0x22, pl, len) == 0)
|
||
n = recv_frame(fd, &rtype, &rid, buf, sizeof buf);
|
||
close(fd);
|
||
if (n < (int)sizeof(m_ack_t) || rtype != MSG_ACK) return -1;
|
||
if (out) memcpy(out, buf, sizeof *out);
|
||
return 0;
|
||
}
|
||
|
||
/* POST /api/audio/route → 手动切输出路由。仅接受 0=auto/2=3.5/3=4.4;
|
||
* **绝不接受 1/4/5**(4=SPDIF、5=audiod 内部强制重烧 FPGA 值)。写级:前端切换前确认弹窗(选错口→无声)。 */
|
||
static int api_audio_route(int sock, const char *body, size_t body_len)
|
||
{
|
||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(body, (int)body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jp = jv_obj_get(root, "port");
|
||
if (!jp || jp->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 port 数字"); }
|
||
long port = (long)jp->inum;
|
||
jv_free(root);
|
||
if (port != 0 && port != 2 && port != 3)
|
||
return send_err(sock, 422, "UNSUPPORTED", "非法路由口(仅允许 0=auto/2=3.5/3=4.4;4=SPDIF、5=audiod 内部值禁止)");
|
||
m_audio_route_t a; a.port = (uint32_t)port;
|
||
m_ack_t st;
|
||
if (audio_write_cmd(MSG_AUDIO_ROUTE, &a, sizeof a, &st) < 0)
|
||
return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"accepted\":true,\"port\":%ld}", port);
|
||
int out = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return out;
|
||
}
|
||
|
||
/* POST /api/audio/volume → 写音量。**必须 clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`,
|
||
* 缺省 100;设备当前按 plan 为 50)。audiod 内部仅 clamp 0-100、无 vol_max 防护——面板此处是唯一把守方
|
||
* (防炸耳红线:平方锥度下靠近 100 极响,M2.96 曾有 dac 写 0=满幅事故)。 */
|
||
static int api_audio_volume(int sock, const char *body, size_t body_len)
|
||
{
|
||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(body, (int)body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jv = jv_obj_get(root, "value");
|
||
if (!jv || jv->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 value 数字"); }
|
||
long v = (long)jv->inum;
|
||
jv_free(root);
|
||
int vmax = audio_vol_max();
|
||
if (v < 0) v = 0;
|
||
if (v > vmax) v = vmax; /* 关键 clamp 红线 */
|
||
m_audio_volume_t a; a.pct = (int32_t)v;
|
||
m_ack_t st;
|
||
if (audio_write_cmd(MSG_AUDIO_VOLUME, &a, sizeof a, &st) < 0)
|
||
return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"accepted\":true,\"value\":%ld,\"applied\":%u,\"vol_max\":%d}", v, st.status, vmax);
|
||
int out = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return out;
|
||
}
|
||
|
||
/* ---------------- 功耗管理:meow_powermgr IPC(MSG_PM_QUERY→MSG_PM_RESULT) ----------------
|
||
* 客户端序列同 perf/power:connect → MSG_HELLO → 读 ACK → MSG_PM_QUERY(subset) → MSG_PM_RESULT(JSON)。
|
||
* powermgr 不在/超时/应答异常 → 负错误码(调用方转 503 POWER_UNAVAILABLE;overview 里转 available:false)。 */
|
||
static int ipc_pm_query(uint32_t subset, char *out, int cap)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return MEOW_ERR_NODEV;
|
||
int rc = MEOW_ERR_INTERNAL;
|
||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||
m_pm_query_t q;
|
||
memset(&q, 0, sizeof q);
|
||
q.subset = subset;
|
||
if (send_frame(fd, MSG_PM_QUERY, 0x31, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||
uint8_t buf[MEOW_IPC_MAX];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
|
||
if (type != MSG_PM_RESULT) { rc = MEOW_ERR_INVAL; goto done; }
|
||
uint32_t c; memcpy(&c, buf, 4);
|
||
if (c != MEOW_OK) { rc = (int)c; goto done; }
|
||
/* powermgr 发**紧凑**负载:json 从偏移 12 起(json_len 在偏移 8);同 m_pm_result_t 布局,兼容全量/紧凑。 */
|
||
uint16_t jl; memcpy(&jl, buf + 8, 2);
|
||
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
|
||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||
memcpy(out, buf + 12, jl);
|
||
out[jl] = 0;
|
||
rc = MEOW_OK;
|
||
done:
|
||
close(fd);
|
||
return rc;
|
||
}
|
||
|
||
/* GET /api/power → powermgr 聚合 JSON(battery/estimate/breakdown,confidence 原样透传)。 */
|
||
static int api_power(int sock)
|
||
{
|
||
char js[MEOW_IPC_MAX];
|
||
int rc = ipc_pm_query(0, js, sizeof js);
|
||
if (rc != MEOW_OK || !utf8_valid_n(js, strlen(js))) /* 上游坏字节 → 不返回,降级 */
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用");
|
||
return send_json(sock, 200, js);
|
||
}
|
||
|
||
/* ---------------- powermgr 期2/期3 扩展消息号(POWER_MANAGER_DESIGN §6.1;本文件局部定,同 meow_ipc.h 号段)----------------
|
||
* meow_ipc.h 只在注释里预留 0x00D2-0x00D7,未定义宏;与 meow_powermgr.c 本地 `#ifndef` 同值对齐。 */
|
||
#ifndef MSG_PM_POLICY_GET
|
||
#define MSG_PM_POLICY_GET 0x00D2 /* ↑ 策略读(空负载 → MSG_PM_RESULT policy JSON;读级任意调用者) */
|
||
#define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份) */
|
||
#define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count,0=默认) */
|
||
#define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON */
|
||
#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count,0=默认) */
|
||
#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON */
|
||
#define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(动作级;本期面板不暴露,预留) */
|
||
#define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(动作级;预留) */
|
||
#endif
|
||
/* 期 3 · 策略写负载(item="profile"/"low-pct"/"crit-pct"/"bri-cap"/"scr-cap")。 */
|
||
typedef struct m_pm_policy_set { char item[16]; char val[32]; } m_pm_policy_set_t; /* 48 B */
|
||
|
||
/* 通用 powermgr IPC:发 req_type + 可选 payload,收任意响应帧。返回值:
|
||
* >0 = json 字节长度(out 已填,*rtype 置响应帧类型;帧为 m_pm_result_t 布局 code+subset+json_len+json);
|
||
* <=0 = 负 MEOW_ERR_*(含连不上 NODEV / MSG_ERR 的 code),*rtype 置收到的帧类型(MSG_ERR=0x0003)。
|
||
* 兼容服务端「紧凑负载」(json 从偏移 12 起,json_len 在偏移 8)。 */
|
||
static int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen,
|
||
uint16_t *rtype, char *out, int cap)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return MEOW_ERR_NODEV;
|
||
int rc = MEOW_ERR_INTERNAL;
|
||
if (rtype) *rtype = 0;
|
||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||
if (send_frame(fd, req_type, 0x41, pl, plen) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||
uint8_t buf[MEOW_IPC_MAX];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n <= 0) { rc = MEOW_ERR_IO; goto done; }
|
||
if (rtype) *rtype = type;
|
||
if (type == MSG_ERR) {
|
||
m_err_t e; memcpy(&e, buf, sizeof e);
|
||
rc = (int)e.code; goto done;
|
||
}
|
||
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
|
||
uint32_t c; memcpy(&c, buf, 4);
|
||
if (c != MEOW_OK) { rc = (int)c; goto done; }
|
||
uint16_t jl; memcpy(&jl, buf + 8, 2);
|
||
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
|
||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||
memcpy(out, buf + 12, jl); out[jl] = 0;
|
||
rc = (int)jl;
|
||
done:
|
||
close(fd);
|
||
return rc;
|
||
}
|
||
|
||
/* GET/POST /api/power/policy → powermgr 期3 策略档位(GET 读级;POST 动作级,前端需二次确认)。
|
||
* GET:MSG_PM_POLICY_GET → policy JSON(profile 四档/低电阈值/亮度息屏上限,默认全关只透传)。 */
|
||
static int api_power_policy_get(int sock)
|
||
{
|
||
char js[MEOW_IPC_MAX];
|
||
uint16_t rt = 0;
|
||
int rc = ipc_pm_send(MSG_PM_POLICY_GET, NULL, 0, &rt, js, sizeof js);
|
||
if (rc <= 0)
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
||
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||
return send_json(sock, 200, js);
|
||
}
|
||
|
||
/* POST /api/power/policy:body {"item":"profile|low-pct|crit-pct|bri-cap|scr-cap","value":<枚举|数字>}。
|
||
* 经 MSG_PM_POLICY_SET 写 settings.conf power.* 键(powermgr 只写键不直接动系统)。
|
||
* 服务端对非法值回 200 + {"kind":"policy","ok":false,"why":..};权限不足回 MSG_ERR(MEOW_ERR_PERM)。 */
|
||
static int api_power_policy_set(int sock, const char *body, size_t body_len)
|
||
{
|
||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(body, (int)body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
|
||
char item[20] = "", val[40] = "";
|
||
if (ji && ji->type == J_STR && ji->str) snprintf(item, sizeof item, "%s", ji->str);
|
||
if (jv && jv->type == J_STR && jv->str) snprintf(val, sizeof val, "%s", jv->str);
|
||
else if (jv && jv->type == J_NUM) snprintf(val, sizeof val, "%lld", jv->inum);
|
||
jv_free(root);
|
||
if (!item[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 item(profile/low-pct/crit-pct/bri-cap/scr-cap)");
|
||
if (!val[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 value");
|
||
|
||
m_pm_policy_set_t s; memset(&s, 0, sizeof s);
|
||
snprintf(s.item, sizeof s.item, "%.15s", item); /* item[16] 留一个 NUL;防越界截断告警 */
|
||
snprintf(s.val, sizeof s.val, "%.31s", val); /* val[32] 同理 */
|
||
char js[MEOW_IPC_MAX];
|
||
uint16_t rt = 0;
|
||
int rc = ipc_pm_send(MSG_PM_POLICY_SET, &s, sizeof s, &rt, js, sizeof js);
|
||
if (rc <= 0) {
|
||
if (rt == MSG_ERR) /* 权限不足:powermgr 拒绝(面板 root 仅当被白名单才放行;如实回传) */
|
||
return send_err(sock, 403, "FORBIDDEN", rc == MEOW_ERR_PERM ? "设备身份不允许写电源策略" : "电源策略写入被拒绝");
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
||
}
|
||
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||
/* 服务端对非法值回 ok:false(HTTP 层 200,code 仍 OK);按其 why 映射 422。 */
|
||
if (strstr(js, "\"ok\":false")) {
|
||
char why[96] = "";
|
||
jv_t *jr = jv_parse(js, (int)strlen(js));
|
||
if (jr) { jv_t *w = jv_obj_get(jr, "why"); if (w && w->type == J_STR && w->str) snprintf(why, sizeof why, "%s", w->str); jv_free(jr); }
|
||
return send_err(sock, 422, "UNSUPPORTED", why[0] ? why : "非法电源策略值");
|
||
}
|
||
return send_json(sock, 200, js);
|
||
}
|
||
|
||
/* GET /api/power/history → MSG_PM_HISTORY 采样环(30s 环形 4h,不落盘)。
|
||
* GET /api/power/events → MSG_PM_EVENTS 异常耗电/充放电/亮灭屏/背光跳变事件(内存环形)。
|
||
* 两者语义对齐 powermgr 期 2;count=0(默认)由服务端选合理上限,读级任意调用者。 */
|
||
static int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail)
|
||
{
|
||
uint32_t count = 0;
|
||
char js[MEOW_IPC_MAX];
|
||
uint16_t rt = 0;
|
||
int rc = ipc_pm_send(req_type, &count, sizeof count, &rt, js, sizeof js);
|
||
if (rc <= 0)
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", unavail);
|
||
if (rt != want_type || !utf8_valid_n(js, strlen(js)))
|
||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||
return send_json(sock, 200, js);
|
||
}
|
||
static int api_power_history(int sock)
|
||
{ return api_power_window(sock, MSG_PM_HISTORY, MSG_PM_HISTORY_RESULT, "电源历史采样不可用(powermgr 未运行)"); }
|
||
static int api_power_events(int sock)
|
||
{ return api_power_window(sock, MSG_PM_EVENTS, MSG_PM_EVENTS_RESULT, "电源事件不可用(powermgr 未运行)"); }
|
||
|
||
/* ---- am/meow_svcd 二进制解析(popen 薄壳,复用有界收割 run_cmd) ----
|
||
* env MEOW_AMS_BIN / MEOW_SVCD_BIN 可覆盖(宿主/qemu 测试用;设备默认 /usr/bin,回退卡上路径)。 */
|
||
static const char *ams_bin(void)
|
||
{
|
||
const char *e = getenv("MEOW_AMS_BIN");
|
||
if (e && e[0]) return e;
|
||
if (!access(AMS_BIN, X_OK)) return AMS_BIN;
|
||
return AMS_BIN_CARD;
|
||
}
|
||
static const char *svcd_bin(void)
|
||
{
|
||
const char *e = getenv("MEOW_SVCD_BIN");
|
||
if (e && e[0]) return e;
|
||
if (!access(SVCD_BIN, X_OK)) return SVCD_BIN;
|
||
return SVCD_BIN_CARD;
|
||
}
|
||
|
||
/* ---------------- 网络管理:meow_netmgrd IPC(MSG_NET_*,期2 观测层/DNS 代理) ----------------
|
||
* 收口于独立 socket /usr/data/meow_net.sock(--sock-net / env MEOW_NET_SOCK 覆盖,宿主测试用)。
|
||
* 读级(STATUS/CONNS/DNS status|stats)netmgrd 放行任意调用者;flush 写级——HTTP 层沿用面板
|
||
* 鉴权门禁(require_auth),IPC 层身份由 netmgrd 依 SO_PEERCRED 判定(面板路径非系统身份时
|
||
* netmgrd 会拒 flush,面板如实回传其错误码/文本,不掩盖)。
|
||
* netmgrd 不在/超时/应答异常 → 503 降级 JSON({"ok":false,...}),面板其它页不受影响。 */
|
||
static int net_ipc(uint16_t req_type, const void *req, uint32_t req_len,
|
||
uint16_t *ret_type, uint32_t *code, uint16_t *meta,
|
||
char *out, size_t cap)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_net, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return -1;
|
||
uint8_t buf[MEOW_IPC_MAX];
|
||
uint16_t type = 0; uint32_t rid = 0;
|
||
int rlen = -1;
|
||
if (send_frame(fd, req_type, 0x1020, req, req_len) == 0)
|
||
rlen = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
close(fd);
|
||
if (rlen <= 0) return -1;
|
||
if (ret_type) *ret_type = type;
|
||
/* m_net_*_result 统一布局:code(u32)|len(u16)|meta/pad(u16)|content[len],content 偏移 8 */
|
||
uint32_t c = 0; uint16_t ln = 0, mv = 0;
|
||
memcpy(&c, buf, 4); memcpy(&ln, buf + 4, 2); memcpy(&mv, buf + 6, 2);
|
||
if (code) *code = c;
|
||
if (meta) *meta = mv;
|
||
if ((size_t)ln >= cap) ln = (uint16_t)(cap - 1);
|
||
if ((size_t)(8 + ln) < sizeof buf) buf[8 + ln] = 0;
|
||
memcpy(out, buf + 8, ln);
|
||
out[ln] = 0;
|
||
return (int)ln;
|
||
}
|
||
|
||
/* GET /api/net/status → 透传 0xE0 概览 JSON(接口表 + WiFi/BT 摘要 + 接口速率)。 */
|
||
static int api_net_status(int sock)
|
||
{
|
||
uint16_t t; uint32_t code; char json[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &t, &code, NULL, json, sizeof json);
|
||
if (n <= 0 || t != MSG_NET_STATUS_RESULT || code != MEOW_OK || !json[0])
|
||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||
if (!utf8_valid_n(json, strlen(json))) /* 上游坏字节(含 ssid)→ 不返回,降级 */
|
||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务应答包含非法字节");
|
||
return send_json(sock, 200, json);
|
||
}
|
||
|
||
/* 连接表一行:entity|lip:lport|rip:rport|state|age_s|domain(netmgrd build_conns_text)。
|
||
* 把每条构造成 `{...}` 对象、逗号分隔(不含外层 [])写入 sbuf,返回条数。 */
|
||
static int net_conns_parse_body(sbuf_t *s, const char *txt)
|
||
{
|
||
const char *p = txt;
|
||
int first = 1, n = 0;
|
||
while (p && *p) {
|
||
const char *nl = strchr(p, '\n');
|
||
size_t ll = nl ? (size_t)(nl - p) : strlen(p);
|
||
if (ll == 0) { p = nl ? nl + 1 : p + 1; continue; }
|
||
char line[384];
|
||
if (ll >= sizeof line) ll = sizeof line - 1;
|
||
memcpy(line, p, ll); line[ll] = 0;
|
||
char *f[6]; int fn = 0; char *cur = line;
|
||
while (fn < 6) {
|
||
f[fn++] = cur;
|
||
char *bar = strchr(cur, '|');
|
||
if (!bar) break;
|
||
*bar = 0; cur = bar + 1;
|
||
}
|
||
if (fn >= 6) {
|
||
if (!first) sA(s, ",");
|
||
first = 0;
|
||
sA(s, "{\"entity\":"); sA_str(s, f[0]);
|
||
sA(s, ",\"local\":"); sA_str(s, f[1]);
|
||
sA(s, ",\"remote\":"); sA_str(s, f[2]);
|
||
sA(s, ",\"state\":"); sA_str(s, f[3]);
|
||
sA(s, ",\"age\":%lld,\"domain\":", atoll(f[4]));
|
||
sA_str(s, strcmp(f[5], "-") == 0 ? "" : f[5]);
|
||
sA(s, "}");
|
||
n++;
|
||
}
|
||
p = nl ? nl + 1 : p + ll;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* GET /api/net/conns?entity=<可选> → 0xE8 → 连接表 JSON(可实体前缀过滤)。 */
|
||
static int api_net_conns(int sock, const char *query)
|
||
{
|
||
m_net_conns_req_t q; memset(&q, 0, sizeof q);
|
||
char ent[128];
|
||
if (qparam(query, "entity", ent, sizeof ent))
|
||
strncpy(q.entity, ent, sizeof q.entity - 1);
|
||
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_CONNS_REQ, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
|
||
if (n <= 0 || t != MSG_NET_CONNS_RESULT || code != MEOW_OK)
|
||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||
sbuf_t a = {0};
|
||
int count = net_conns_parse_body(&a, txt);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"truncated\":%s,\"count\":%d,\"conns\":[", meta ? "true" : "false", count);
|
||
if (a.n) sA_append(&s, a.b, a.n);
|
||
sA(&s, "]}");
|
||
sbuf_free(&a);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* DNS 代理统计对象 JSON 到 s(即 `{...}` 本体,不含外层 "dns" 键)。
|
||
* 返回 0=成功;-1=传输不可达(netmgrd 不在);-2=code!=OK(DNS 代理不可用)。
|
||
* 供 /api/net/dns 单发与 /api/overview 的 net 块复用(首页严格单请求)。 */
|
||
static int net_dns_obj_fill(sbuf_t *s)
|
||
{
|
||
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
|
||
strcpy(q.cmd, "status");
|
||
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
|
||
if (n <= 0 || t != MSG_NET_DNS_RESULT) return -1;
|
||
if (code != MEOW_OK) return -2;
|
||
/* txt: `dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d` */
|
||
char mode[32] = "", upstream[64] = "";
|
||
int enabled = 0, ch = 0, ct = 0, rmod = 0;
|
||
unsigned long long qq = 0, deny = 0, first = 0;
|
||
(void)sscanf(txt, "dns mode=%31s enabled=%d upstream=%63s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d",
|
||
mode, &enabled, upstream, &ch, &ct, &qq, &deny, &first, &rmod);
|
||
sA(s, "{\"mode\":");
|
||
sA_str(s, mode);
|
||
sA(s, ",\"enabled\":%s,\"upstream\":", enabled ? "true" : "false");
|
||
sA_str(s, upstream);
|
||
sA(s, ",\"cache_hits\":%d,\"cache_total\":%d,\"queries\":%llu,\"denies\":%llu,\"first_seen\":%llu,\"resolv_modified\":%s}",
|
||
ch, ct, qq, deny, first, rmod ? "true" : "false");
|
||
return 0;
|
||
}
|
||
|
||
/* GET /api/net/dns → 0xEC status(netmgrd 对 status/stats 同输出)→ DNS 代理统计 JSON。 */
|
||
static int api_net_dns(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
int rc = net_dns_obj_fill(&s);
|
||
if (rc == -1) { sbuf_free(&s); return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); }
|
||
if (rc == -2) { sbuf_free(&s); return send_err(sock, 503, "NET_NODEV", "DNS 代理不可用"); }
|
||
int out = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return out;
|
||
}
|
||
|
||
/* overview 用的 net 块:netmgrd 状态 JSON + 本面板 DNS 统计(并入 "dns" 子键),首页可严格单请求。
|
||
* 返回 0=成功(已追加 `,"net":{...}`);-1=netmgrd 状态不可用(调用方回 available:false,绝不假数据)。 */
|
||
static int net_block_fill(sbuf_t *s)
|
||
{
|
||
uint16_t nt; uint32_t ncode; char njson[MEOW_IPC_MAX];
|
||
int nn = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &nt, &ncode, NULL, njson, sizeof njson);
|
||
if (nn <= 0 || nt != MSG_NET_STATUS_RESULT || ncode != MEOW_OK || !njson[0]) return -1;
|
||
/* 状态 JSON 为对象:剥外层大括号,拼入 DNS 子键(DNS 不可用则省略该键,仍回 net 状态,不谎报)。 */
|
||
size_t l = strlen(njson);
|
||
while (l > 0 && (njson[l - 1] == ' ' || njson[l - 1] == '\t' || njson[l - 1] == '\n' || njson[l - 1] == '\r')) njson[--l] = 0;
|
||
if (l < 2 || njson[0] != '{' || njson[l - 1] != '}') return -1;
|
||
if (!utf8_valid_n(njson, l)) return -1; /* 任一上游(netmgrd 含 ssid)产出非法 UTF-8 → 整块降级,不拖垮整页 */
|
||
njson[l - 1] = 0; /* 去尾部 } */
|
||
sbuf_t dns = {0};
|
||
int drc = net_dns_obj_fill(&dns);
|
||
sA(s, ",\"net\":{");
|
||
sA(s, "%s", njson + 1); /* 去首 { 的状态内层 */
|
||
if (drc == 0 && dns.b) sA(s, ",\"dns\":%s", dns.b);
|
||
sA(s, "}"); /* net 块只开了一个 {,一个 } 收尾(批2 曾写 }} 多一括号,浏览器 Extra data 破页 2026-09-02) */
|
||
sbuf_free(&dns);
|
||
return 0;
|
||
}
|
||
|
||
/* POST /api/net/dns/flush → 0xEC flush(写级:HTTP 层走 require_auth;IPC 层由 netmgrd 依身份判定)。 */
|
||
static int api_net_dns_flush(int sock)
|
||
{
|
||
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
|
||
strcpy(q.cmd, "flush");
|
||
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
|
||
if (n <= 0 || t != MSG_NET_DNS_RESULT)
|
||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||
if (code == (uint32_t)MEOW_ERR_PERM)
|
||
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "DNS flush 需系统身份");
|
||
if (code != MEOW_OK)
|
||
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS flush 失败");
|
||
/* txt: `dns cache flushed (N entries)` */
|
||
int entries = 0;
|
||
(void)sscanf(txt, "dns cache flushed (%d entries)", &entries);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"flushed\":true,\"entries\":%d}", entries);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
#define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限(内存约束:不整读 64MB 滚动文件) */
|
||
|
||
/* GET /api/net/log → 读 TF MeowOS/logs/ 最新的 net_<bootts>.log 尾部(同日志页模式)。 */
|
||
static int api_net_log(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
DIR *d = opendir(LOGS_DIR);
|
||
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||
char best[512] = ""; long best_mt = -1;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (strncmp(e->d_name, "net_", 4) != 0) continue;
|
||
if (e->d_name[0] == '.') continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||
if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
|
||
}
|
||
closedir(d);
|
||
if (!best[0])
|
||
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
|
||
int fd = open(full, O_RDONLY);
|
||
struct stat st;
|
||
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
|
||
if (fd >= 0) close(fd);
|
||
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||
}
|
||
off_t off = st.st_size > NET_LOG_TAIL_MAX ? st.st_size - NET_LOG_TAIL_MAX : 0;
|
||
size_t buflen = (size_t)(st.st_size - off);
|
||
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
|
||
if (off) (void)lseek(fd, off, SEEK_SET);
|
||
char *buf = malloc(buflen + 1);
|
||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||
ssize_t n = read(fd, buf, buflen);
|
||
close(fd);
|
||
if (n <= 0) { free(buf); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"file\":");
|
||
sA_str(&s, best);
|
||
sA(&s, ",\"lines\":[");
|
||
const char *p = buf, *end = buf + n;
|
||
int linecnt = 0;
|
||
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
|
||
if (linecnt > 0 && end[-1] != '\n') linecnt++;
|
||
int maxlines = LOG_MAX_LINES;
|
||
int startrow = linecnt - maxlines;
|
||
if (startrow < 0) startrow = 0;
|
||
int truncated = (off > 0 || startrow > 0) ? 1 : 0;
|
||
int rowidx = 0, gotlines = 0;
|
||
p = buf;
|
||
while (p < end) {
|
||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||
const char *line_end = nl ? nl : end;
|
||
if (rowidx >= startrow) {
|
||
if (gotlines) sA(&s, ",");
|
||
/* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */
|
||
sA_str_n(&s, p, (size_t)(line_end - p));
|
||
gotlines++;
|
||
}
|
||
rowidx++;
|
||
if (!nl) break;
|
||
p = nl + 1;
|
||
}
|
||
free(buf);
|
||
sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- netmgrd 期2/期3:接口列表 + 管控策略(只读展示 / 写级经 require_auth) ----------------
|
||
* GET /api/net/ifaces → 0xE2(name 空=全部)接口数组 + 0xE0 状态里的 rssi(WiFi 信号)+ rfkill + dns。
|
||
* 上游任一源坏字节 → available:false(绝不伪造)。netmgrd 不在 → available:false(HTTP 200,不报错)。
|
||
* GET /api/net/policy → 0xEA SHOW(只读)→ 策略文本 parse 成 JSON(enforce=dns_only 如实透传)。
|
||
* POST /api/net/policy → 0xEA 写级(SET_NET/SET_DOMAIN/DEL);HTTP 层已过 require_auth,IPC 层由 netmgrd
|
||
* 依 SO_PEERCRED 判系统身份(面板精确登记 os.meow.panel),拒绝时如实回传错误码,绝不掩盖。 */
|
||
static int api_net_ifaces(int sock)
|
||
{
|
||
m_net_iface_req_t q; memset(&q, 0, sizeof q);
|
||
uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr);
|
||
if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0])
|
||
return send_json(sock, 200, "{\"available\":false}");
|
||
/* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */
|
||
if (!utf8_valid_n(arr, strlen(arr)))
|
||
return send_json(sock, 200, "{\"available\":false}");
|
||
|
||
/* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */
|
||
uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX];
|
||
int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj);
|
||
long long rssi = -9999; int got_rssi = 0;
|
||
sbuf_t rfkill = {0}, dns = {0};
|
||
if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) {
|
||
jv_t *jr = jv_parse(sj, (int)strlen(sj));
|
||
if (jr) {
|
||
jv_t *w = jv_obj_get(jr, "wifi");
|
||
jv_t *r = w ? jv_obj_get(w, "rssi") : NULL;
|
||
if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; }
|
||
jv_t *rf = jv_obj_get(jr, "rfkill");
|
||
if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf);
|
||
jv_t *bt = jv_obj_get(jr, "bt");
|
||
jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL;
|
||
if (dn && dn->type == J_ARR) jv_to_json(&dns, dn);
|
||
jv_free(jr);
|
||
}
|
||
}
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"available\":true,\"rssi\":");
|
||
if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null");
|
||
sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr);
|
||
sbuf_free(&rfkill); sbuf_free(&dns);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 策略 SHOW 文本 → JSON。netmgrd 首行:`# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(...)`;
|
||
* 其后每实体一行:`<entity> net=block domain=+h1,-h2` 或 `(allow)`(无规则)。 */
|
||
static void net_policy_parse(const char *txt, sbuf_t *s)
|
||
{
|
||
char lvl[24] = "", enf[24] = "dns_only";
|
||
const char *p = txt;
|
||
sA(s, "{\"available\":true,\"enforce\":");
|
||
/* 首行找 enforce=... */
|
||
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
|
||
const char *en = strstr(p, "enforce=");
|
||
if (en) {
|
||
en += 8;
|
||
int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; }
|
||
enf[i] = 0;
|
||
}
|
||
const char *lg = strstr(p, "log_level=");
|
||
if (lg) {
|
||
lg += 10;
|
||
int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; }
|
||
lvl[i] = 0;
|
||
}
|
||
sA_str(s, enf);
|
||
sA(s, ",\"log_level\":");
|
||
sA_str(s, lvl);
|
||
sA(s, ",\"rules\":[");
|
||
int first = 1;
|
||
const char *ln = txt;
|
||
while (ln && *ln) {
|
||
const char *nl = strchr(ln, '\n');
|
||
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||
if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; }
|
||
/* 跳注释行(以 # 开头)与空;数据行为 `<entity> net=... domain=...`(entity 含冒号无空格) */
|
||
const char *c = ln;
|
||
while (c < ln + ll && (*c == ' ' || *c == '\t')) c++;
|
||
int is_comment = (c < ln + ll && *c == '#');
|
||
int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0);
|
||
if (!is_comment && !is_none && c < ln + ll) {
|
||
/* entity = 首个空白符之前 */
|
||
char entity[64] = ""; size_t ei = 0;
|
||
const char *e = c;
|
||
while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; }
|
||
entity[ei] = 0;
|
||
/* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */
|
||
char net[8] = "allow"; int has_net = 0;
|
||
char allow_h[512] = "", deny_h[512] = "";
|
||
const char *k = e;
|
||
if (k < ln + ll) {
|
||
const char *seg = k;
|
||
while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++;
|
||
const char *nh = strstr(seg, "net=");
|
||
if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; }
|
||
const char *dh = strstr(seg, "domain=");
|
||
if (dh) {
|
||
dh += 7;
|
||
/* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */
|
||
size_t ao = 0, do2 = 0;
|
||
const char *h = dh;
|
||
while (*h && *h != ' ' && *h != '\n' && *h != '\r') {
|
||
if (*h == '+') {
|
||
h++;
|
||
if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ',';
|
||
while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++;
|
||
} else if (*h == '-') {
|
||
h++;
|
||
if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ',';
|
||
while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++;
|
||
} else h++;
|
||
if (*h == ',') h++;
|
||
}
|
||
allow_h[ao] = 0; deny_h[do2] = 0;
|
||
}
|
||
if (!first) sA(s, ",");
|
||
first = 0;
|
||
sA(s, "{\"entity\":"); sA_str(s, entity);
|
||
sA(s, ",\"net\":"); sA_str(s, has_net ? net : "");
|
||
sA(s, ",\"has_net\":%s", has_net ? "true" : "false");
|
||
sA(s, ",\"allow_hosts\":[");
|
||
{ const char *hp = allow_h; int hf = 1;
|
||
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
|
||
if (!hf) { sA(s, ","); }
|
||
hf = 0; sA_str_n(s, hp, hl);
|
||
hp = comma ? comma + 1 : hp + hl; } }
|
||
sA(s, "],\"deny_hosts\":[");
|
||
{ const char *hp = deny_h; int hf = 1;
|
||
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
|
||
if (!hf) { sA(s, ","); }
|
||
hf = 0; sA_str_n(s, hp, hl);
|
||
hp = comma ? comma + 1 : hp + hl; } }
|
||
sA(s, "]}");
|
||
}
|
||
}
|
||
ln = nl ? nl + 1 : ln + ll;
|
||
}
|
||
sA(s, "]}");
|
||
}
|
||
|
||
/* GET /api/net/policy(只读):0xEA SHOW → JSON(rules + enforce=dns_only 如实)。 */
|
||
static int api_net_policy_get(int sock)
|
||
{
|
||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||
q.op = NET_POLICY_SHOW;
|
||
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
|
||
if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK)
|
||
return send_json(sock, 200, "{\"available\":false}");
|
||
if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */
|
||
return send_json(sock, 200, "{\"available\":false}");
|
||
sbuf_t s = {0};
|
||
net_policy_parse(txt, &s);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* POST /api/net/policy:body {"op":"set_net|set_domain|del","entity":"<app:..|svc:..|proc:..|global>","arg":"<allow|block|+host|-host>"}。
|
||
* op 缺失默认 set_net;op 非法 → 422。arg 校验交给 netmgrd(防自锁硬拒在 netmgrd 服务端)。 */
|
||
static int api_net_policy_set(int sock, const char *body, size_t body_len)
|
||
{
|
||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(body, (int)body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
|
||
char op[16] = "set_net", entity[64] = "", arg[64] = "";
|
||
if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str);
|
||
if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str);
|
||
if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str);
|
||
jv_free(root);
|
||
if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:<pkg>/svc:<svc>/proc:<comm>)");
|
||
if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法");
|
||
int nop = 0;
|
||
if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET;
|
||
else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN;
|
||
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
|
||
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
|
||
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
|
||
|
||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||
q.op = (uint32_t)nop;
|
||
snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */
|
||
snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */
|
||
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
|
||
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
|
||
if (n <= 0 || t != MSG_NET_POLICY_RESULT)
|
||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||
if (code == (uint32_t)MEOW_ERR_PERM)
|
||
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?");
|
||
if (code == (uint32_t)MEOW_ERR_INVAL)
|
||
return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法");
|
||
if (code != MEOW_OK)
|
||
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"accepted\":true,\"message\":");
|
||
sA_str(&s, txt);
|
||
sA(&s, "}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- amsd / svcd 服务表 + 首页聚合(批1 只读) ---------------- */
|
||
|
||
/* 就地去掉 ANSI 转义序列(\033[...final);svcd status all 走管道时 CLI 会去色,此处双保险)。 */
|
||
static void panel_strip_ansi(char *s)
|
||
{
|
||
char *r = s, *w = s;
|
||
while (*r) {
|
||
if (*r == '\033' && r[1] == '[') {
|
||
r += 2;
|
||
while (*r && !(*r >= '@' && *r <= '~')) r++;
|
||
if (*r) r++;
|
||
} else { *w++ = *r++; }
|
||
}
|
||
*w = 0;
|
||
}
|
||
|
||
/* 切分成空白分隔 token(就地改 buf,返回个数;<maxtoks)。 */
|
||
static int txt_tokens(char *buf, char **toks, int maxtoks)
|
||
{
|
||
int n = 0;
|
||
char *p = buf;
|
||
while (*p && n < maxtoks) {
|
||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
||
if (!*p) break;
|
||
toks[n++] = p;
|
||
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
|
||
if (*p) *p++ = 0;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* join toks[from..to) 空格连接进 out(exec/name 可能含空格)。 */
|
||
static void join_tokens(char **toks, int from, int to, char *out, size_t cap)
|
||
{
|
||
size_t o = 0;
|
||
for (int i = from; i < to; i++) {
|
||
size_t need = strlen(toks[i]) + (o ? 1 : 0);
|
||
if (o + need >= cap) { out[cap - 1] = 0; return; }
|
||
if (o) out[o++] = ' ';
|
||
memcpy(out + o, toks[i], strlen(toks[i]));
|
||
o += strlen(toks[i]);
|
||
}
|
||
out[o] = 0;
|
||
}
|
||
|
||
static void ams_fg_fill(sbuf_t *s); /* 定义于下;ams_fill 内引用其前台摘要(前向声明) */
|
||
|
||
/* GET /api/ams:`am list`(注册表+状态)parse → App 表。amsd 不在 → available:false(绝无假数据)。 */
|
||
static void ams_fill(sbuf_t *s)
|
||
{
|
||
char out[16384];
|
||
char cmd[300];
|
||
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
||
panel_strip_ansi(out);
|
||
char *toks[256];
|
||
sbuf_t apps = {0};
|
||
int first = 1, count = 0;
|
||
char *ln = out;
|
||
while (*ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
if (strncmp(ln, "APP ", 4) == 0) {
|
||
int n = txt_tokens(ln, toks, 256);
|
||
if (n >= 9) {
|
||
/* 布局:APP <pkg> <name...> <state> <pid> <launches> <crashes> <last_kind> <last_code>
|
||
* tok[0]="APP" 前缀;自右向左固定:state=tok[n-6], pid=n-5, launches=n-4, crashes=n-3,
|
||
* last_kind=n-2, last_code=n-1;pkg=tok[1],name=join(tok[2..n-6))。 */
|
||
char nm[128];
|
||
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
||
if (!first) sA(&apps, ",");
|
||
first = 0; count++;
|
||
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
|
||
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
|
||
sA(&apps, ",\"state\":"); sA_str(&apps, toks[n-6]);
|
||
sA(&apps, ",\"pid\":%d", atoi(toks[n-5]));
|
||
sA(&apps, ",\"launches\":%d", atoi(toks[n-4]));
|
||
sA(&apps, ",\"crashes\":%d", atoi(toks[n-3]));
|
||
sA(&apps, ",\"last_kind\":"); sA_str(&apps, toks[n-2]);
|
||
sA(&apps, ",\"last_code\":%d}", atoi(toks[n-1]));
|
||
}
|
||
}
|
||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||
}
|
||
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : "");
|
||
sbuf_free(&apps);
|
||
ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */
|
||
sA(s, "}");
|
||
}
|
||
|
||
/* `am status` → `,"fg":{...},"running":[...]`(供 /api/ams 与 overview 的 ams 块复用;仅前台摘要,轻)。
|
||
* am status 行:`RUN %s %s %ld %llu %s\n` = pkg state pid dur activity(可含空格)。
|
||
* activity=join(tok[5..n));fg=首个 state=="fg"。amsd 不在 → fg:null,running:[](绝无假数据)。 */
|
||
static void ams_fg_fill(sbuf_t *s)
|
||
{
|
||
char out[16384];
|
||
char cmd[300];
|
||
snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin());
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; }
|
||
panel_strip_ansi(out);
|
||
char *toks[256];
|
||
char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0;
|
||
sbuf_t run = {0}; int first = 1;
|
||
char *ln = out;
|
||
while (*ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
if (strncmp(ln, "RUN ", 4) == 0) {
|
||
int n = txt_tokens(ln, toks, 256);
|
||
if (n >= 6) { /* RUN pkg state pid dur activity... */
|
||
char act[160];
|
||
join_tokens(toks, 5, n, act, sizeof act);
|
||
const char *st = toks[2];
|
||
if (!first) sA(&run, ",");
|
||
first = 0;
|
||
sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]);
|
||
sA(&run, ",\"state\":"); sA_str(&run, st);
|
||
sA(&run, ",\"pid\":%d", atoi(toks[3]));
|
||
sA(&run, ",\"activity\":"); sA_str(&run, act);
|
||
sA(&run, "}");
|
||
if (strcmp(st, "fg") == 0 && !fg_pkg[0]) {
|
||
snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]);
|
||
snprintf(fg_act, sizeof fg_act, "%s", act);
|
||
fg_pid = atoi(toks[3]);
|
||
}
|
||
}
|
||
}
|
||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||
}
|
||
sA(s, ",\"fg\":");
|
||
if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); }
|
||
else sA(s, "null");
|
||
sA(s, ",\"running\":[%s]", run.b ? run.b : "");
|
||
sbuf_free(&run);
|
||
}
|
||
|
||
static int api_ams(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
ams_fill(&s);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 包名合法性校验(ams history/start/kill 共用):反向域名命名字符集,拒 /、: 与空白。 */
|
||
static int ams_pkg_ok(const char *pkg)
|
||
{
|
||
if (!pkg || !pkg[0]) return 0;
|
||
if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0;
|
||
for (const char *p = pkg; *p; p++) {
|
||
unsigned char c = (unsigned char)*p;
|
||
if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0;
|
||
if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* GET /api/ams/:pkg → `am history <pkg>`(amsd 现成:incident 登记 + 崩溃根因 detail)。
|
||
* 输出行:`EVT <epoch> <up> <kind> <pkg> <detail...>`(detail 可含空格,为崩溃根因/事件文本)。
|
||
* 上游 text 含崩溃 detail,须 utf8 校验坏字节降级 available:false(绝不发非法 JSON)。
|
||
* 解析部分独立成 ams_history_parse(供 selftest 喂截断 detail 断言合法 JSON)。 */
|
||
static void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */
|
||
{
|
||
if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */
|
||
panel_strip_ansi(out);
|
||
char *toks[256];
|
||
sbuf_t ev = {0};
|
||
int first = 1, count = 0;
|
||
char *ln = out;
|
||
while (*ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
if (strncmp(ln, "EVT ", 4) == 0) {
|
||
int n = txt_tokens(ln, toks, 256);
|
||
if (n >= 5) { /* EVT epoch up kind pkg detail... */
|
||
char det[256];
|
||
join_tokens(toks, 5, n, det, sizeof det);
|
||
if (!first) sA(&ev, ",");
|
||
first = 0; count++;
|
||
sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2]));
|
||
sA_str(&ev, toks[3]);
|
||
sA(&ev, ",\"detail\":"); sA_str(&ev, det);
|
||
sA(&ev, "}");
|
||
}
|
||
}
|
||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||
}
|
||
sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg);
|
||
sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : "");
|
||
sbuf_free(&ev);
|
||
}
|
||
static void ams_history_fill(sbuf_t *s, const char *pkg)
|
||
{
|
||
char out[16384];
|
||
char cmd[320];
|
||
snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg);
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; }
|
||
ams_history_parse(out, s, pkg);
|
||
}
|
||
static int api_ams_history(int sock, const char *pkg)
|
||
{
|
||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
sbuf_t s = {0};
|
||
ams_history_fill(&s, pkg);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* POST /api/ams/:pkg/start → `am start <pkg>`(amsd→launcher MSG_AMS_START 拉起;launcher 离线回 ERR)。
|
||
* POST /api/ams/:pkg/kill → `am kill <pkg>`(SIGTERM 语义,前台需前端二次确认)。均 require_auth 之后。 */
|
||
static int api_ams_ctl(int sock, const char *pkg, int kill)
|
||
{
|
||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char out[256];
|
||
char cmd[320];
|
||
snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg);
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code == 0 && strstr(out, "OK ")) {
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start");
|
||
sA_str(&s, pkg);
|
||
sA(&s, ",\"message\":"); sA_str(&s, out);
|
||
sA(&s, "}");
|
||
int rc2 = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc2;
|
||
}
|
||
/* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */
|
||
const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败");
|
||
if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail);
|
||
if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail);
|
||
if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail);
|
||
if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达");
|
||
return send_err(sock, 500, "INTERNAL", detail);
|
||
}
|
||
|
||
/* GET /api/svcd:`meow_svcd status all` parse → 服务表(state/pid/restart 计数/exec/来源)。
|
||
* svcd 未运行(exit!=0)→ available:false(绝无假数据)。 */
|
||
static void svcd_fill(sbuf_t *s)
|
||
{
|
||
char out[16384];
|
||
char cmd[300];
|
||
snprintf(cmd, sizeof cmd, "%s status all 2>/dev/null", svcd_bin());
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0) { sA(s, "{\"available\":false}"); return; }
|
||
panel_strip_ansi(out);
|
||
char *toks[256];
|
||
sbuf_t svcs = {0};
|
||
int first = 1, count = 0;
|
||
char *ln = out;
|
||
while (*ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
int n = txt_tokens(ln, toks, 256);
|
||
/* 数据行:NAME STATE PID RESTART BACKOFF TRIGGER SRC EXIT EXIT_TIME EXEC...
|
||
* state=tok[1] 须为合法状态;exec=join(tok[9..n))。 */
|
||
if (n >= 10) {
|
||
const char *st = toks[1];
|
||
int is_known = (!strcmp(st,"running") || !strcmp(st,"stopped") ||
|
||
!strcmp(st,"stopping") || !strcmp(st,"disabled") || !strcmp(st,"failed"));
|
||
if (is_known) {
|
||
char exec[512];
|
||
join_tokens(toks, 9, n, exec, sizeof exec);
|
||
int restarts = 0;
|
||
{ const char *slash = strchr(toks[3], '/'); (void)slash; restarts = atoi(toks[3]); }
|
||
if (!first) sA(&svcs, ",");
|
||
first = 0; count++;
|
||
sA(&svcs, "{\"name\":"); sA_str(&svcs, toks[0]);
|
||
sA(&svcs, ",\"state\":"); sA_str(&svcs, toks[1]);
|
||
sA(&svcs, ",\"pid\":%d", atoi(toks[2]));
|
||
sA(&svcs, ",\"restart\":%d", restarts);
|
||
sA(&svcs, ",\"restart_str\":");
|
||
sA_str(&svcs, toks[3]);
|
||
sA(&svcs, ",\"backoff_ms\":%d", atoi(toks[4]));
|
||
sA(&svcs, ",\"trigger\":");
|
||
sA_str(&svcs, toks[5]);
|
||
sA(&svcs, ",\"src\":");
|
||
sA_str(&svcs, toks[6]);
|
||
sA(&svcs, ",\"exit\":");
|
||
sA_str(&svcs, toks[7]);
|
||
sA(&svcs, ",\"exit_time\":");
|
||
sA_str(&svcs, toks[8]);
|
||
sA(&svcs, ",\"exec\":");
|
||
sA_str(&svcs, exec);
|
||
sA(&svcs, "}");
|
||
}
|
||
}
|
||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||
}
|
||
sA(s, "{\"available\":true,\"count\":%d,\"services\":[%s]}", count, svcs.b ? svcs.b : "");
|
||
sbuf_free(&svcs);
|
||
}
|
||
|
||
static int api_svcd(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
svcd_fill(&s);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* GET /api/overview:首页生命线。**单线程后端只暴露聚合**(不放六个单测端点给首页轮询)。
|
||
* 后端顺序 fan-out perf/power/audio/net/amsd/svcd 六源合成一份 JSON;任一源失败 → 该子块 `available:false`,
|
||
* 绝不假数据(对齐 netmgrd 503 降级语义)。 */
|
||
static int api_overview(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"ts\":%lld,", (long long)time(NULL));
|
||
char pb[4096];
|
||
if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK && utf8_valid_n(pb, strlen(pb)))
|
||
sA(&s, "\"perf\":%s,", pb);
|
||
else sA(&s, "\"perf\":{\"available\":false},");
|
||
char pw[MEOW_IPC_MAX];
|
||
if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw)))
|
||
sA(&s, "\"power\":%s,", pw);
|
||
else sA(&s, "\"power\":{\"available\":false},");
|
||
/* 最近异常耗电/充放电/亮灭屏/背光跳变事件(摘要 top2 进 overview,完整列表走 /api/power/events)。 */
|
||
{
|
||
uint32_t ecnt = 2;
|
||
char ej[MEOW_IPC_MAX];
|
||
uint16_t ert = 0;
|
||
int ert_rc = ipc_pm_send(MSG_PM_EVENTS, &ecnt, sizeof ecnt, &ert, ej, sizeof ej);
|
||
if (ert_rc > 0 && ert == MSG_PM_EVENTS_RESULT && utf8_valid_n(ej, strlen(ej)))
|
||
sA(&s, "\"events\":%s,", ej);
|
||
else
|
||
sA(&s, "\"events\":{\"available\":false},");
|
||
}
|
||
sA(&s, "\"audio\":");
|
||
audio_fill(&s);
|
||
if (net_block_fill(&s) == 0)
|
||
; /* net 块已追加(状态 + 合并的 dns 子键) */
|
||
else
|
||
sA(&s, ",\"net\":{\"available\":false}");
|
||
sA(&s, ",\"ams\":");
|
||
ams_fill(&s);
|
||
sA(&s, ",\"svcd\":");
|
||
svcd_fill(&s);
|
||
sA(&s, "}");
|
||
/* 服务端自校验:发出前 jv_parse 自查,坏了留证+回 500(防「非 JSON 响应」破页,2026-09-02 间歇性事故)
|
||
* 加严:内嵌 NUL(strlen 截断浏览器收残体)/非法 UTF-8(浏览器 JSON.parse 拒收,宽松解析器放行)*/
|
||
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
|
||
size_t slen = s.b ? strlen(s.b) : 0;
|
||
int nul_inside = s.b && (slen != s.n); /* 内嵌 NUL:strlen < s.n */
|
||
int utf8_bad = s.b && !utf8_valid_n(s.b, s.n);
|
||
int unbalanced = s.b && !json_strict_balanced(s.b, s.n); /* 尾随垃圾/多括号(jv_parse 容忍,浏览器不容忍) */
|
||
if (s.b && (nul_inside || utf8_bad || unbalanced)) { chk = NULL; }
|
||
if (!chk && s.b) {
|
||
FILE *df = fopen("/tmp/overview_bad.json", "w");
|
||
if (df) { fwrite(s.b, 1, s.n, df); fclose(df); }
|
||
panel_log("overview: SELF-CHECK fail (nul=%d utf8_bad=%d unbalanced=%d n=%zu strlen=%zu), raw dumped /tmp/overview_bad.json", nul_inside, utf8_bad, unbalanced, s.n, slen);
|
||
sbuf_free(&s);
|
||
return send_err(sock, 500, "INTERNAL", "overview 聚合输出未通过 JSON 自校验(原始数据已留 /tmp/overview_bad.json)");
|
||
}
|
||
if (chk) jv_free(chk);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 路径安全 ---------------- */
|
||
static int tf_present(void)
|
||
{
|
||
if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */
|
||
FILE *f = fopen("/proc/mounts", "r");
|
||
if (!f) return 0;
|
||
char line[512];
|
||
int present = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strstr(line, TF_ROOT)) { present = 1; break; }
|
||
}
|
||
fclose(f);
|
||
return present;
|
||
}
|
||
|
||
/* 判断某个规范化路径是否在卡根白名单内(==TF_ROOT 或属于某个白名单一级子目录)。 */
|
||
static int path_in_whitelist(const char *abs)
|
||
{
|
||
size_t rl = strlen(TF_ROOT);
|
||
if (strncmp(abs, TF_ROOT, rl) != 0) return 0;
|
||
if (abs[rl] == 0) return 1; /* 根自身 */
|
||
if (abs[rl] != '/') return 0;
|
||
const char *rest = abs + rl + 1;
|
||
for (int i = 0; i < ALLOWED_COUNT; i++) {
|
||
size_t dl = strlen(g_allowed_dirs[i]);
|
||
if (strncmp(rest, g_allowed_dirs[i], dl) == 0) {
|
||
if (rest[dl] == 0 || rest[dl] == '/') return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int is_sensitive_name(const char *base)
|
||
{
|
||
for (int i = 0; i < SENSITIVE_COUNT; i++)
|
||
if (!strcmp(base, g_sensitive_names[i])) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* 规范化输入路径(去除 . / ..;绝对/相对容错)。
|
||
* 输入可能是绝对(`/usr/data/mnt/sd_0/MeowOS/x`,前端下载/删除等用)或相对 root(`/MeowOS/x`)。
|
||
* 输出(buf)为规范化后的绝对路径;若 escape 出卡根,返回 -1。 */
|
||
static int normalize_path(const char *in, char *buf, size_t buflen)
|
||
{
|
||
size_t rl = strlen(TF_ROOT);
|
||
if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */
|
||
char joined[2048];
|
||
if (in[0] == '/') {
|
||
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
|
||
snprintf(joined, sizeof joined, "%s", in);
|
||
} else {
|
||
/* 相对 root 的展示路径,如 /MeowOS/x → 拼到 root */
|
||
if (in[0] == '/' && in[1] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in + 1);
|
||
else if (in[0] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in);
|
||
else snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||
}
|
||
} else {
|
||
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||
}
|
||
/* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。
|
||
* 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */
|
||
char out[2048];
|
||
size_t n = 0;
|
||
out[0] = '/';
|
||
out[1] = 0;
|
||
n = 1;
|
||
char *p = joined;
|
||
while (*p) {
|
||
while (*p == '/') p++;
|
||
if (!*p) break;
|
||
char seg[256];
|
||
int k = 0;
|
||
while (*p && *p != '/') { if (k < 255) seg[k++] = *p; p++; }
|
||
seg[k] = 0;
|
||
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
|
||
if (!strcmp(seg, "..")) {
|
||
/* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */
|
||
if (n <= 1) return -1;
|
||
while (n > 1 && out[n - 1] != '/') n--;
|
||
if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */
|
||
out[n] = 0;
|
||
continue;
|
||
}
|
||
if (k > 0) {
|
||
size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1;
|
||
if (need >= sizeof out) return -1;
|
||
if (n > 1) { out[n++] = '/'; out[n] = 0; }
|
||
memcpy(out + n, seg, (size_t)k);
|
||
n += (size_t)k;
|
||
out[n] = 0;
|
||
}
|
||
}
|
||
/* 空/根(n==1,仅 "/")→ 卡根 */
|
||
if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT);
|
||
size_t ol = strlen(out);
|
||
if (ol >= buflen) return -1;
|
||
memcpy(buf, out, ol + 1); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
|
||
return 0;
|
||
}
|
||
|
||
/* 展示 rel(相对 root):如 /MeowOS/x。 */
|
||
static void make_rel(const char *abs, char *buf, size_t buflen)
|
||
{
|
||
size_t rl = strlen(TF_ROOT);
|
||
if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; }
|
||
const char *rest = abs + rl;
|
||
if (*rest == 0) snprintf(buf, buflen, "/");
|
||
else snprintf(buf, buflen, "%s", rest);
|
||
}
|
||
|
||
/* 安全地打开一个可读文件(白名单内 + 非敏感名)。失败返回 -1。 */
|
||
static int open_safe_for_read(const char *abs, int follow_rel)
|
||
{
|
||
(void)follow_rel;
|
||
if (!path_in_whitelist(abs)) return -1;
|
||
const char *base = strrchr(abs, '/');
|
||
base = base ? base + 1 : abs;
|
||
if (is_sensitive_name(base)) return -1;
|
||
struct stat st;
|
||
if (stat(abs, &st) < 0) return -1;
|
||
if (!S_ISREG(st.st_mode)) return -1;
|
||
return open(abs, O_RDONLY);
|
||
}
|
||
|
||
/* ---------------- 文件浏览 ---------------- */
|
||
static void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime)
|
||
{
|
||
sA(s, "{\"name\":");
|
||
sA_str(s, name);
|
||
sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}",
|
||
isdir ? "dir" : "file", size, mtime);
|
||
}
|
||
|
||
struct fent { char name[256]; int isdir; long long size, mtime; };
|
||
|
||
/* 自然字符串比较:数字段按数值比较(track2 < track10),ASCII 字母大小写不敏感
|
||
* (同字母时原始字节作 tie-break),其余按**无符号字节序**(UTF-8 中文等直接按字节序,
|
||
* 设备无 locale,避免 strcasecmp 对高位字节的 locale 依赖导致乱序)。
|
||
* 实现:逐字节;遇到连续数字则各取整段,比「有效位数→数值→原始长度」;否则比字节。 */
|
||
static int nat_strcmp(const char *a, const char *b)
|
||
{
|
||
const unsigned char *pa = (const unsigned char *)a;
|
||
const unsigned char *pb = (const unsigned char *)b;
|
||
while (*pa && *pb) {
|
||
unsigned char ca = *pa, cb = *pb;
|
||
if (ca >= '0' && ca <= '9' && cb >= '0' && cb <= '9') {
|
||
const unsigned char *na = pa, *nb = pb;
|
||
while (*na == '0') na++;
|
||
while (*nb == '0') nb++;
|
||
const unsigned char *da = na, *db = nb;
|
||
while (*da >= '0' && *da <= '9') da++;
|
||
while (*db >= '0' && *db <= '9') db++;
|
||
int la = (int)(da - na), lb = (int)(db - nb); /* 有效数字位数(去前导0) */
|
||
if (la != lb) return la - lb; /* 位数小者数小 */
|
||
int c = memcmp(na, nb, (size_t)la);
|
||
if (c) return c;
|
||
int ta = (int)(da - pa), tb = (int)(db - pb); /* 原始数字串长(含前导0) */
|
||
if (ta != tb) return ta - tb;
|
||
pa = da; pb = db;
|
||
continue; /* 数字段完全相同,继续 */
|
||
}
|
||
unsigned char la = (ca >= 'A' && ca <= 'Z') ? (unsigned char)(ca + 32) : ca;
|
||
unsigned char lbc = (cb >= 'A' && cb <= 'Z') ? (unsigned char)(cb + 32) : cb;
|
||
if (la != lbc) return (int)la - (int)lbc;
|
||
if (ca != cb) return (int)ca - (int)cb; /* 大小写 tie-break:大写在前 */
|
||
pa++; pb++;
|
||
}
|
||
return (int)*pa - (int)*pb;
|
||
}
|
||
|
||
/* 目录优先 + 名称自然排序(数字感知 + 无符号字节序) */
|
||
static int fent_cmp(const void *a, const void *b)
|
||
{
|
||
const struct fent *fa = a, *fb = b;
|
||
if (fa->isdir != fb->isdir) return fb->isdir - fa->isdir;
|
||
return nat_strcmp(fa->name, fb->name);
|
||
}
|
||
|
||
static int files_list(int sock, const char *relpath)
|
||
{
|
||
char abs[1024], rel[1024];
|
||
if (normalize_path(relpath, abs, sizeof abs) < 0)
|
||
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
if (!path_in_whitelist(abs))
|
||
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
struct stat st;
|
||
if (stat(abs, &st) < 0)
|
||
return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||
if (!S_ISDIR(st.st_mode))
|
||
return send_err(sock, 404, "NOT_FOUND", "非目录");
|
||
DIR *d = opendir(abs);
|
||
if (!d)
|
||
return send_err(sock, 500, "INTERNAL", "读目录失败");
|
||
make_rel(abs, rel, sizeof rel);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"root\":");
|
||
sA_str(&s, TF_ROOT);
|
||
sA(&s, ",\"path\":");
|
||
sA_str(&s, abs);
|
||
sA(&s, ",\"rel\":");
|
||
sA_str(&s, rel);
|
||
sA(&s, ",\"allowed\":[");
|
||
for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); }
|
||
sA(&s, "],\"items\":[");
|
||
/* M2.78:排序后输出(原 readdir 原始顺序混杂难用)——目录优先,同组按名称
|
||
* 不区分大小写排序;条目上限 1024(与 files App 一致,防巨目录爆内存)。 */
|
||
int cap = 1024, n = 0;
|
||
struct fent *ents = malloc((size_t)cap * sizeof *ents);
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1400];
|
||
snprintf(full, sizeof full, "%s/%s", abs, e->d_name);
|
||
struct stat fs;
|
||
if (stat(full, &fs) < 0) continue;
|
||
if (n >= cap) break;
|
||
struct fent *en = &ents[n++];
|
||
snprintf(en->name, sizeof en->name, "%s", e->d_name);
|
||
en->isdir = S_ISDIR(fs.st_mode);
|
||
en->size = en->isdir ? 0 : (long long)fs.st_size;
|
||
en->mtime = (long long)fs.st_mtime;
|
||
}
|
||
closedir(d);
|
||
qsort(ents, (size_t)n, sizeof *ents, fent_cmp);
|
||
for (int i = 0; i < n; i++) {
|
||
if (i) sA(&s, ",");
|
||
fmt_json_item(&s, ents[i].name, ents[i].isdir, ents[i].size, ents[i].mtime);
|
||
}
|
||
free(ents);
|
||
sA(&s, "]}");
|
||
return send_json(sock, 200, s.b ? s.b : "");
|
||
}
|
||
|
||
/* ---------------- HTTP 响应(JSON 信封) ---------------- */
|
||
/* send_json 前置声明 */
|
||
static int send_json(int sock, int status, const char *data);
|
||
|
||
/* 读文本文件(预览上限 TEXT_PREVIEW_MAX,超出截断返回 truncated):
|
||
* 返回 malloc 内容(NUL 结尾);二进制检测 -2;打不开 -1。size=文件真实字节数(非截断后)。 */
|
||
static char *read_text_file_limited(const char *abs, long long *size, int *truncated)
|
||
{
|
||
int fd = open_safe_for_read(abs, 0);
|
||
if (fd < 0) return NULL;
|
||
struct stat st;
|
||
if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; }
|
||
long long total = (long long)st.st_size;
|
||
size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total;
|
||
char *buf = malloc(cap + 1);
|
||
if (!buf) { close(fd); return NULL; }
|
||
ssize_t n = read(fd, buf, cap);
|
||
close(fd);
|
||
if (n < 0) { free(buf); return NULL; }
|
||
buf[n] = 0;
|
||
/* 二进制检测:含 NUL 或大量控制字符 */
|
||
int ctrl = 0;
|
||
for (ssize_t i = 0; i < n; i++) {
|
||
unsigned char c = (unsigned char)buf[i];
|
||
if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */
|
||
if (c < 0x09) ctrl++;
|
||
}
|
||
if (ctrl > n / 10) { free(buf); return (char *)-2; }
|
||
if (size) *size = total;
|
||
if (truncated) *truncated = (total > (long long)cap) ? 1 : 0;
|
||
return buf;
|
||
}
|
||
|
||
/* ---------------- 按键注入(经 /dev/input/event* 写 EV_KEY;uinput_tap inject 同机制) ----------------
|
||
* 设备映射(launcher meow_launcher_internal.h):
|
||
* /dev/input/event0 = md-gpio-keys: KEY_POWER(116), KEY_PREVIOUSSONG(165)
|
||
* /dev/input/event2 = jz adc keyboard: KEY_VOLUMEUP(115), KEY_VOLUMEDOWN(114), KEY_NEXTSONG(163), KEY_PLAYPAUSE(164)
|
||
* 注入 = open(O_WRONLY) 写 EV_KEY press/release + SYN_REPORT(evdev 支持 write 注入,uinput_tap.c inject 先例)。
|
||
* 红线(APPS.md 物理键 / 任务约束):只注入音量/电源/播放/上下曲,**禁止映射 返回(158)/主页(172)/确认/exit**。
|
||
*/
|
||
#define PWR_KEYDEV "/dev/input/event0"
|
||
#define ADC_KEYDEV "/dev/input/event2"
|
||
|
||
static int keycode_to_device(int code, const char **dev)
|
||
{
|
||
switch (code) {
|
||
case KEY_POWER: *dev = PWR_KEYDEV; return KEY_POWER;
|
||
case KEY_PREVIOUSSONG: *dev = PWR_KEYDEV; return KEY_PREVIOUSSONG;
|
||
case KEY_VOLUMEUP: *dev = ADC_KEYDEV; return KEY_VOLUMEUP;
|
||
case KEY_VOLUMEDOWN: *dev = ADC_KEYDEV; return KEY_VOLUMEDOWN;
|
||
case KEY_NEXTSONG: *dev = ADC_KEYDEV; return KEY_NEXTSONG;
|
||
case KEY_PLAYPAUSE: *dev = ADC_KEYDEV; return KEY_PLAYPAUSE;
|
||
default: return -1;
|
||
}
|
||
}
|
||
|
||
static int inject_key_event(const char *dev, int code, int down)
|
||
{
|
||
int fd = open(dev, O_WRONLY | O_NONBLOCK);
|
||
if (fd < 0) return -1;
|
||
struct input_event ev;
|
||
memset(&ev, 0, sizeof ev);
|
||
ev.type = EV_KEY;
|
||
ev.code = code;
|
||
ev.value = down ? 1 : 0;
|
||
int ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev);
|
||
if (ok) {
|
||
memset(&ev, 0, sizeof ev);
|
||
ev.type = EV_SYN;
|
||
ev.code = SYN_REPORT;
|
||
ev.value = 0;
|
||
ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev);
|
||
}
|
||
close(fd);
|
||
return ok ? 0 : -1;
|
||
}
|
||
|
||
/* 注入 tap(按下+抬起)或 long(按住 ms)。返回 0=成功。 */
|
||
static int inject_key_release(int code, int press_ms)
|
||
{
|
||
const char *dev = NULL;
|
||
int kc = keycode_to_device(code, &dev);
|
||
if (kc < 0) return -1;
|
||
int release_ms = press_ms > 0 ? press_ms : 0;
|
||
if (inject_key_event(dev, code, 1) < 0) return -1;
|
||
if (release_ms > 0) usleep((useconds_t)release_ms * 1000);
|
||
else usleep(80 * 1000);
|
||
if (inject_key_event(dev, code, 0) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */
|
||
static int read_small_file(const char *path, char *buf, int cap)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
int n = 0;
|
||
while (n < cap - 1) {
|
||
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
|
||
if (r <= 0) break;
|
||
n += (int)r;
|
||
}
|
||
close(fd);
|
||
buf[n] = 0;
|
||
return n;
|
||
}
|
||
|
||
/* 读某应用目录的 app.json(用 jv 解析提取字段,供扫描/详情)。 */
|
||
static int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail)
|
||
{
|
||
char pj[1024];
|
||
snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name);
|
||
char buf[8192];
|
||
if (read_small_file(pj, buf, sizeof buf) <= 0) {
|
||
/* 解析失败项:invalid */
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, name);
|
||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||
}
|
||
return -1;
|
||
}
|
||
jv_t *root = jv_parse(buf, (int)strlen(buf));
|
||
if (!root) {
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, name);
|
||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||
}
|
||
return -1;
|
||
}
|
||
jv_t *jpkg = jv_obj_get(root, "package");
|
||
jv_t *jname = jv_obj_get(root, "name");
|
||
jv_t *jver = jv_obj_get(root, "version");
|
||
jv_t *jexec = jv_obj_get(root, "exec");
|
||
jv_t *jfw = jv_obj_get(root, "framework");
|
||
jv_t *jim = jv_obj_get(root, "immersive");
|
||
jv_t *jfull = jv_obj_get(root, "fullscreen");
|
||
const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name;
|
||
const char *nm = (jname && jname->type == J_STR) ? jname->str : "";
|
||
const char *ver = (jver && jver->type == J_STR) ? jver->str : "";
|
||
const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : "";
|
||
/* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */
|
||
long long dsize = 0;
|
||
snprintf(pj, sizeof pj, "%s/%s", appdir, name);
|
||
DIR *d = opendir(pj);
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) == 0) dsize += (long long)st.st_size;
|
||
}
|
||
closedir(d);
|
||
}
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, pkg);
|
||
sA(out, ",\"name\":");
|
||
sA_str(out, nm[0] ? nm : NULL);
|
||
sA(out, ",\"version\":");
|
||
sA_str(out, ver[0] ? ver : NULL);
|
||
sA(out, ",\"exec\":");
|
||
sA_str(out, exec);
|
||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}",
|
||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL));
|
||
} else {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, pkg);
|
||
sA(out, ",\"name\":");
|
||
sA_str(out, nm[0] ? nm : NULL);
|
||
sA(out, ",\"version\":");
|
||
sA_str(out, ver[0] ? ver : NULL);
|
||
sA(out, ",\"exec\":");
|
||
sA_str(out, exec);
|
||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":",
|
||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize);
|
||
sA_str(out, pj);
|
||
sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}");
|
||
}
|
||
jv_free(root);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- HTTP 请求读取 ---------------- */
|
||
typedef struct {
|
||
char method[8];
|
||
char path[512]; /* 解码后的路径(不含 query) */
|
||
char query[1024]; /* 原始 query(key=value&…,未解码;取值时用 qparam) */
|
||
char *body;
|
||
size_t body_len;
|
||
char content_type[128]; /* 请求 Content-Type(若非空) */
|
||
char authorization[256]; /* Authorization 头(Bearer <token>) */
|
||
char x_meow_token[256]; /* X-MeowToken 头 */
|
||
char cookie[512]; /* Cookie 头(meow_token=<token> 等) */
|
||
} req_t;
|
||
|
||
static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
|
||
static int multipart_extract_file(const req_t *r, const char *outpath); /* 定义在文件后部;字体导入用(写 multipart file 到临时文件) */
|
||
|
||
static int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
|
||
static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return c - 'A' + 10; }
|
||
|
||
/* URL 解码到 buf。 */
|
||
static void url_decode(const char *in, char *buf, int buflen)
|
||
{
|
||
int o = 0;
|
||
for (int i = 0; in[i] && o < buflen - 1; i++) {
|
||
if (in[i] == '%' && is_hex(in[i + 1]) && is_hex(in[i + 2])) {
|
||
buf[o++] = (char)((hexval(in[i + 1]) << 4) | hexval(in[i + 2]));
|
||
i += 2;
|
||
} else if (in[i] == '+') buf[o++] = ' ';
|
||
else buf[o++] = in[i];
|
||
}
|
||
buf[o] = 0;
|
||
}
|
||
|
||
/* 从 query(key=value&…)取第一个值(url 解码后写入 out),未找到返回 0。 */
|
||
static int qparam(const char *query, const char *key, char *out, int outcap)
|
||
{
|
||
size_t kl = strlen(key);
|
||
const char *p = query;
|
||
while (p && *p) {
|
||
const char *amp = strchr(p, '&');
|
||
size_t seg = amp ? (size_t)(amp - p) : strlen(p);
|
||
if (seg >= kl + 1 && strncmp(p, key, kl) == 0 && p[kl] == '=') {
|
||
char tmp[1024];
|
||
size_t vlen = seg - kl - 1;
|
||
if (vlen >= sizeof tmp) vlen = sizeof tmp - 1;
|
||
memcpy(tmp, p + kl + 1, vlen);
|
||
tmp[vlen] = 0;
|
||
url_decode(tmp, out, outcap);
|
||
return 1;
|
||
}
|
||
if (!amp) break;
|
||
p = amp + 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 从请求 path 中取动态段(如 /api/settings/:key)。返回是否匹配。 */
|
||
static int path_segment(const char *prefix, const char *full, char *out, int outcap)
|
||
{
|
||
size_t pl = strlen(prefix);
|
||
if (strncmp(full, prefix, pl) != 0) return 0;
|
||
const char *rest = full + pl;
|
||
if (*rest == '/') rest++;
|
||
if (!*rest) return 0;
|
||
size_t l = strlen(rest);
|
||
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
|
||
memcpy(out, rest, l);
|
||
out[l] = 0;
|
||
/* 去掉后续 / 段(取第一段) */
|
||
char *slash = strchr(out, '/');
|
||
if (slash) *slash = 0;
|
||
return 1;
|
||
}
|
||
|
||
/* 从 "GET /path?query HTTP/1.1" 的 path 部分切出 path 与 query。 */
|
||
static void split_path_query(const char *src, char *path, int plc, char *query, int qlc)
|
||
{
|
||
const char *q = strchr(src, '?');
|
||
if (q) {
|
||
size_t pl = (size_t)(q - src);
|
||
if (pl >= (size_t)plc) pl = (size_t)plc - 1;
|
||
memcpy(path, src, pl);
|
||
path[pl] = 0;
|
||
snprintf(query, qlc, "%s", q + 1);
|
||
} else {
|
||
snprintf(path, plc, "%s", src);
|
||
query[0] = 0;
|
||
}
|
||
}
|
||
|
||
/* 在 buf[0..n) 中找首个 "\r\n\r\n",返回其起始下标;找不到返回 -1。 */
|
||
static ptrdiff_t find_header_end(const char *buf, size_t n)
|
||
{
|
||
if (n < 4) return -1;
|
||
for (size_t i = 0; i + 3 < n; i++) {
|
||
if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n')
|
||
return (ptrdiff_t)i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* 读请求:先读头(到 \r\n\r\n),解析 Content-Length/Content-Type,再精确读 body。
|
||
* 处理「头+body 同一 TCP 段到达」(小 POST 常见):单次 read 可能把 body 一并读入,
|
||
* 故在缓冲区内**搜索** \r\n\r\n 定位头边界,body 从边界后搬移。成功 0;超时/过大/失败 -1。 */
|
||
static int read_request(int sock, req_t *r)
|
||
{
|
||
char *buf = malloc(MAX_HTTP_HEADER);
|
||
if (!buf) return -1;
|
||
size_t hn = 0;
|
||
int deadline_ms = 5000;
|
||
ptrdiff_t hend = -1;
|
||
for (;;) {
|
||
struct pollfd pf = { sock, POLLIN, 0 };
|
||
int pr = poll(&pf, 1, deadline_ms);
|
||
if (pr <= 0) { free(buf); return -1; }
|
||
ssize_t rd = read(sock, buf + hn, MAX_HTTP_HEADER - 1 - hn);
|
||
if (rd <= 0) { free(buf); return -1; }
|
||
hn += (size_t)rd;
|
||
buf[hn] = 0;
|
||
hend = find_header_end(buf, hn);
|
||
if (hend >= 0) break;
|
||
if (hn >= MAX_HTTP_HEADER - 2) { free(buf); return -1; }
|
||
}
|
||
/* 头结束在 hend,body 起点在 hend+4 */
|
||
char *hdr = buf;
|
||
char *line_end = strstr(hdr, "\r\n");
|
||
if (!line_end) { free(buf); return -1; }
|
||
*line_end = 0;
|
||
char method[8], pathraw[512], proto[16];
|
||
if (sscanf(hdr, "%7s %511s %15s", method, pathraw, proto) < 2) { free(buf); return -1; }
|
||
snprintf(r->method, sizeof r->method, "%s", method);
|
||
split_path_query(pathraw, r->path, sizeof r->path, r->query, sizeof r->query);
|
||
size_t clen = 0;
|
||
char *hp = line_end + 2;
|
||
while (hp < buf + hend && strncmp(hp, "\r\n", 2)) {
|
||
char *he = strstr(hp, "\r\n");
|
||
if (!he || he > buf + hend) break;
|
||
*he = 0;
|
||
if (!strncasecmp(hp, "Content-Length:", 15)) clen = (size_t)strtoul(hp + 15, NULL, 10);
|
||
else if (!strncasecmp(hp, "Content-Type:", 13)) {
|
||
char *v = hp + 13;
|
||
while (*v == ' ') v++;
|
||
snprintf(r->content_type, sizeof r->content_type, "%s", v);
|
||
}
|
||
else if (!strncasecmp(hp, "Authorization:", 14)) {
|
||
char *v = hp + 14;
|
||
while (*v == ' ') v++;
|
||
snprintf(r->authorization, sizeof r->authorization, "%s", v);
|
||
}
|
||
else if (!strncasecmp(hp, "X-MeowToken:", 12)) {
|
||
char *v = hp + 12;
|
||
while (*v == ' ') v++;
|
||
snprintf(r->x_meow_token, sizeof r->x_meow_token, "%s", v);
|
||
}
|
||
else if (!strncasecmp(hp, "Cookie:", 7)) {
|
||
char *v = hp + 7;
|
||
while (*v == ' ') v++;
|
||
snprintf(r->cookie, sizeof r->cookie, "%s", v);
|
||
}
|
||
hp = he + 2;
|
||
}
|
||
char *body_start = buf + hend + 4; /* body 实际起点 */
|
||
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
|
||
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。
|
||
* 字体库 tar.gz 端点例外:gbk/full 字库 png+idx 可能 10-20MB,放宽到 MAX_FONT_UPLOAD(32MB)。 */
|
||
size_t cap = MAX_HTTP_BODY;
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0)
|
||
cap = (!strcmp(r->path, "/api/fonts/import")) ? MAX_FONT_UPLOAD : MAX_UPLOAD_BODY;
|
||
if (clen > cap) { free(buf); return -2; }
|
||
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
|
||
char *body = malloc(clen + 1);
|
||
if (!body) { free(buf); return -1; }
|
||
size_t got = 0;
|
||
if (buffered > 0) {
|
||
size_t take = buffered < clen ? buffered : clen;
|
||
memcpy(body, body_start, take);
|
||
got = take;
|
||
}
|
||
free(buf);
|
||
while (got < clen) {
|
||
struct pollfd pf = { sock, POLLIN, 0 };
|
||
int pr = poll(&pf, 1, 5000);
|
||
if (pr <= 0) { free(body); return -1; }
|
||
ssize_t rd = read(sock, body + got, clen - got);
|
||
if (rd <= 0) { free(body); return -1; }
|
||
got += (size_t)rd;
|
||
}
|
||
body[clen] = 0;
|
||
r->body = body;
|
||
r->body_len = clen;
|
||
return 0;
|
||
}
|
||
|
||
/* ================= 访问级 PIN 鉴权(PANEL_ENHANCE_PLAN 用户定案 2026-09-01) =================
|
||
* 模型(用户二次定案,覆盖 282「默认无鉴权」前提):
|
||
* - **访问面板即需登录**:未认证请求一律 401(静态登录页自身 + css/js 放行——serve_static 先于本门禁)。
|
||
* - 4 位数字 PIN,设置键 `panel.pin`:sha256+salt 存 settings.conf(格式 `<salt_hex>:<sha256_hex(salt||pin)>`,
|
||
* auth_make_hash 生成)。与未来 MeowOS 锁屏 PIN 同源:设置键 `system.pin` 预留(本后端只读面板键,见 g_sets 注记)。
|
||
* - token 随机 **128bit**(/dev/urandom 16B → 32 hex),**只存面板进程内存**(`g_sess[]`,重启自然失效,无卡上持久化)。
|
||
* - 会话有效期 **3 天**(AUTH_TOKEN_TTL,**固定不滑动续期**)或设备重启(内存清、先到为准);多会话并存。
|
||
* - **未设 PIN**:首次访问走 `POST /api/auth/setup` 引导设置;未设 PIN 前无法建立任何会话,故全部
|
||
* `/api/` 路径被拦(读端点也按「访问级」全拦——定案即全拦,登录/设置流程除外),写端点自然一律拒绝。
|
||
* - **错误锁定**:连续 PIN_MAX_FAILS=5 次错 PIN 锁 PIN_LOCK_SEC=300s(内存态,重启清)。
|
||
* - **旧密码机制兼容迁移**(`panel.auth_enabled`/`panel.auth_pass`):旧密码存在且未设 PIN 时,
|
||
* 视为「已设访问屏障」的同等物——登录暂可用旧密码(body `password` 字段),status 透出 `legacy:true`
|
||
* 提示改设 4 位 PIN;一旦设置 `panel.pin`,**新代码以 PIN 为准**(登录只认 PIN,旧密码不再作为凭据)。
|
||
*/
|
||
#define AUTH_TOKEN_BYTES 16 /* 128bit → 32 hex */
|
||
#define AUTH_TOKEN_TTL (3 * 86400) /* 3 天(固定,不滑动续期) */
|
||
#define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */
|
||
#define AUTH_HASH_LEN 64 /* sha256 hex 长度 */
|
||
#define PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
|
||
#define PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
|
||
static time_t g_auth_ttl = AUTH_TOKEN_TTL; /* 会话 TTL(env MEOW_AUTH_TTL 可覆盖,宿主测试用) */
|
||
|
||
/* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 mp_ 避免符号冲突) ---- */
|
||
static const uint32_t mp_sha256_k[64] = {
|
||
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
|
||
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
|
||
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
|
||
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
|
||
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
|
||
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
|
||
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
|
||
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
|
||
};
|
||
#define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||
#define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
|
||
#define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||
#define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22))
|
||
#define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25))
|
||
#define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3))
|
||
#define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10))
|
||
|
||
typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; size_t buflen; } mp_sha256_ctx;
|
||
|
||
static void mp_sha256_init(mp_sha256_ctx *c)
|
||
{
|
||
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
|
||
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
|
||
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
|
||
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
|
||
c->count = 0; c->buflen = 0;
|
||
}
|
||
|
||
static void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p)
|
||
{
|
||
uint32_t w[64];
|
||
int i;
|
||
for (i = 0; i < 16; i++)
|
||
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
|
||
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
|
||
for (i = 16; i < 64; i++)
|
||
w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16];
|
||
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
|
||
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
|
||
for (i = 0; i < 64; i++) {
|
||
uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i];
|
||
uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc);
|
||
h = g; g = f; f = e; e = d + t1;
|
||
d = cc; cc = b; b = a; a = t1 + t2;
|
||
}
|
||
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
|
||
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
|
||
}
|
||
|
||
static void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len)
|
||
{
|
||
c->count += (uint64_t)len;
|
||
if (c->buflen) {
|
||
size_t need = 64 - c->buflen;
|
||
size_t take = len < need ? len : need;
|
||
memcpy(c->buf + c->buflen, data, take);
|
||
c->buflen += take; data += take; len -= take;
|
||
if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; }
|
||
}
|
||
while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; }
|
||
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
|
||
}
|
||
|
||
static void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32])
|
||
{
|
||
uint64_t bitlen = c->count * 8;
|
||
uint8_t *b = c->buf;
|
||
size_t bl = c->buflen;
|
||
int i;
|
||
b[bl++] = 0x80;
|
||
if (bl > 56) {
|
||
while (bl < 64) b[bl++] = 0;
|
||
mp_sha256_block(c, b);
|
||
bl = 0;
|
||
}
|
||
while (bl < 56) b[bl++] = 0;
|
||
for (i = 0; i < 8; i++)
|
||
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
|
||
mp_sha256_block(c, b);
|
||
for (i = 0; i < 8; i++) {
|
||
out[i*4] = (uint8_t)(c->state[i] >> 24);
|
||
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
|
||
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
|
||
out[i*4+3] = (uint8_t)(c->state[i]);
|
||
}
|
||
}
|
||
#undef MP_ROR
|
||
#undef MP_CH
|
||
#undef MP_MAJ
|
||
#undef MP_BSIG0
|
||
#undef MP_BSIG1
|
||
#undef MP_SSIG0
|
||
#undef MP_SSIG1
|
||
|
||
/* ---- 鉴权基础 ----
|
||
* 密码存储格式:`<salt_hex>:<sha256_hex(salt || password)>`。salt 由 /dev/urandom 生成(16B → 32 hex)。
|
||
* 校验时重算并比较,绝不反解、绝不在日志/响应中输出 hash 或明文。
|
||
*/
|
||
static int auth_read_random(unsigned char *buf, size_t n)
|
||
{
|
||
int fd = open("/dev/urandom", O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
size_t got = 0;
|
||
while (got < n) {
|
||
ssize_t rd = read(fd, buf + got, n - got);
|
||
if (rd <= 0) { close(fd); return -1; }
|
||
got += (size_t)rd;
|
||
}
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
|
||
static void auth_hex(const unsigned char *raw, size_t n, char *out)
|
||
{
|
||
static const char hex[] = "0123456789abcdef";
|
||
for (size_t i = 0; i < n; i++) {
|
||
out[i*2] = hex[raw[i] >> 4];
|
||
out[i*2+1] = hex[raw[i] & 0xf];
|
||
}
|
||
out[n*2] = 0;
|
||
}
|
||
|
||
static int auth_token_gen(char *out, int outcap) /* 16B → 32 hex(128bit) */
|
||
{
|
||
unsigned char raw[AUTH_TOKEN_BYTES];
|
||
if (outcap < AUTH_TOKEN_BYTES * 2 + 1) return -1;
|
||
if (auth_read_random(raw, sizeof raw) < 0) return -1;
|
||
auth_hex(raw, sizeof raw, out);
|
||
return 0;
|
||
}
|
||
|
||
static int auth_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
|
||
{
|
||
unsigned char raw[16];
|
||
if (outcap < 33) return -1;
|
||
if (auth_read_random(raw, sizeof raw) < 0) return -1;
|
||
auth_hex(raw, sizeof raw, out);
|
||
return 0;
|
||
}
|
||
|
||
static int auth_pass_hash(const char *salt, const char *password, char *out, int outcap)
|
||
{
|
||
mp_sha256_ctx c;
|
||
uint8_t digest[32];
|
||
if (outcap < AUTH_HASH_LEN + 1) return -1;
|
||
mp_sha256_init(&c);
|
||
mp_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
|
||
mp_sha256_update(&c, (const uint8_t *)password, strlen(password));
|
||
mp_sha256_final(&c, digest);
|
||
auth_hex(digest, sizeof digest, out);
|
||
return 0;
|
||
}
|
||
|
||
static int auth_make_hash(const char *password, char *out, int outcap) /* salt:hex */
|
||
{
|
||
char salt[33], h[AUTH_HASH_LEN + 1];
|
||
if (auth_make_salt(salt, sizeof salt) < 0) return -1;
|
||
if (auth_pass_hash(salt, password, h, sizeof h) < 0) return -1;
|
||
snprintf(out, outcap, "%s:%s", salt, h);
|
||
return 0;
|
||
}
|
||
|
||
/* ---- PIN / 旧密码 凭据校验(都走「salt:hex」哈希校验,恒定比较) ---- */
|
||
|
||
/* 校验 `salt:hex` 哈希是否匹配明文(恒定比较,不反解)。stored 为 `salt:hex` 完整串。 */
|
||
static int hash_matches(const char *stored, const char *password)
|
||
{
|
||
if (!stored || !stored[0] || !password) return 0;
|
||
char tmp[160];
|
||
snprintf(tmp, sizeof tmp, "%s", stored);
|
||
char *colon = strchr(tmp, ':');
|
||
if (!colon) return 0;
|
||
*colon = 0;
|
||
char want[AUTH_HASH_LEN + 1];
|
||
if (auth_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
|
||
if (strlen(colon + 1) != AUTH_HASH_LEN) return 0;
|
||
return memcmp(want, colon + 1, AUTH_HASH_LEN) == 0;
|
||
}
|
||
|
||
static int pin_set(void) /* panel.pin 是否已设(访问屏障开启) */
|
||
{
|
||
char *v = settings_get_value("panel.pin");
|
||
int s = (v && v[0]) ? 1 : 0;
|
||
free(v);
|
||
return s;
|
||
}
|
||
|
||
static int auth_pass_set(void) /* 旧密码是否已设(兼容迁移用) */
|
||
{
|
||
char *v = settings_get_value("panel.auth_pass");
|
||
int s = (v && v[0]) ? 1 : 0;
|
||
free(v);
|
||
return s;
|
||
}
|
||
|
||
static int auth_verify(const char *password) /* 旧密码校验(迁移期;见 login) */
|
||
{
|
||
char *stored = settings_get_value("panel.auth_pass");
|
||
int ok = hash_matches(stored, password);
|
||
free(stored);
|
||
return ok;
|
||
}
|
||
|
||
static int pin_verify(const char *pin) /* PIN 校验 */
|
||
{
|
||
char *stored = settings_get_value("panel.pin");
|
||
int ok = hash_matches(stored, pin);
|
||
free(stored);
|
||
return ok;
|
||
}
|
||
|
||
/* PIN 合法:恰好 4 位数字(0-9)。 */
|
||
static int pin_validate(const char *pin)
|
||
{
|
||
if (!pin) return 0;
|
||
size_t l = strlen(pin);
|
||
if (l != 4) return 0;
|
||
for (size_t i = 0; i < l; i++)
|
||
if (pin[i] < '0' || pin[i] > '9') return 0;
|
||
return 1;
|
||
}
|
||
|
||
/* ---- 错误锁定(内存态,重启清;4 位 PIN 防爆破必备) ---- */
|
||
static int g_pin_fails = 0;
|
||
static time_t g_pin_locked_until = 0;
|
||
|
||
static long long pin_lock_remaining(void) /* 剩余锁定秒;0=未锁 */
|
||
{
|
||
time_t now = time(NULL);
|
||
if (g_pin_locked_until <= now) return 0;
|
||
return (long long)(g_pin_locked_until - now);
|
||
}
|
||
static void pin_lock_reset(void)
|
||
{
|
||
g_pin_fails = 0;
|
||
g_pin_locked_until = 0;
|
||
}
|
||
static void pin_record_fail(void)
|
||
{
|
||
if (++g_pin_fails >= PIN_MAX_FAILS) {
|
||
g_pin_locked_until = time(NULL) + PIN_LOCK_SEC;
|
||
g_pin_fails = 0;
|
||
}
|
||
}
|
||
|
||
/* ---- 会话(**纯内存**:重启自然失效,无卡上持久化) ---- */
|
||
typedef struct { char token[65]; time_t expires; } auth_sess_t;
|
||
static auth_sess_t g_sess[AUTH_MAX_SESS];
|
||
static int g_sess_n = 0;
|
||
|
||
static int sess_find(const char *token)
|
||
{
|
||
for (int i = 0; i < g_sess_n; i++)
|
||
if (!strcmp(g_sess[i].token, token)) return i;
|
||
return -1;
|
||
}
|
||
|
||
static int sess_valid(const char *token)
|
||
{
|
||
int i = sess_find(token);
|
||
if (i < 0) return 0;
|
||
if (g_sess[i].expires > 0 && time(NULL) >= g_sess[i].expires) return 0;
|
||
return 1;
|
||
}
|
||
|
||
static void sess_add(const char *token, time_t expires)
|
||
{
|
||
int i = sess_find(token);
|
||
if (i >= 0) { g_sess[i].expires = expires; return; }
|
||
if (g_sess_n >= AUTH_MAX_SESS) { /* 满:淘汰最早过期的一条 */
|
||
int e = 0;
|
||
for (int k = 1; k < g_sess_n; k++)
|
||
if (g_sess[k].expires < g_sess[e].expires) e = k;
|
||
if (e < g_sess_n - 1)
|
||
memmove(&g_sess[e], &g_sess[e + 1], (size_t)(g_sess_n - 1 - e) * sizeof(auth_sess_t));
|
||
g_sess_n--;
|
||
}
|
||
snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", token);
|
||
g_sess[g_sess_n].expires = expires;
|
||
g_sess_n++;
|
||
}
|
||
|
||
static void sess_remove(const char *token)
|
||
{
|
||
int i = sess_find(token);
|
||
if (i < 0) return;
|
||
if (i < g_sess_n - 1)
|
||
memmove(&g_sess[i], &g_sess[i + 1], (size_t)(g_sess_n - 1 - i) * sizeof(auth_sess_t));
|
||
g_sess_n--;
|
||
}
|
||
|
||
static void auth_cp(char *dst, int dcap, const char *src) /* 有界拷贝(防 -Wformat-truncation) */
|
||
{
|
||
size_t l = strlen(src);
|
||
if (l >= (size_t)dcap) l = (size_t)dcap - 1;
|
||
memcpy(dst, src, l);
|
||
dst[l] = 0;
|
||
}
|
||
|
||
/* ---- 从请求提取 token(X-MeowToken → Authorization: Bearer → Cookie meow_token) ---- */
|
||
static int extract_token(const req_t *r, char *out, int outcap)
|
||
{
|
||
out[0] = 0;
|
||
if (r->x_meow_token[0]) { auth_cp(out, outcap, r->x_meow_token); return out[0] ? 1 : 0; }
|
||
if (!strncasecmp(r->authorization, "Bearer ", 7)) {
|
||
auth_cp(out, outcap, r->authorization + 7);
|
||
return out[0] ? 1 : 0;
|
||
}
|
||
if (r->cookie[0]) {
|
||
const char *p = strstr(r->cookie, "meow_token=");
|
||
if (p) {
|
||
p += 11;
|
||
const char *e = p;
|
||
while (*e && *e != ';' && *e != ' ' && *e != '\t') e++;
|
||
size_t l = (size_t)(e - p);
|
||
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
|
||
memcpy(out, p, l);
|
||
out[l] = 0;
|
||
return out[0] ? 1 : 0;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ---- 鉴权门禁:**访问面板即需登录**——除 /api/auth 系列外,全部 /api/ 端点需有效会话。
|
||
* 未设 PIN 时无法建立会话 → 读端点也一律 401(「全拦」定案),写端点自然一律拒绝;登录/设置流程除外。 ---- */
|
||
static int require_auth(int sock, const req_t *r)
|
||
{
|
||
const char *p = r->path;
|
||
if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") ||
|
||
!strcmp(p, "/api/auth/status") || !strcmp(p, "/api/auth/setup")) return 1;
|
||
char t[65];
|
||
if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1;
|
||
return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效");
|
||
}
|
||
|
||
/* GET /api/auth/status(公开):访问屏障状态 + 当前会话 + 锁定信息。 */
|
||
static int api_auth_status(int sock, const req_t *r)
|
||
{
|
||
int ps = pin_set();
|
||
int lg = auth_pass_set();
|
||
int sess = 0;
|
||
char t[65];
|
||
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
|
||
long long rem = pin_lock_remaining();
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"auth_enabled\":true,\"session\":%s,\"pin_set\":%s,\"legacy\":%s,\"locked\":%s",
|
||
sess ? "true" : "false", ps ? "true" : "false", lg ? "true" : "false", rem > 0 ? "true" : "false");
|
||
if (rem > 0) sA(&s, ",\"lock_remaining_ms\":%lld", rem * 1000LL);
|
||
sA(&s, "}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 登录统一入口:PIN(panel.pin 已设,权威)或旧密码(仅未设 PIN 时;迁移期)。
|
||
* 连续 PIN_MAX_FAILS 次错 → 锁 PIN_LOCK_SEC(429 LOCKED)。 */
|
||
static int api_auth_login(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jpin = jv_obj_get(root, "pin");
|
||
jv_t *jpw = jv_obj_get(root, "password");
|
||
char pin[8], pw[256];
|
||
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0;
|
||
if (jpw && jpw->type == J_STR && jpw->str) snprintf(pw, sizeof pw, "%s", jpw->str); else pw[0] = 0;
|
||
jv_free(root);
|
||
|
||
if (pin_lock_remaining() > 0) { /* 已在锁定期 */
|
||
sbuf_t e = {0};
|
||
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"已锁定,请稍后重试\"},\"lock_remaining_ms\":%lld}",
|
||
pin_lock_remaining() * 1000LL);
|
||
int rc = send_json(sock, 429, e.b ? e.b : "");
|
||
sbuf_free(&e);
|
||
return rc;
|
||
}
|
||
|
||
int use_legacy = 0;
|
||
const char *cred = NULL;
|
||
if (pin_set()) { cred = pin; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 pin 字段"); }
|
||
else if (auth_pass_set()) { use_legacy = 1; cred = pw; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); }
|
||
else return send_err(sock, 400, "BAD_REQUEST", "请先设置面板 PIN(/api/auth/setup)");
|
||
|
||
int ok = use_legacy ? auth_verify(cred) : pin_verify(cred);
|
||
if (!ok) {
|
||
pin_record_fail();
|
||
if (pin_lock_remaining() > 0) { /* 本错已触发锁定 */
|
||
sbuf_t e = {0};
|
||
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"连续错误次数过多,已锁定 5 分钟\"},\"lock_remaining_ms\":%lld}",
|
||
pin_lock_remaining() * 1000LL);
|
||
int rc = send_json(sock, 429, e.b ? e.b : "");
|
||
sbuf_free(&e);
|
||
return rc;
|
||
}
|
||
return send_err(sock, 401, "UNAUTHORIZED", "PIN 错误");
|
||
}
|
||
pin_lock_reset();
|
||
char token[65];
|
||
if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败");
|
||
time_t exp = time(NULL) + g_auth_ttl; /* 固定 3 天(MEOW_AUTH_TTL 可覆盖测试用),不滑动续期 */
|
||
sess_add(token, exp);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"token\":");
|
||
sA_str(&s, token);
|
||
sA(&s, ",\"expires_at\":%lld,\"auth_enabled\":true}", (long long)exp);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* POST /api/auth/setup(公开):首次设置 4 位 PIN(仅未设 PIN 时可用;设置后走 login)。
|
||
* 未设 PIN 前写端点一律拒绝;本端点即「设置流程」例外,用于引导建立首个访问屏障。 */
|
||
static int api_auth_setup(int sock, const req_t *r)
|
||
{
|
||
if (pin_set()) return send_err(sock, 409, "CONFLICT", "面板 PIN 已设置,请直接登录");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jpin = jv_obj_get(root, "pin");
|
||
char pin[8];
|
||
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str);
|
||
else pin[0] = 0;
|
||
jv_free(root);
|
||
if (!pin_validate(pin)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
|
||
char h[200];
|
||
if (auth_make_hash(pin, h, sizeof h) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
|
||
if (settings_write_one("panel.pin", h) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
pin_lock_reset();
|
||
return send_json(sock, 200, "{\"pin_set\":true}");
|
||
}
|
||
|
||
static int api_auth_logout(int sock, const req_t *r)
|
||
{
|
||
char t[65];
|
||
if (extract_token(r, t, sizeof t) && t[0]) sess_remove(t);
|
||
return send_json(sock, 200, "{\"logged_out\":true}");
|
||
}
|
||
|
||
/* ---------------- HTTP 响应写入 ---------------- */
|
||
static int write_full(int sock, const void *b, size_t n)
|
||
{
|
||
const char *p = (const char *)b;
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t w = write(sock, p + off, n - off);
|
||
if (w <= 0) return -1;
|
||
off += (size_t)w;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra)
|
||
{
|
||
char h[512];
|
||
const char *sts = "OK";
|
||
switch (status) {
|
||
case 200: sts = "OK"; break;
|
||
case 201: sts = "Created"; break;
|
||
case 202: sts = "Accepted"; break;
|
||
case 400: sts = "Bad Request"; break;
|
||
case 401: sts = "Unauthorized"; break;
|
||
case 403: sts = "Forbidden"; break;
|
||
case 404: sts = "Not Found"; break;
|
||
case 405: sts = "Method Not Allowed"; break;
|
||
case 409: sts = "Conflict"; break;
|
||
case 413: sts = "Payload Too Large"; break;
|
||
case 415: sts = "Unsupported Media Type"; break;
|
||
case 422: sts = "Unprocessable Entity"; break;
|
||
case 500: sts = "Internal Server Error"; break;
|
||
case 501: sts = "Not Implemented"; break;
|
||
case 503: sts = "Service Unavailable"; break;
|
||
default: sts = "OK"; break;
|
||
}
|
||
snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n",
|
||
status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : "");
|
||
return write_full(sock, h, strlen(h));
|
||
}
|
||
|
||
/* 发送原生 bytes(已含 body)。 */
|
||
static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen)
|
||
{
|
||
if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1;
|
||
if (bodylen && write_full(sock, body, bodylen) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* 成功 JSON 信封:{"ok":true,"data":<data>}。
|
||
* 前缀 "{"ok":true,"data":" 共 18 字节。用 memcpy 精确构包(sA 的 %s 有 ~511B 单次截断,不能用于大 payload)。 */
|
||
static int send_ok(int sock, const char *data)
|
||
{
|
||
static const char prefix[] = "{\"ok\":true,\"data\":";
|
||
size_t pl = sizeof prefix - 1; /* 18 */
|
||
size_t dl = strlen(data);
|
||
size_t tot = pl + dl + 1;
|
||
char *b = malloc(tot + 1);
|
||
if (!b) {
|
||
const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}";
|
||
return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e));
|
||
}
|
||
memcpy(b, prefix, pl);
|
||
memcpy(b + pl, data, dl);
|
||
b[pl + dl] = '}';
|
||
b[pl + dl + 1] = 0;
|
||
int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot);
|
||
free(b);
|
||
return rc;
|
||
}
|
||
|
||
/* 错误 JSON 信封:{"ok":false,"error":{"code":..,"message":..}} */
|
||
static int send_err(int sock, int status, const char *code, const char *msg)
|
||
{
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"ok\":false,\"error\":{\"code\":");
|
||
sA_str(&s, code ? code : "INTERNAL");
|
||
sA(&s, ",\"message\":");
|
||
sA_str(&s, msg ? msg : "");
|
||
sA(&s, "}}");
|
||
int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n);
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int send_json(int sock, int status, const char *data)
|
||
{
|
||
if (status == 200) return send_ok(sock, data);
|
||
return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data));
|
||
}
|
||
|
||
/* 流式发送磁盘文件(下载)。 */
|
||
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename)
|
||
{
|
||
int fd = open(abspath, O_RDONLY);
|
||
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在");
|
||
struct stat st;
|
||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
|
||
char extra[256];
|
||
if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename);
|
||
else extra[0] = 0;
|
||
if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; }
|
||
char buf[8192];
|
||
ssize_t n;
|
||
while ((n = read(fd, buf, sizeof buf)) > 0) {
|
||
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; }
|
||
}
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
|
||
/* 二进制安全子串查找:strstr 会因数据中的 NUL 提前停止,二进制(zip/mpk/图)必须用 memmem。
|
||
* 返回 needle 在 hay[0..hlen) 中首次出现的指针;找不到 NULL。
|
||
* 仅当 mot 出现,用 memmem(_GNU_SOURCE 已定义,glibc/musl 均提供)。
|
||
* 若无 memmem(极端环境),退回手写字节扫描,保证可移植。 */
|
||
static const char *mem_find(const void *hay, size_t hlen, const char *needle)
|
||
{
|
||
size_t nl = strlen(needle);
|
||
if (!nl || hlen < nl) return NULL;
|
||
const char *h = (const char *)hay;
|
||
for (size_t i = 0; i + nl <= hlen; i++) {
|
||
if (h[i] == needle[0] && memcmp(h + i, needle, nl) == 0)
|
||
return h + i;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* ---------------- multipart/form-data 解析(文件上传) ----------------
|
||
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
|
||
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
|
||
*/
|
||
typedef struct { char path[1400]; char filename[320]; } upfile_t;
|
||
|
||
/* 安全文件名:取 basename(去目录分隔符),拒绝空/`.`/`..`/含控制字符或分隔符。
|
||
* 返回 0=ok 写入 out;-1=拒绝。用于防上传文件名逃逸白名单(如 `../../etc/passwd`)。 */
|
||
static int safe_filename(const char *name, char *out, size_t cap)
|
||
{
|
||
if (!name || !name[0]) return -1;
|
||
const char *b = name;
|
||
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
|
||
const char *slash = strrchr(b, '/');
|
||
const char *bsl = strrchr(b, '\\');
|
||
if (bsl && (!slash || bsl > slash)) slash = bsl;
|
||
if (slash) b = slash + 1;
|
||
if (!b[0]) return -1;
|
||
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
|
||
for (const char *p = b; *p; p++) {
|
||
unsigned char c = (unsigned char)*p;
|
||
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
|
||
}
|
||
snprintf(out, cap, "%s", b);
|
||
return 0;
|
||
}
|
||
|
||
static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
||
{
|
||
/* 取 boundary */
|
||
const char *ct = r->content_type;
|
||
const char *b = strstr(ct, "boundary=");
|
||
if (!b) return -1;
|
||
b += 9;
|
||
if (*b == '"') { b++; }
|
||
char boundary[128];
|
||
int bi = 0;
|
||
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
|
||
boundary[bi] = 0;
|
||
char dmark[160];
|
||
snprintf(dmark, sizeof dmark, "--%s", boundary);
|
||
char emark[160];
|
||
snprintf(emark, sizeof emark, "--%s--", boundary);
|
||
/* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */
|
||
const char *p = r->body;
|
||
const char *end = r->body + r->body_len;
|
||
int nfiles = 0;
|
||
char curpath[1024] = "";
|
||
while (p < end) {
|
||
const char *fstart = mem_find(p, (size_t)(end - p), dmark);
|
||
if (!fstart) break;
|
||
fstart += strlen(dmark);
|
||
if (*fstart == '-') break; /* 结束标记 --boundary-- */
|
||
if (*fstart == '\r' || *fstart == '\n') fstart++;
|
||
/* 解析该 part 的头部(直至空行) */
|
||
const char *headers_end = strstr(fstart, "\r\n\r\n");
|
||
if (!headers_end) break;
|
||
char hdr[512];
|
||
size_t hl = (size_t)(headers_end - fstart);
|
||
if (hl >= sizeof hdr) hl = sizeof hdr - 1;
|
||
memcpy(hdr, fstart, hl);
|
||
hdr[hl] = 0;
|
||
const char *data_start = headers_end + 4;
|
||
const char *name = NULL, *fname = NULL;
|
||
char *save = NULL;
|
||
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||
char *l = ln;
|
||
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||
/* name 与 filename 常同行(name="file"; filename="up.txt")。
|
||
* 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */
|
||
char *fn = strstr(l, "filename=\"");
|
||
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
|
||
char *nm = strstr(l, "name=\"");
|
||
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
|
||
}
|
||
}
|
||
/* 数据结束:下一个 "--boundary" 之前(二进制安全) */
|
||
const char *data_end = mem_find(data_start, (size_t)(end - data_start), dmark);
|
||
if (!data_end) data_end = end;
|
||
/* 去掉数据尾部 \r\n */
|
||
while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--;
|
||
if (name && !strcmp(name, "path")) {
|
||
size_t dl = (size_t)(data_end - data_start);
|
||
if (dl >= sizeof curpath) dl = sizeof curpath - 1;
|
||
memcpy(curpath, data_start, dl);
|
||
curpath[dl] = 0;
|
||
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
|
||
size_t dl = (size_t)(data_end - data_start);
|
||
char safe[260];
|
||
if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */
|
||
/* 目标目录规范化 + 白名单校验 */
|
||
char absdir[1024];
|
||
if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; }
|
||
/* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */
|
||
char joined[1400];
|
||
snprintf(joined, sizeof joined, "%s/%s", absdir, safe);
|
||
char abs[1400];
|
||
if (normalize_path(joined, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { p = data_end; continue; }
|
||
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", safe);
|
||
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", abs);
|
||
int fd = open(abs, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) {
|
||
write_full(fd, data_start, dl);
|
||
close(fd);
|
||
nfiles++;
|
||
}
|
||
/* fd<0(不可写)→ 不计数,跳过该文件 */
|
||
}
|
||
p = data_end;
|
||
}
|
||
return nfiles; /* >=0:已保存文件数;-1 边界缺失/解析失败 */
|
||
}
|
||
|
||
/* ---------------- 日志(stderr;svcd supervisor 捕获) ---------------- */
|
||
static void panel_log(const char *fmt, ...)
|
||
{
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
fprintf(stderr, "[MeowPanel] ");
|
||
vfprintf(stderr, fmt, ap);
|
||
fprintf(stderr, "\n");
|
||
va_end(ap);
|
||
}
|
||
|
||
/* ---------------- 设置项 JSON 序列化 ---------------- */
|
||
static int str_truthy(const char *v)
|
||
{
|
||
char q[32];
|
||
snprintf(q, sizeof q, "%s", v);
|
||
char *p = q;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!strcasecmp(p, "true") || !strcasecmp(p, "yes") || !strcasecmp(p, "on")) return 1;
|
||
long n = strtol(p, NULL, 10);
|
||
return (p[0] && n != 0);
|
||
}
|
||
|
||
static int int_meta_max(const setdef_t *d)
|
||
{
|
||
if (d->max > 0) return d->max;
|
||
int dn = atoi(d->def);
|
||
if (dn > 0) return dn * 2 + 100;
|
||
return 100;
|
||
}
|
||
|
||
/* 非改写的 CSV 辅助:取第 idx 个项(抄入 buf,NUL 结尾),未命中返回空串。
|
||
* 用于 enum 名/标签 —— 不再 strtok_r(那会在只读字面量上写 NUL SEGV)。 */
|
||
static const char *csv_at(const char *s, int idx, char *buf, int buflen)
|
||
{
|
||
buf[0] = 0;
|
||
if (!s || !s[0]) return buf;
|
||
const char *p = s;
|
||
int i = 0;
|
||
for (;;) {
|
||
const char *start = p;
|
||
while (*p && *p != ',') p++;
|
||
if (i == idx) {
|
||
size_t k = (size_t)(p - start);
|
||
if (k >= (size_t)buflen) k = (size_t)buflen - 1;
|
||
memcpy(buf, start, k);
|
||
buf[k] = 0;
|
||
break;
|
||
}
|
||
if (!*p) break;
|
||
p++;
|
||
i++;
|
||
}
|
||
return buf;
|
||
}
|
||
|
||
static int csv_count(const char *s)
|
||
{
|
||
if (!s || !s[0]) return 0;
|
||
int n = 1;
|
||
for (const char *p = s; *p; p++) if (*p == ',') n++;
|
||
return n;
|
||
}
|
||
|
||
static void enum_options_json(sbuf_t *s, const char *names, const char *labels)
|
||
{
|
||
sA(s, "[");
|
||
int cnt = csv_count(names);
|
||
for (int i = 0; i < cnt; i++) {
|
||
char nb[128], lb[128];
|
||
csv_at(names, i, nb, sizeof nb);
|
||
csv_at(labels, i, lb, sizeof lb);
|
||
if (i) sA(s, ",");
|
||
sA(s, "{\"name\":");
|
||
sA_str(s, nb);
|
||
sA(s, ",\"label\":");
|
||
sA_str(s, lb[0] ? lb : nb);
|
||
sA(s, "}");
|
||
}
|
||
sA(s, "]");
|
||
}
|
||
|
||
static void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val)
|
||
{
|
||
const char *tn = "str";
|
||
if (d->type == ST_INT) tn = "int";
|
||
else if (d->type == ST_BOOL) tn = "bool";
|
||
else if (d->type == ST_ENUM) tn = "enum";
|
||
sA(s, "{\"key\":");
|
||
sA_str(s, d->key);
|
||
sA(s, ",\"type\":\"%s\",\"value\":", tn);
|
||
if (d->type == ST_INT) {
|
||
int v = atoi(val);
|
||
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
|
||
else if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
|
||
else if (v < d->min) v = d->min;
|
||
sA(s, "%d", v);
|
||
sA(s, ",\"min\":%d,\"max\":%d", d->min, int_meta_max(d));
|
||
} else if (d->type == ST_BOOL) {
|
||
sA(s, "%s", str_truthy(val) ? "true" : "false");
|
||
} else {
|
||
if (!strcmp(d->key, "panel.auth_pass") || !strcmp(d->key, "panel.pin")) {
|
||
/* 密码/PIN 哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设) */
|
||
sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false");
|
||
} else {
|
||
sA_str(s, val);
|
||
}
|
||
}
|
||
sA(s, ",\"def\":");
|
||
sA_str(s, d->def);
|
||
if (d->type == ST_ENUM) {
|
||
sA(s, ",\"options\":");
|
||
enum_options_json(s, d->enum_names, d->enum_labels);
|
||
sA(s, ",\"default_name\":");
|
||
sA_str(s, d->def);
|
||
}
|
||
sA(s, "}");
|
||
}
|
||
|
||
/* 读某键当前值(无则用默认;str 用默认)。返回 malloc 字符串。 */
|
||
static char *settings_current_value(const setdef_t *d)
|
||
{
|
||
char *v = settings_get_value(d->key);
|
||
if (v) return v;
|
||
return strdup(d->def);
|
||
}
|
||
|
||
/* ---------------- 设置端点 ---------------- */
|
||
static int api_settings_get_any(int sock, const char *key)
|
||
{
|
||
const setdef_t *d = setdef_find(key);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
|
||
char *val = settings_current_value(d);
|
||
sbuf_t s = {0};
|
||
settings_item_json(&s, d, val);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
free(val);
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_get_all(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"items\":[");
|
||
char groups[16][32];
|
||
int ngroups = 0;
|
||
for (int i = 0; i < SETS_COUNT; i++) {
|
||
const setdef_t *d = &g_sets[i];
|
||
char *val = settings_current_value(d);
|
||
if (i) sA(&s, ",");
|
||
settings_item_json(&s, d, val);
|
||
free(val);
|
||
if (d->group && d->group[0]) {
|
||
int found = 0;
|
||
for (int g = 0; g < ngroups; g++) if (!strcmp(groups[g], d->group)) { found = 1; break; }
|
||
if (!found && ngroups < 16) snprintf(groups[ngroups++], 32, "%s", d->group);
|
||
}
|
||
}
|
||
sA(&s, "],\"count\":%d,\"groups\":[", SETS_COUNT);
|
||
for (int g = 0; g < ngroups; g++) {
|
||
if (g) sA(&s, ",");
|
||
sA_str(&s, groups[g]);
|
||
}
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_put(int sock, const char *key, const req_t *r)
|
||
{
|
||
const setdef_t *d = setdef_find(key);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *val = jv_obj_get(root, "value");
|
||
if (!val) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 value 字段"); }
|
||
char raw[256];
|
||
switch (val->type) {
|
||
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
|
||
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
|
||
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
|
||
default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持");
|
||
}
|
||
jv_free(root);
|
||
char hashbuf[200];
|
||
/* panel.pin:4 位数字 PIN,只落带盐哈希;空串=清除(回未设状态);非法(非 4 位数字)→ 422 */
|
||
if (!strcmp(key, "panel.pin")) {
|
||
if (raw[0]) {
|
||
if (!pin_validate(raw)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
|
||
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
|
||
return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
|
||
} else {
|
||
hashbuf[0] = 0; /* 清空 → 回未设状态 */
|
||
}
|
||
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
pin_lock_reset();
|
||
sbuf_t s = {0};
|
||
settings_item_json(&s, d, hashbuf);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
/* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */
|
||
if (!strcmp(key, "panel.auth_pass")) {
|
||
if (raw[0]) {
|
||
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
|
||
return send_err(sock, 500, "INTERNAL", "生成密码哈希失败");
|
||
} else {
|
||
hashbuf[0] = 0; /* 清空 → 禁用密码 */
|
||
}
|
||
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
sbuf_t s = {0};
|
||
settings_item_json(&s, d, hashbuf);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
/* panel.auth_enabled(旧机制,兼容迁移):置 1 前须已有访问屏障(PIN 或旧密码) */
|
||
if (!strcmp(key, "panel.auth_enabled")) {
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0)
|
||
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
|
||
if (!strcmp(coerced, "1") && !pin_set() && !auth_pass_set())
|
||
return send_err(sock, 422, "UNSUPPORTED", "请先设置面板 PIN 或密码");
|
||
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
sbuf_t s = {0};
|
||
settings_item_json(&s, d, coerced);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
|
||
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
|
||
}
|
||
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
/* boot.default 同步权威文件(同 meow_settings_save) */
|
||
if (!strcmp(key, "boot.default")) {
|
||
int v = (!strcmp(coerced, "hiby")) ? 1 : 0;
|
||
FILE *bf = fopen(BOOT_DEFAULT_FILE, "w");
|
||
if (bf) { fprintf(bf, "%d\n", v); fclose(bf); }
|
||
}
|
||
sbuf_t s = {0};
|
||
settings_item_json(&s, d, coerced);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_batch(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root || root->type != J_OBJ) { if (root) jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON 对象"); }
|
||
sbuf_t ok = {0}, fail = {0};
|
||
int nok = 0, nfail = 0;
|
||
for (const jv_t *p = root->child; p; p = p->next) {
|
||
const setdef_t *d = setdef_find(p->str);
|
||
if (!d) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"NOT_FOUND\",\"message\":\"未知设置项\"}"); nfail++; continue; }
|
||
jv_t *val = p->child;
|
||
char raw[256];
|
||
switch (val->type) {
|
||
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
|
||
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
|
||
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
|
||
default: if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"BAD_REQUEST\",\"message\":\"非法值类型\"}"); nfail++; continue;
|
||
}
|
||
/* panel.pin:只落带盐哈希(空=清除);非法(非 4 位数字)→ 422 */
|
||
if (!strcmp(p->str, "panel.pin")) {
|
||
char hashbuf[200];
|
||
if (raw[0]) {
|
||
if (!pin_validate(raw)) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"PIN 必须为 4 位数字\"}"); nfail++; continue; }
|
||
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成 PIN 哈希失败\"}"); nfail++; continue; }
|
||
} else hashbuf[0] = 0;
|
||
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
|
||
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
|
||
continue;
|
||
}
|
||
/* panel.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */
|
||
if (!strcmp(p->str, "panel.auth_pass")) {
|
||
char hashbuf[200];
|
||
if (raw[0]) { if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成密码哈希失败\"}"); nfail++; continue; } }
|
||
else hashbuf[0] = 0;
|
||
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
|
||
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
|
||
continue;
|
||
}
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
|
||
if (nfail) sA(&fail, ",");
|
||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"值域/枚举非法\"}");
|
||
nfail++;
|
||
continue;
|
||
}
|
||
/* panel.auth_enabled(迁移):启用前须已有访问屏障(PIN 或旧密码) */
|
||
if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) {
|
||
if (nfail) sA(&fail, ",");
|
||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板 PIN 或密码\"}");
|
||
nfail++;
|
||
continue;
|
||
}
|
||
if (settings_write_one(p->str, coerced) == 0) {
|
||
if (nok) sA(&ok, ",");
|
||
sA_str(&ok, p->str);
|
||
nok++;
|
||
} else {
|
||
if (nfail) sA(&fail, ",");
|
||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}");
|
||
nfail++;
|
||
}
|
||
}
|
||
jv_free(root);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"updated\":[");
|
||
if (ok.b) sA(&s, "%s", ok.b);
|
||
sA(&s, "],\"failed\":[");
|
||
if (fail.b) sA(&s, "%s", fail.b);
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s); sbuf_free(&ok); sbuf_free(&fail);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_restore(int sock)
|
||
{
|
||
if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败");
|
||
return send_json(sock, 200, "{\"reset\":true}");
|
||
}
|
||
|
||
/* ---------------- 主题端点 ---------------- */
|
||
static void theme_json(sbuf_t *s)
|
||
{
|
||
const setdef_t *dst = setdef_find("ui.style");
|
||
const setdef_t *dth = setdef_find("display.theme");
|
||
const setdef_t *dbg = setdef_find("display.background");
|
||
char *style = settings_current_value(dst);
|
||
char *theme = settings_current_value(dth);
|
||
char *bg = settings_current_value(dbg);
|
||
/* 当前值 → 中文标签(csv 里同下标查 label) */
|
||
char slabel[128] = "", tlabel[128] = "";
|
||
for (int i = 0; i < csv_count(dst->enum_names); i++) {
|
||
char nb[128], lb[128];
|
||
csv_at(dst->enum_names, i, nb, sizeof nb);
|
||
csv_at(dst->enum_labels, i, lb, sizeof lb);
|
||
if (!strcmp(nb, style)) { snprintf(slabel, sizeof slabel, "%s", lb[0] ? lb : nb); break; }
|
||
}
|
||
for (int i = 0; i < csv_count(dth->enum_names); i++) {
|
||
char nb[128], lb[128];
|
||
csv_at(dth->enum_names, i, nb, sizeof nb);
|
||
csv_at(dth->enum_labels, i, lb, sizeof lb);
|
||
if (!strcmp(nb, theme)) { snprintf(tlabel, sizeof tlabel, "%s", lb[0] ? lb : nb); break; }
|
||
}
|
||
sA(s, "{\"style\":");
|
||
sA_str(s, style);
|
||
sA(s, ",\"theme\":");
|
||
sA_str(s, theme);
|
||
sA(s, ",\"background\":");
|
||
sA_str(s, bg);
|
||
sA(s, ",\"style_label\":");
|
||
sA_str(s, slabel);
|
||
sA(s, ",\"theme_label\":");
|
||
sA_str(s, tlabel);
|
||
sA(s, ",\"style_options\":");
|
||
enum_options_json(s, dst->enum_names, dst->enum_labels);
|
||
sA(s, ",\"theme_options\":");
|
||
enum_options_json(s, dth->enum_names, dth->enum_labels);
|
||
sA(s, ",\"background_options\":[{\"name\":\"black\",\"label\":\"黑\"},{\"name\":\"blue\",\"label\":\"深蓝\"},{\"name\":\"green\",\"label\":\"墨绿\"}]}");
|
||
free(style); free(theme); free(bg);
|
||
}
|
||
|
||
static int api_theme_get(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
theme_json(&s);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_theme_put(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
/* 允许 style/theme/background 任意子集 */
|
||
const char *keys[3] = { "style", "theme", "background" };
|
||
const char *setkeys[3] = { "ui.style", "display.theme", "display.background" };
|
||
for (int i = 0; i < 3; i++) {
|
||
jv_t *v = jv_obj_get(root, keys[i]);
|
||
if (!v) continue;
|
||
const setdef_t *d = setdef_find(setkeys[i]);
|
||
if (!d) continue;
|
||
char raw[256];
|
||
if (v->type == J_STR) snprintf(raw, sizeof raw, "%s", v->str);
|
||
else if (v->type == J_NUM) snprintf(raw, sizeof raw, "%lld", v->inum);
|
||
else continue;
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "非法值"); }
|
||
settings_write_one(setkeys[i], coerced);
|
||
}
|
||
jv_free(root);
|
||
return api_theme_get(sock);
|
||
}
|
||
|
||
/* ---------------- 系统信息 / 性能 ---------------- */
|
||
static int api_perf(int sock, const char *path)
|
||
{
|
||
char json[4096];
|
||
int rc = ipc_perf_query(perf_subset_for_path(path), json, sizeof json);
|
||
if (rc != MEOW_OK || !utf8_valid_n(json, strlen(json))) /* 上游坏字节 → 不返回,降级 */
|
||
return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
|
||
return send_json(sock, 200, json);
|
||
}
|
||
|
||
/* 从 meow_svc_perf 聚合 JSON 提取顶层 "network":{...} 段原始文本(含外层 {}),
|
||
* 供 /api/system 透传(单一权威源,后端不再各自采集)。找不到顶层 network 返回 -1。 */
|
||
static int perf_extract_network(const char *json, char *out, int cap)
|
||
{
|
||
const char *p = json;
|
||
for (;;) {
|
||
p = strstr(p, "\"network\"");
|
||
if (!p) return -1;
|
||
const char *q = p + 9; /* strlen("\"network\"") */
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
if (*q == ':') { p = q + 1; break; } /* 顶层 network 键 */
|
||
p += 9; /* 嵌套同名键跳过 */
|
||
}
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (*p != '{') return -1;
|
||
int depth = 0, in_str = 0, esc = 0;
|
||
const char *q;
|
||
for (q = p; *q; q++) {
|
||
char ch = *q;
|
||
if (in_str) {
|
||
if (esc) esc = 0;
|
||
else if (ch == '\\') esc = 1;
|
||
else if (ch == '"') in_str = 0;
|
||
continue;
|
||
}
|
||
if (ch == '"') { in_str = 1; continue; }
|
||
if (ch == '{') depth++;
|
||
else if (ch == '}') { depth--; if (depth == 0) { q++; break; } }
|
||
}
|
||
if (depth != 0) return -1;
|
||
size_t len = (size_t)(q - p);
|
||
if (len == 0) return -1;
|
||
if (len >= (size_t)cap) len = (size_t)cap - 1;
|
||
memcpy(out, p, len); out[len] = 0;
|
||
return 0;
|
||
}
|
||
|
||
/* /api/system:性能服务 SYSTEM 子集 + boot/ota 局部补全。
|
||
* network 段由 meow_svc_perf 聚合(SYSTEM_FRAMEWORK_SERVICES §4.1 已定案),此处**透传 perf 的 network**,
|
||
* 后端不再各自采集 wifi/蓝牙;perf 若缺 network 段才回退 hostname_local(保持不崩)。 */
|
||
static int api_system(int sock)
|
||
{
|
||
char json[4096];
|
||
int rc = ipc_perf_query(0, json, sizeof json);
|
||
if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
|
||
jv_t *root = jv_parse(json, (int)strlen(json));
|
||
if (!root) return send_err(sock, 500, "INTERNAL", "性能服务 JSON 解析失败");
|
||
jv_t *jsys = jv_obj_get(root, "system");
|
||
jv_t *jupt = jv_obj_get(root, "uptime_sec");
|
||
jv_t *jst = jv_obj_get(root, "storage");
|
||
jv_t *jtf = jv_obj_get(root, "tfcard");
|
||
const char *ver = "-", *host = "-", *ker = "-", *arch = "-";
|
||
if (jsys && jsys->type == J_OBJ) {
|
||
jv_t *v = jv_obj_get(jsys, "version"); if (v && v->type == J_STR) ver = v->str;
|
||
v = jv_obj_get(jsys, "hostname"); if (v && v->type == J_STR) host = v->str;
|
||
v = jv_obj_get(jsys, "kernel"); if (v && v->type == J_STR) ker = v->str;
|
||
v = jv_obj_get(jsys, "arch"); if (v && v->type == J_STR) arch = v->str;
|
||
}
|
||
long long upt = jupt ? (long long)jupt->dnum : 0;
|
||
/* boot default:读权威 /data/meow_boot_default(0=meow 默认, 1=hiby),失败回退 meow */
|
||
int bdf = -1;
|
||
{
|
||
char b[16];
|
||
FILE *f = fopen(BOOT_DEFAULT_FILE, "r");
|
||
if (f) { if (fgets(b, sizeof b, f)) bdf = atoi(b); fclose(f); }
|
||
}
|
||
const char *bootname = bdf < 0 ? "meow" : (bdf ? "hiby" : "meow");
|
||
/* 当前运行模式:读 /tmp/meow_boot_mode(meow/hiby),缺失回退 default(PANEL_ENHANCE_PLAN 批1 前端读 boot.mode) */
|
||
char modebuf[16] = {0};
|
||
{
|
||
FILE *f = fopen("/tmp/meow_boot_mode", "r");
|
||
if (f) {
|
||
if (fgets(modebuf, sizeof modebuf, f)) {
|
||
char *p = modebuf;
|
||
while (*p) { if (*p == '\n' || *p == '\r' || *p == ' ') { *p = 0; break; } p++; }
|
||
}
|
||
fclose(f);
|
||
}
|
||
}
|
||
const char *bootmode = (modebuf[0] == 'm' || modebuf[0] == 'h') ? modebuf : bootname;
|
||
char host_local[80];
|
||
snprintf(host_local, sizeof host_local, "%s.local", host);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"version\":");
|
||
sA_str(&s, ver);
|
||
sA(&s, ",\"hostname\":");
|
||
sA_str(&s, host);
|
||
sA(&s, ",\"kernel\":");
|
||
sA_str(&s, ker);
|
||
sA(&s, ",\"arch\":");
|
||
sA_str(&s, arch);
|
||
sA(&s, ",\"uptime_sec\":%lld,\"boot\":{\"default\":", upt);
|
||
sA_str(&s, bootname);
|
||
sA(&s, ",\"mode\":");
|
||
sA_str(&s, bootmode);
|
||
sA(&s, "},\"network\":");
|
||
char netbuf[1024];
|
||
/* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local);含非法 UTF-8(如坏 ssid)→ 回退,不嵌坏字节 */
|
||
if (perf_extract_network(json, netbuf, sizeof netbuf) == 0 && utf8_valid_n(netbuf, strlen(netbuf))) {
|
||
sA(&s, "%s", netbuf);
|
||
} else {
|
||
/* 回退:perf 未含 network 段 → 仅 hostname_local(保持原语义,不崩) */
|
||
sA(&s, "{\"hostname_local\":");
|
||
sA_str(&s, host_local);
|
||
sA(&s, "}");
|
||
}
|
||
sA(&s, ",\"storage\":{");
|
||
if (jst && jst->type == J_OBJ) {
|
||
jv_t *t = jv_obj_get(jst, "total"); long long tot = t ? (long long)t->dnum : 0;
|
||
jv_t *u = jv_obj_get(jst, "used"); long long usd = u ? (long long)u->dnum : 0;
|
||
jv_t *a = jv_obj_get(jst, "avail"); long long ava = a ? (long long)a->dnum : 0;
|
||
sA(&s, "\"root\":{\"total\":%lld,\"used\":%lld,\"avail\":%lld}", tot, usd, ava);
|
||
} else {
|
||
sA(&s, "\"root\":{\"total\":0,\"used\":0,\"avail\":0}");
|
||
}
|
||
sA(&s, ",\"tfcard\":{");
|
||
if (jtf && jtf->type == J_OBJ) {
|
||
jv_t *pr = jv_obj_get(jtf, "present"); int present = pr && pr->bval;
|
||
long long tot = 0, usd = 0, ava = 0;
|
||
jv_t *t = jv_obj_get(jtf, "total"); if (t) tot = (long long)t->dnum;
|
||
jv_t *u = jv_obj_get(jtf, "used"); if (u) usd = (long long)u->dnum;
|
||
jv_t *a = jv_obj_get(jtf, "avail"); if (a) ava = (long long)a->dnum;
|
||
sA(&s, "\"present\":%s,\"total\":%lld,\"used\":%lld,\"avail\":%lld}", present ? "true" : "false", tot, usd, ava);
|
||
} else {
|
||
sA(&s, "\"present\":false,\"total\":0,\"used\":0,\"avail\":0}");
|
||
}
|
||
sA(&s, "},\"ota\":{\"current\":");
|
||
sA_str(&s, ver);
|
||
sA(&s, ",\"latest\":");
|
||
sA_str(&s, ver);
|
||
sA(&s, ",\"available\":false,\"url\":null}");
|
||
/* 重启原因 / 升级状态:svc_power 读级查询(POWER_REBOOT_REASON(8)/POWER_UPGRADE_STATUS(9),只读、
|
||
* 任意调用者)。st.msg 分别为 "coldboot=reboot|shutdown|unknown" 与 "upgrade=yes|no"。
|
||
* 前端批1-B 已留位(读 d.reboot_reason / d.upgrade_status),此处点亮。查询失败保持空串(不谎报)。 */
|
||
char rreason[48] = "", ustatus[32] = "";
|
||
{
|
||
m_power_status_t st;
|
||
memset(&st, 0, sizeof st);
|
||
if (ipc_power_action(POWER_REBOOT_REASON, 0, NULL, &st) == MEOW_OK)
|
||
svc_msg_kv(st.msg, "coldboot", rreason, sizeof rreason);
|
||
memset(&st, 0, sizeof st);
|
||
if (ipc_power_action(POWER_UPGRADE_STATUS, 0, NULL, &st) == MEOW_OK)
|
||
svc_msg_kv(st.msg, "upgrade", ustatus, sizeof ustatus);
|
||
}
|
||
sA(&s, ",\"reboot_reason\":");
|
||
sA_str(&s, rreason);
|
||
sA(&s, ",\"upgrade_status\":");
|
||
sA_str(&s, ustatus);
|
||
sA(&s, "}");
|
||
jv_free(root);
|
||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return ret;
|
||
}
|
||
|
||
/* ---------------- 锁屏一次性解锁(写 /tmp/meow_pin_unlock_once,launcher 锁屏页消费) ---------------- */
|
||
static int api_pin_unlock_once(int sock)
|
||
{
|
||
const char *path = "/tmp/meow_pin_unlock_once";
|
||
FILE *f = fopen(path, "w");
|
||
if (!f) return send_err(sock, 500, "INTERNAL", "无法写入一次性解锁标志");
|
||
fputs("1\n", f);
|
||
fclose(f);
|
||
return send_json(sock, 200, "{\"unlock_once\":true}");
|
||
}
|
||
|
||
/* ---------------- 系统动作(经 meow_svc_power IPC) ---------------- */
|
||
static int api_system_action(int sock, const char *which, const req_t *r)
|
||
{
|
||
(void)r;
|
||
uint32_t cmd;
|
||
if (!strcmp(which, "reboot")) cmd = POWER_REBOOT;
|
||
else if (!strcmp(which, "shutdown")) cmd = POWER_SHUTDOWN;
|
||
else if (!strcmp(which, "ota")) cmd = POWER_UPGRADE;
|
||
else return send_err(sock, 404, "NOT_FOUND", "未知系统动作");
|
||
m_power_status_t st;
|
||
memset(&st, 0, sizeof st);
|
||
int rc = ipc_power_action(cmd, 0, NULL, &st);
|
||
if (rc == MEOW_OK) return send_json(sock, 200, "{\"accepted\":true}");
|
||
/* 系统动作类服务端通常「先应答受理再执行」;此处若返回非 OK 则说明服务未受理 */
|
||
const char *why = st.msg[0] ? st.msg : power_code_str(rc);
|
||
if (rc == MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", "电源服务拒绝(非 root 调用)");
|
||
if (rc == MEOW_ERR_NODEV) return send_err(sock, 503, "NO_CARD", "升级文件缺失或服务不可达");
|
||
if (rc == MEOW_ERR_NOENT || rc == MEOW_ERR_IO) return send_err(sock, 503, "NO_CARD", why);
|
||
return send_err(sock, 500, "INTERNAL", why);
|
||
}
|
||
|
||
/* ---------------- 远程控制(按键注入) ---------------- */
|
||
static int api_key_single(int sock, int code, const req_t *r)
|
||
{
|
||
int press_ms = 0;
|
||
char pressbuf[16] = "tap";
|
||
const char *press = pressbuf;
|
||
if (r->body && r->body[0]) {
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (root) {
|
||
jv_t *p = jv_obj_get(root, "press");
|
||
if (p && p->type == J_STR) snprintf(pressbuf, sizeof pressbuf, "%s", p->str);
|
||
jv_t *m = jv_obj_get(root, "ms");
|
||
if (m && m->type == J_NUM) press_ms = (int)m->inum;
|
||
jv_free(root);
|
||
}
|
||
}
|
||
if (!strcmp(press, "hold"))
|
||
return send_err(sock, 501, "NOT_IMPLEMENTED", "hold 持续注入需常驻通道(阶段 D),后端未实现");
|
||
if (strcmp(press, "tap") && strcmp(press, "long"))
|
||
return send_err(sock, 400, "BAD_REQUEST", "press 非法");
|
||
if (press_ms <= 0) {
|
||
if (!strcmp(press, "long")) press_ms = 3000;
|
||
}
|
||
/* 物理键红线:禁止映射 返回(158)/主页(172)/确认/exit(APPS.md)。 */
|
||
if (code == 158 || code == 172 || code == 32 || code == 28 || code == 96)
|
||
return send_err(sock, 403, "FORBIDDEN", "该键码(返回/主页/确认)按物理键红线禁止映射");
|
||
const char *tmpdev = NULL;
|
||
int dispatchable = (keycode_to_device(code, &tmpdev) >= 0);
|
||
if (!dispatchable)
|
||
return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/电源/播放/上下曲)");
|
||
if (inject_key_release(code, press_ms) < 0)
|
||
return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press);
|
||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return ret;
|
||
}
|
||
|
||
static int api_key_batch(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *keys = jv_obj_get(root, "keys");
|
||
if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); }
|
||
sbuf_t s = {0};
|
||
int first = 1;
|
||
for (const jv_t *k = keys->child; k; k = k->next) {
|
||
if (k->type != J_OBJ) continue;
|
||
jv_t *c = jv_obj_get(k, "code");
|
||
jv_t *p = jv_obj_get(k, "press");
|
||
jv_t *m = jv_obj_get(k, "ms");
|
||
if (!c || c->type != J_NUM) continue;
|
||
int code = (int)c->inum;
|
||
if (code == 158 || code == 172) continue; /* 红线跳过 */
|
||
int press_ms = (m && m->type == J_NUM) ? (int)m->inum : 0;
|
||
if (p && p->type == J_STR && !strcmp(p->str, "long") && press_ms <= 0) press_ms = 3000;
|
||
const char *tmpdev = NULL;
|
||
if (keycode_to_device(code, &tmpdev) < 0) continue;
|
||
if (inject_key_release(code, press_ms) < 0) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "%d", code);
|
||
}
|
||
jv_free(root);
|
||
sbuf_t o = {0};
|
||
if (first) sA(&o, "{\"dispatched\":[]}");
|
||
else sA(&o, "{\"dispatched\":[%s]}", s.b);
|
||
int ret = send_json(sock, 200, o.b ? o.b : "");
|
||
sbuf_free(&s); sbuf_free(&o);
|
||
return ret;
|
||
}
|
||
|
||
/* ---------------- 日志 ---------------- */
|
||
|
||
/* 日志来源分组(批2 崩溃/日志中心):按文件名前缀归组,供前端分组展示 + 前缀过滤。
|
||
* meow_* → system(MeowOS 服务); crash_* → crash; ams_* → ams; audio_monitor_* → audio_monitor;
|
||
* net_* → net; power_* → power; 其余 → app(App/stdout 兜底)。 */
|
||
static const char *log_group(const char *name)
|
||
{
|
||
if (!strncmp(name, "crash_", 6)) return "crash";
|
||
if (!strncmp(name, "ams_", 4)) return "ams";
|
||
if (!strncmp(name, "audio_monitor_", 14)) return "audio_monitor";
|
||
if (!strncmp(name, "net_", 4)) return "net";
|
||
if (!strncmp(name, "power_", 6)) return "power";
|
||
if (!strncmp(name, "meow_", 5)) return "system";
|
||
return "app";
|
||
}
|
||
|
||
static int api_logs_list(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
DIR *d = opendir(LOGS_DIR);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"dir\":");
|
||
sA_str(&s, LOGS_DIR);
|
||
sA(&s, ",\"files\":[");
|
||
int first = 1;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
if (is_sensitive_name(e->d_name)) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "{\"name\":");
|
||
sA_str(&s, e->d_name);
|
||
sA(&s, ",\"size\":%lld,\"mtime\":%lld,\"group\":", (long long)st.st_size, (long long)st.st_mtime);
|
||
sA_str(&s, log_group(e->d_name));
|
||
sA(&s, "}");
|
||
}
|
||
closedir(d);
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_logs_read(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char file[512], lines[16];
|
||
if (!qparam(r->query, "file", file, sizeof file))
|
||
return send_err(sock, 400, "BAD_REQUEST", "缺 file 参数");
|
||
int maxlines = 200;
|
||
if (qparam(r->query, "lines", lines, sizeof lines)) {
|
||
int n = atoi(lines);
|
||
if (n > 0) maxlines = n;
|
||
if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES;
|
||
}
|
||
/* 防逃逸:file 相对 LOGS_DIR,禁止 .. 与绝对路径 */
|
||
if (file[0] == '/' || strstr(file, "..")) return send_err(sock, 403, "FORBIDDEN", "文件路径逃逸");
|
||
if (is_sensitive_name(strrchr(file, '/') ? strrchr(file, '/') + 1 : file))
|
||
return send_err(sock, 403, "FORBIDDEN", "敏感文件");
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, file);
|
||
int fd = open(full, O_RDONLY);
|
||
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "日志文件不存在");
|
||
struct stat st;
|
||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
|
||
/* 逐行读,保留末尾 maxlines 行 */
|
||
const char *buf = malloc((size_t)st.st_size + 1);
|
||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||
ssize_t n = read(fd, (char *)buf, (size_t)st.st_size);
|
||
close(fd);
|
||
if (n < 0) { free((void *)buf); return send_err(sock, 500, "INTERNAL", "读日志失败"); }
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"file\":");
|
||
sA_str(&s, file);
|
||
sA(&s, ",\"lines\":[");
|
||
/* 直接扫一遍,保留末尾 maxlines 行文本 */
|
||
const char *p = buf;
|
||
const char *end = buf + n;
|
||
int total = 0, truncated = 0;
|
||
int gotlines = 0;
|
||
int linecnt = 0;
|
||
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
|
||
if (linecnt > 0 && end[-1] != '\n') linecnt++;
|
||
int startrow = linecnt - maxlines;
|
||
if (startrow < 0) startrow = 0;
|
||
if (startrow > 0) truncated = 1;
|
||
int rowidx = 0;
|
||
p = buf;
|
||
while (p < end) {
|
||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||
const char *line_end = nl ? nl : end;
|
||
if (rowidx >= startrow) {
|
||
if (gotlines) sA(&s, ",");
|
||
/* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */
|
||
sA_str_n(&s, p, (size_t)(line_end - p));
|
||
gotlines++;
|
||
}
|
||
rowidx++;
|
||
if (!nl) break;
|
||
p = nl + 1;
|
||
}
|
||
total = linecnt;
|
||
free((void *)buf);
|
||
sA(&s, "],\"total\":%d,\"truncated\":%s}", total, truncated ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---- 崩溃汇总(批2 崩溃/日志中心):解析最新 crash_<bootts>.log(meow_crash 落盘格式)
|
||
* 一行一条:`<epoch_sec> <uptime_sec> <svc|dmesg> <detail>`;svc 的 detail=`<name> exit=<code>`/`<name> signal=<sig>`,
|
||
* dmesg 的 detail=内核行原文。聚合出「每实体崩溃数 + 最近崩溃时间/信号」。只读、无副作用。 */
|
||
#define CRASH_TAIL_MAX (256u * 1024u) /* 崩溃日志单次读取上限(内存约束,不整读大文件) */
|
||
#define CRASH_ENT_MAX 64 /* 实体汇总上限 */
|
||
#define CRASH_DMESG_TAIL 5 /* dmesg 保留最近行数 */
|
||
|
||
static int api_logs_crash(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
/* 找最新 crash_*.log(按 mtime);无 → 空汇总(不 404,不谎报)。 */
|
||
DIR *d = opendir(LOGS_DIR);
|
||
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}");
|
||
char best[512] = ""; long best_mt = -1;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (strncmp(e->d_name, "crash_", 6) != 0 || e->d_name[0] == '.') continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||
if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
|
||
}
|
||
closedir(d);
|
||
if (!best[0])
|
||
return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}");
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
|
||
int fd = open(full, O_RDONLY);
|
||
struct stat st;
|
||
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { if (fd >= 0) close(fd); return send_err(sock, 500, "INTERNAL", "崩溃日志不可读"); }
|
||
off_t off = st.st_size > (off_t)CRASH_TAIL_MAX ? st.st_size - (off_t)CRASH_TAIL_MAX : 0;
|
||
size_t buflen = (size_t)(st.st_size - off);
|
||
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}"); }
|
||
if (off) (void)lseek(fd, off, SEEK_SET);
|
||
char *buf = malloc(buflen + 1);
|
||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||
ssize_t n = read(fd, buf, buflen);
|
||
close(fd);
|
||
if (n < 0) { free(buf); return send_err(sock, 500, "INTERNAL", "读崩溃日志失败"); }
|
||
buf[n] = 0;
|
||
|
||
/* 每实体聚合(svc 行);dmesg 单独计数 + 留最近几行。
|
||
* 有界缓冲(det ≤ 127 字符)保证 snprintf 到 name/detail 不越界且 -Wformat-truncation 零告警。 */
|
||
struct { char name[128]; int count; long long last_epoch; char kind[8]; int code; char detail[160]; } ents[CRASH_ENT_MAX];
|
||
int entn = 0, svc_count = 0, dmesg_count = 0;
|
||
char dmesg_tail[CRASH_DMESG_TAIL][128]; int dmesg_tail_n = 0;
|
||
char *ln = buf;
|
||
while (*ln && (*ln == '\n')) ln++;
|
||
while (*ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
long long epoch = 0, up = 0; char src[16] = "", det[128] = "";
|
||
int f = sscanf(ln, "%lld %lld %15s %127[^\n]", &epoch, &up, src, det);
|
||
if (f >= 3) {
|
||
if (!strcmp(src, "svc")) {
|
||
svc_count++;
|
||
/* det = `<name> exit=<code>` 或 `<name> signal=<sig>`;name 为第一 token(就地切分) */
|
||
char name[128] = "", kind[8] = ""; int code = 0, k2 = 0;
|
||
char *sp = strchr(det, ' ');
|
||
if (sp) {
|
||
*sp = 0;
|
||
snprintf(name, sizeof name, "%s", det); /* det ≤127 → name[128] 不截断 */
|
||
char *kp = sp + 1;
|
||
if (!strncmp(kp, "exit=", 5)) { snprintf(kind, sizeof kind, "exit"); code = atoi(kp + 5); k2 = 1; }
|
||
else if (!strncmp(kp, "signal=", 7)) { snprintf(kind, sizeof kind, "sig"); code = atoi(kp + 7); k2 = 1; }
|
||
else { snprintf(kind, sizeof kind, "crash"); k2 = 0; }
|
||
} else {
|
||
snprintf(name, sizeof name, "%s", det);
|
||
snprintf(kind, sizeof kind, "crash");
|
||
}
|
||
/* 聚合到 ents(按 name 匹配;超限则丢弃剩余陌生实体,仅计入总数) */
|
||
int found = -1;
|
||
for (int i = 0; i < entn; i++) if (!strcmp(ents[i].name, name)) { found = i; break; }
|
||
if (found < 0 && entn < CRASH_ENT_MAX) {
|
||
found = entn++;
|
||
snprintf(ents[found].name, sizeof ents[found].name, "%s", name);
|
||
ents[found].count = 0; ents[found].last_epoch = 0;
|
||
ents[found].kind[0] = 0; ents[found].code = 0; ents[found].detail[0] = 0;
|
||
}
|
||
if (found >= 0) {
|
||
ents[found].count++;
|
||
if (epoch >= ents[found].last_epoch) {
|
||
ents[found].last_epoch = epoch;
|
||
snprintf(ents[found].kind, sizeof ents[found].kind, "%s", k2 ? kind : "crash");
|
||
ents[found].code = code;
|
||
snprintf(ents[found].detail, sizeof ents[found].detail, "%s", det);
|
||
}
|
||
}
|
||
} else if (!strcmp(src, "dmesg")) {
|
||
dmesg_count++;
|
||
if (dmesg_tail_n < CRASH_DMESG_TAIL) snprintf(dmesg_tail[dmesg_tail_n++], sizeof dmesg_tail[0], "%s", det);
|
||
else { /* 前移一行,末尾放最新 */ for (int i = 0; i < dmesg_tail_n - 1; i++) memcpy(dmesg_tail[i], dmesg_tail[i + 1], sizeof dmesg_tail[0]); snprintf(dmesg_tail[dmesg_tail_n - 1], sizeof dmesg_tail[0], "%s", det); }
|
||
}
|
||
}
|
||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||
while (*ln == '\n') ln++;
|
||
}
|
||
free(buf);
|
||
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"file\":");
|
||
sA_str(&s, best);
|
||
sA(&s, ",\"count\":%d,\"dmesg_count\":%d,\"dmesg_tail\":[", svc_count, dmesg_count);
|
||
for (int i = 0; i < dmesg_tail_n; i++) { if (i) sA(&s, ","); sA_str(&s, dmesg_tail[i]); }
|
||
sA(&s, "],\"entities\":[");
|
||
int first = 1;
|
||
for (int i = 0; i < entn; i++) {
|
||
if (!ents[i].count) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "{\"name\":");
|
||
sA_str(&s, ents[i].name);
|
||
sA(&s, ",\"source\":\"svc\",\"count\":%d,\"last_epoch\":%lld,\"last_kind\":", ents[i].count, ents[i].last_epoch);
|
||
sA_str(&s, ents[i].kind[0] ? ents[i].kind : "crash");
|
||
sA(&s, ",\"last_code\":%d,\"last_detail\":", ents[i].code);
|
||
sA_str(&s, ents[i].detail);
|
||
/* 最近崩溃时间(epoch → 可读);strftime 本地时区,失败回退 epoch */
|
||
char tbuf[32] = ""; time_t t = (time_t)ents[i].last_epoch;
|
||
struct tm tmv;
|
||
if (ents[i].last_epoch > 0 && localtime_r(&t, &tmv)) strftime(tbuf, sizeof tbuf, "%Y-%m-%d %H:%M:%S", &tmv);
|
||
sA(&s, ",\"last_time\":");
|
||
sA_str(&s, tbuf);
|
||
sA(&s, "}");
|
||
}
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 通知(数据源 = 优先 launcher 内存活跃集,回退 TF 归档) ----------------
|
||
* 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务):
|
||
* 1) launcher IPC 内存活跃集(实时)—— `MSG_NOTIFY_QUERY`(0x25)→`MSG_NOTIFY_QUERY_RESULT`(0x27)
|
||
* (launcher `util.c` `case MSG_NOTIFY_QUERY`,**主循环单写者**序列化;agent-199 已落地)。
|
||
* 2) IPC 不可达/超时/应答失败 → 回退 TF 卡通知转储文件 `<卡根>/MeowOS/notifications/notifications.log`:
|
||
* launcher 把每条通知归档时追加一行 CSV(meow_notif.c tf_line:CSV 追加 + 10MB 滚动,开机清空)。
|
||
* 字段顺序:timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,
|
||
* ongoing,media,dismissed(前 6/7/11/12/13 为裸整数,其余引号包裹 + 内部 " 翻倍)。
|
||
* 注意:**归档发生在置 dismissed=1 之前(meow_notif_dismiss 先 archive_one 后置 dismissed)**,
|
||
* 故日志的 dismissed 列恒 ≈0(不可靠),且日志是「会话级归档」,非活跃快照(回退时为此语义)。
|
||
* 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},**不伪造数据**。
|
||
* 撤/清(delete / dismiss_all)同样**优先走 IPC**联动 launcher 内存(launcher 撤下时自动归档留档),
|
||
* IPC 不可达时回退重写 TF 日志。clear(dismiss_all)语义:保留活动中的媒体/进度通知
|
||
* (ongoing||media||progress>=0||category=="media"),其余移除。 */
|
||
typedef struct {
|
||
long long ms; /* created_ms(epoch ms) */
|
||
unsigned id;
|
||
char app_id[40];
|
||
char app_icon[64];
|
||
char category[40];
|
||
int urgency;
|
||
int progress; /* -1=无 */
|
||
char summary[96];
|
||
char body[128];
|
||
char target[64];
|
||
int ongoing, media, dismissed;
|
||
} notif_rec_t;
|
||
|
||
/* 取一个 CSV 字段(引号包裹 → 解转义;裸字段 → 原样;末后跳过 ',')。返回 0。 */
|
||
static int notif_csv_field(const char **pp, char *out, int outcap)
|
||
{
|
||
const char *p = *pp;
|
||
int o = 0;
|
||
if (*p == '"') {
|
||
p++;
|
||
while (*p && *p != '"') {
|
||
if (*p == '"' && p[1] == '"') {
|
||
if (o < outcap - 1) out[o++] = '"';
|
||
p += 2;
|
||
} else {
|
||
if (o < outcap - 1) out[o++] = *p;
|
||
p++;
|
||
}
|
||
}
|
||
if (*p == '"') p++;
|
||
} else {
|
||
while (*p && *p != ',' && *p != '\n' && *p != '\r') {
|
||
if (o < outcap - 1) out[o++] = *p;
|
||
p++;
|
||
}
|
||
}
|
||
if (o < outcap) { out[o] = 0; }
|
||
else if (outcap > 0) { out[outcap - 1] = 0; }
|
||
if (*p == ',') p++;
|
||
*pp = p;
|
||
return 0;
|
||
}
|
||
|
||
static int notif_parse_line(const char *line, notif_rec_t *r)
|
||
{
|
||
const char *p = line;
|
||
char f1[32], f2[16], f6[8], f7[16], f11[8], f12[8], f13[8];
|
||
char app[40], icon[64], cat[40], sum[96], body[128], tgt[64];
|
||
notif_csv_field(&p, f1, sizeof f1);
|
||
notif_csv_field(&p, f2, sizeof f2);
|
||
notif_csv_field(&p, app, sizeof app);
|
||
notif_csv_field(&p, icon, sizeof icon);
|
||
notif_csv_field(&p, cat, sizeof cat);
|
||
notif_csv_field(&p, f6, sizeof f6);
|
||
notif_csv_field(&p, f7, sizeof f7);
|
||
notif_csv_field(&p, sum, sizeof sum);
|
||
notif_csv_field(&p, body, sizeof body);
|
||
notif_csv_field(&p, tgt, sizeof tgt);
|
||
notif_csv_field(&p, f11, sizeof f11);
|
||
notif_csv_field(&p, f12, sizeof f12);
|
||
notif_csv_field(&p, f13, sizeof f13);
|
||
r->ms = strtoll(f1, NULL, 10);
|
||
r->id = (unsigned)strtoul(f2, NULL, 10);
|
||
r->urgency = atoi(f6);
|
||
r->progress = atoi(f7);
|
||
r->ongoing = atoi(f11);
|
||
r->media = atoi(f12);
|
||
r->dismissed = atoi(f13);
|
||
snprintf(r->app_id, sizeof r->app_id, "%s", app);
|
||
snprintf(r->app_icon, sizeof r->app_icon, "%s", icon);
|
||
snprintf(r->category, sizeof r->category, "%s", cat);
|
||
snprintf(r->summary, sizeof r->summary, "%s", sum);
|
||
snprintf(r->body, sizeof r->body, "%s", body);
|
||
snprintf(r->target, sizeof r->target, "%s", tgt);
|
||
return 0;
|
||
}
|
||
|
||
/* 读通知日志,保留最新 maxrec 条(环形缓冲,避免全量进内存)。返回条数;-1=文件不存在/无卡。 */
|
||
static int notif_load_list(notif_rec_t *recs, int maxrec)
|
||
{
|
||
FILE *f = fopen(g_notif_log, "r");
|
||
if (!f) return -1;
|
||
int total = 0, head = 0;
|
||
char line[1400];
|
||
notif_rec_t r;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (notif_parse_line(line, &r) != 0) continue;
|
||
recs[head] = r;
|
||
head = (head + 1) % maxrec;
|
||
if (total < maxrec) total++;
|
||
}
|
||
fclose(f);
|
||
if (total < maxrec) return total; /* 未满窗:recs[0..total-1] 即 oldest..newest */
|
||
/* 满窗:oldest 在 recs[head],重排为 oldest..newest 连续 */
|
||
notif_rec_t tmp[NOTIF_PREVIEW_MAX];
|
||
for (int i = 0; i < maxrec; i++) tmp[i] = recs[(head + i) % maxrec];
|
||
memcpy(recs, tmp, (size_t)maxrec * sizeof recs[0]);
|
||
return total;
|
||
}
|
||
|
||
/* app_id → 展示名("system" → "系统";其余用 app_id 兜底,前端可回退)。 */
|
||
static const char *notif_app_name(const char *app_id)
|
||
{
|
||
if (!strcmp(app_id, "system")) return "系统";
|
||
return app_id;
|
||
}
|
||
|
||
static void notif_emit_item(sbuf_t *out, const notif_rec_t *r)
|
||
{
|
||
sA(out, "{\"id\":%u,\"app_id\":", r->id);
|
||
sA_str(out, r->app_id);
|
||
sA(out, ",\"app_name\":");
|
||
sA_str(out, notif_app_name(r->app_id));
|
||
sA(out, ",\"app_icon\":");
|
||
sA_str(out, r->app_icon);
|
||
sA(out, ",\"category\":");
|
||
sA_str(out, r->category);
|
||
sA(out, ",\"summary\":");
|
||
sA_str(out, r->summary);
|
||
sA(out, ",\"body\":");
|
||
sA_str(out, r->body);
|
||
sA(out, ",\"urgency\":%d,\"ts\":%lld,\"progress\":", r->urgency, r->ms > 0 ? r->ms / 1000 : 0);
|
||
if (r->progress >= 0) sA(out, "%d", r->progress); else sA(out, "null");
|
||
sA(out, ",\"ongoing\":%s,\"media\":%s,\"target_activity\":",
|
||
r->ongoing ? "true" : "false", r->media ? "true" : "false");
|
||
sA_str(out, r->target);
|
||
sA(out, ",\"dismissed\":%s}", r->dismissed ? "true" : "false");
|
||
}
|
||
|
||
/* ---------------- 通知实时化:launcher IPC 活跃集 ----------------
|
||
* 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务):
|
||
* 1) launcher IPC 内存活跃集(实时)—— `MSG_NOTIFY_QUERY`(0x25)→`MSG_NOTIFY_QUERY_RESULT`(0x27),
|
||
* JSON 字段同 TF 归档(id/app_id/category/summary/body/urgency/ts(ms)/progress/ongoing/media/
|
||
* target_activity/app_icon)。launcher 在**主循环单写者**序列化(绝不把通知存储读到 IPC 线程)。
|
||
* 2) IPC 不可达/超时/应答失败 → 回退 TF 归档 `notifications.log`(保留旧行为,见 §5.8)。
|
||
* 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},不伪造数据。
|
||
* 撤/清(delete / dismiss_all)同样**优先走 IPC**联动 launcher 内存(launcher 撤下时自动归档留档),
|
||
* IPC 不可达时回退重写 TF 日志。 */
|
||
|
||
/* 发起 MSG_NOTIFY_QUERY(空 payload=全部 app),把 launcher 返回的 JSON 数组解析为 recs[]。
|
||
* 返回条数(>=0,按 ms 升序 oldest..newest,与 TF 路径一致);-1=IPC 不可达/应答失败/解析失败。
|
||
* 注:launcher 按固定 slot 表顺序序列化,非时间序,故此处按 ms 升序重排后再返回。 */
|
||
static int notif_ipc_query(notif_rec_t *recs, int maxrec)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return -1;
|
||
int cnt = -1;
|
||
if (ipc_hello(fd) < 0) goto done;
|
||
if (send_frame(fd, MSG_NOTIFY_QUERY, 8, NULL, 0) < 0) goto done;
|
||
uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_notify_query_result_t)) goto done;
|
||
if (type != MSG_NOTIFY_QUERY_RESULT) goto done;
|
||
m_notify_query_result_t qr;
|
||
memcpy(&qr, buf, sizeof qr);
|
||
if (qr.code != MEOW_OK) goto done;
|
||
uint16_t tl = qr.text_len;
|
||
if (tl >= sizeof qr.text) tl = (uint16_t)(sizeof qr.text - 1);
|
||
if (tl == 0) { cnt = 0; goto done; } /* 无活跃通知 */
|
||
jv_t *arr = jv_parse(qr.text, tl);
|
||
if (!arr) goto done;
|
||
if (arr->type != J_ARR) { jv_free(arr); goto done; }
|
||
int c = 0;
|
||
for (jv_t *e = arr->child; e && c < maxrec; e = e->next) {
|
||
if (e->type != J_OBJ) continue;
|
||
notif_rec_t r; memset(&r, 0, sizeof r);
|
||
r.progress = -1;
|
||
r.urgency = 0;
|
||
jv_t *v;
|
||
if ((v = jv_obj_get(e, "id")) && v->type == J_NUM) r.id = (unsigned)v->inum;
|
||
if ((v = jv_obj_get(e, "app_id")) && v->type == J_STR) snprintf(r.app_id, sizeof r.app_id, "%s", v->str);
|
||
if ((v = jv_obj_get(e, "app_icon")) && v->type == J_STR) snprintf(r.app_icon, sizeof r.app_icon, "%s", v->str);
|
||
if ((v = jv_obj_get(e, "category")) && v->type == J_STR) snprintf(r.category, sizeof r.category, "%s", v->str);
|
||
if ((v = jv_obj_get(e, "summary")) && v->type == J_STR) snprintf(r.summary, sizeof r.summary, "%s", v->str);
|
||
if ((v = jv_obj_get(e, "body")) && v->type == J_STR) snprintf(r.body, sizeof r.body, "%s", v->str);
|
||
if ((v = jv_obj_get(e, "target_activity")) && v->type == J_STR) snprintf(r.target, sizeof r.target, "%s", v->str);
|
||
if ((v = jv_obj_get(e, "urgency")) && v->type == J_NUM) r.urgency = (int)v->inum;
|
||
/* ts 为 epoch 毫秒 → 存入 r.ms(emit 时 /1000 出秒,与 TF 同口径) */
|
||
if ((v = jv_obj_get(e, "ts")) && v->type == J_NUM) r.ms = (long long)v->inum;
|
||
if ((v = jv_obj_get(e, "progress")) && v->type == J_NUM) r.progress = (int)v->inum; /* <0=无;json 无该键则 -1 */
|
||
if ((v = jv_obj_get(e, "ongoing")) && v->type == J_NUM) r.ongoing = (int)v->inum;
|
||
if ((v = jv_obj_get(e, "media")) && v->type == J_NUM) r.media = (int)v->inum;
|
||
recs[c++] = r;
|
||
}
|
||
jv_free(arr);
|
||
/* 按 ms 升序(oldest..newest),与 TF 路径一致(emit 时倒序遍历= newest first)。 */
|
||
for (int i = 1; i < c; i++) { notif_rec_t key = recs[i]; int j = i - 1;
|
||
while (j >= 0 && recs[j].ms > key.ms) { recs[j + 1] = recs[j]; j--; } recs[j + 1] = key; }
|
||
cnt = c;
|
||
done:
|
||
close(fd);
|
||
return cnt;
|
||
}
|
||
|
||
/* 撤下单条通知 → MSG_NOTIFY_DISMISS(0x22),期望 MSG_ACK。返回 0=受理;-1=IPC 不可达/应答失败/未受理。 */
|
||
static int notif_ipc_dismiss(unsigned id)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return -1;
|
||
int rc = -1;
|
||
if (ipc_hello(fd) < 0) goto done;
|
||
uint32_t nid = (uint32_t)id;
|
||
if (send_frame(fd, MSG_NOTIFY_DISMISS, 9, &nid, sizeof nid) < 0) goto done;
|
||
uint8_t buf[64];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_ack_t)) goto done;
|
||
if (type != MSG_ACK) goto done;
|
||
m_ack_t a; memcpy(&a, buf, sizeof a);
|
||
rc = (a.code == MEOW_OK) ? 0 : -1;
|
||
done:
|
||
close(fd);
|
||
return rc;
|
||
}
|
||
|
||
/* 一键清除 → MSG_NOTIFY_DISMISS_ALL(0x26),期望 MSG_ACK(status=清除数,保留 ongoing/media/progress)。
|
||
* 返回 0=受理(*out_count 填清除数);-1=IPC 不可达/应答失败。 */
|
||
static int notif_ipc_dismiss_all(int *out_count)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return -1;
|
||
int rc = -1;
|
||
if (ipc_hello(fd) < 0) goto done;
|
||
if (send_frame(fd, MSG_NOTIFY_DISMISS_ALL, 10, NULL, 0) < 0) goto done;
|
||
uint8_t buf[64];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_ack_t)) goto done;
|
||
if (type != MSG_ACK) goto done;
|
||
m_ack_t a; memcpy(&a, buf, sizeof a);
|
||
if (a.code == MEOW_OK) { if (out_count) *out_count = (int)a.status; rc = 0; }
|
||
done:
|
||
close(fd);
|
||
return rc;
|
||
}
|
||
|
||
static int api_notifications(int sock)
|
||
{
|
||
notif_rec_t recs[NOTIF_PREVIEW_MAX];
|
||
const char *source = "ipc";
|
||
int n = notif_ipc_query(recs, NOTIF_PREVIEW_MAX);
|
||
if (n < 0) { /* IPC 不可达 → 回退 TF 归档 */
|
||
n = notif_load_list(recs, NOTIF_PREVIEW_MAX);
|
||
source = (n < 0) ? "none" : "tf";
|
||
if (n < 0) n = 0;
|
||
}
|
||
sbuf_t arr = {0};
|
||
int first = 1;
|
||
for (int i = n - 1; i >= 0; i--) { /* newest first */
|
||
if (!first) sA(&arr, ",");
|
||
first = 0;
|
||
notif_emit_item(&arr, &recs[i]);
|
||
}
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"count\":%d,\"source\":\"%s\",\"items\":[", n, source);
|
||
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||
sA(&s, "],\"notifications\":[");
|
||
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||
sA(&s, "]}");
|
||
sbuf_free(&arr);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 重写通知日志。mode=1:保留活动媒体/进度(ongoing||media||progress>=0||category=="media"),
|
||
* 其余删除(dismiss_all 语义,对应通知中心既定语义);mode=0:删除指定 id(单条删除)。
|
||
* 返回删除行数;-1=IO 错;0=无文件(无可删)。写临时文件后 rename(避免与 launcher append 竞写)。 */
|
||
static int notif_rewrite(int mode, unsigned del_id)
|
||
{
|
||
if (!g_notif_log || !g_notif_log[0]) return -1;
|
||
char tmppath[1400];
|
||
snprintf(tmppath, sizeof tmppath, "%s.panel.tmp", g_notif_log);
|
||
FILE *in = fopen(g_notif_log, "r");
|
||
if (!in) return 0;
|
||
FILE *out = fopen(tmppath, "w");
|
||
if (!out) { fclose(in); return -1; }
|
||
int removed = 0;
|
||
char line[1400];
|
||
while (fgets(line, sizeof line, in)) {
|
||
notif_rec_t r;
|
||
int parsed = (notif_parse_line(line, &r) == 0);
|
||
int drop = 0;
|
||
if (mode == 1) {
|
||
if (!parsed) drop = 1;
|
||
else drop = !(r.ongoing || r.media || r.progress >= 0 || !strcmp(r.category, "media"));
|
||
} else {
|
||
if (parsed && r.id == del_id) drop = 1;
|
||
}
|
||
if (drop) removed++;
|
||
else fputs(line, out);
|
||
}
|
||
fclose(in);
|
||
if (fclose(out) != 0) return -1;
|
||
if (rename(tmppath, g_notif_log) < 0) { unlink(tmppath); return -1; }
|
||
return removed;
|
||
}
|
||
|
||
static int api_notification_delete(int sock, unsigned id)
|
||
{
|
||
/* 优先 IPC 联动 launcher 内存(launcher 撤下时自动归档留档);IPC 不可达/未受理回退 TF 重写。 */
|
||
if (notif_ipc_dismiss(id) == 0) {
|
||
char d[64];
|
||
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
|
||
return send_json(sock, 200, d);
|
||
}
|
||
int removed = notif_rewrite(0, id);
|
||
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||
if (removed == 0) return send_err(sock, 404, "NOT_FOUND", "通知不存在");
|
||
char d[64];
|
||
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
|
||
return send_json(sock, 200, d);
|
||
}
|
||
|
||
static int api_notification_dismiss_all(int sock)
|
||
{
|
||
int cleared = 0;
|
||
if (notif_ipc_dismiss_all(&cleared) == 0) { /* 联动 launcher 内存,返回清除数 */
|
||
char d[64];
|
||
snprintf(d, sizeof d, "{\"dismissed\":%d}", cleared);
|
||
return send_json(sock, 200, d);
|
||
}
|
||
int removed = notif_rewrite(1, 0); /* IPC 不可达 → 回退重写 TF 日志 */
|
||
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||
char d[64];
|
||
snprintf(d, sizeof d, "{\"dismissed\":%d}", removed);
|
||
return send_json(sock, 200, d);
|
||
}
|
||
|
||
/* ---------------- 文件操作 ---------------- */
|
||
static int api_files_list(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024] = "/";
|
||
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
|
||
return files_list(sock, path);
|
||
}
|
||
|
||
static int api_files_read(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024];
|
||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||
char abs[1024];
|
||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
long long size; int trunc;
|
||
char *txt = read_text_file_limited(abs, &size, &trunc);
|
||
if (txt == (char *)-2) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "二进制文件不支持文本读取");
|
||
if (!txt) return send_err(sock, 404, "NOT_FOUND", "文件不存在或过大");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"path\":");
|
||
sA_str(&s, abs);
|
||
sA(&s, ",\"content\":");
|
||
sA_str(&s, txt);
|
||
sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false");
|
||
free(txt);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_files_download(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024];
|
||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||
char abs[1024];
|
||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
const char *base = strrchr(abs, '/');
|
||
base = base ? base + 1 : abs;
|
||
if (is_sensitive_name(base)) return send_err(sock, 403, "FORBIDDEN", "敏感文件");
|
||
return send_file(sock, 200, "application/octet-stream", abs, base);
|
||
}
|
||
|
||
static int api_files_upload(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0)
|
||
return send_err(sock, 415, "UNSUPPORTED_MEDIA", "需 multipart/form-data");
|
||
upfile_t files[16];
|
||
memset(files, 0, sizeof files);
|
||
int n = parse_multipart(r, files, 16);
|
||
if (n < 0) return send_err(sock, 400, "BAD_REQUEST", "multipart 解析失败");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"item\":[");
|
||
int first = 1;
|
||
for (int i = 0; i < n && i < 16; i++) {
|
||
if (!files[i].filename[0]) continue;
|
||
if (!strstr(files[i].path, "/")) continue; /* 无目标目录的跳过 */
|
||
struct stat st;
|
||
if (stat(files[i].path, &st) < 0) continue;
|
||
const char *base = strrchr(files[i].path, '/');
|
||
base = base ? base + 1 : files[i].path;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "{\"name\":");
|
||
sA_str(&s, base);
|
||
sA(&s, ",\"type\":\"file\",\"size\":%lld,\"mtime\":%lld}",
|
||
(long long)st.st_size, (long long)st.st_mtime);
|
||
}
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 递归删除目录/文件。 */
|
||
static int rm_recursive(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (lstat(path, &st) < 0) return -1;
|
||
if (S_ISDIR(st.st_mode)) {
|
||
DIR *d = opendir(path);
|
||
if (!d) return -1;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", path, e->d_name);
|
||
rm_recursive(full);
|
||
}
|
||
closedir(d);
|
||
return rmdir(path);
|
||
}
|
||
return unlink(path);
|
||
}
|
||
|
||
static int api_files_mkdir(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
if (!pj || pj->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path"); }
|
||
char abs[1024];
|
||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||
struct stat st;
|
||
if (stat(abs, &st) == 0) { jv_free(root); return send_err(sock, 409, "CONFLICT", "目录已存在"); }
|
||
if (mkdir(abs, 0755) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "创建目录失败"); }
|
||
const char *base = strrchr(abs, '/');
|
||
const char *b = base ? base + 1 : abs;
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"item\":{\"name\":");
|
||
sA_str(&s, b);
|
||
sA(&s, ",\"type\":\"dir\",\"size\":0,\"mtime\":%lld}}", (long long)time(NULL));
|
||
jv_free(root);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_files_rename(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
jv_t *pnn = jv_obj_get(root, "new_name");
|
||
if (!pj || pj->type != J_STR || !pnn || pnn->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path/new_name"); }
|
||
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
|
||
char abs[1024], newabs[1400];
|
||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||
/* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */
|
||
char parent[1024];
|
||
snprintf(parent, sizeof parent, "%s", abs);
|
||
char *sl = strrchr(parent, '/');
|
||
if (!sl || sl == parent) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "无法重命名根路径"); }
|
||
*sl = 0;
|
||
if (!parent[0]) snprintf(parent, sizeof parent, "%s", TF_ROOT); /* abs 顶层(如 /tmp/mptf/x)→ 父=根 */
|
||
snprintf(newabs, sizeof newabs, "%s/%s", parent, pnn->str);
|
||
/* newabs 复校白名单(new_name 无 /,父目录已校验;防御) */
|
||
if (!path_in_whitelist(newabs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"item\":{\"name\":");
|
||
sA_str(&s, pnn->str);
|
||
sA(&s, ",\"type\":\"");
|
||
struct stat st;
|
||
if (stat(newabs, &st) == 0 && S_ISDIR(st.st_mode)) sA(&s, "dir"); else sA(&s, "file");
|
||
sA(&s, "\",\"size\":%lld,\"mtime\":%lld}}", (long long)st.st_size, (long long)st.st_mtime);
|
||
jv_free(root);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_files_delete(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024];
|
||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||
char recursive[8] = "0";
|
||
qparam(r->query, "recursive", recursive, sizeof recursive);
|
||
char abs[1024];
|
||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
if (!strcmp(abs, TF_ROOT)) return send_err(sock, 403, "FORBIDDEN", "禁止删除卡根");
|
||
struct stat st;
|
||
if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||
if (S_ISDIR(st.st_mode) && atoi(recursive) != 1) {
|
||
DIR *d = opendir(abs);
|
||
int nonempty = 0;
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
nonempty = 1; break;
|
||
}
|
||
closedir(d);
|
||
}
|
||
if (nonempty) return send_err(sock, 409, "CONFLICT", "目录非空(需 recursive=1)");
|
||
}
|
||
if (rm_recursive(abs) < 0) return send_err(sock, 500, "INTERNAL", "删除失败");
|
||
return send_json(sock, 200, "{\"removed\":true}");
|
||
}
|
||
|
||
/* ---------------- 字体库 tar.gz 导入 / 导出(MeowOS/fonts/<family>/<weight>/) ----------------
|
||
* 定案(用户 2026-08-31):**与 .mpk 同构,放弃 zip**——设备 busybox tar/gzip 必定存在,
|
||
* 后端全部走 `gzip -dc | tar` 管道(nyai .mpk 同款),零新增依赖、不再自实现 zip 读写/miniz。
|
||
* 导入(POST /api/fonts/import,multipart file=<.tar.gz> [&force=1]):
|
||
* - 上传 tar.gz(上限 MAX_FONT_UPLOAD=32MB,gbk/full 字库 png+idx 可能 10-20MB)→
|
||
* `gzip -dc | tar -t` 预校验成员名(sanitize_member:拒绝对路径/`..`/shell 危险字符,含三个以上条目疑拒);
|
||
* 解压到临时目录 → opendir/lstat 逐个校验:恰为 font.png/font.idx/font.json 三个**普通文件**
|
||
* (拒软链/目录/设备,拒解压后超大)→ 缺件/坏 json 硬失败。
|
||
* - family/weight 取 font.json 的 family(缺省回退 name)/weight(缺省 regular),safe_dir_segment 消毒。
|
||
* - rename 到 MeowOS/fonts/<family>/<weight>/;目标已存在则 409(前端确认后带 force=1 重试)。
|
||
* - sha256_png/sha256_idx 仅 WARN(含在响应 warning 里);导入后**不触发 launcher 重扫**
|
||
* (扫描时机=启动/进设置页,勿加热路径),响应用 hint 提示。
|
||
* 导出(GET /api/fonts/export?family=&weight=):
|
||
* - `tar -cf - -C <font_dir> font.png font.idx font.json | gzip -c > <tmp>` 打包为
|
||
* `<family>-<weight>.font.tar.gz` 下载流(先落临时文件求 Content-Length 再流式)。
|
||
* 列表(GET /api/fonts):扫描 fonts/<family>/<weight>/(三件套齐)供导出选择。
|
||
*/
|
||
#define FONT_EXTRACT_MAX (32u * 1024u * 1024u) /* 三件套解压后总大小上限(防 gzip 炸弹;gbk/full 亦在界内) */
|
||
|
||
static void fonts_root(char *buf, size_t cap) { snprintf(buf, cap, "%s/MeowOS/fonts", TF_ROOT); }
|
||
|
||
static int safe_dir_segment(const char *s, char *out, size_t cap)
|
||
{
|
||
if (!s || !s[0]) return -1;
|
||
if (!strcmp(s, ".") || !strcmp(s, "..")) return -1;
|
||
for (const char *p = s; *p; p++) {
|
||
unsigned char c = (unsigned char)*p;
|
||
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1;
|
||
}
|
||
if (strlen(s) + 1 > cap) return -1;
|
||
snprintf(out, cap, "%s", s);
|
||
return 0;
|
||
}
|
||
|
||
/* ---- tar 成员 sanitize(沿用 nyai 语义:拒绝对路径/`..` 组件/shell 危险字符) ---- */
|
||
static int unsafe_path(const char *s)
|
||
{
|
||
return s && strpbrk(s, " ;`$'\"&|<>") != NULL;
|
||
}
|
||
static int has_dotdot_comp(const char *p)
|
||
{
|
||
const char *s = p;
|
||
while (*s) {
|
||
if (s[0] == '.' && s[1] == '.' &&
|
||
(s == p || s[-1] == '/') && (s[2] == 0 || s[2] == '/'))
|
||
return 1;
|
||
s++;
|
||
}
|
||
return 0;
|
||
}
|
||
/* 校验并相对化为平铺成员名:去前导 '/' 与重复 './',禁绝对/`..`/危险字符。返回 0=ok 写 out。 */
|
||
static int sanitize_member(const char *in, char *out, int n)
|
||
{
|
||
if (!in || !*in) return -1;
|
||
const char *p = in;
|
||
while (*p == '/' || (p[0] == '.' && p[1] == '/')) p++;
|
||
if (*p == '/' || !*p) return -1;
|
||
if (has_dotdot_comp(p)) return -1;
|
||
if (unsafe_path(p)) return -1;
|
||
size_t l = strlen(p);
|
||
if (l + 1 > (size_t)n) return -1;
|
||
memcpy(out, p, l + 1); /* memcpy 而非 snprintf:p 可能来自长行缓冲,防 -Wformat-truncation 误报 */
|
||
return 0;
|
||
}
|
||
/* 递归建目录(mkdir 系统调用;已存在 EEXIST 忽略)。 */
|
||
static int mkdir_p(const char *path)
|
||
{
|
||
char buf[1600];
|
||
snprintf(buf, sizeof buf, "%s", path);
|
||
for (char *p = buf + 1; *p; p++) {
|
||
if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; }
|
||
}
|
||
return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1;
|
||
}
|
||
|
||
/* 逐一手写从 font.json 取字段(family/weight/name/tier/glyph_count/sha256_png/sha256_idx)。
|
||
* 返回 0=ok;*pf 指向解析出的 J_STR(供复制)。 */
|
||
static const jv_t *font_json_str(const jv_t *root, const char *key)
|
||
{
|
||
const jv_t *v = jv_obj_get(root, key);
|
||
if (v && v->type == J_STR && v->str) return v;
|
||
return NULL;
|
||
}
|
||
|
||
/* 得到已装载字体库(family/weight 三级扫描 + 三件套齐)JSON 数组。 */
|
||
static int api_fonts_list(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char root[1100];
|
||
fonts_root(root, sizeof root);
|
||
DIR *fd = opendir(root);
|
||
if (!fd) return send_json(sock, 200, "{\"fonts\":[]}");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"fonts\":[");
|
||
int first = 1;
|
||
struct dirent *fe;
|
||
while ((fe = readdir(fd))) {
|
||
if (fe->d_name[0] == '.') continue;
|
||
char famdir[1700];
|
||
snprintf(famdir, sizeof famdir, "%s/%s", root, fe->d_name);
|
||
struct stat fst;
|
||
if (stat(famdir, &fst) < 0 || !S_ISDIR(fst.st_mode)) continue;
|
||
DIR *wd = opendir(famdir);
|
||
if (!wd) continue;
|
||
struct dirent *we;
|
||
while ((we = readdir(wd))) {
|
||
if (we->d_name[0] == '.') continue;
|
||
char wdir[2200];
|
||
snprintf(wdir, sizeof wdir, "%s/%s", famdir, we->d_name);
|
||
struct stat wst;
|
||
if (stat(wdir, &wst) < 0 || !S_ISDIR(wst.st_mode)) continue;
|
||
char pp[2560], pi[2560], pj[2560];
|
||
snprintf(pp, sizeof pp, "%s/font.png", wdir);
|
||
snprintf(pi, sizeof pi, "%s/font.idx", wdir);
|
||
snprintf(pj, sizeof pj, "%s/font.json", wdir);
|
||
if (access(pp, R_OK) || access(pi, R_OK) || access(pj, R_OK)) continue;
|
||
/* 解析 font.json(可选增强字段) */
|
||
char name[256] = "", tier[64] = "";
|
||
long long gc = 0;
|
||
char jbuf[4096];
|
||
if (read_small_file(pj, jbuf, sizeof jbuf) >= 0) {
|
||
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
|
||
if (jr) {
|
||
const jv_t *v = font_json_str(jr, "name");
|
||
if (v) snprintf(name, sizeof name, "%s", v->str);
|
||
v = font_json_str(jr, "tier");
|
||
if (v) snprintf(tier, sizeof tier, "%s", v->str);
|
||
else { v = font_json_str(jr, "charset"); if (v) snprintf(tier, sizeof tier, "%s", v->str); }
|
||
v = jv_obj_get(jr, "glyph_count");
|
||
if (v && v->type == J_NUM) gc = v->inum;
|
||
jv_free(jr);
|
||
}
|
||
}
|
||
if (!name[0]) snprintf(name, sizeof name, "%s", fe->d_name);
|
||
if (!first) sA(&s, ",");
|
||
sA(&s, "{\"family\":");
|
||
sA_str(&s, fe->d_name);
|
||
sA(&s, ",\"weight\":");
|
||
sA_str(&s, we->d_name);
|
||
sA(&s, ",\"name\":");
|
||
sA_str(&s, name);
|
||
sA(&s, ",\"tier\":");
|
||
sA_str(&s, tier);
|
||
sA(&s, ",\"glyph_count\":%lld,\"mtime\":%lld}", gc, (long long)wst.st_mtime);
|
||
first = 0;
|
||
}
|
||
closedir(wd);
|
||
}
|
||
closedir(fd);
|
||
sA(&s, "]}");
|
||
return send_json(sock, 200, s.b ? s.b : "{\"fonts\":[]}");
|
||
}
|
||
|
||
/* 在 multipart body 中定位 name="file" 字段数据区间(r->body 内)。 */
|
||
/* 对文件算 sha256 hex(64 字符,流式,不整读进内存)。返回 0=ok;-1=打开/读失败。 */
|
||
static int sha256_file_hex(const char *path, char out[65])
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
mp_sha256_ctx c;
|
||
mp_sha256_init(&c);
|
||
uint8_t buf[8192];
|
||
ssize_t n;
|
||
while ((n = read(fd, buf, sizeof buf)) > 0) mp_sha256_update(&c, buf, (size_t)n);
|
||
close(fd);
|
||
if (n < 0) return -1;
|
||
uint8_t h[32];
|
||
mp_sha256_final(&c, h);
|
||
auth_hex(h, sizeof h, out);
|
||
out[64] = 0;
|
||
return 0;
|
||
}
|
||
|
||
static int api_fonts_import(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char force[8] = "0";
|
||
qparam(r->query, "force", force, sizeof force);
|
||
long uni = (long)time(NULL);
|
||
char tmpdir[1500], tmpgz[1500], stg[1500];
|
||
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
|
||
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
|
||
snprintf(tmpgz, sizeof tmpgz, "%s/tmp/mp_font_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
|
||
snprintf(stg, sizeof stg, "%s/tmp/mp_font_stg_%d_%ld", TF_CARD_DIR, (int)getpid(), uni);
|
||
|
||
/* ① 写 multipart file 到临时 tar.gz */
|
||
if (multipart_extract_file(r, tmpgz) < 0)
|
||
return send_err(sock, 400, "BAD_REQUEST", "缺字体库 tar.gz(multipart 字段 file)");
|
||
|
||
/* ② 成员名预校验:gzip -dc | tar -t → 每名 sanitize,且恰为三件套平铺(拒绝对/.. / 多出条目) */
|
||
{
|
||
char cmd[3200];
|
||
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null", tmpgz);
|
||
FILE *pf = popen(cmd, "r");
|
||
if (!pf) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "无法列 tar 成员"); }
|
||
int bad = 0, have_png = 0, have_idx = 0, have_json = 0, nent = 0;
|
||
char line[512];
|
||
while (fgets(line, sizeof line, pf)) {
|
||
char *e = strchr(line, '\n'); if (e) *e = 0;
|
||
if (!line[0]) continue;
|
||
char san[128];
|
||
if (sanitize_member(line, san, sizeof san) < 0) { bad = 1; break; }
|
||
if (!strcmp(san, "font.png")) have_png = 1;
|
||
else if (!strcmp(san, "font.idx")) have_idx = 1;
|
||
else if (!strcmp(san, "font.json")) have_json = 1;
|
||
else { bad = 1; break; }
|
||
nent++;
|
||
}
|
||
pclose(pf);
|
||
if (bad || nent != 3 || !have_png || !have_idx || !have_json) {
|
||
unlink(tmpgz);
|
||
return send_err(sock, 400, "BAD_REQUEST", "tar 缺三件套或含非法成员(拒绝绝对路径/../多出条目)");
|
||
}
|
||
}
|
||
|
||
/* ③ 解压到临时目录 */
|
||
rm_recursive(stg);
|
||
if (mkdir_p(stg) != 0) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "建临时目录失败"); }
|
||
{
|
||
char cmd[3200];
|
||
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null", tmpgz, stg);
|
||
if (system(cmd) != 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 解压失败(非 gzip/tar?)"); }
|
||
}
|
||
|
||
/* ④ 目录锁:恰为 3 个普通文件(lstat 拒软链/目录/设备),总大小 ≤ FONT_EXTRACT_MAX(防炸弹) */
|
||
{
|
||
static const char *names[3] = { "font.png", "font.idx", "font.json" };
|
||
int seen[3] = {0,0,0}, cnt = 0, ok = 1;
|
||
DIR *d = opendir(stg);
|
||
if (!d) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "读临时目录失败"); }
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue;
|
||
int idx = -1;
|
||
for (int i = 0; i < 3; i++) if (!strcmp(de->d_name, names[i])) { idx = i; break; }
|
||
if (idx < 0) { ok = 0; break; }
|
||
char fp[1800]; snprintf(fp, sizeof fp, "%s/%s", stg, de->d_name);
|
||
struct stat st;
|
||
if (lstat(fp, &st) != 0 || !S_ISREG(st.st_mode)) { ok = 0; break; } /* 拒软链/目录/设备 */
|
||
seen[idx] = 1; cnt++;
|
||
}
|
||
closedir(d);
|
||
long long tot = 0;
|
||
for (int i = 0; i < 3; i++) {
|
||
if (!seen[i]) { ok = 0; break; }
|
||
char fp[1600]; snprintf(fp, sizeof fp, "%s/%s", stg, names[i]);
|
||
struct stat st;
|
||
if (stat(fp, &st) != 0) { ok = 0; break; }
|
||
tot += (long long)st.st_size;
|
||
}
|
||
if (!ok || cnt != 3 || tot > (long long)FONT_EXTRACT_MAX) {
|
||
rm_recursive(stg); unlink(tmpgz);
|
||
return send_err(sock, 400, "BAD_REQUEST", "tar 内容非法(缺件/软链/多出条目/解压后超大)");
|
||
}
|
||
}
|
||
|
||
/* ⑤ 读 font.json → family/weight(可解析硬失败) */
|
||
char jpath[1600]; snprintf(jpath, sizeof jpath, "%s/font.json", stg);
|
||
char jbuf[8192];
|
||
if (read_small_file(jpath, jbuf, sizeof jbuf) < 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 缺失"); }
|
||
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
|
||
if (!jr) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 解析失败"); }
|
||
char family[128] = "font", weight[64] = "regular";
|
||
const jv_t *v = font_json_str(jr, "family");
|
||
if (v) snprintf(family, sizeof family, "%s", v->str);
|
||
else { v = font_json_str(jr, "name"); if (v) snprintf(family, sizeof family, "%s", v->str); }
|
||
v = font_json_str(jr, "weight");
|
||
if (v) snprintf(weight, sizeof weight, "%s", v->str);
|
||
char fam_s[128], wt_s[64];
|
||
if (safe_dir_segment(family, fam_s, sizeof fam_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json family 非法"); }
|
||
if (safe_dir_segment(weight, wt_s, sizeof wt_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json weight 非法"); }
|
||
snprintf(family, sizeof family, "%s", fam_s);
|
||
snprintf(weight, sizeof weight, "%s", wt_s);
|
||
|
||
/* ⑥ 目标目录 + 覆盖冲突(409,除非 force=1) */
|
||
char root[1100], target[1500];
|
||
fonts_root(root, sizeof root);
|
||
snprintf(target, sizeof target, "%s/%s/%s", root, family, weight);
|
||
if (strcmp(force, "1") != 0) {
|
||
static const char *names3[] = { "font.png", "font.idx", "font.json" };
|
||
char tp[1600]; int conflicted = 0;
|
||
for (int i = 0; i < 3; i++) { snprintf(tp, sizeof tp, "%s/%s", target, names3[i]); if (access(tp, F_OK) == 0) { conflicted = 1; break; } }
|
||
if (conflicted) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 409, "CONFLICT", "目标字库已存在,请确认覆盖"); }
|
||
}
|
||
|
||
/* ⑦ sha256 仅 WARN(对齐 font.json 约定,不硬失败) */
|
||
char warn[256] = "";
|
||
{
|
||
const jv_t *wp = font_json_str(jr, "sha256_png");
|
||
const jv_t *wi = font_json_str(jr, "sha256_idx");
|
||
char need[65], got[65];
|
||
if (wp && wp->str[0]) {
|
||
snprintf(need, sizeof need, "%s", wp->str);
|
||
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.png", stg);
|
||
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
|
||
snprintf(warn, sizeof warn, "sha256_png 不匹配(源库校验失败,但已导入)");
|
||
}
|
||
if (wi && wi->str[0] && !warn[0]) {
|
||
snprintf(need, sizeof need, "%s", wi->str);
|
||
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.idx", stg);
|
||
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
|
||
snprintf(warn, sizeof warn, "sha256_idx 不匹配(源库校验失败,但已导入)");
|
||
}
|
||
}
|
||
|
||
/* ⑧ 建目标目录 + rename 三件套(原子替换) */
|
||
{
|
||
char p1[1400];
|
||
snprintf(p1, sizeof p1, "%s/%s", root, family);
|
||
mkdir(p1, 0755);
|
||
mkdir(target, 0755);
|
||
}
|
||
{
|
||
static const char *names3[] = { "font.png", "font.idx", "font.json" };
|
||
int ok = 1;
|
||
for (int i = 0; i < 3; i++) {
|
||
char sp[1600], tp[1600];
|
||
snprintf(sp, sizeof sp, "%s/%s", stg, names3[i]);
|
||
snprintf(tp, sizeof tp, "%s/%s", target, names3[i]);
|
||
if (rename(sp, tp) < 0) ok = 0;
|
||
}
|
||
if (!ok) { rm_recursive(stg); unlink(tmpgz); jv_free(jr); return send_err(sock, 500, "INTERNAL", "写入字体文件失败(磁盘满?)"); }
|
||
}
|
||
rm_recursive(stg);
|
||
unlink(tmpgz);
|
||
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"family\":");
|
||
sA_str(&s, family);
|
||
sA(&s, ",\"weight\":");
|
||
sA_str(&s, weight);
|
||
sA(&s, ",\"imported\":true,\"hint\":\"字体已导入。请进设备「设置 → 字体」页,字体列表在进入时自动重扫生效。\",\"warning\":");
|
||
sA_str(&s, warn[0] ? warn : "");
|
||
sA(&s, "}");
|
||
jv_free(jr);
|
||
return send_json(sock, 200, s.b ? s.b : "");
|
||
}
|
||
|
||
static int api_fonts_export(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char family[128], weight[64];
|
||
if (!qparam(r->query, "family", family, sizeof family)) family[0] = 0;
|
||
if (!qparam(r->query, "weight", weight, sizeof weight)) weight[0] = 0;
|
||
char f[128], w[64];
|
||
if (safe_dir_segment(family, f, sizeof f) < 0) return send_err(sock, 400, "BAD_REQUEST", "family 非法");
|
||
if (!weight[0]) snprintf(w, sizeof w, "%s", "regular");
|
||
else if (safe_dir_segment(weight, w, sizeof w) < 0) return send_err(sock, 400, "BAD_REQUEST", "weight 非法");
|
||
char root[1100], dir[1500];
|
||
fonts_root(root, sizeof root);
|
||
snprintf(dir, sizeof dir, "%s/%s/%s", root, f, w);
|
||
static const char *names3[] = { "font.png", "font.idx", "font.json" };
|
||
for (int i = 0; i < 3; i++) {
|
||
char ap[1600];
|
||
snprintf(ap, sizeof ap, "%s/%s", dir, names3[i]);
|
||
struct stat st;
|
||
if (stat(ap, &st) < 0) {
|
||
sbuf_t s = {0};
|
||
sA(&s, "font."); sA_str(&s, names3[i]);
|
||
sA(&s, " 缺失(字库 <");
|
||
sA_str(&s, f); sA(&s, "/"); sA_str(&s, w); sA(&s, "> 不完整)");
|
||
int rc = send_err(sock, 404, "NOT_FOUND", s.b ? s.b : "字体三件套缺失");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
}
|
||
/* 打包:tar -cf - -C <dir> 三件套 | gzip -c > <tmp>,落临时文件求 Content-Length 再流式下发 */
|
||
long uni = (long)time(NULL);
|
||
char tmpdir[1500], tmp[1500];
|
||
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
|
||
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/mp_font_exp_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
|
||
char cmd[3600];
|
||
snprintf(cmd, sizeof cmd, "tar -cf - -C '%s' font.png font.idx font.json 2>/dev/null | gzip -c > '%s' 2>/dev/null", dir, tmp);
|
||
if (system(cmd) != 0) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包失败"); }
|
||
struct stat st;
|
||
if (stat(tmp, &st) < 0 || !S_ISREG(st.st_mode)) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包文件不可读"); }
|
||
char fname[220];
|
||
snprintf(fname, sizeof fname, "%s-%s.font.tar.gz", f, w);
|
||
char extra[300];
|
||
snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", fname);
|
||
if (send_head(sock, 200, "application/gzip", (long long)st.st_size, extra) < 0) { unlink(tmp); return -1; }
|
||
int fd = open(tmp, O_RDONLY);
|
||
if (fd < 0) { unlink(tmp); return -1; }
|
||
char buf[8192];
|
||
ssize_t n;
|
||
while ((n = read(fd, buf, sizeof buf)) > 0)
|
||
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); unlink(tmp); return -1; }
|
||
close(fd);
|
||
unlink(tmp);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */
|
||
/* 执行命令(sh -c),捕获 stdout(上限 cap-1),返回退出状态(-1=无法启动)。 */
|
||
static int run_cmd(const char *cmd, char *out, int outcap)
|
||
{
|
||
FILE *f = popen(cmd, "r");
|
||
if (!f) return -1;
|
||
size_t off = 0;
|
||
while (off < (size_t)outcap - 1) {
|
||
size_t rd = fread(out + off, 1, (size_t)outcap - 1 - off, f);
|
||
if (rd == 0) break;
|
||
off += rd;
|
||
}
|
||
out[off] = 0;
|
||
int st = pclose(f);
|
||
int code = -1;
|
||
if (WIFEXITED(st)) code = WEXITSTATUS(st);
|
||
return code;
|
||
}
|
||
|
||
static int api_apps_list(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
DIR *d = opendir(MEOWAPPS_DIR);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"count\":0,\"apps\":[");
|
||
int first = 1, count = 0;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue;
|
||
if (strstr(e->d_name, ".bak")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0);
|
||
count++;
|
||
}
|
||
closedir(d);
|
||
sA(&s, "]}");
|
||
/* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */
|
||
/* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
(void)count;
|
||
return rc;
|
||
}
|
||
|
||
static int api_apps_info(int sock, const char *pkg)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char appdir[1024];
|
||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg);
|
||
struct stat st;
|
||
if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装");
|
||
sbuf_t s = {0};
|
||
if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0)
|
||
return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* multipart 提取第一个 file part 并写入 outpath。返回 0=成功。 */
|
||
static int multipart_extract_file(const req_t *r, const char *outpath)
|
||
{
|
||
const char *b = strstr(r->content_type, "boundary=");
|
||
if (!b) return -1;
|
||
b += 9;
|
||
if (*b == '"') b++;
|
||
char boundary[128]; int bi = 0;
|
||
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
|
||
boundary[bi] = 0;
|
||
char dmark[160];
|
||
snprintf(dmark, sizeof dmark, "--%s", boundary);
|
||
const char *p = r->body;
|
||
while (p < r->body + r->body_len) {
|
||
const char *fs = strstr(p, dmark);
|
||
if (!fs) break;
|
||
fs += strlen(dmark);
|
||
if (*fs == '-') break;
|
||
if (*fs == '\r' || *fs == '\n') fs++;
|
||
const char *he = strstr(fs, "\r\n\r\n");
|
||
if (!he) break;
|
||
char hdr[512];
|
||
size_t hl = (size_t)(he - fs); if (hl >= sizeof hdr) hl = sizeof hdr - 1;
|
||
memcpy(hdr, fs, hl); hdr[hl] = 0;
|
||
int isfile = 0;
|
||
char *save = NULL;
|
||
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||
char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||
if (strstr(l, "name=\"file\"")) isfile = 1;
|
||
}
|
||
}
|
||
const char *ds = he + 4;
|
||
const char *de = mem_find(ds, (size_t)((r->body + r->body_len) - ds), dmark);
|
||
if (!de) de = r->body + r->body_len;
|
||
while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--;
|
||
if (isfile) {
|
||
int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd < 0) return -1;
|
||
write_full(fd, ds, (size_t)(de - ds));
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
p = de;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static const char *nyai_bin(void)
|
||
{
|
||
/* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */
|
||
if (!access(NYAI_PATH, X_OK)) return NYAI_PATH;
|
||
return NYAI_PATH_CARD;
|
||
}
|
||
|
||
static int api_apps_install(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (access(nyai_bin(), X_OK) < 0) return send_err(sock, 500, "INTERNAL", "nyai 不可用(固件与卡上均无)");
|
||
char mpkpath[1024];
|
||
int ispath = 0;
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) {
|
||
/* JSON {path} */
|
||
if (r->body && r->body[0]) {
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
if (pj && pj->type == J_STR) {
|
||
snprintf(mpkpath, sizeof mpkpath, "%s", pj->str);
|
||
ispath = 1;
|
||
}
|
||
jv_free(root);
|
||
}
|
||
if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file");
|
||
} else {
|
||
/* 保存上传 file 到临时文件 */
|
||
snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR);
|
||
mkdir(mpkpath, 0777);
|
||
char tmp[1024];
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR);
|
||
if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败");
|
||
snprintf(mpkpath, sizeof mpkpath, "%s", tmp);
|
||
}
|
||
if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在");
|
||
char cmd[1400];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' install '%s' 2>&1", nyai_bin(), mpkpath);
|
||
char out[4096];
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败");
|
||
/* 包名 = 文件名茎;版本从 app.json 读 */
|
||
char stem[256];
|
||
const char *base = strrchr(mpkpath, '/');
|
||
base = base ? base + 1 : mpkpath;
|
||
snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base);
|
||
char *dot = strrchr(stem, '.');
|
||
if (dot) *dot = 0;
|
||
char appdir[1024], pj[1100];
|
||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem);
|
||
jv_t *approot = NULL;
|
||
char version[16] = "";
|
||
int upgraded = 0;
|
||
snprintf(pj, sizeof pj, "%s/app.json", appdir);
|
||
char jbuf[8192];
|
||
if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0)
|
||
approot = jv_parse(jbuf, (int)strlen(jbuf));
|
||
if (approot) {
|
||
jv_t *v = jv_obj_get(approot, "version");
|
||
if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str);
|
||
jv_free(approot);
|
||
}
|
||
if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1;
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"package\":");
|
||
sA_str(&s, stem);
|
||
sA(&s, ",\"version\":");
|
||
sA_str(&s, version[0] ? version : NULL);
|
||
sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_apps_export(int sock, const char *pkg)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char tmp[1100];
|
||
char card_tmp[1200];
|
||
snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR);
|
||
mkdir(card_tmp, 0777);
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
|
||
unlink(tmp);
|
||
char cmd[1400];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' export '%s' '%s' 2>&1", nyai_bin(), pkg, tmp);
|
||
char out[1024];
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败");
|
||
char dn[256];
|
||
snprintf(dn, sizeof dn, "%s.mpk", pkg);
|
||
return send_file(sock, 200, "application/octet-stream", tmp, dn);
|
||
}
|
||
|
||
static int api_apps_remove(int sock, const char *pkg, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char purgebuf[8] = "0";
|
||
if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf);
|
||
int use_purge = atoi(purgebuf);
|
||
char cmd[1400];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' remove '%s'%s 2>&1", nyai_bin(), pkg, use_purge ? " --purge" : "");
|
||
char out[1024];
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 实时画面(GET /api/screen.jpeg) ---------------- */
|
||
/* fb0 读当前显示帧(只读,不影响渲染):按 yoffset 读活动缓冲,16bpp RGB565。 */
|
||
static int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
||
{
|
||
int fd = open(dev, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
unsigned char vinfo[160], finfo[68];
|
||
memset(vinfo, 0, sizeof vinfo);
|
||
memset(finfo, 0, sizeof finfo);
|
||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
|
||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { close(fd); return -1; }
|
||
uint32_t xres = *(uint32_t *)(vinfo + 0);
|
||
uint32_t yres = *(uint32_t *)(vinfo + 4);
|
||
uint32_t yoff = *(uint32_t *)(vinfo + 20);
|
||
uint32_t bpp = *(uint32_t *)(vinfo + 24);
|
||
uint32_t line = *(uint32_t *)(finfo + 44);
|
||
if (!line) line = xres * (bpp / 8);
|
||
if (bpp != 16 || xres == 0 || yres == 0 || line < xres * 2) { close(fd); return -1; }
|
||
size_t framelen = (size_t)line * (size_t)yres;
|
||
uint8_t *frame = (uint8_t *)malloc(framelen);
|
||
if (!frame) { close(fd); return -1; }
|
||
ssize_t got = pread(fd, frame, framelen, (off_t)line * (off_t)yoff);
|
||
close(fd);
|
||
if (got != (ssize_t)framelen) { free(frame); return -1; }
|
||
/* 若 line != xres*2(行尾填充),压缩为紧凑 RGB565(xres*yres*2) */
|
||
if (line != xres * 2) {
|
||
size_t cplen = (size_t)xres * 2;
|
||
for (uint32_t y = 0; y < yres; y++)
|
||
memmove(frame + (size_t)y * cplen, frame + (size_t)y * line, cplen);
|
||
}
|
||
*out = frame; *w = (int)xres; *h = (int)yres;
|
||
return 0;
|
||
}
|
||
|
||
/* 仅读 fb 几何(不抓帧),用于坐标 clamp 上限。 */
|
||
static int fb_dims(const char *dev, int *w, int *h)
|
||
{
|
||
int fd = open(dev, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
unsigned char vinfo[160];
|
||
memset(vinfo, 0, sizeof vinfo);
|
||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0) { close(fd); return -1; }
|
||
uint32_t xres = *(uint32_t *)(vinfo + 0);
|
||
uint32_t yres = *(uint32_t *)(vinfo + 4);
|
||
close(fd);
|
||
if (!xres || !yres) return -1;
|
||
*w = (int)xres; *h = (int)yres;
|
||
return 0;
|
||
}
|
||
|
||
/* 把压缩帧缓存为「上一帧」基线(几何变化时重分配)。供 delta 模式 8×8 块比对。 */
|
||
static void screen_prev_store(const uint8_t *frame, int w, int h)
|
||
{
|
||
size_t need = (size_t)w * (size_t)h * 2;
|
||
if (w != g_screen_pw || h != g_screen_ph) {
|
||
free(g_screen_prev);
|
||
g_screen_prev = NULL;
|
||
g_screen_pw = w; g_screen_ph = h;
|
||
g_screen_prev = (uint8_t *)malloc(need);
|
||
}
|
||
if (g_screen_prev) memcpy(g_screen_prev, frame, need);
|
||
}
|
||
|
||
/* 8×8 块比对:返回变化块数;bbox 为块坐标(bx/by=min 块,bw/bh=块数),像素原点=bx*8/by*8。
|
||
* changed==0 时 bbox 无效。*total_blocks 返回总块数。 */
|
||
static int screen_diff_blocks(const uint8_t *cur, const uint8_t *prev, int w, int h,
|
||
int *bx, int *by, int *bw, int *bh, int *total_blocks)
|
||
{
|
||
const int B = SCREEN_BLOCK;
|
||
int nbx = (w + B - 1) / B, nby = (h + B - 1) / B;
|
||
if (total_blocks) *total_blocks = nbx * nby;
|
||
int changed = 0;
|
||
int minx = nbx, miny = nby, maxx = -1, maxy = -1;
|
||
for (int byi = 0; byi < nby; byi++) {
|
||
for (int bxi = 0; bxi < nbx; bxi++) {
|
||
int x0 = bxi * B, y0 = byi * B;
|
||
int wdt = B, hgt = B;
|
||
if (x0 + wdt > w) wdt = w - x0;
|
||
if (y0 + hgt > h) hgt = h - y0;
|
||
int diff = 0;
|
||
for (int y = 0; y < hgt && !diff; y++) {
|
||
const uint8_t *a = cur + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2;
|
||
const uint8_t *b = prev + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2;
|
||
if (memcmp(a, b, (size_t)wdt * 2) != 0) diff = 1;
|
||
}
|
||
if (diff) {
|
||
changed++;
|
||
if (bxi < minx) minx = bxi;
|
||
if (byi < miny) miny = byi;
|
||
if (bxi > maxx) maxx = bxi;
|
||
if (byi > maxy) maxy = byi;
|
||
}
|
||
}
|
||
}
|
||
if (changed) { *bx = minx; *by = miny; *bw = maxx - minx + 1; *bh = maxy - miny + 1; }
|
||
return changed;
|
||
}
|
||
|
||
static int api_screen_jpeg(int sock, const req_t *r)
|
||
{
|
||
int q = 70; /* 默认质量(体积/清晰折中) */
|
||
char qv[8];
|
||
if (qparam(r->query, "q", qv, sizeof qv)) {
|
||
int t = atoi(qv);
|
||
if (t >= 1 && t <= 100) q = t;
|
||
}
|
||
char dv[8];
|
||
int delta = (qparam(r->query, "delta", dv, sizeof dv) && atoi(dv) == 1);
|
||
|
||
const char *dev = getenv("MEOW_FB_DEV"); if (!dev || !dev[0]) dev = FB_DEV;
|
||
uint8_t *frame = NULL; int w = 0, h = 0;
|
||
if (screen_read_frame(dev, &frame, &w, &h) != 0)
|
||
return send_err(sock, 500, "INTERNAL", "读取帧缓冲失败(/dev/fb0)");
|
||
|
||
if (!delta) { /* 默认整帧(不破坏原行为) */
|
||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
|
||
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
|
||
screen_prev_store(frame, w, h); /* 维护 delta 基线 */
|
||
int ret = send_raw(sock, 200, "image/jpeg", jpeg, jlen);
|
||
free(jpeg);
|
||
free(frame);
|
||
return ret;
|
||
}
|
||
|
||
/* ---- delta 模式 ---- */
|
||
const int B = SCREEN_BLOCK;
|
||
int changed = 0, bx = 0, by = 0, bw = 0, bh = 0, total = 0;
|
||
int force_full = (!g_screen_prev || g_screen_pw != w || g_screen_ph != h);
|
||
if (!force_full) {
|
||
changed = screen_diff_blocks(frame, g_screen_prev, w, h, &bx, &by, &bw, &bh, &total);
|
||
if (bw * B >= w && bh * B >= h) force_full = 1; /* 全屏变化 → 直接发整帧 */
|
||
} else {
|
||
total = ((w + B - 1) / B) * ((h + B - 1) / B);
|
||
}
|
||
screen_prev_store(frame, w, h); /* 基线更新为当前帧 */
|
||
|
||
char hdr[224];
|
||
int ret = -1;
|
||
if (changed == 0 && !force_full) { /* 无变化:空 body,前端跳过叠加 */
|
||
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
|
||
"X-Meow-Changed: 0\r\nX-Meow-Blocks: %d", w, h, total);
|
||
ret = send_head(sock, 200, "image/jpeg", 0, hdr);
|
||
free(frame);
|
||
return ret;
|
||
}
|
||
if (force_full) { /* 冷藏起/几何变化/全屏变化 → 整帧 */
|
||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
|
||
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
|
||
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 1\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
|
||
"X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, changed ? changed : total, total);
|
||
ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr);
|
||
if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1;
|
||
free(jpeg);
|
||
free(frame);
|
||
return ret;
|
||
}
|
||
/* 子图 = 变化块 bbox 外接矩形(8 对齐,钳到屏内),重编码为独立 JPEG 并以响应头回传偏移。 */
|
||
int x0 = bx * B, y0 = by * B;
|
||
int sw = bw * B, sh = bh * B;
|
||
if (x0 + sw > w) sw = w - x0;
|
||
if (y0 + sh > h) sh = h - y0;
|
||
uint8_t *sub = (uint8_t *)malloc((size_t)sw * (size_t)sh * 2);
|
||
if (!sub) { free(frame); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||
for (int y = 0; y < sh; y++)
|
||
memcpy(sub + (size_t)y * (size_t)sw * 2,
|
||
frame + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2,
|
||
(size_t)sw * 2);
|
||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||
int rc = mp_jpeg_encode_rgb565(sub, sw, sh, q, &jpeg, &jlen);
|
||
free(sub);
|
||
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
|
||
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
|
||
"X-Meow-Sub-X: %d\r\nX-Meow-Sub-Y: %d\r\nX-Meow-Sub-W: %d\r\nX-Meow-Sub-H: %d\r\n"
|
||
"X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, x0, y0, sw, sh, changed, total);
|
||
ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr);
|
||
if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1;
|
||
free(jpeg);
|
||
free(frame);
|
||
return ret;
|
||
}
|
||
|
||
/* ---------------- 触摸注入(POST /api/input/tap) ----------------
|
||
* 本机厂商内核 write 进 goodix(事件1) 会投递给读者(实测:写 tap 打开 App 成功),故复用
|
||
* 「写 evdev」注入(同按键注入机制,但用 MT 协议 B + legacy ABS_X/Y + BTN_TOUCH,等于真实
|
||
* goodix 事件序列)。仲裁:本机 event1 最近活动后 5s 内忽略注入(本机优先打断)。 */
|
||
#define TOUCH_ACTIVE_MS 5000
|
||
|
||
static int64_t now_mono_ms(void)
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||
}
|
||
|
||
static int64_t g_last_self_inject_ms = 0; /* 本进程上次注入时刻(用于剔除自注入事件) */
|
||
|
||
/* 排空触摸设备,返回「最近一次本机真实触摸」的单调毫秒时间(0=无)。剔除自注入事件。
|
||
* MIPS 的 struct input_event 无 time 成员(ABI),按 launcher 同款读裸 16 字节、
|
||
* 取偏移 0(int32 tv_sec)/4(int32 tv_usec)。 */
|
||
static int64_t touch_last_local_ms(const char *dev)
|
||
{
|
||
int fd = open(dev, O_RDONLY | O_NONBLOCK);
|
||
if (fd < 0) return 0;
|
||
int64_t last = 0;
|
||
unsigned char ev[16];
|
||
int64_t inject_deadline = g_last_self_inject_ms + 200; /* 注入事件时间 ≈ 注入时刻 */
|
||
while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||
int64_t sec = (int64_t)(*(int32_t *)(ev + 0));
|
||
int64_t usec = (int64_t)(*(int32_t *)(ev + 4));
|
||
int64_t t = sec * 1000 + usec / 1000;
|
||
if (t <= inject_deadline) continue; /* 自注入,跳过 */
|
||
if (t > last) last = t;
|
||
}
|
||
close(fd);
|
||
return last;
|
||
}
|
||
|
||
static int inject_tap(const char *dev, int x, int y)
|
||
{
|
||
int fd = open(dev, O_WRONLY);
|
||
if (fd < 0) return -1;
|
||
struct input_event ev;
|
||
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
|
||
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
|
||
/* 事件顺序:先 BTN_TOUCH=1(按下边沿),后 ABS 坐标 —— launcher/meow_touch 在按下边沿
|
||
* 会重置坐标 (-1),只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)。若先发
|
||
* ABS 再发 BTN_TOUCH,原点永不锁定 → 抬手表被判为无效 tap(只高亮不启动)。 */
|
||
__WE(EV_KEY, BTN_TOUCH, 1);
|
||
__WE(EV_ABS, ABS_MT_SLOT, 0);
|
||
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
|
||
__WE(EV_ABS, ABS_MT_POSITION_X, x);
|
||
__WE(EV_ABS, ABS_MT_POSITION_Y, y);
|
||
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
|
||
__WE(EV_ABS, ABS_X, x);
|
||
__WE(EV_ABS, ABS_Y, y);
|
||
__WE(EV_SYN, SYN_REPORT, 0);
|
||
usleep(100000); /* 按住 100ms 再抬起,抬起到按下的边沿被 launcher 视为一次 tap */
|
||
__WE(EV_KEY, BTN_TOUCH, 0);
|
||
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
|
||
__WE(EV_SYN, SYN_REPORT, 0);
|
||
#undef __WE
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
|
||
static int api_input_tap(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jx = jv_obj_get(root, "x");
|
||
jv_t *jy = jv_obj_get(root, "y");
|
||
if (!jx || jx->type != J_NUM || !jy || jy->type != J_NUM) {
|
||
jv_free(root);
|
||
return send_err(sock, 400, "BAD_REQUEST", "缺 x/y(数值)");
|
||
}
|
||
int x = (int)jx->inum, y = (int)jy->inum;
|
||
jv_free(root);
|
||
|
||
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
|
||
/* 坐标 clamp 到屏幕范围(前端已按 480x720 映射,此处再兜底) */
|
||
int sw = 480, sh = 720;
|
||
{ int fx = 0, fy = 0; const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
|
||
if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
|
||
if (x < 0) x = 0;
|
||
if (x >= sw) x = sw - 1;
|
||
if (y < 0) y = 0;
|
||
if (y >= sh) y = sh - 1;
|
||
|
||
/* 仲裁:本机最近触摸后 5s 内忽略注入 */
|
||
int64_t now = now_mono_ms();
|
||
int64_t last = touch_last_local_ms(dev);
|
||
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
|
||
(long long)(TOUCH_ACTIVE_MS - (now - last)));
|
||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return ret;
|
||
}
|
||
|
||
g_last_self_inject_ms = now_mono_ms();
|
||
if (inject_tap(dev, x, y) < 0)
|
||
return send_err(sock, 500, "INTERNAL", "触摸注入失败(无法打开触摸设备)");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"dispatched\":true,\"x\":%d,\"y\":%d}", x, y);
|
||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return ret;
|
||
}
|
||
|
||
/* ---------------- 静态文件托管(web_root = 二进制所在目录) ---------------- */
|
||
static char g_webroot[512];
|
||
static const char *web_root(void)
|
||
{
|
||
if (g_webroot[0]) return g_webroot;
|
||
char exe[512];
|
||
ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1);
|
||
if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; }
|
||
exe[n] = 0;
|
||
char *slash = strrchr(exe, '/');
|
||
if (slash) *slash = 0;
|
||
snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : ".");
|
||
return g_webroot;
|
||
}
|
||
|
||
static const char *mime_for(const char *path)
|
||
{
|
||
const char *ext = strrchr(path, '.');
|
||
if (!ext) return "application/octet-stream";
|
||
if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8";
|
||
if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8";
|
||
if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8";
|
||
if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8";
|
||
if (!strcasecmp(ext, ".png")) return "image/png";
|
||
if (!strcasecmp(ext, ".ico")) return "image/x-icon";
|
||
if (!strcasecmp(ext, ".svg")) return "image/svg+xml";
|
||
return "application/octet-stream";
|
||
}
|
||
|
||
static int serve_static(int sock, const char *path)
|
||
{
|
||
const char *root = web_root();
|
||
char rel[512];
|
||
/* / → index.html;/assets/<f> → <f>;/<f> → <f>(meowpanel.css/js 等) */
|
||
if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html");
|
||
else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8);
|
||
else snprintf(rel, sizeof rel, "%s", path + 1);
|
||
if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸");
|
||
/* 只允许安全文件(防读到二进制/任意文件) */
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", root, rel);
|
||
/* 敏感名拒绝 */
|
||
const char *base = strrchr(full, '/');
|
||
base = base ? base + 1 : full;
|
||
if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host")
|
||
|| !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件");
|
||
FILE *f = fopen(full, "rb");
|
||
if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在");
|
||
fclose(f);
|
||
return send_file(sock, 200, mime_for(full), full, NULL);
|
||
}
|
||
|
||
/* ---------------- 路由分发 ---------------- */
|
||
static int handle(int sock, req_t *r)
|
||
{
|
||
const char *p = r->path;
|
||
if (strncmp(p, "/api/", 5) != 0) {
|
||
if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0)
|
||
return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET");
|
||
return serve_static(sock, p);
|
||
}
|
||
|
||
/* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */
|
||
if (require_auth(sock, r) != 1) return 0;
|
||
|
||
/* ---- 鉴权端点(登录/设置/登出/状态:公开——访问面板即需登录,须先经此建立会话) ---- */
|
||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r);
|
||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/setup")) return api_auth_setup(sock, r);
|
||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/logout")) return api_auth_logout(sock, r);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/auth/status")) return api_auth_status(sock, r);
|
||
|
||
/* ---- 性能/状态 ---- */
|
||
if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf")))
|
||
return api_perf(sock, p);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system"))
|
||
return api_system(sock);
|
||
|
||
/* ---- 字体库(导入/导出/列表) ---- */
|
||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/fonts/import")) return api_fonts_import(sock, r);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r);
|
||
|
||
/* ---- 音频状态(meow_audiod 快照,只读;含 rms/peak/lvlseq 电平键) ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
|
||
|
||
/* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock);
|
||
if (!strcmp(r->method, "GET")) {
|
||
char seg[256];
|
||
if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0])
|
||
return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */
|
||
}
|
||
|
||
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock);
|
||
|
||
/* ---- 实时画面(阶段 C) ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
|
||
return api_screen_jpeg(sock, r);
|
||
|
||
/* ---- 系统动作 ---- */
|
||
if (!strcmp(r->method, "POST")) {
|
||
if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r);
|
||
if (!strcmp(p, "/api/system/shutdown")) return api_system_action(sock, "shutdown", r);
|
||
if (!strcmp(p, "/api/system/ota")) return api_system_action(sock, "ota", r);
|
||
if (!strcmp(p, "/api/system/pin-unlock-once")) return api_pin_unlock_once(sock);
|
||
if (!strcmp(p, "/api/debug/report_bad_json")) {
|
||
/* 前端 NOT_JSON 自动回传浏览器收到的原始字节(抓间歇性破包) */
|
||
FILE *df = fopen("/tmp/overview_client_bad.txt", "w");
|
||
if (df && r->body && r->body_len) fwrite(r->body, 1, r->body_len, df);
|
||
if (df) fclose(df);
|
||
panel_log("debug: client bad json captured %zu bytes -> /tmp/overview_client_bad.txt", (size_t)r->body_len);
|
||
return send_json(sock, 200, "{\"captured\":true}");
|
||
}
|
||
if (!strcmp(p, "/api/settings")) return api_settings_batch(sock, r);
|
||
if (!strcmp(p, "/api/settings/restore")) return api_settings_restore(sock);
|
||
if (!strcmp(p, "/api/theme")) return api_theme_put(sock, r);
|
||
if (!strcmp(p, "/api/files/upload")) return api_files_upload(sock, r);
|
||
if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r);
|
||
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
|
||
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
||
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
|
||
if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock);
|
||
if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len);
|
||
if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len);
|
||
if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len);
|
||
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
||
char seg[256];
|
||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||
return api_key_single(sock, atoi(seg), r);
|
||
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
|
||
if (!strncmp(p, "/api/ams/", 9)) {
|
||
size_t pl = strlen(p);
|
||
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
||
char pkg[256];
|
||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||
return api_ams_ctl(sock, pkg, 0);
|
||
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
||
char pkg[256];
|
||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||
return api_ams_ctl(sock, pkg, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- 设置 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/settings")) return api_settings_get_all(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/theme")) return api_theme_get(sock);
|
||
if (!strcmp(r->method, "PUT")) {
|
||
char seg[256];
|
||
if (path_segment("/api/settings/", p, seg, sizeof seg))
|
||
return api_settings_put(sock, seg, r);
|
||
}
|
||
if (!strcmp(r->method, "GET")) {
|
||
char seg[256];
|
||
if (path_segment("/api/settings/", p, seg, sizeof seg))
|
||
return api_settings_get_any(sock, seg);
|
||
}
|
||
|
||
/* ---- 文件 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
||
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
||
|
||
/* ---- 应用 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/apps")) return api_apps_list(sock);
|
||
if (!strcmp(r->method, "GET")) {
|
||
char seg[256];
|
||
if (path_segment("/api/apps/", p, seg, sizeof seg)) {
|
||
/* :pkg/export 结尾(path_segment 已把 seg 截为 pkg,这里检查原 path 尾缀) */
|
||
size_t pl = strlen(p);
|
||
int is_export = (pl >= 7 && !strcmp(p + pl - 7, "/export"));
|
||
if (is_export) return api_apps_export(sock, seg);
|
||
return api_apps_info(sock, seg);
|
||
}
|
||
}
|
||
if (!strcmp(r->method, "DELETE")) {
|
||
char seg[256];
|
||
if (path_segment("/api/apps/", p, seg, sizeof seg))
|
||
return api_apps_remove(sock, seg, r);
|
||
}
|
||
|
||
/* ---- 日志 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs")) return api_logs_list(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/crash")) return api_logs_crash(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/read")) return api_logs_read(sock, r);
|
||
|
||
/* ---- 通知 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock);
|
||
if (!strcmp(r->method, "DELETE")) {
|
||
char seg[256];
|
||
if (path_segment("/api/notifications/", p, seg, sizeof seg)) {
|
||
unsigned nid = (unsigned)strtoul(seg, NULL, 10);
|
||
return api_notification_delete(sock, nid);
|
||
}
|
||
}
|
||
|
||
return send_err(sock, 404, "NOT_FOUND", "端点不存在");
|
||
}
|
||
|
||
/* ---------------- 主循环 ---------------- */
|
||
static int g_port = DEFAULT_PORT;
|
||
|
||
static int read_port_config(void)
|
||
{
|
||
/* 配置文件 meowpanel.conf(web 根目录),格式 key=value,取 port */
|
||
char path[1024];
|
||
snprintf(path, sizeof path, "%s/meowpanel.conf", web_root());
|
||
FILE *f = fopen(path, "r");
|
||
if (f) {
|
||
char line[160];
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *eq = strchr(line, '=');
|
||
if (!eq) continue;
|
||
char *k = line; while (*k == ' ' || *k == '\t') k++;
|
||
if (!strncmp(k, "port", 4) && k[4] == '=') g_port = atoi(eq + 1);
|
||
}
|
||
fclose(f);
|
||
}
|
||
const char *env = getenv("MEOWPORT");
|
||
if (env && env[0]) g_port = atoi(env);
|
||
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
|
||
return g_port;
|
||
}
|
||
|
||
static void handle_conn(int sock)
|
||
{
|
||
req_t r;
|
||
memset(&r, 0, sizeof r);
|
||
int rr = read_request(sock, &r);
|
||
if (rr != 0) {
|
||
if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)");
|
||
close(sock);
|
||
return;
|
||
}
|
||
handle(sock, &r);
|
||
req_free(&r);
|
||
close(sock);
|
||
}
|
||
|
||
/* ---------------- 宿主自测(`meowpanel --selftest`) ----------------
|
||
* 复现 2026-09-02 事故:amsd 的 am list 把 App 显示名「流媒体客户端」(18B)截成 16B,
|
||
* 腰斩「端」字(U+7AEF = E7 AB AF → 只剩首字节 E7)。面板必须把这类非法 UTF-8 消毒成合法字符,
|
||
* JSON 才不会被浏览器拒收。直接喂截断串给 utf8_sanitize / sA_str / ams_fill 桩,断言:
|
||
* 1) utf8_sanitize 改写截断串、保持字节长度、产物为合法 UTF-8,且不误改完整合法名;
|
||
* 2) sA_str 嵌入 JSON 字符串后整串为合法 UTF-8、jv_parse 可完整解析;
|
||
* 3) ams_fill 桩(APP 行解析 + sA_str)产物同理可解析。
|
||
* 仅宿主 `meowpanel --selftest` 进入;设备服务循环不触发。 */
|
||
static int panel_selftest(void)
|
||
{
|
||
const char *trunc = "流媒体客户\xE7"; /* 事故里被腰斩的显示名(16B,末字节为「端」首字节) */
|
||
const char *oknat = "流媒体客户端"; /* 完整合法名(18B,回归确认不误伤) */
|
||
size_t n = strlen(trunc);
|
||
|
||
/* 1a) utf8_sanitize:截断串必被改写,长度保持,产物合法 UTF-8 */
|
||
char san[64];
|
||
if (!utf8_sanitize(san, trunc, n)) { printf("SELFTEST FAIL: utf8_sanitize 未改写截断串\n"); return 1; }
|
||
if (strlen(san) != n) { printf("SELFTEST FAIL: utf8_sanitize 长度变化 %zu->%zu\n", n, strlen(san)); return 1; }
|
||
if (!utf8_valid_n(san, strlen(san))) { printf("SELFTEST FAIL: utf8_sanitize 产物含非法字节\n"); return 1; }
|
||
|
||
/* 1b) 完整合法名不得被改写(误伤检测) */
|
||
char san2[64];
|
||
if (utf8_sanitize(san2, oknat, strlen(oknat)) != 0 || strcmp(san2, oknat) != 0) {
|
||
printf("SELFTEST FAIL: utf8_sanitize 误改合法 UTF-8\n"); return 1;
|
||
}
|
||
|
||
/* 2) sA_str 嵌入:产物合法 UTF-8 且 jv_parse 完整解析 */
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"pkg\":");
|
||
sA_str(&s, "com.example.trunc");
|
||
sA(&s, ",\"name\":");
|
||
sA_str(&s, trunc);
|
||
sA(&s, ",\"state\":");
|
||
sA_str(&s, "running");
|
||
sA(&s, "}");
|
||
if (!s.b) { printf("SELFTEST FAIL: sA_str 输出空\n"); return 1; }
|
||
if (!utf8_valid_n(s.b, s.n)) { printf("SELFTEST FAIL: sA_str 产物含非法字节\n"); sbuf_free(&s); return 1; }
|
||
jv_t *r = jv_parse(s.b, (int)s.n);
|
||
if (!r) { printf("SELFTEST FAIL: sA_str 产物 JSON 无法解析\n"); sbuf_free(&s); return 1; }
|
||
jv_t *nm = jv_obj_get(r, "name");
|
||
if (!nm || nm->type != J_STR) { printf("SELFTEST FAIL: sA_str 产物缺 name\n"); jv_free(r); sbuf_free(&s); return 1; }
|
||
jv_free(r);
|
||
sbuf_free(&s);
|
||
|
||
/* 3) ams_fill 桩:模拟 APP 行 parse 出的截断名(join_tokens 后)→ apps 数组可解析 */
|
||
sbuf_t a = {0};
|
||
sA(&a, "{\"available\":true,\"count\":1,\"apps\":[{\"pkg\":");
|
||
sA_str(&a, "com.example.trunc");
|
||
sA(&a, ",\"name\":");
|
||
sA_str(&a, trunc);
|
||
sA(&a, ",\"state\":");
|
||
sA_str(&a, "running");
|
||
sA(&a, ",\"pid\":123}]}");
|
||
if (!a.b || !utf8_valid_n(a.b, a.n)) { printf("SELFTEST FAIL: ams_fill 桩产物含非法字节/为空\n"); sbuf_free(&a); return 1; }
|
||
jv_t *ar = jv_parse(a.b, (int)a.n);
|
||
if (!ar) { printf("SELFTEST FAIL: ams_fill 桩产物 JSON 无法解析\n"); sbuf_free(&a); return 1; }
|
||
jv_free(ar);
|
||
sbuf_free(&a);
|
||
|
||
/* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。
|
||
* 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */
|
||
{
|
||
static const char ptext[] =
|
||
"# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n"
|
||
"app:com.example.trunc net=block domain=+good.example,-bad\xE7\n"
|
||
"svc:meowpanel net=allow domain=+svc.local\n";
|
||
sbuf_t p = {0};
|
||
net_policy_parse(ptext, &p);
|
||
if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; }
|
||
jv_t *pr = jv_parse(p.b, (int)p.n);
|
||
if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; }
|
||
jv_t *rules = jv_obj_get(pr, "rules");
|
||
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||
jv_t *enf = jv_obj_get(pr, "enforce");
|
||
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||
jv_free(pr);
|
||
sbuf_free(&p);
|
||
}
|
||
|
||
/* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。
|
||
* detail 合法时 → 合法 JSON 可解析。 */
|
||
{
|
||
char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n";
|
||
sbuf_t h = {0};
|
||
ams_history_parse(bad, &h, "com.example.trunc");
|
||
if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; }
|
||
sbuf_free(&h);
|
||
|
||
char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n";
|
||
sbuf_t h2 = {0};
|
||
ams_history_parse(good, &h2, "com.example.trunc");
|
||
if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; }
|
||
sbuf_free(&h2);
|
||
}
|
||
|
||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
|
||
return 0;
|
||
}
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
/* 参数:-p PORT(可选)。默认端口 8080。 */
|
||
int port = DEFAULT_PORT;
|
||
for (int i = 1; i < argc; i++) {
|
||
if (!strcmp(argv[i], "--selftest")) return panel_selftest();
|
||
if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock-net") && i + 1 < argc) g_sock_net = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock-pm") && i + 1 < argc) g_sock_pm = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; }
|
||
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
||
printf("MeowPanel backend %s\n", PANEL_VERSION);
|
||
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P] [--sock-net P] [--sock-pm P]\n");
|
||
return 0;
|
||
}
|
||
}
|
||
init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */
|
||
const char *env = getenv("MEOW_IPC_PERF_SOCK");
|
||
if (env && env[0]) g_sock_perf = env;
|
||
env = getenv("MEOW_IPC_POWER_SOCK");
|
||
if (env && env[0]) g_sock_power = env;
|
||
env = getenv("MEOW_IPC_LAUNCHER_SOCK");
|
||
if (env && env[0]) g_sock_launcher = env;
|
||
env = getenv("MEOW_AUDIO_SOCK");
|
||
if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */
|
||
env = getenv("MEOW_NET_SOCK");
|
||
if (env && env[0]) g_sock_net = env; /* 与 netmgrd/meow_net 同 env(宿主/qemu 测试用) */
|
||
env = getenv("MEOW_PM_SOCK");
|
||
if (env && env[0]) g_sock_pm = env; /* 与 powermgr/meow_power 同 env(宿主/qemu 测试用) */
|
||
env = getenv("MEOW_AUTH_TTL");
|
||
if (env && env[0]) g_auth_ttl = (time_t)atol(env); /* 会话 TTL 覆盖(宿主测试用) */
|
||
env = getenv("MEOW_SETTINGS_FILE");
|
||
if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */
|
||
|
||
(void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */
|
||
/* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */
|
||
if (port != DEFAULT_PORT) g_port = port;
|
||
else g_port = read_port_config();
|
||
/* 再按 env MEOWPORT 覆盖 conf(-p 最高) */
|
||
if (port == DEFAULT_PORT) {
|
||
const char *ep = getenv("MEOWPORT");
|
||
if (ep && ep[0]) g_port = atoi(ep);
|
||
}
|
||
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
|
||
|
||
signal(SIGPIPE, SIG_IGN);
|
||
web_root();
|
||
panel_log("MeowPanel %s listening on port %d (web root %s)", PANEL_VERSION, g_port, web_root());
|
||
|
||
int lfd = socket(AF_INET, SOCK_STREAM, 0);
|
||
if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; }
|
||
int on = 1;
|
||
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
|
||
struct sockaddr_in addr;
|
||
memset(&addr, 0, sizeof addr);
|
||
addr.sin_family = AF_INET;
|
||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||
addr.sin_port = htons((uint16_t)g_port);
|
||
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) {
|
||
panel_log("bind :%d fail: %s", g_port, strerror(errno));
|
||
close(lfd);
|
||
return 1;
|
||
}
|
||
if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; }
|
||
|
||
for (;;) {
|
||
int c = accept(lfd, NULL, NULL);
|
||
if (c < 0) { if (errno == EINTR) continue; continue; }
|
||
handle_conn(c);
|
||
}
|
||
return 0;
|
||
}
|