/* 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/ → ;/ → (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/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//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", "端点不存在"); } 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_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; }