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:
1 parent
30e2ec901f
commit
d12164305d
6 files changed
+627
-155
No files matched your search
+504
-114
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user