脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。
- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
(P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步
范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
828 lines
43 KiB
C
828 lines
43 KiB
C
/* mp_server.c — HTTP 服务器/路由 + 静态托管 + main(单线程 + poll 串行)。 */
|
||
#include "mp.h"
|
||
|
||
static char g_webroot[512];
|
||
static int g_port = DEFAULT_PORT;
|
||
|
||
|
||
void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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";
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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 电平键;monitor=实时监控日志尾读) ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio/monitor")) return api_audio_monitor(sock, r->query);
|
||
|
||
/* ---- 批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/ams/bg")) return api_ams_bg(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/bt")) return api_net_bt(sock); /* F1-③ 蓝牙只读详情(0xE6 读级聚合) */
|
||
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);
|
||
if (!strcmp(p, "/api/input/gesture")) return api_input_gesture(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|killfg(管启/停;require_auth 已护,kill 前台前端二次确认;
|
||
* killfg=前台救命强制终止:am kill 优先,launcher 拒→直接 pid 信号,前端二次确认+标注前台) */
|
||
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 > 7 && !strcmp(p + pl - 7, "/killfg")) {
|
||
char pkg[256];
|
||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||
return api_ams_kill_fg(sock, pkg);
|
||
} 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, "GET") && !strcmp(p, "/api/files/search")) return api_files_search(sock, r); /* F1-④ 搜索(只读) */
|
||
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", "端点不存在");
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
void handle_conn(int sock)
|
||
{
|
||
req_t r;
|
||
memset(&r, 0, sizeof r);
|
||
int rr = read_request(sock, &r);
|
||
if (rr != 0) {
|
||
if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)");
|
||
close(sock);
|
||
return;
|
||
}
|
||
handle(sock, &r);
|
||
req_free(&r);
|
||
close(sock);
|
||
}
|
||
|
||
int 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_t *pwe = jv_obj_get(pr, "policy_write_enabled"); /* guard③ 状态键必须为布尔,且拼读无括号错位 */
|
||
if (!pwe || pwe->type != J_BOOL) { printf("SELFTEST FAIL: net_policy_parse 缺/损坏 policy_write_enabled\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);
|
||
}
|
||
|
||
/* 6) ams_bg_parse:喂含 bg/fg 混合 state 的 am list 文本 → 仅 bg 条目、合法 JSON,
|
||
* cpu_pct/rss_kb 为 null(读不到/首采样)或数字——绝不编造不一致字段。 */
|
||
{
|
||
char list[] =
|
||
"# amd register+state\n"
|
||
"APP os.meow.a 名A bg 1 1 0 clean 0\n"
|
||
"APP os.meow.b 名B fg 9999992 1 0 clean 0\n"
|
||
"APP os.meow.c 名C bg 9999991 2 1 exit 1\n";
|
||
sbuf_t b = {0};
|
||
ams_bg_parse(list, &b);
|
||
if (!b.b || !utf8_valid_n(b.b, b.n)) { printf("SELFTEST FAIL: ams_bg_parse 产物非法字节/为空\n"); sbuf_free(&b); return 1; }
|
||
jv_t *br = jv_parse(b.b, (int)b.n);
|
||
if (!br) { printf("SELFTEST FAIL: ams_bg_parse 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
|
||
jv_t *bapps = jv_obj_get(br, "apps");
|
||
if (!bapps || bapps->type != J_ARR) { printf("SELFTEST FAIL: ams_bg_parse 缺 apps 数组\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
int bcnt = 0;
|
||
for (jv_t *e = bapps->child; e; e = e->next) {
|
||
bcnt++;
|
||
jv_t *st = jv_obj_get(e, "state");
|
||
if (!st || st->type != J_STR || strcmp(st->str, "bg") != 0) { printf("SELFTEST FAIL: ams_bg_parse 条目 state 非 bg\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_t *cp = jv_obj_get(e, "cpu_pct");
|
||
jv_t *rs = jv_obj_get(e, "rss_kb");
|
||
if (!cp || (cp->type != J_NULL && cp->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse cpu_pct 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
if (!rs || (rs->type != J_NULL && rs->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse rss_kb 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
}
|
||
if (bcnt != 2) { printf("SELFTEST FAIL: ams_bg_parse bg 条目应 2 个,实际 %d\n", bcnt); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_free(br);
|
||
sbuf_free(&b);
|
||
}
|
||
|
||
/* 7) net_locked_entity:防自锁实体精确匹配(svc:/proc:/裸名);非关键实体放行(guard①)。 */
|
||
{
|
||
struct { const char *e; int want; } c[] = {
|
||
{ "svc:dropbear", 1 }, { "proc:netmgrd", 1 }, { "udhcpc", 1 },
|
||
{ "svc:sysui", 1 }, { "svc:wpa_supplicant", 1 }, { "svcd", 1 }, { "init", 1 },
|
||
{ "app:com.example", 0 }, { "svc:avahi", 0 }, { "proc:sh", 0 },
|
||
{ "app:com.netmgrd.helper", 0 }, /* 含子串但不等于裸名,不得误判 */
|
||
{ "", 0 }, { NULL, 0 }
|
||
};
|
||
for (int i = 0; c[i].e; i++) {
|
||
int got = net_locked_entity(c[i].e);
|
||
if (got != c[i].want) { printf("SELFTEST FAIL: net_locked_entity(%s)=%d 期望 %d\n", c[i].e, got, c[i].want); return 1; }
|
||
}
|
||
}
|
||
|
||
/* 8) policy 默认关 gate(guard③):无设置文件 → panel_policy_write_enabled()==0(默认关);
|
||
* settings_write_one("panel.policy_enabled","1") 后 ==1(写临时 settings 文件验证,测完清理)。 */
|
||
{
|
||
const char *saved = g_settings_file;
|
||
char tmpf[] = "/tmp/meowpanel_policy_gate_XXXXXX";
|
||
int tfd = mkstemp(tmpf);
|
||
if (tfd < 0) { printf("SELFTEST FAIL: mkstemp 失败\n"); return 1; }
|
||
close(tfd);
|
||
g_settings_file = tmpf;
|
||
int before = panel_policy_write_enabled();
|
||
if (settings_write_one("panel.policy_enabled", "1") != 0) {
|
||
printf("SELFTEST FAIL: 写 panel.policy_enabled 失败\n"); unlink(tmpf); g_settings_file = saved; return 1;
|
||
}
|
||
int after = panel_policy_write_enabled();
|
||
unlink(tmpf);
|
||
g_settings_file = saved;
|
||
if (before != 0 || after != 1) { printf("SELFTEST FAIL: policy gate 默认关/开启 %d->%d\n", before, after); return 1; }
|
||
}
|
||
|
||
/* 9) power_policy_append_flag:对象 JSON 剥尾拼入 policy_write_enabled 后仍可解析、键为布尔、原字段保留
|
||
* ——防括号错位重演(overview 教训)。测试在无 panel.policy_enabled 的临时 settings 下进行(期望 false=默认关)。 */
|
||
{
|
||
const char *saved = g_settings_file;
|
||
char tmpf[] = "/tmp/meowpanel_policy_append_XXXXXX";
|
||
int tfd = mkstemp(tmpf);
|
||
if (tfd >= 0) { close(tfd); g_settings_file = tmpf; }
|
||
sbuf_t p = {0};
|
||
if (power_policy_append_flag(&p, "{\"profile\":\"balanced\",\"enacted\":true") != 0) {
|
||
printf("SELFTEST FAIL: power_policy_append_flag 拼接失败\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
|
||
}
|
||
if (!utf8_valid_n(p.b, p.n) || !strstr(p.b, "\"policy_write_enabled\":false")) {
|
||
printf("SELFTEST FAIL: power_policy_append_flag 产物异常\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
|
||
}
|
||
jv_t *pr = jv_parse(p.b, (int)p.n);
|
||
if (!pr) { printf("SELFTEST FAIL: power_policy_append_flag 产物 JSON 无法解析\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||
jv_t *pwe = jv_obj_get(pr, "policy_write_enabled");
|
||
if (!pwe || pwe->type != J_BOOL || pwe->bval != 0) { printf("SELFTEST FAIL: power_policy_append_flag policy_write_enabled 非 false(默认关)\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||
jv_t *pfl = jv_obj_get(pr, "profile");
|
||
if (!pfl || pfl->type != J_STR || strcmp(pfl->str, "balanced") != 0) { printf("SELFTEST FAIL: power_policy_append_flag 丢失原字段\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||
jv_free(pr);
|
||
sbuf_free(&p);
|
||
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
|
||
}
|
||
|
||
/* 10) net_bt_aggregate(F1-③):0xE6 五读文本聚合 → 合法 JSON + 字段断言;
|
||
* 设备名含截断坏字节 → sA_str_n 消毒为 '?' 且产物仍合法;子文本 NULL → 空数组;status NULL → available:false。 */
|
||
{
|
||
sbuf_t b = {0};
|
||
const char *st = "bt available=1 powered=1 address=4C:BC:98:B0:98:DB paired=2 connected=1";
|
||
const char *pa = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 流媒体客户\xE7\n";
|
||
const char *co = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
|
||
const char *a2 = "A2DP 33:DE:2E:21:AD:30 codec=sbc rate=44100 channels=2 bits=16 acquired=1 role=src\n";
|
||
const char *dv = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 17OP15\n";
|
||
if (net_bt_aggregate(st, pa, co, a2, dv, &b) != 0) { printf("SELFTEST FAIL: net_bt_aggregate 正常文本返回非 0\n"); sbuf_free(&b); return 1; }
|
||
if (!b.b || !utf8_valid_n(b.b, b.n) || !json_strict_balanced(b.b, b.n)) { printf("SELFTEST FAIL: net_bt_aggregate 产物非法字节/括号错位\n"); sbuf_free(&b); return 1; }
|
||
jv_t *br = jv_parse(b.b, (int)b.n);
|
||
if (!br) { printf("SELFTEST FAIL: net_bt_aggregate 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
|
||
jv_t *pw = jv_obj_get(br, "powered");
|
||
if (!pw || pw->type != J_BOOL || !pw->bval) { printf("SELFTEST FAIL: net_bt_aggregate powered 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_t *pr = jv_obj_get(br, "paired");
|
||
if (!pr || pr->type != J_ARR || !pr->child || !pr->child->next || pr->child->next->next) { printf("SELFTEST FAIL: net_bt_aggregate paired 应 2 条\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_t *cn = jv_obj_get(br, "connected");
|
||
if (!cn || cn->type != J_ARR || !cn->child || cn->child->next) { printf("SELFTEST FAIL: net_bt_aggregate connected 应 1 条\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_t *a2p = jv_obj_get(br, "a2dp");
|
||
if (!a2p || a2p->type != J_ARR || !a2p->child) { printf("SELFTEST FAIL: net_bt_aggregate 缺 a2dp 条目\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_t *cd = jv_obj_get(a2p->child, "codec");
|
||
if (!cd || cd->type != J_STR || !cd->str || strcmp(cd->str, "sbc") != 0) { printf("SELFTEST FAIL: net_bt_aggregate a2dp codec 非 sbc\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_t *aq = jv_obj_get(a2p->child, "acquired");
|
||
if (!aq || aq->type != J_BOOL || !aq->bval) { printf("SELFTEST FAIL: net_bt_aggregate a2dp acquired 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||
jv_free(br);
|
||
sbuf_free(&b);
|
||
/* status NULL → available:false;子文本 NULL → 空数组 */
|
||
sbuf_t b2 = {0};
|
||
net_bt_aggregate(NULL, NULL, NULL, NULL, NULL, &b2);
|
||
if (!b2.b || strstr(b2.b, "\"available\":false") == NULL) { printf("SELFTEST FAIL: net_bt_aggregate status NULL 未降级 available:false\n"); sbuf_free(&b2); return 1; }
|
||
sbuf_free(&b2);
|
||
sbuf_t b3 = {0};
|
||
net_bt_aggregate(st, NULL, NULL, NULL, NULL, &b3);
|
||
jv_t *b3r = b3.b ? jv_parse(b3.b, (int)b3.n) : NULL;
|
||
jv_t *b3p = b3r ? jv_obj_get(b3r, "paired") : NULL;
|
||
if (!b3p || b3p->type != J_ARR || b3p->child) { printf("SELFTEST FAIL: net_bt_aggregate 子文本 NULL 应为空数组\n"); if (b3r) jv_free(b3r); sbuf_free(&b3); return 1; }
|
||
jv_free(b3r);
|
||
sbuf_free(&b3);
|
||
}
|
||
|
||
/* 11) files_search_json(F1-④):沙盒卡根递归搜索(限深/限量/类型过滤/rel 回显)→ 合法 JSON + 计数断言;
|
||
* 白名单外起点 → -1;name 空 → -3;非法 type → -4。 */
|
||
{
|
||
char tmpl[] = "/tmp/mp_search_XXXXXX";
|
||
char *root = mkdtemp(tmpl);
|
||
if (!root) { printf("SELFTEST FAIL: mkdtemp 失败\n"); return 1; }
|
||
char saved_root[1024];
|
||
snprintf(saved_root, sizeof saved_root, "%s", g_tf_root_buf);
|
||
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", root);
|
||
char d1[1200], d2[1200], f1[1200], f2[1200], f3[1200];
|
||
snprintf(d1, sizeof d1, "%s/MeowOS", root);
|
||
snprintf(d2, sizeof d2, "%s/MeowOS/sub", root);
|
||
snprintf(f1, sizeof f1, "%s/MeowOS/Test Song.mp3", root);
|
||
snprintf(f2, sizeof f2, "%s/MeowOS/another.txt", root);
|
||
snprintf(f3, sizeof f3, "%s/MeowOS/sub/x Song2.log", root);
|
||
int setup_ok = (mkdir(d1, 0755) == 0 && mkdir(d2, 0755) == 0);
|
||
const char *mk[3] = { f1, f2, f3 };
|
||
for (int i = 0; i < 3 && setup_ok; i++) {
|
||
FILE *f = fopen(mk[i], "w");
|
||
if (!f) { setup_ok = 0; break; }
|
||
fputs("x", f); fclose(f);
|
||
}
|
||
if (!setup_ok) { printf("SELFTEST FAIL: 搜索沙盒构建失败\n"); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); return 1; }
|
||
int fails = 0;
|
||
sbuf_t s = {0};
|
||
if (files_search_json(d1, "song", NULL, 0, &s) != 0) { printf("SELFTEST FAIL: files_search_json 正常搜索返回非 0\n"); fails = 1; }
|
||
jv_t *sr = (!fails && s.b) ? jv_parse(s.b, (int)s.n) : NULL;
|
||
if (!fails && (!sr || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n))) { printf("SELFTEST FAIL: files_search_json 产物非法\n"); fails = 1; }
|
||
jv_t *cnt = (!fails && sr) ? jv_obj_get(sr, "count") : NULL;
|
||
if (!fails && (!cnt || cnt->type != J_NUM || cnt->inum != 2)) { printf("SELFTEST FAIL: files_search_json song 应中 2 条(递归命中子目录)\n"); fails = 1; }
|
||
jv_t *its = (!fails && sr) ? jv_obj_get(sr, "items") : NULL;
|
||
jv_t *rel0 = (its && its->child) ? jv_obj_get(its->child, "rel") : NULL;
|
||
if (!fails && (!rel0 || rel0->type != J_STR || !rel0->str || strncmp(rel0->str, "/MeowOS/", 8) != 0)) { printf("SELFTEST FAIL: files_search_json rel 未按卡根相对回显\n"); fails = 1; }
|
||
if (sr) jv_free(sr);
|
||
sbuf_free(&s);
|
||
/* type=dir 过滤:建 Songs 目录应仅中目录 */
|
||
char d3[1200];
|
||
snprintf(d3, sizeof d3, "%s/MeowOS/Songs", root);
|
||
if (!fails && mkdir(d3, 0755) != 0) { printf("SELFTEST FAIL: 沙盒 Songs 目录创建失败\n"); fails = 1; }
|
||
sbuf_t s2 = {0};
|
||
if (!fails && files_search_json(d1, "song", "dir", 0, &s2) == 0) {
|
||
jv_t *sr2 = jv_parse(s2.b, (int)s2.n);
|
||
jv_t *c2 = sr2 ? jv_obj_get(sr2, "count") : NULL;
|
||
jv_t *i2 = sr2 ? jv_obj_get(sr2, "items") : NULL;
|
||
jv_t *t0 = (i2 && i2->child) ? jv_obj_get(i2->child, "type") : NULL;
|
||
if (!c2 || c2->type != J_NUM || c2->inum != 1 || !t0 || t0->type != J_STR || strcmp(t0->str, "dir") != 0) {
|
||
printf("SELFTEST FAIL: files_search_json type=dir 应仅中 1 个目录\n"); fails = 1;
|
||
}
|
||
if (sr2) jv_free(sr2);
|
||
} else if (!fails) { printf("SELFTEST FAIL: files_search_json type=dir 搜索失败\n"); fails = 1; }
|
||
sbuf_free(&s2);
|
||
/* 无匹配 → count 0;白名单外 → -1;空 name → -3;非法 type → -4 */
|
||
sbuf_t s3 = {0};
|
||
if (!fails && files_search_json(d1, "zzzz", NULL, 0, &s3) == 0) {
|
||
if (!strstr(s3.b, "\"count\":0")) { printf("SELFTEST FAIL: files_search_json 无匹配应 count 0\n"); fails = 1; }
|
||
} else if (!fails) { printf("SELFTEST FAIL: files_search_json 无匹配搜索失败\n"); fails = 1; }
|
||
sbuf_free(&s3);
|
||
sbuf_t s4 = {0};
|
||
if (!fails && files_search_json("/etc", "x", NULL, 0, &s4) != -1) { printf("SELFTEST FAIL: files_search_json 白名单外未拒\n"); fails = 1; }
|
||
if (!fails && files_search_json(d1, "", NULL, 0, &s4) != -3) { printf("SELFTEST FAIL: files_search_json 空 name 未拒\n"); fails = 1; }
|
||
if (!fails && files_search_json(d1, "x", "bogus", 0, &s4) != -4) { printf("SELFTEST FAIL: files_search_json 非法 type 未拒\n"); fails = 1; }
|
||
sbuf_free(&s4);
|
||
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root);
|
||
rm_recursive(root);
|
||
rmdir(root);
|
||
if (fails) return 1;
|
||
}
|
||
|
||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 端点池 + 蓝牙 0xE6 聚合 + 文件搜索 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\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;
|
||
}
|