Files
MeowOS-For-R3P2/patch_v2/meowpanel/src/mp_net.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编: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 与容器一致)。
2026-11-04 20:02:28 +08:00

625 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* mp_net.c — 网络管理域:meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3)。 */
#include "mp.h"
#define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限 */
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;
}
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);
}
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
/* ---- F1-③ 蓝牙只读详情(FUTURE_MEOWPANEL §2.6):netmgrd MSG_NET_BT_CTL(0xE6) 读级五命令
* (status/paired/connected/a2dp/devices)聚合为一份 JSON。读级任意调用者(bt_cmd_is_write=读),
* 无阻塞(net_ipc 2s 超时兜底);子命令失败/坏字节 → 该子块空数组降级,绝不拖垮整卡、绝不发非法 JSON。 ---- */
/* 单发 0xE6 读命令;成功返回文本长度(>0),IPC/服务端失败 -1,坏 UTF-8 -2(该子块降级)。 */
static int bt_ctl_read(const char *cmd, char *out, size_t cap)
{
m_net_cmd_t q; memset(&q, 0, sizeof q);
snprintf(q.cmd, sizeof q.cmd, "%s", cmd);
uint16_t t; uint32_t code;
int n = net_ipc(MSG_NET_BT_CTL, &q, sizeof q, &t, &code, NULL, out, cap);
if (n <= 0 || t != MSG_NET_BT_RESULT || code != MEOW_OK) return -1;
if (!utf8_valid_n(out, (size_t)n)) return -2;
return n;
}
static int bt_mac_ok(const char *m)
{
for (int i = 0; i < 17; i++) {
if (i % 3 == 2) { if (m[i] != ':') return 0; }
else if (!is_hex(m[i])) return 0;
}
return 1;
}
/* "Device <MAC> <Name>" 行集 → JSON 数组项(不合法行跳过;名经 sA_str_n 消毒)。返回条数。 */
static int bt_devlines_json(sbuf_t *s, const char *txt, int max)
{
int n = 0;
const char *ln = txt;
while (ln && *ln && n < max) {
const char *nl = strchr(ln, '\n');
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
const char *p = ln;
while ((size_t)(p - ln) < ll && (*p == ' ' || *p == '\t')) p++;
if ((size_t)(ln + ll - p) >= 7 + 17 && !strncmp(p, "Device ", 7) && bt_mac_ok(p + 7)) {
const char *mac = p + 7;
const char *nm = mac + 17;
while ((size_t)(nm - ln) < ll && (*nm == ' ' || *nm == '\t')) nm++;
size_t nlen = ll - (size_t)(nm - ln);
while (nlen && (nm[nlen - 1] == ' ' || nm[nlen - 1] == '\t' || nm[nlen - 1] == '\r')) nlen--;
if (n) sA(s, ",");
sA(s, "{\"mac\":");
sA_str_n(s, mac, 17);
sA(s, ",\"name\":");
sA_str_n(s, nm, nlen);
sA(s, "}");
n++;
}
ln = nl ? nl + 1 : NULL;
}
return n;
}
/* 行内 " key=value"(空白/行尾收尾)取值。 */
static void bt_line_kv(const char *line, const char *key, char *out, size_t cap)
{
out[0] = 0;
const char *v = strstr(line, key);
if (!v) return;
v += strlen(key);
size_t i = 0;
while (v[i] && v[i] != ' ' && v[i] != '\t' && v[i] != '\n' && v[i] != '\r' && i + 1 < cap) { out[i] = v[i]; i++; }
out[i] = 0;
}
/* "A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=<0|1> role=<..>" 行集 → JSON 数组项。 */
static int bt_a2dp_json(sbuf_t *s, const char *txt, int max)
{
int n = 0;
const char *ln = txt;
while (ln && *ln && n < max) {
const char *nl = strchr(ln, '\n');
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
char line[320];
if (ll >= sizeof line) ll = sizeof line - 1;
memcpy(line, ln, ll); line[ll] = 0;
if (!strncmp(line, "A2DP ", 5) && strncmp(line, "A2DP none", 9) && bt_mac_ok(line + 5)) {
char codec[24], rate[16], chn[8], bits[8], acq[8], role[8];
bt_line_kv(line, " codec=", codec, sizeof codec);
bt_line_kv(line, " rate=", rate, sizeof rate);
bt_line_kv(line, " channels=", chn, sizeof chn);
bt_line_kv(line, " bits=", bits, sizeof bits);
bt_line_kv(line, " acquired=", acq, sizeof acq);
bt_line_kv(line, " role=", role, sizeof role);
if (n) sA(s, ",");
sA(s, "{\"mac\":");
sA_str_n(s, line + 5, 17);
sA(s, ",\"codec\":"); sA_str(s, codec);
sA(s, ",\"rate\":"); sA_str(s, rate);
sA(s, ",\"channels\":"); sA_str(s, chn);
sA(s, ",\"bits\":"); sA_str(s, bits);
sA(s, ",\"acquired\":%s", !strcmp(acq, "1") ? "true" : "false");
sA(s, ",\"role\":"); sA_str(s, role);
sA(s, "}");
n++;
}
ln = nl ? nl + 1 : NULL;
}
return n;
}
/* 五读文本聚合(任一 NULL=该子块空数组;st=NULL → {"available":false})。 */
int net_bt_aggregate(const char *st, const char *paired, const char *connected,
const char *a2dp, const char *devices, sbuf_t *s)
{
if (!st) { sA(s, "{\"available\":false}"); return -1; }
int avail = 0, powered = 0; long long paired_n = -1, connected_n = -1;
char addr[24] = "";
(void)sscanf(st, "bt available=%d powered=%d address=%23s paired=%lld connected=%lld",
&avail, &powered, addr, &paired_n, &connected_n);
if (!strcmp(addr, "-")) addr[0] = 0;
sA(s, "{\"available\":%s", avail ? "true" : "false");
sA(s, ",\"powered\":%s", powered ? "true" : "false");
sA(s, ",\"address\":"); sA_str(s, addr);
if (paired_n >= 0) sA(s, ",\"paired_n\":%lld", paired_n); else sA(s, ",\"paired_n\":null");
if (connected_n >= 0) sA(s, ",\"connected_n\":%lld", connected_n); else sA(s, ",\"connected_n\":null");
sA(s, ",\"paired\":["); if (paired) bt_devlines_json(s, paired, 32); sA(s, "]");
sA(s, ",\"connected\":["); if (connected) bt_devlines_json(s, connected, 16); sA(s, "]");
sA(s, ",\"a2dp\":["); if (a2dp) bt_a2dp_json(s, a2dp, 8); sA(s, "]");
sA(s, ",\"nearby\":["); if (devices) bt_devlines_json(s, devices, 16); sA(s, "]");
sA(s, "}");
return 0;
}
int api_net_bt(int sock)
{
char st[MEOW_IPC_MAX], pa[MEOW_IPC_MAX], co[MEOW_IPC_MAX], a2[MEOW_IPC_MAX], dv[MEOW_IPC_MAX];
int ns = bt_ctl_read("status", st, sizeof st);
if (ns <= 0) /* netmgrd 不可达/status 坏字节 → 整体降级(net 域惯例:200 available:false) */
return send_json(sock, 200, "{\"available\":false}");
int np = bt_ctl_read("paired", pa, sizeof pa);
int nc = bt_ctl_read("connected", co, sizeof co);
int na = bt_ctl_read("a2dp", a2, sizeof a2);
int nd = bt_ctl_read("devices", dv, sizeof dv);
sbuf_t s = {0};
net_bt_aggregate(st, np > 0 ? pa : NULL, nc > 0 ? co : NULL, na > 0 ? a2 : NULL, nd > 0 ? dv : NULL, &s);
/* 放行前自校验(与 overview 同口径:浏览器 JSON.parse 不容忍尾随垃圾/括号错位/内嵌 NUL/坏字节) */
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
if (chk) jv_free(chk);
if (bad) {
FILE *df = fopen("/tmp/net_bt_bad.json", "w");
if (df && s.b) { fwrite(s.b, 1, s.n, df); fclose(df); }
panel_log("net/bt: SELF-CHECK fail, raw dumped /tmp/net_bt_bad.json");
sbuf_free(&s);
return send_err(sock, 500, "INTERNAL", "蓝牙聚合输出未通过 JSON 自校验(原始数据已留 /tmp/net_bt_bad.json)");
}
int rc = send_json(sock, 200, s.b);
sbuf_free(&s);
return rc;
}
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;
}
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;
}
/* 防自锁实体:对本机关键网络/服务组件禁设「阻断」(net=block / set_domain 域名限制),
* 否则会把能连上设备/面板/系统核心的网络能力一并锁死(SSH / WiFi / DHCP / DNS / 面板 / init)。
* netmgrd 加载时也会剔除自锁项,面板侧抢先硬拒(guard①),保证 0 warning + 如实回错误码。
* 匹配口径:取 `svc:|app:|proc:` 之后的裸名精确比对(`svc:netmgrd`/`proc:sysui`/裸 `udhcpc` 均命中)。 */
int net_locked_entity(const char *entity)
{
if (!entity || !entity[0]) return 0;
const char *base = entity;
const char *colon = strrchr(entity, ':');
if (colon) base = colon + 1;
static const char *locked[] = {
"dropbear", "wpa_supplicant", "udhcpc", "netmgrd", "svcd", "sysui", "init", NULL
};
for (int i = 0; locked[i]; i++)
if (!strcmp(base, locked[i])) return 1;
return 0;
}
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,\"policy_write_enabled\":%s,\"enforce\":",
panel_policy_write_enabled() ? "true" : "false");
/* 首行找 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, "]}");
}
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;
}
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");
/* guard③ policy 默认关:写端点默认关闭,须在设置开启 panel.policy_enabled 才放行(只读 GET 不受此限)。 */
if (!panel_policy_write_enabled())
return send_err(sock, 403, "POLICY_DISABLED", "网络策略写默认关闭:先到面板设置开启 panel.policy_enabled(危险操作)");
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");
/* guard① 防自锁:对系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)禁设阻断。
* 「放行 / 删除规则」不阻断(那是恢复连通),仍放行;仅 set_net=block 与 set_domain(域名限制)命中即拒。 */
{
int blocking = ((nop == NET_POLICY_SET_NET) && !strcmp(arg, "block")) ||
(nop == NET_POLICY_SET_DOMAIN);
if (blocking && net_locked_entity(entity))
return send_err(sock, 422, "SELF_LOCK", "防自锁:禁止阻断系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)的网络");
}
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;
}