feat(meowpanel): 可选密码鉴权 (agent-196, 用户定案) — panel.auth_enabled(默认0)+panel.auth_pass(带盐sha256不明文); token会话(/dev/urandom,TTL12h,内存+卡持久化,静态屏蔽session文件); 未启用零回归,启用后 /api/* 全要会话; 前端登录页+401回跳+设置卡片; 顺带修 settings_write_one 容忍等号两侧空格(原会重复追加行)

meowpanel→f91856a4; 0 warning; 宿主curl 20例+headless Chromium 全过
This commit is contained in:
MeowOS committed 2026-08-31 05:25:28 +08:00
1 parent 5f7e7542bd
commit feb3df681c
7 files changed
+701 -35

No files matched your search

+522 -14
View File
@@ -56,6 +56,8 @@
#define DEFAULT_PORT 8080
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
#define SETTINGS_HDR "# meow-settings 1"
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;默认设备路径)。 */
static const char *g_settings_file = SETTINGS_FILE;
/* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。
* TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */
static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
@@ -204,6 +206,7 @@ static int send_err(int sock, int status, const char *code, const char *msg);
static int send_json(int sock, int status, const char *data);
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
static void panel_log(const char *fmt, ...);
static const char *web_root(void); /* 定义在文件后部;此处前向声明供鉴权会话路径使用 */
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
@@ -481,6 +484,9 @@ 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 自身,可选鉴权) */
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
};
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
@@ -492,10 +498,40 @@ static const setdef_t *setdef_find(const char *key) {
/* ---------------- settings.conf 解析(照 meow_ui_vol_read / settings_get_int_trim) ----------------
* '=' 切分,key 去首尾空白精确匹配(防 audio.volume 误配 audio.volume_max)。
* 路径取 g_settings_file(env MEOW_SETTINGS_FILE 可覆盖,宿主测试用)。
*/
static void settings_mkdir_p(void)
{
/* 逐级创建 settings.conf 所在目录(mkdir -p 语义,容忍已存在)。 */
char tmp[1024];
snprintf(tmp, sizeof tmp, "%s", g_settings_file);
char *slash = strrchr(tmp, '/');
if (!slash || slash == tmp) return; /* 无目录或根部 */
*slash = 0;
if (!tmp[0]) return;
char buf[1024]; buf[0] = 0;
size_t i = 0;
if (tmp[0] == '/') buf[i++] = '/';
const char *s = tmp + (tmp[0] == '/' ? 1 : 0);
while (*s) {
const char *tom = strchr(s, '/');
size_t l = tom ? (size_t)(tom - s) : strlen(s);
if (l) {
if (i && buf[i - 1] != '/') buf[i++] = '/';
if (i + l >= sizeof buf) break;
memcpy(buf + i, s, l);
i += l;
buf[i] = 0;
mkdir(buf, 0777);
}
if (!tom) break;
s = tom + 1;
}
}
static char *settings_get_value(const char *key)
{
FILE *f = fopen(SETTINGS_FILE, "r");
FILE *f = fopen(g_settings_file, "r");
if (!f) return NULL;
size_t kl = strlen(key);
char line[256];
@@ -544,8 +580,8 @@ static int brightness_clamp(int v)
* O_TRUNC 重写。成功返回 0。 */
static int settings_write_one(const char *key, const char *val)
{
mkdir("/usr/data/meow", 0777); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
FILE *f = fopen(SETTINGS_FILE, "r");
settings_mkdir_p(); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
FILE *f = fopen(g_settings_file, "r");
char tmp[2048];
size_t n = 0;
int c;
@@ -554,22 +590,28 @@ static int settings_write_one(const char *key, const char *val)
fclose(f);
}
tmp[n] = 0;
FILE *nf = fopen(SETTINGS_FILE, "w");
FILE *nf = fopen(g_settings_file, "w");
if (!nf) return -1;
size_t klen = strlen(key);
int found = 0;
if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR);
/* 逐行重写:头照抄;命中 key 行替换;其余照抄 */
/* 逐行重写:头照抄;命中 key 行替换;其余照抄。匹配容忍 `key = value` 两侧空格(与 settings_get_value 同口径)。 */
char *save = NULL;
for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
char *p = ln;
while (*p == ' ' || *p == '\t') p++;
if (!strncmp(p, key, klen) && p[klen] == '=') {
fprintf(nf, "%s = %s\n", key, val);
found = 1;
} else {
fprintf(nf, "%s\n", ln);
char *eq = strchr(p, '=');
if (eq) {
char *k = p;
size_t km = (size_t)(eq - k);
while (km > 0 && (k[km - 1] == ' ' || k[km - 1] == '\t')) km--;
if (km == klen && strncmp(k, key, klen) == 0) {
fprintf(nf, "%s = %s\n", key, val);
found = 1;
continue;
}
}
fprintf(nf, "%s\n", ln);
}
if (!found) fprintf(nf, "%s = %s\n", key, val);
fclose(nf);
@@ -630,8 +672,8 @@ static int settings_coerce(const setdef_t *d, const char *input, char *out, size
/* 恢复全部默认:重写整个文件为 头 + 所有键 def。 */
static int settings_reset_all(void)
{
mkdir("/usr/data/meow", 0777);
FILE *nf = fopen(SETTINGS_FILE, "w");
settings_mkdir_p();
FILE *nf = fopen(g_settings_file, "w");
if (!nf) return -1;
fprintf(nf, "%s\n", SETTINGS_HDR);
for (int i = 0; i < SETS_COUNT; i++)
@@ -1232,6 +1274,9 @@ typedef struct {
char *body;
size_t body_len;
char content_type[128]; /* 请求 Content-Type(若非空) */
char authorization[256]; /* Authorization 头(Bearer <token>) */
char x_meow_token[256]; /* X-MeowToken 头 */
char cookie[512]; /* Cookie 头(meow_token=<token> 等) */
} req_t;
static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
@@ -1364,6 +1409,21 @@ static int read_request(int sock, req_t *r)
while (*v == ' ') v++;
snprintf(r->content_type, sizeof r->content_type, "%s", v);
}
else if (!strncasecmp(hp, "Authorization:", 14)) {
char *v = hp + 14;
while (*v == ' ') v++;
snprintf(r->authorization, sizeof r->authorization, "%s", v);
}
else if (!strncasecmp(hp, "X-MeowToken:", 12)) {
char *v = hp + 12;
while (*v == ' ') v++;
snprintf(r->x_meow_token, sizeof r->x_meow_token, "%s", v);
}
else if (!strncasecmp(hp, "Cookie:", 7)) {
char *v = hp + 7;
while (*v == ' ') v++;
snprintf(r->cookie, sizeof r->cookie, "%s", v);
}
hp = he + 2;
}
char *body_start = buf + hend + 4; /* body 实际起点 */
@@ -1396,6 +1456,391 @@ 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),存内存 + 卡上文件持久化(防重启失效),带过期。
* - 未启用(默认)时鉴权门禁直接放行,行为与现状完全一致(零回归);危险端点不额外限制。
*/
#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;
}
/* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 mp_ 避免符号冲突) ---- */
static const uint32_t mp_sha256_k[64] = {
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
};
#define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
#define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
#define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22))
#define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25))
#define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3))
#define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10))
typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; size_t buflen; } mp_sha256_ctx;
static void mp_sha256_init(mp_sha256_ctx *c)
{
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
c->count = 0; c->buflen = 0;
}
static void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p)
{
uint32_t w[64];
int i;
for (i = 0; i < 16; i++)
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
for (i = 16; i < 64; i++)
w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16];
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
for (i = 0; i < 64; i++) {
uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i];
uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc);
h = g; g = f; f = e; e = d + t1;
d = cc; cc = b; b = a; a = t1 + t2;
}
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
}
static void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len)
{
c->count += (uint64_t)len;
if (c->buflen) {
size_t need = 64 - c->buflen;
size_t take = len < need ? len : need;
memcpy(c->buf + c->buflen, data, take);
c->buflen += take; data += take; len -= take;
if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; }
}
while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; }
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
}
static void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32])
{
uint64_t bitlen = c->count * 8;
uint8_t *b = c->buf;
size_t bl = c->buflen;
int i;
b[bl++] = 0x80;
if (bl > 56) {
while (bl < 64) b[bl++] = 0;
mp_sha256_block(c, b);
bl = 0;
}
while (bl < 56) b[bl++] = 0;
for (i = 0; i < 8; i++)
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
mp_sha256_block(c, b);
for (i = 0; i < 8; i++) {
out[i*4] = (uint8_t)(c->state[i] >> 24);
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
out[i*4+3] = (uint8_t)(c->state[i]);
}
}
#undef MP_ROR
#undef MP_CH
#undef MP_MAJ
#undef MP_BSIG0
#undef MP_BSIG1
#undef MP_SSIG0
#undef MP_SSIG1
/* ---- 鉴权基础 ----
* 密码存储格式:`<salt_hex>:<sha256_hex(salt || password)>`。salt 由 /dev/urandom 生成(16B → 32 hex)。
* 校验时重算并比较,绝不反解、绝不在日志/响应中输出 hash 或明文。
*/
static int auth_read_random(unsigned char *buf, size_t n)
{
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
size_t got = 0;
while (got < n) {
ssize_t rd = read(fd, buf + got, n - got);
if (rd <= 0) { close(fd); return -1; }
got += (size_t)rd;
}
close(fd);
return 0;
}
static void auth_hex(const unsigned char *raw, size_t n, char *out)
{
static const char hex[] = "0123456789abcdef";
for (size_t i = 0; i < n; i++) {
out[i*2] = hex[raw[i] >> 4];
out[i*2+1] = hex[raw[i] & 0xf];
}
out[n*2] = 0;
}
static int auth_token_gen(char *out, int outcap) /* 32B → 64 hex */
{
unsigned char raw[32];
if (outcap < 65) return -1;
if (auth_read_random(raw, sizeof raw) < 0) return -1;
auth_hex(raw, sizeof raw, out);
return 0;
}
static int auth_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
{
unsigned char raw[16];
if (outcap < 33) return -1;
if (auth_read_random(raw, sizeof raw) < 0) return -1;
auth_hex(raw, sizeof raw, out);
return 0;
}
static int auth_pass_hash(const char *salt, const char *password, char *out, int outcap)
{
mp_sha256_ctx c;
uint8_t digest[32];
if (outcap < AUTH_HASH_LEN + 1) return -1;
mp_sha256_init(&c);
mp_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
mp_sha256_update(&c, (const uint8_t *)password, strlen(password));
mp_sha256_final(&c, digest);
auth_hex(digest, sizeof digest, out);
return 0;
}
static int auth_make_hash(const char *password, char *out, int outcap) /* salt:hex */
{
char salt[33], h[AUTH_HASH_LEN + 1];
if (auth_make_salt(salt, sizeof salt) < 0) return -1;
if (auth_pass_hash(salt, password, h, sizeof h) < 0) return -1;
snprintf(out, outcap, "%s:%s", salt, h);
return 0;
}
static int auth_enabled(void)
{
return settings_get_int("panel.auth_enabled", 0) != 0;
}
static int auth_pass_set(void)
{
char *v = settings_get_value("panel.auth_pass");
int s = (v && v[0]) ? 1 : 0;
free(v);
return s;
}
static int auth_verify(const char *password)
{
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;
}
}
free(stored);
return ok;
}
/* ---- 会话(内存 + 卡上持久化) ---- */
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;
static int sess_find(const char *token)
{
for (int i = 0; i < g_sess_n; i++)
if (!strcmp(g_sess[i].token, token)) return i;
return -1;
}
static int sess_valid(const char *token)
{
int i = sess_find(token);
if (i < 0) return 0;
if (g_sess[i].expires > 0 && time(NULL) >= g_sess[i].expires) return 0;
return 1;
}
static void sess_add(const char *token, time_t expires)
{
int i = sess_find(token);
if (i >= 0) { g_sess[i].expires = expires; return; }
if (g_sess_n >= AUTH_MAX_SESS) { /* 满:淘汰最早过期的一条 */
int e = 0;
for (int k = 1; k < g_sess_n; k++)
if (g_sess[k].expires < g_sess[e].expires) e = k;
if (e < g_sess_n - 1)
memmove(&g_sess[e], &g_sess[e + 1], (size_t)(g_sess_n - 1 - e) * sizeof(auth_sess_t));
g_sess_n--;
}
snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", token);
g_sess[g_sess_n].expires = expires;
g_sess_n++;
}
static void sess_remove(const char *token)
{
int i = sess_find(token);
if (i < 0) return;
if (i < g_sess_n - 1)
memmove(&g_sess[i], &g_sess[i + 1], (size_t)(g_sess_n - 1 - i) * sizeof(auth_sess_t));
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);
if (l >= (size_t)dcap) l = (size_t)dcap - 1;
memcpy(dst, src, l);
dst[l] = 0;
}
/* ---- 从请求提取 token(X-MeowToken → Authorization: Bearer → Cookie meow_token) ---- */
static int extract_token(const req_t *r, char *out, int outcap)
{
out[0] = 0;
if (r->x_meow_token[0]) { auth_cp(out, outcap, r->x_meow_token); return out[0] ? 1 : 0; }
if (!strncasecmp(r->authorization, "Bearer ", 7)) {
auth_cp(out, outcap, r->authorization + 7);
return out[0] ? 1 : 0;
}
if (r->cookie[0]) {
const char *p = strstr(r->cookie, "meow_token=");
if (p) {
p += 11;
const char *e = p;
while (*e && *e != ';' && *e != ' ' && *e != '\t') e++;
size_t l = (size_t)(e - p);
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
memcpy(out, p, l);
out[l] = 0;
return out[0] ? 1 : 0;
}
}
return 0;
}
/* ---- 鉴权门禁:启用后除 /api/auth 系列外全部 /api/ 端点需会话 ---- */
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;
char t[65];
if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1;
return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效");
}
static int api_auth_status(int sock, const req_t *r)
{
int en = auth_enabled();
int sess = 0;
if (en) {
char t[65];
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
}
sbuf_t s = {0};
sA(&s, "{\"auth_enabled\":%s,\"session\":%s}", en ? "true" : "false", sess ? "true" : "false");
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
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", "缺少密码");
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_free(root);
if (!ok) return send_err(sock, 401, "UNAUTHORIZED", "密码错误");
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;
sess_add(token, exp);
sess_save();
sbuf_t s = {0};
sA(&s, "{\"token\":");
sA_str(&s, token);
sA(&s, ",\"expires_at\":%lld,\"auth_enabled\":true}", (long long)exp);
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
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(); }
return send_json(sock, 200, "{\"logged_out\":true}");
}
/* ---------------- HTTP 响应写入 ---------------- */
static int write_full(int sock, const void *b, size_t n)
{
@@ -1721,7 +2166,12 @@ 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 {
sA_str(s, val);
if (!strcmp(d->key, "panel.auth_pass")) {
/* 密码哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设密码) */
sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false");
} else {
sA_str(s, val);
}
}
sA(s, ",\"def\":");
sA_str(s, d->def);
@@ -1802,6 +2252,36 @@ static int api_settings_put(int sock, const char *key, const req_t *r)
default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持");
}
jv_free(root);
char hashbuf[200];
/* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */
if (!strcmp(key, "panel.auth_pass")) {
if (raw[0]) {
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
return send_err(sock, 500, "INTERNAL", "生成密码哈希失败");
} else {
hashbuf[0] = 0; /* 清空 → 禁用密码 */
}
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
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_enabled:启用前必须先设密码 */
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 (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
sbuf_t s = {0};
settings_item_json(&s, d, coerced);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
@@ -1838,6 +2318,15 @@ 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.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */
if (!strcmp(p->str, "panel.auth_pass")) {
char hashbuf[200];
if (raw[0]) { 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\":\"生成密码哈希失败\"}"); 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;
}
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
if (nfail) sA(&fail, ",");
@@ -1845,6 +2334,13 @@ 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()) {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板密码\"}");
nfail++;
continue;
}
if (settings_write_one(p->str, coerced) == 0) {
if (nok) sA(&ok, ",");
sA_str(&ok, p->str);
@@ -2961,7 +3457,8 @@ static int serve_static(int sock, const char *path)
/* 敏感名拒绝 */
const char *base = strrchr(full, '/');
base = base ? base + 1 : full;
if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行文件");
if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host")
|| !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件");
FILE *f = fopen(full, "rb");
if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在");
fclose(f);
@@ -2978,6 +3475,14 @@ static int handle(int sock, req_t *r)
return serve_static(sock, p);
}
/* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */
if (require_auth(sock, r) != 1) return 0;
/* ---- 鉴权端点 ---- */
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r);
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/logout")) return api_auth_logout(sock, r);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/auth/status")) return api_auth_status(sock, r);
/* ---- 性能/状态 ---- */
if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf")))
return api_perf(sock, p);
@@ -3118,6 +3623,8 @@ int main(int argc, char **argv)
if (env && env[0]) g_sock_perf = env;
env = getenv("MEOW_IPC_POWER_SOCK");
if (env && env[0]) g_sock_power = env;
env = getenv("MEOW_SETTINGS_FILE");
if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */
(void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */
/* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */
@@ -3132,6 +3639,7 @@ 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);