feat(panel, PANEL_ENHANCE_PLAN 批1-A): 访问级 4 位 PIN 鉴权 + 后端只读端点 power/overview/ams/svcd + audio 电平键

鉴权(用户定案:访问面板即需登录):
- PANEL.pin 存 sha256+salt(salt:hex,绝不含明文);与未来锁屏 PIN 同源预留 system.pin(g_sets 注记)。
- 未设 PIN → /api/auth/setup 首次设置流程;未设 PIN 前读/写端点全拦(require_auth 无条件,登录/设置流程除外)。
- token 128bit(16B→32hex) 只存进程内存(重启自然失效),3 天固定不滑动续期,多会话并存。
- 连错 5 次 PIN 锁 5 分钟(429 LOCKED,status 透出 lock_remaining_ms)。
- 旧密码机制 panel.auth_enabled/panel.auth_pass 兼容迁移:未设 PIN 时可暂用旧密码登录(status=legacy),设 PIN 后以 PIN 为准。

新只读端点(均需 PIN 会话):
- /api/power: meow_powermgr MSG_PM_QUERY(0xD0)→RESULT(0xD1) battery/estimate/breakdown,confidence 原样透传。
- /api/audio: 补 rms/peak/lvlseq(追加键,ABI 兼容),抽 audio_fill() 供 overview 复用。
- /api/ams: am list 解析 App 表(pkg/name/state/pid/launches/crashes/last_kind/last_code)。
- /api/svcd: meow_svcd status all 解析服务表(name/state/pid/restart/backoff_ms/trigger/src/exit/exec)。
- /api/overview: 单线程后端只暴露聚合——perf/power/audio/net/ams/svcd 六源顺序 fan-out,任一源失败该子块 available:false,绝不假数据。

前端最小支撑:登录页(PIN 输入+首次设置双态)+ 401 自动跳登录 + token 存 localStorage 随请求带 X-MeowToken(后端亦认 Authorization: Bearer)。
红线:仅新增读级端点,未加任何写端点(音量/路由/策略等);本任务不含批1-B 页面。

md5: meowpanel 3696e85b5eaf111f530c5c39fa68886e(mipsel 静态)/ meowpanel_host 3180e4f87e77db8717597619097805eb。
验证:host+mips -Werror 0 warning;假 IPC/am/svcd 桩六源聚合(含 power 宕机降级)PASS;PIN 全流程(设置→登录→连错5次锁→token 过期)PASS;qemu-mipsel 冒烟 PASS。
This commit is contained in:
MeowOS committed 2026-09-01 21:30:41 +08:00
1 parent 30e2ec901f
commit d12164305d
6 files changed
+627 -155

No files matched your search

