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:
1 parent
5f7e7542bd
commit
feb3df681c
7 files changed
+701
-35
No files matched your search
+522
-14
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user