+504 -114
View File
@@ -94,8 +94,13 @@ static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notificat
#define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */
#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */
#define NET_SOCK "/usr/data/meow_net.sock" /* meow_netmgrd 网络管理(MSG_NET_*,期2 观测层/DNS 代理) */
#define PM_SOCK "/usr/data/meow_pm.sock" /* meow_powermgr 功耗管理(MSG_PM_QUERY→RESULT) */
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
#define AMS_BIN "/usr/bin/am" /* 固件内置 amsd 用户侧 CLI(权威) */
#define AMS_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/am" /* 旧卡上路径(回退) */
#define SVCD_BIN "/usr/bin/meow_svcd" /* 固件内置 svcd 服务表 CLI(权威) */
#define SVCD_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/meow_svcd" /* 旧卡上路径(回退) */
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
#define VERSION_FILE "/etc/meowos_version"
#define HOSTNAME_FILE ""
@@ -127,6 +132,7 @@ static const char *g_sock_power = POWER_SOCK;
static const char *g_sock_launcher = LAUNCHER_SOCK;
static const char *g_sock_audio = AUDIO_SOCK;
static const char *g_sock_net = NET_SOCK; /* meow_netmgrd(--sock-net / env MEOW_NET_SOCK 覆盖) */
static const char *g_sock_pm = PM_SOCK; /* meow_powermgr(--sock-pm / env MEOW_PM_SOCK 覆盖) */
/* ---------------- 增量帧缓存(GET /api/screen.jpeg?delta=1,阶段 C 增强) ----------------
* 服务端缓存上一帧紧凑 RGB565,按 8×8 块比对,只编码变化块(外接 bbox 子图)并以响应头回传偏移。
@@ -239,6 +245,7 @@ static void panel_log(const char *fmt, ...);
static int qparam(const char *query, const char *key, char *out, int outcap); /* 定义在文件后部;网络连接表 query 取值 */
static int tf_present(void); /* 定义在文件后部;net 日志卡根判定 */
static const char *web_root(void); /* 定义在文件后部;此处前向声明供鉴权会话路径使用 */
static int run_cmd(const char *cmd, char *out, int outcap); /* 定义在文件后部;/api/ams、/api/svcd、/api/overview 用(有界收割) */
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
@@ -516,7 +523,12 @@ static const setdef_t g_sets[] = {
{ "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" },
/* 系统 */
{ "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" },
/* 面板(MeowPanel 自身,可选鉴权) */
/* 面板(MeowPanel 自身,访问级 PIN 鉴权)
* panel.pin = 4 位数字 PIN 的 sha256+salt 哈希(`<salt_hex>:<sha256_hex(salt||pin)>`,绝不存明文)。
* 与未来 MeowOS 锁屏 PIN 同源:锁屏键 `system.pin` 预留——本后端只读面板键 panel.pin;将来锁屏启用时
* 以 system.pin 为准并做同源迁移(panel.pin 视为其缓存/别名),此处仅留兼容注记,不实现。
* panel.auth_enabled/panel.auth_pass 为旧密码机制,保留兼容迁移(见鉴权段注释),新代码以 PIN 为准。 */
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
};
@@ -945,41 +957,109 @@ static long long audio_kv_ll(const char *txt, const char *key, long long defv)
return strtoll(b, NULL, 10);
}
static int api_audio(int sock)
/* 音频快照填充:把 `{"available":...}` 追加进 sbuf(供 /api/audio 单发与 /api/overview 聚合共用)。
* audiod 不在/超时/应答异常一律 `{"available":false}`(HTTP 200,不报错)——面板/首页据此降级,绝不假数据。 */
static void audio_fill(sbuf_t *s)
{
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
if (fd < 0) return send_json(sock, 200, "{\"available\":false}");
if (fd < 0) { sA(s, "{\"available\":false}"); return; }
uint8_t buf[MEOW_IPC_MAX];
uint16_t type = 0; uint32_t rid = 0;
int n = -1;
if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0)
n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1);
close(fd);
if (n <= 0 || type != MSG_AUDIO_STATE)
return send_json(sock, 200, "{\"available\":false}");
if (n <= 0 || type != MSG_AUDIO_STATE) { sA(s, "{\"available\":false}"); return; }
buf[n] = 0;
const char *txt = (const char *)buf;
char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16];
audio_kv(txt, "app", app, sizeof app);
audio_kv(txt, "fmt", fmt, sizeof fmt);
audio_kv(txt, "route_mode", rmode, sizeof rmode);
sbuf_t s = {0};
sA(&s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0));
sA_str(&s, app);
sA(&s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":",
sA(s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0));
sA_str(s, app);
sA(s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":",
audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0));
sA_str(&s, fmt);
sA(&s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":",
sA_str(s, fmt);
sA(s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":",
audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0),
audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0));
sA_str(&s, rmode);
sA(&s, ",\"port\":%lld,\"vol\":%lld}",
sA_str(s, rmode);
sA(s, ",\"port\":%lld,\"vol\":%lld",
audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1));
int rc = send_json(sock, 200, s.b ? s.b : "");
/* 电平监控(261):rms/peak deci-dB(-990=-inf),lvlseq=快照代际;透传时别漏(ABI 追加键)。 */
sA(s, ",\"rms\":%lld,\"peak\":%lld,\"lvlseq\":%lld}",
audio_kv_ll(txt, "rms", -990), audio_kv_ll(txt, "peak", -990), audio_kv_ll(txt, "lvlseq", 0));
}
static int api_audio(int sock)
{
sbuf_t s = {0};
audio_fill(&s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
/* ---------------- 功耗管理:meow_powermgr IPC(MSG_PM_QUERY→MSG_PM_RESULT) ----------------
* 客户端序列同 perf/power:connect → MSG_HELLO → 读 ACK → MSG_PM_QUERY(subset) → MSG_PM_RESULT(JSON)。
* powermgr 不在/超时/应答异常 → 负错误码(调用方转 503 POWER_UNAVAILABLE;overview 里转 available:false)。 */
static int ipc_pm_query(uint32_t subset, char *out, int cap)
{
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
if (fd < 0) return MEOW_ERR_NODEV;
int rc = MEOW_ERR_INTERNAL;
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
m_pm_query_t q;
memset(&q, 0, sizeof q);
q.subset = subset;
if (send_frame(fd, MSG_PM_QUERY, 0x31, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; goto done; }
uint8_t buf[MEOW_IPC_MAX];
uint16_t type; uint32_t rid;
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
if (type != MSG_PM_RESULT) { rc = MEOW_ERR_INVAL; goto done; }
uint32_t c; memcpy(&c, buf, 4);
if (c != MEOW_OK) { rc = (int)c; goto done; }
/* powermgr 发**紧凑**负载:json 从偏移 12 起(json_len 在偏移 8);同 m_pm_result_t 布局,兼容全量/紧凑。 */
uint16_t jl; memcpy(&jl, buf + 8, 2);
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
memcpy(out, buf + 12, jl);
out[jl] = 0;
rc = MEOW_OK;
done:
close(fd);
return rc;
}
/* GET /api/power → powermgr 聚合 JSON(battery/estimate/breakdown,confidence 原样透传)。 */
static int api_power(int sock)
{
char js[MEOW_IPC_MAX];
int rc = ipc_pm_query(0, js, sizeof js);
if (rc != MEOW_OK)
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用");
return send_json(sock, 200, js);
}
/* ---- am/meow_svcd 二进制解析(popen 薄壳,复用有界收割 run_cmd) ----
* env MEOW_AMS_BIN / MEOW_SVCD_BIN 可覆盖(宿主/qemu 测试用;设备默认 /usr/bin,回退卡上路径)。 */
static const char *ams_bin(void)
{
const char *e = getenv("MEOW_AMS_BIN");
if (e && e[0]) return e;
if (!access(AMS_BIN, X_OK)) return AMS_BIN;
return AMS_BIN_CARD;
}
static const char *svcd_bin(void)
{
const char *e = getenv("MEOW_SVCD_BIN");
if (e && e[0]) return e;
if (!access(SVCD_BIN, X_OK)) return SVCD_BIN;
return SVCD_BIN_CARD;
}
/* ---------------- 网络管理:meow_netmgrd IPC(MSG_NET_*,期2 观测层/DNS 代理) ----------------
* 收口于独立 socket /usr/data/meow_net.sock(--sock-net / env MEOW_NET_SOCK 覆盖,宿主测试用)。
* 读级(STATUS/CONNS/DNS status|stats)netmgrd 放行任意调用者;flush 写级——HTTP 层沿用面板
@@ -1216,6 +1296,199 @@ static int api_net_log(int sock)
return rc;
}
/* ---------------- amsd / svcd 服务表 + 首页聚合(批1 只读) ---------------- */
/* 就地去掉 ANSI 转义序列(\033[...final);svcd status all 走管道时 CLI 会去色,此处双保险)。 */
static void panel_strip_ansi(char *s)
{
char *r = s, *w = s;
while (*r) {
if (*r == '\033' && r[1] == '[') {
r += 2;
while (*r && !(*r >= '@' && *r <= '~')) r++;
if (*r) r++;
} else { *w++ = *r++; }
}
*w = 0;
}
/* 切分成空白分隔 token(就地改 buf,返回个数;<maxtoks)。 */
static int txt_tokens(char *buf, char **toks, int maxtoks)
{
int n = 0;
char *p = buf;
while (*p && n < maxtoks) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (!*p) break;
toks[n++] = p;
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
if (*p) *p++ = 0;
}
return n;
}
/* join toks[from..to) 空格连接进 out(exec/name 可能含空格)。 */
static void join_tokens(char **toks, int from, int to, char *out, size_t cap)
{
size_t o = 0;
for (int i = from; i < to; i++) {
size_t need = strlen(toks[i]) + (o ? 1 : 0);
if (o + need >= cap) { out[cap - 1] = 0; return; }
if (o) out[o++] = ' ';
memcpy(out + o, toks[i], strlen(toks[i]));
o += strlen(toks[i]);
}
out[o] = 0;
}
/* GET /api/ams:`am list`(注册表+状态)parse → App 表。amsd 不在 → available:false(绝无假数据)。 */
static void ams_fill(sbuf_t *s)
{
char out[16384];
char cmd[300];
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
int code = run_cmd(cmd, out, sizeof out);
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
panel_strip_ansi(out);
char *toks[256];
sbuf_t apps = {0};
int first = 1, count = 0;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
if (strncmp(ln, "APP ", 4) == 0) {
int n = txt_tokens(ln, toks, 256);
if (n >= 9) {
/* 布局:APP <pkg> <name...> <state> <pid> <launches> <crashes> <last_kind> <last_code>
* tok[0]="APP" 前缀;自右向左固定:state=tok[n-6], pid=n-5, launches=n-4, crashes=n-3,
* last_kind=n-2, last_code=n-1;pkg=tok[1],name=join(tok[2..n-6))。 */
char nm[128];
join_tokens(toks, 2, n - 6, nm, sizeof nm);
if (!first) sA(&apps, ",");
first = 0; count++;
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
sA(&apps, ",\"state\":"); sA_str(&apps, toks[n-6]);
sA(&apps, ",\"pid\":%d", atoi(toks[n-5]));
sA(&apps, ",\"launches\":%d", atoi(toks[n-4]));
sA(&apps, ",\"crashes\":%d", atoi(toks[n-3]));
sA(&apps, ",\"last_kind\":"); sA_str(&apps, toks[n-2]);
sA(&apps, ",\"last_code\":%d}", atoi(toks[n-1]));
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]}", count, apps.b ? apps.b : "");
sbuf_free(&apps);
}
static int api_ams(int sock)
{
sbuf_t s = {0};
ams_fill(&s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
/* GET /api/svcd:`meow_svcd status all` parse → 服务表(state/pid/restart 计数/exec/来源)。
* svcd 未运行(exit!=0)→ available:false(绝无假数据)。 */
static void svcd_fill(sbuf_t *s)
{
char out[16384];
char cmd[300];
snprintf(cmd, sizeof cmd, "%s status all 2>/dev/null", svcd_bin());
int code = run_cmd(cmd, out, sizeof out);
if (code != 0) { sA(s, "{\"available\":false}"); return; }
panel_strip_ansi(out);
char *toks[256];
sbuf_t svcs = {0};
int first = 1, count = 0;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
int n = txt_tokens(ln, toks, 256);
/* 数据行:NAME STATE PID RESTART BACKOFF TRIGGER SRC EXIT EXIT_TIME EXEC...
* state=tok[1] 须为合法状态;exec=join(tok[9..n))。 */
if (n >= 10) {
const char *st = toks[1];
int is_known = (!strcmp(st,"running") || !strcmp(st,"stopped") ||
!strcmp(st,"stopping") || !strcmp(st,"disabled") || !strcmp(st,"failed"));
if (is_known) {
char exec[512];
join_tokens(toks, 9, n, exec, sizeof exec);
int restarts = 0;
{ const char *slash = strchr(toks[3], '/'); (void)slash; restarts = atoi(toks[3]); }
if (!first) sA(&svcs, ",");
first = 0; count++;
sA(&svcs, "{\"name\":"); sA_str(&svcs, toks[0]);
sA(&svcs, ",\"state\":"); sA_str(&svcs, toks[1]);
sA(&svcs, ",\"pid\":%d", atoi(toks[2]));
sA(&svcs, ",\"restart\":%d", restarts);
sA(&svcs, ",\"restart_str\":");
sA_str(&svcs, toks[3]);
sA(&svcs, ",\"backoff_ms\":%d", atoi(toks[4]));
sA(&svcs, ",\"trigger\":");
sA_str(&svcs, toks[5]);
sA(&svcs, ",\"src\":");
sA_str(&svcs, toks[6]);
sA(&svcs, ",\"exit\":");
sA_str(&svcs, toks[7]);
sA(&svcs, ",\"exit_time\":");
sA_str(&svcs, toks[8]);
sA(&svcs, ",\"exec\":");
sA_str(&svcs, exec);
sA(&svcs, "}");
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"count\":%d,\"services\":[%s]}", count, svcs.b ? svcs.b : "");
sbuf_free(&svcs);
}
static int api_svcd(int sock)
{
sbuf_t s = {0};
svcd_fill(&s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
/* GET /api/overview:首页生命线。**单线程后端只暴露聚合**(不放六个单测端点给首页轮询)。
* 后端顺序 fan-out perf/power/audio/net/amsd/svcd 六源合成一份 JSON;任一源失败 → 该子块 `available:false`,
* 绝不假数据(对齐 netmgrd 503 降级语义)。 */
static int api_overview(int sock)
{
sbuf_t s = {0};
sA(&s, "{\"ts\":%lld,", (long long)time(NULL));
char pb[4096];
if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK) sA(&s, "\"perf\":%s,", pb);
else sA(&s, "\"perf\":{\"available\":false},");
char pw[MEOW_IPC_MAX];
if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK) sA(&s, "\"power\":%s,", pw);
else sA(&s, "\"power\":{\"available\":false},");
sA(&s, "\"audio\":");
audio_fill(&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])
sA(&s, ",\"net\":%s", njson);
else
sA(&s, ",\"net\":{\"available\":false}");
sA(&s, ",\"ams\":");
ams_fill(&s);
sA(&s, ",\"svcd\":");
svcd_fill(&s);
sA(&s, "}");
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
/* ---------------- 路径安全 ---------------- */
static int tf_present(void)
{
@@ -1828,26 +2101,27 @@ static int read_request(int sock, req_t *r)
return 0;
}
/* ================= 可选鉴权(panel.auth_enabled / panel.auth_pass) =================
* 设计(MEOWPANEL_PROTOCOL §1.3 落地):
* - 键 `panel.auth_enabled`(默认 0=关)开关;`panel.auth_pass` 存「sha256 带盐哈希」格式 `salt:hex`,
* 绝不落明文,也绝不进日志/响应。
* - 启用后,除 /api/auth/{login,logout,status} 外,所有 /api/ 端点需有效会话
* (`Authorization: Bearer <token>` / `X-MeowToken: <token>` / `Cookie: meow_token=<token>`)。
* - 会话 token 由 /dev/urandom 生成(32B → 64 hex),存内存 + 卡上文件持久化(防重启失效),带过期。
* - 未启用(默认)时鉴权门禁直接放行,行为与现状完全一致(零回归);危险端点不额外限制。
/* ================= 访问级 PIN 鉴权(PANEL_ENHANCE_PLAN 用户定案 2026-09-01) =================
* 模型(用户二次定案,覆盖 282「默认无鉴权」前提):
* - **访问面板即需登录**:未认证请求一律 401(静态登录页自身 + css/js 放行——serve_static 先于本门禁)。
* - 4 位数字 PIN,设置键 `panel.pin`:sha256+salt 存 settings.conf(格式 `<salt_hex>:<sha256_hex(salt||pin)>`,
* auth_make_hash 生成)。与未来 MeowOS 锁屏 PIN 同源:设置键 `system.pin` 预留(本后端只读面板键,见 g_sets 注记)。
* - token 随机 **128bit**(/dev/urandom 16B → 32 hex),**只存面板进程内存**(`g_sess[]`,重启自然失效,无卡上持久化)。
* - 会话有效期 **3 天**(AUTH_TOKEN_TTL,**固定不滑动续期**)或设备重启(内存清、先到为准);多会话并存。
* - **未设 PIN**:首次访问走 `POST /api/auth/setup` 引导设置;未设 PIN 前无法建立任何会话,故全部
* `/api/` 路径被拦(读端点也按「访问级」全拦——定案即全拦,登录/设置流程除外),写端点自然一律拒绝。
* - **错误锁定**:连续 PIN_MAX_FAILS=5 次错 PIN 锁 PIN_LOCK_SEC=300s(内存态,重启清)。
* - **旧密码机制兼容迁移**(`panel.auth_enabled`/`panel.auth_pass`):旧密码存在且未设 PIN 时,
* 视为「已设访问屏障」的同等物——登录暂可用旧密码(body `password` 字段),status 透出 `legacy:true`
* 提示改设 4 位 PIN;一旦设置 `panel.pin`,**新代码以 PIN 为准**(登录只认 PIN,旧密码不再作为凭据)。
*/
#define AUTH_TOKEN_TTL (12 * 3600) /* 会话有效期:12 小时 */
#define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */
#define AUTH_HASH_LEN 64 /* sha256 hex 长度 */
static char g_auth_session_file[600];
static const char *auth_session_file(void)
{
if (!g_auth_session_file[0])
snprintf(g_auth_session_file, sizeof g_auth_session_file, "%s/meowpanel.session", web_root());
return g_auth_session_file;
}
#define AUTH_TOKEN_BYTES 16 /* 128bit → 32 hex */
#define AUTH_TOKEN_TTL (3 * 86400) /* 3 天(固定,不滑动续期) */
#define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */
#define AUTH_HASH_LEN 64 /* sha256 hex 长度 */
#define PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
#define PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
static time_t g_auth_ttl = AUTH_TOKEN_TTL; /* 会话 TTL(env MEOW_AUTH_TTL 可覆盖,宿主测试用) */
/* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 mp_ 避免符号冲突) ---- */
static const uint32_t mp_sha256_k[64] = {
@@ -1973,10 +2247,10 @@ static void auth_hex(const unsigned char *raw, size_t n, char *out)
out[n*2] = 0;
}
static int auth_token_gen(char *out, int outcap) /* 32B → 64 hex */
static int auth_token_gen(char *out, int outcap) /* 16B → 32 hex(128bit) */
{
unsigned char raw[32];
if (outcap < 65) return -1;
unsigned char raw[AUTH_TOKEN_BYTES];
if (outcap < AUTH_TOKEN_BYTES * 2 + 1) return -1;
if (auth_read_random(raw, sizeof raw) < 0) return -1;
auth_hex(raw, sizeof raw, out);
return 0;
@@ -2013,12 +2287,32 @@ static int auth_make_hash(const char *password, char *out, int outcap) /* salt:
return 0;
}
static int auth_enabled(void)
/* ---- PIN / 旧密码 凭据校验(都走「salt:hex」哈希校验,恒定比较) ---- */
/* 校验 `salt:hex` 哈希是否匹配明文(恒定比较,不反解)。stored 为 `salt:hex` 完整串。 */
static int hash_matches(const char *stored, const char *password)
{
return settings_get_int("panel.auth_enabled", 0) != 0;
if (!stored || !stored[0] || !password) return 0;
char tmp[160];
snprintf(tmp, sizeof tmp, "%s", stored);
char *colon = strchr(tmp, ':');
if (!colon) return 0;
*colon = 0;
char want[AUTH_HASH_LEN + 1];
if (auth_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
if (strlen(colon + 1) != AUTH_HASH_LEN) return 0;
return memcmp(want, colon + 1, AUTH_HASH_LEN) == 0;
}
static int auth_pass_set(void)
static int pin_set(void) /* panel.pin 是否已设(访问屏障开启) */
{
char *v = settings_get_value("panel.pin");
int s = (v && v[0]) ? 1 : 0;
free(v);
return s;
}
static int auth_pass_set(void) /* 旧密码是否已设(兼容迁移用) */
{
char *v = settings_get_value("panel.auth_pass");
int s = (v && v[0]) ? 1 : 0;
@@ -2026,25 +2320,57 @@ static int auth_pass_set(void)
return s;
}
static int auth_verify(const char *password)
static int auth_verify(const char *password) /* 旧密码校验(迁移期;见 login) */
{
char *stored = settings_get_value("panel.auth_pass");
int ok = 0;
if (stored && stored[0]) {
char *colon = strchr(stored, ':');
if (colon) {
*colon = 0;
char want[AUTH_HASH_LEN + 1];
if (auth_pass_hash(stored, password, want, sizeof want) == 0 && strlen(colon + 1) == AUTH_HASH_LEN
&& strcmp(want, colon + 1) == 0)
ok = 1;
}
}
int ok = hash_matches(stored, password);
free(stored);
return ok;
}
/* ---- 会话(内存 + 卡上持久化) ---- */
static int pin_verify(const char *pin) /* PIN 校验 */
{
char *stored = settings_get_value("panel.pin");
int ok = hash_matches(stored, pin);
free(stored);
return ok;
}
/* PIN 合法:恰好 4 位数字(0-9)。 */
static int pin_validate(const char *pin)
{
if (!pin) return 0;
size_t l = strlen(pin);
if (l != 4) return 0;
for (size_t i = 0; i < l; i++)
if (pin[i] < '0' || pin[i] > '9') return 0;
return 1;
}
/* ---- 错误锁定(内存态,重启清;4 位 PIN 防爆破必备) ---- */
static int g_pin_fails = 0;
static time_t g_pin_locked_until = 0;
static long long pin_lock_remaining(void) /* 剩余锁定秒;0=未锁 */
{
time_t now = time(NULL);
if (g_pin_locked_until <= now) return 0;
return (long long)(g_pin_locked_until - now);
}
static void pin_lock_reset(void)
{
g_pin_fails = 0;
g_pin_locked_until = 0;
}
static void pin_record_fail(void)
{
if (++g_pin_fails >= PIN_MAX_FAILS) {
g_pin_locked_until = time(NULL) + PIN_LOCK_SEC;
g_pin_fails = 0;
}
}
/* ---- 会话(**纯内存**:重启自然失效,无卡上持久化) ---- */
typedef struct { char token[65]; time_t expires; } auth_sess_t;
static auth_sess_t g_sess[AUTH_MAX_SESS];
static int g_sess_n = 0;
@@ -2090,38 +2416,6 @@ static void sess_remove(const char *token)
g_sess_n--;
}
static void sess_save(void)
{
FILE *f = fopen(auth_session_file(), "w");
if (!f) return;
time_t now = time(NULL);
for (int i = 0; i < g_sess_n; i++) {
if (g_sess[i].expires > 0 && g_sess[i].expires <= now) continue;
fprintf(f, "%s %lld\n", g_sess[i].token, (long long)g_sess[i].expires);
}
fclose(f);
}
static void sess_load(void)
{
FILE *f = fopen(auth_session_file(), "r");
if (!f) return;
char line[128];
time_t now = time(NULL);
g_sess_n = 0;
while (g_sess_n < AUTH_MAX_SESS && fgets(line, sizeof line, f)) {
char tok[65];
long long exp = 0;
if (sscanf(line, "%64s %lld", tok, &exp) >= 1) {
if (exp > 0 && exp <= (long long)now) continue; /* 已过期 */
snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", tok);
g_sess[g_sess_n].expires = (time_t)exp;
g_sess_n++;
}
}
fclose(f);
}
static void auth_cp(char *dst, int dcap, const char *src) /* 有界拷贝(防 -Wformat-truncation) */
{
size_t l = strlen(src);
@@ -2155,48 +2449,84 @@ static int extract_token(const req_t *r, char *out, int outcap)
return 0;
}
/* ---- 鉴权门禁:启用后除 /api/auth 系列外全部 /api/ 端点需会话 ---- */
/* ---- 鉴权门禁:**访问面板即需登录**——除 /api/auth 系列外,全部 /api/ 端点需有效会话。
* 未设 PIN 时无法建立会话 → 读端点也一律 401(「全拦」定案),写端点自然一律拒绝;登录/设置流程除外。 ---- */
static int require_auth(int sock, const req_t *r)
{
if (!auth_enabled()) return 1; /* 默认关:零回归 */
const char *p = r->path;
if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") || !strcmp(p, "/api/auth/status")) return 1;
if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") ||
!strcmp(p, "/api/auth/status") || !strcmp(p, "/api/auth/setup")) return 1;
char t[65];
if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1;
return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效");
}
/* GET /api/auth/status(公开):访问屏障状态 + 当前会话 + 锁定信息。 */
static int api_auth_status(int sock, const req_t *r)
{
int en = auth_enabled();
int ps = pin_set();
int lg = auth_pass_set();
int sess = 0;
if (en) {
char t[65];
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
}
char t[65];
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
long long rem = pin_lock_remaining();
sbuf_t s = {0};
sA(&s, "{\"auth_enabled\":%s,\"session\":%s}", en ? "true" : "false", sess ? "true" : "false");
sA(&s, "{\"auth_enabled\":true,\"session\":%s,\"pin_set\":%s,\"legacy\":%s,\"locked\":%s",
sess ? "true" : "false", ps ? "true" : "false", lg ? "true" : "false", rem > 0 ? "true" : "false");
if (rem > 0) sA(&s, ",\"lock_remaining_ms\":%lld", rem * 1000LL);
sA(&s, "}");
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
/* 登录统一入口:PIN(panel.pin 已设,权威)或旧密码(仅未设 PIN 时;迁移期)。
* 连续 PIN_MAX_FAILS 次错 → 锁 PIN_LOCK_SEC(429 LOCKED)。 */
static int api_auth_login(int sock, const req_t *r)
{
if (!auth_enabled()) return send_err(sock, 400, "BAD_REQUEST", "面板鉴权未启用");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少密码");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *pw = jv_obj_get(root, "password");
if (!pw || pw->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); }
int ok = auth_verify(pw->str);
jv_t *jpin = jv_obj_get(root, "pin");
jv_t *jpw = jv_obj_get(root, "password");
char pin[8], pw[256];
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0;
if (jpw && jpw->type == J_STR && jpw->str) snprintf(pw, sizeof pw, "%s", jpw->str); else pw[0] = 0;
jv_free(root);
if (!ok) return send_err(sock, 401, "UNAUTHORIZED", "密码错误");
if (pin_lock_remaining() > 0) { /* 已在锁定期 */
sbuf_t e = {0};
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"已锁定,请稍后重试\"},\"lock_remaining_ms\":%lld}",
pin_lock_remaining() * 1000LL);
int rc = send_json(sock, 429, e.b ? e.b : "");
sbuf_free(&e);
return rc;
}
int use_legacy = 0;
const char *cred = NULL;
if (pin_set()) { cred = pin; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 pin 字段"); }
else if (auth_pass_set()) { use_legacy = 1; cred = pw; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); }
else return send_err(sock, 400, "BAD_REQUEST", "请先设置面板 PIN(/api/auth/setup)");
int ok = use_legacy ? auth_verify(cred) : pin_verify(cred);
if (!ok) {
pin_record_fail();
if (pin_lock_remaining() > 0) { /* 本错已触发锁定 */
sbuf_t e = {0};
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"连续错误次数过多,已锁定 5 分钟\"},\"lock_remaining_ms\":%lld}",
pin_lock_remaining() * 1000LL);
int rc = send_json(sock, 429, e.b ? e.b : "");
sbuf_free(&e);
return rc;
}
return send_err(sock, 401, "UNAUTHORIZED", "PIN 错误");
}
pin_lock_reset();
char token[65];
if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败");
time_t exp = time(NULL) + AUTH_TOKEN_TTL;
time_t exp = time(NULL) + g_auth_ttl; /* 固定 3 天(MEOW_AUTH_TTL 可覆盖测试用),不滑动续期 */
sess_add(token, exp);
sess_save();
sbuf_t s = {0};
sA(&s, "{\"token\":");
sA_str(&s, token);
@@ -2206,10 +2536,31 @@ static int api_auth_login(int sock, const req_t *r)
return rc;
}
/* POST /api/auth/setup(公开):首次设置 4 位 PIN(仅未设 PIN 时可用;设置后走 login)。
* 未设 PIN 前写端点一律拒绝;本端点即「设置流程」例外,用于引导建立首个访问屏障。 */
static int api_auth_setup(int sock, const req_t *r)
{
if (pin_set()) return send_err(sock, 409, "CONFLICT", "面板 PIN 已设置,请直接登录");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jpin = jv_obj_get(root, "pin");
char pin[8];
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str);
else pin[0] = 0;
jv_free(root);
if (!pin_validate(pin)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
char h[200];
if (auth_make_hash(pin, h, sizeof h) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
if (settings_write_one("panel.pin", h) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
pin_lock_reset();
return send_json(sock, 200, "{\"pin_set\":true}");
}
static int api_auth_logout(int sock, const req_t *r)
{
char t[65];
if (extract_token(r, t, sizeof t) && t[0]) { sess_remove(t); sess_save(); }
if (extract_token(r, t, sizeof t) && t[0]) sess_remove(t);
return send_json(sock, 200, "{\"logged_out\":true}");
}
@@ -2554,8 +2905,8 @@ static void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val)
} else if (d->type == ST_BOOL) {
sA(s, "%s", str_truthy(val) ? "true" : "false");
} else {
if (!strcmp(d->key, "panel.auth_pass")) {
/* 密码哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设密码) */
if (!strcmp(d->key, "panel.auth_pass") || !strcmp(d->key, "panel.pin")) {
/* 密码/PIN 哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设) */
sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false");
} else {
sA_str(s, val);
@@ -2641,6 +2992,23 @@ static int api_settings_put(int sock, const char *key, const req_t *r)
}
jv_free(root);
char hashbuf[200];
/* panel.pin:4 位数字 PIN,只落带盐哈希;空串=清除(回未设状态);非法(非 4 位数字)→ 422 */
if (!strcmp(key, "panel.pin")) {
if (raw[0]) {
if (!pin_validate(raw)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
} else {
hashbuf[0] = 0; /* 清空 → 回未设状态 */
}
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
pin_lock_reset();
sbuf_t s = {0};
settings_item_json(&s, d, hashbuf);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */
if (!strcmp(key, "panel.auth_pass")) {
if (raw[0]) {
@@ -2656,13 +3024,13 @@ static int api_settings_put(int sock, const char *key, const req_t *r)
sbuf_free(&s);
return rc;
}
/* panel.auth_enabled:启用前必须先设密码 */
/* panel.auth_enabled(旧机制,兼容迁移):置 1 前须已有访问屏障(PIN 或旧密码) */
if (!strcmp(key, "panel.auth_enabled")) {
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0)
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
if (!strcmp(coerced, "1") && !auth_pass_set())
return send_err(sock, 422, "UNSUPPORTED", "请先设置面板密码");
if (!strcmp(coerced, "1") && !pin_set() && !auth_pass_set())
return send_err(sock, 422, "UNSUPPORTED", "请先设置面板 PIN 或密码");
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
sbuf_t s = {0};
settings_item_json(&s, d, coerced);
@@ -2706,6 +3074,17 @@ static int api_settings_batch(int sock, const req_t *r)
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
default: if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"BAD_REQUEST\",\"message\":\"非法值类型\"}"); nfail++; continue;
}
/* panel.pin:只落带盐哈希(空=清除);非法(非 4 位数字)→ 422 */
if (!strcmp(p->str, "panel.pin")) {
char hashbuf[200];
if (raw[0]) {
if (!pin_validate(raw)) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"PIN 必须为 4 位数字\"}"); nfail++; continue; }
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成 PIN 哈希失败\"}"); nfail++; continue; }
} else hashbuf[0] = 0;
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
continue;
}
/* panel.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */
if (!strcmp(p->str, "panel.auth_pass")) {
char hashbuf[200];
@@ -2722,10 +3101,10 @@ static int api_settings_batch(int sock, const req_t *r)
nfail++;
continue;
}
/* panel.auth_enabled:启用前必须先设密码 */
if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !auth_pass_set()) {
/* panel.auth_enabled(迁移):启用前须已有访问屏障(PIN 或旧密码) */
if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板密码\"}");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板 PIN 或密码\"}");
nfail++;
continue;
}
@@ -4677,8 +5056,9 @@ static int handle(int sock, req_t *r)
/* ---- 可选鉴权门禁(启用后除 /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);
@@ -4693,9 +5073,15 @@ static int handle(int sock, req_t *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 快照,只读) ---- */
/* ---- 音频状态(meow_audiod 快照,只读;含 rms/peak/lvlseq 电平键) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
/* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/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);
/* ---- 网络管理(meow_netmgrd 期2 透传:读级任意调用者;flush 写级走 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/conns")) return api_net_conns(sock, r->query);
@@ -4833,10 +5219,11 @@ int main(int argc, char **argv)
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]\n");
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;
}
}
@@ -4851,6 +5238,10 @@ int main(int argc, char **argv)
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) */
@@ -4867,7 +5258,6 @@ int main(int argc, char **argv)
signal(SIGPIPE, SIG_IGN);
web_root();
sess_load(); /* 加载卡上持久化的会话(防重启失效) */
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);