3160 lines
127 KiB
C
3160 lines
127 KiB
C
#define _GNU_SOURCE /* gethostname/strcasestr/strnlen 等 GNU/musl 扩展 */
|
||
|
||
/* meowpanel.c — MeowOS Web 面板后端(MeowPanel)。
|
||
*
|
||
* 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件
|
||
* (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 `MeowOS/services.d/meowpanel.conf`
|
||
* 启动、meow_sysui 监管。**不进固件**;无卡时系统核心绝不对它有任何依赖。
|
||
*
|
||
* 架构(MEOWPANEL_PROTOCOL §1/§2 + SYSTEM_FRAMEWORK_SERVICES §4):
|
||
* - 单进程单线程 + poll 串行处理(面板访问量极小);内嵌 HTTP server。
|
||
* - 读侧(/api/status,/api/perf,/api/system)经 meow_svc_perf IPC(MSG_PERF_QUERY→RESULT)
|
||
* 聚合,**绝不各自爬 /proc**(单一权威源)。
|
||
* - 动作侧(/api/system/{reboot,shutdown,ota})经 meow_svc_power IPC(MSG_POWER_ACTION→STATUS),
|
||
* 不再直连 poweroff/写 mtd5。
|
||
* - 设置镜像(/api/settings*,/api/theme*)直接读写板载 /usr/data/meow/settings.conf——
|
||
* **与 meow_ui_vol_read / meow_svc_power settings_get_int_trim 同款解析**('=' 切分 +
|
||
* key 去首尾空白精确匹配;写用 "key = value\n")。后端是独立进程,无法链接 launcher 内的
|
||
* meow_settings_*(那是 launcher 进程内存表),故直接镜像文件,保持同一存储契约。
|
||
* - 文件/应用/日志/通知:白名单根 /usr/data/mnt/sd_0 下 MeowOS/Music/MeowData/MeowAPPs/book。
|
||
*
|
||
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。
|
||
* 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库。设备无 python3/lua。
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <strings.h> /* strcasecmp/strncasecmp */
|
||
#include <stdarg.h>
|
||
#include <stdint.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <signal.h>
|
||
#include <poll.h>
|
||
#include <time.h>
|
||
#include <ctype.h>
|
||
#include <dirent.h>
|
||
#include <limits.h>
|
||
#include <math.h>
|
||
#include <sys/types.h>
|
||
#include <sys/socket.h>
|
||
#include <sys/un.h>
|
||
#include <sys/time.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/wait.h>
|
||
#include <sys/utsname.h>
|
||
#include <sys/ioctl.h>
|
||
#include <netinet/in.h>
|
||
#include <arpa/inet.h>
|
||
#include <linux/input.h> /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */
|
||
|
||
#include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */
|
||
|
||
/* ---------------- 常量 ---------------- */
|
||
#define PANEL_VERSION "0.1"
|
||
#define DEFAULT_PORT 8080
|
||
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||
#define SETTINGS_HDR "# meow-settings 1"
|
||
/* TF 卡路径(运行期可覆盖,宿主测试用 env MEOW_TF_ROOT;默认设备卡挂载点)。
|
||
* TF_ROOT 保持为「数组宏」而非指针——让 -Wformat-truncation 能按 1024 尺寸分析拼接,避免误报。 */
|
||
static char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
|
||
static char g_tf_card_buf[2048];
|
||
static char g_apps_dir_buf[2048];
|
||
static char g_logs_dir_buf[2048];
|
||
static char g_notif_log_buf[2048];
|
||
static const char *g_tf_card = NULL; /* 派生:<root>/MeowOS */
|
||
static const char *g_apps_dir = NULL; /* 派生:<root>/MeowAPPs */
|
||
static const char *g_logs_dir = NULL; /* 派生:<card>/logs */
|
||
static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notifications/notifications.log */
|
||
#define TF_ROOT g_tf_root_buf
|
||
#define TF_CARD_DIR g_tf_card
|
||
#define MEOWAPPS_DIR g_apps_dir
|
||
#define LOGS_DIR g_logs_dir
|
||
#define PERF_SOCK "/usr/data/meow_perf.sock"
|
||
#define POWER_SOCK "/usr/data/meow_power.sock"
|
||
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
|
||
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
|
||
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
|
||
#define VERSION_FILE "/etc/meowos_version"
|
||
#define HOSTNAME_FILE ""
|
||
|
||
#define IPC_TIMEOUT_MS 2000
|
||
#define MAX_HTTP_HEADER (64 * 1024)
|
||
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */
|
||
#define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */
|
||
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
|
||
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
|
||
#define LOG_MAX_LINES 1000
|
||
#define NOTIF_PREVIEW_MAX 200 /* 通知列表返回上限(每条约 0.2KB,总 <50KB) */
|
||
#define TEXT_PREVIEW_MAX (1024 * 1024) /* 文本预览上限(超出截断,truncated=true;14MB 约束下 1MB 缓冲安全) */
|
||
|
||
/* 白名单子目录(TF 卡根下) */
|
||
static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
|
||
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
|
||
|
||
/* 日志敏感文件黑名单(相对 LOGS_DIR 或文件名) */
|
||
static const char *g_sensitive_names[] = {
|
||
"settings.conf", "setting.bak", ".env", "passwd", "shadow",
|
||
"meow.conf", "user.ini", "config.json",
|
||
};
|
||
#define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0]))
|
||
|
||
static const char *g_sock_perf = PERF_SOCK;
|
||
static const char *g_sock_power = POWER_SOCK;
|
||
|
||
/* 初始化 TF 卡派生路径(main 开头调用)。env MEOW_TF_ROOT 可覆盖卡根(宿主测试用);
|
||
* env MEOW_NOTIF_LOG 可覆盖通知转储日志路径(宿主测试用)。默认设备卡路径。 */
|
||
static void init_tf_paths(void)
|
||
{
|
||
const char *env = getenv("MEOW_TF_ROOT");
|
||
if (env && env[0]) {
|
||
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", env);
|
||
/* 去掉末尾 `/`(规范化处统一按无尾斜杠处理) */
|
||
size_t l = strlen(g_tf_root_buf);
|
||
while (l > 1 && g_tf_root_buf[l - 1] == '/') g_tf_root_buf[--l] = 0;
|
||
}
|
||
snprintf(g_tf_card_buf, sizeof g_tf_card_buf, "%s/MeowOS", TF_ROOT);
|
||
snprintf(g_apps_dir_buf, sizeof g_apps_dir_buf, "%s/MeowAPPs", TF_ROOT);
|
||
snprintf(g_logs_dir_buf, sizeof g_logs_dir_buf, "%s/MeowOS/logs", TF_ROOT);
|
||
snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s/MeowOS/notifications/notifications.log", TF_ROOT);
|
||
env = getenv("MEOW_NOTIF_LOG");
|
||
if (env && env[0]) snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s", env);
|
||
g_tf_card = g_tf_card_buf;
|
||
g_apps_dir = g_apps_dir_buf;
|
||
g_logs_dir = g_logs_dir_buf;
|
||
g_notif_log = g_notif_log_buf;
|
||
}
|
||
|
||
/* ---------------- 有界动态字符串(sbuf) ---------------- */
|
||
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
|
||
|
||
static int sb_reserve(sbuf_t *s, size_t extra)
|
||
{
|
||
if (s->n + extra + 1 <= s->cap) return 0;
|
||
size_t ncap = s->cap ? s->cap : 256;
|
||
while (ncap < s->n + extra + 1) ncap *= 2;
|
||
char *nb = realloc(s->b, ncap);
|
||
if (!nb) return -1;
|
||
s->b = nb;
|
||
s->cap = ncap;
|
||
return 0;
|
||
}
|
||
|
||
static void sA(sbuf_t *s, const char *fmt, ...)
|
||
{
|
||
char tmp[512];
|
||
va_list ap, ap2;
|
||
va_start(ap, fmt);
|
||
va_copy(ap2, ap);
|
||
int w = vsnprintf(tmp, sizeof tmp, fmt, ap);
|
||
va_end(ap);
|
||
if (w < 0) { va_end(ap2); return; }
|
||
if ((size_t)w < sizeof tmp) {
|
||
if (sb_reserve(s, (size_t)w) < 0) { va_end(ap2); return; }
|
||
memcpy(s->b + s->n, tmp, (size_t)w);
|
||
s->n += (size_t)w;
|
||
} else {
|
||
/* 超长 %s 等:格式化结果超出固定缓冲,动态分配后再追加(防截断坏 JSON) */
|
||
char *big = malloc((size_t)w + 1);
|
||
if (!big) { va_end(ap2); return; }
|
||
int w2 = vsnprintf(big, (size_t)w + 1, fmt, ap2);
|
||
va_end(ap2);
|
||
if (w2 < 0) { free(big); return; }
|
||
if (sb_reserve(s, (size_t)w2) < 0) { free(big); return; }
|
||
memcpy(s->b + s->n, big, (size_t)w2);
|
||
s->n += (size_t)w2;
|
||
free(big);
|
||
}
|
||
s->b[s->n] = 0;
|
||
}
|
||
|
||
/* JSON 字符串转义(version/hostname/文本值;UTF-8 按字节透传,控制字符转 \u00xx) */
|
||
static void sA_str(sbuf_t *s, const char *v)
|
||
{
|
||
if (!v) v = "";
|
||
sA(s, "\"");
|
||
for (const char *p = v; *p; p++) {
|
||
unsigned char c = (unsigned char)*p;
|
||
if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); }
|
||
else if (c < 0x20) { sA(s, "\\u%04x", c); }
|
||
else sA(s, "%c", (int)c);
|
||
}
|
||
sA(s, "\"");
|
||
}
|
||
|
||
/* 追加一段原始字节到 sbuf(长度明确,绕过 %s 的固定缓冲限制;用于大 JSON 数组复用)。 */
|
||
static void sA_append(sbuf_t *s, const char *b, size_t n)
|
||
{
|
||
if (!b || n == 0) return;
|
||
if (sb_reserve(s, n) < 0) return;
|
||
memcpy(s->b + s->n, b, n);
|
||
s->n += n;
|
||
s->b[s->n] = 0;
|
||
}
|
||
|
||
static void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
|
||
|
||
/* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */
|
||
static int write_full(int sock, const void *b, size_t n);
|
||
static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra);
|
||
static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen);
|
||
static int send_ok(int sock, const char *data);
|
||
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, ...);
|
||
|
||
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
|
||
static const char *csv_at(const char *s, int idx, char *buf, int buflen);
|
||
static int csv_count(const char *s);
|
||
|
||
/* ---------------- 最简 JSON 解析器(无第三方库) ----------------
|
||
* 只覆盖 MeowPanel 实际用到的:对象 / 数组 / 字符串(含转义) / 数字 / bool / null。
|
||
* value 树:J_OBJ 的 child 为键值对链(str=key,child=value,next=下个 pair);
|
||
* J_ARR 的 child 为元素链(next=下个元素)。
|
||
*/
|
||
enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ };
|
||
|
||
typedef struct jv {
|
||
int type;
|
||
int bval; /* J_BOOL */
|
||
long long inum; /* J_NUM 整数值 */
|
||
double dnum; /* J_NUM 浮点值 */
|
||
char *str; /* J_STR: 值; J_OBJ: key */
|
||
struct jv *child; /* J_ARR: 头; J_OBJ: 头 pair */
|
||
struct jv *next; /* 链 */
|
||
} jv_t;
|
||
|
||
typedef struct { const char *p; const char *end; int err; } jvp_t;
|
||
|
||
static jv_t *jv_alloc(int type)
|
||
{
|
||
jv_t *v = calloc(1, sizeof *v);
|
||
if (v) v->type = type;
|
||
return v;
|
||
}
|
||
|
||
static void jv_free(jv_t *v)
|
||
{
|
||
if (!v) return;
|
||
jv_t *c = v->child;
|
||
while (c) { jv_t *nx = c->next; jv_free(c->child); free(c->str); free(c); c = nx; }
|
||
free(v->str);
|
||
free(v);
|
||
}
|
||
|
||
static void jvp_skip_ws(jvp_t *t)
|
||
{
|
||
while (t->p < t->end && (*t->p == ' ' || *t->p == '\t' || *t->p == '\n' || *t->p == '\r')) t->p++;
|
||
}
|
||
|
||
static char *jv_parse_string(jvp_t *t)
|
||
{
|
||
if (t->p >= t->end || *t->p != '"') { t->err = 1; return NULL; }
|
||
t->p++;
|
||
sbuf_t s = {0};
|
||
while (t->p < t->end && *t->p != '"') {
|
||
unsigned char c = (unsigned char)*t->p++;
|
||
if (c == '\\') {
|
||
if (t->p >= t->end) { t->err = 1; break; }
|
||
char e = *t->p++;
|
||
switch (e) {
|
||
case 'n': sA(&s, "\n"); break;
|
||
case 't': sA(&s, "\t"); break;
|
||
case 'r': sA(&s, "\r"); break;
|
||
case 'b': sA(&s, "\b"); break;
|
||
case 'f': sA(&s, "\f"); break;
|
||
case 'u': {
|
||
if (t->end - t->p < 4) { t->err = 1; sbuf_free(&s); return NULL; }
|
||
unsigned int cp = 0;
|
||
for (int i = 0; i < 4; i++) { char h = *t->p++; cp <<= 4; if (h>='0'&&h<='9') cp|=h-'0'; else if (h>='a'&&h<='f') cp|=h-'a'+10; else if (h>='A'&&h<='F') cp|=h-'A'+10; else { t->err=1; sbuf_free(&s); return NULL; } }
|
||
/* 只支持 BMP 常见字符;代理对按 UTF-8 重算(简化:cp<0x80 单字节,否则保守转发) */
|
||
if (cp < 0x80) sA(&s, "%c", (int)cp);
|
||
else if (cp < 0x800) { sA(&s, "%c", 0xc0|(cp>>6)); sA(&s, "%c", 0x80|(cp&0x3f)); }
|
||
else { sA(&s, "%c", 0xe0|(cp>>12)); sA(&s, "%c", 0x80|((cp>>6)&0x3f)); sA(&s, "%c", 0x80|(cp&0x3f)); }
|
||
break; }
|
||
default: sA(&s, "%c", (int)e); break;
|
||
}
|
||
} else if (c < 0x20) { t->err = 1; break; }
|
||
else sA(&s, "%c", (int)c);
|
||
}
|
||
if (t->p >= t->end || *t->p != '"') { t->err = 1; sbuf_free(&s); return NULL; }
|
||
t->p++;
|
||
if (!s.b) { s.b = calloc(1, 1); }
|
||
return s.b;
|
||
}
|
||
|
||
static jv_t *jv_parse_value(jvp_t *t);
|
||
|
||
static jv_t *jv_parse_object(jvp_t *t)
|
||
{
|
||
t->p++;
|
||
jv_t *obj = jv_alloc(J_OBJ);
|
||
if (!obj) { t->err = 1; return NULL; }
|
||
jv_t **tail = &obj->child;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == '}') { t->p++; return obj; }
|
||
for (;;) {
|
||
jvp_skip_ws(t);
|
||
if (t->p >= t->end) { t->err = 1; jv_free(obj); return NULL; }
|
||
char *key = jv_parse_string(t);
|
||
if (!key) { jv_free(obj); return NULL; }
|
||
jvp_skip_ws(t);
|
||
if (t->p >= t->end || *t->p != ':') { t->err = 1; free(key); jv_free(obj); return NULL; }
|
||
t->p++;
|
||
jv_t *val = jv_parse_value(t);
|
||
if (!val) { t->err = 1; free(key); jv_free(obj); return NULL; }
|
||
jv_t *pair = calloc(1, sizeof *pair);
|
||
if (!pair) { t->err = 1; free(key); jv_free(val); jv_free(obj); return NULL; }
|
||
pair->type = J_OBJ;
|
||
pair->str = key;
|
||
pair->child = val;
|
||
*tail = pair;
|
||
tail = &pair->next;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == ',') { t->p++; continue; }
|
||
if (t->p < t->end && *t->p == '}') { t->p++; return obj; }
|
||
t->err = 1;
|
||
jv_free(obj);
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
static jv_t *jv_parse_array(jvp_t *t)
|
||
{
|
||
t->p++;
|
||
jv_t *arr = jv_alloc(J_ARR);
|
||
if (!arr) { t->err = 1; return NULL; }
|
||
jv_t **tail = &arr->child;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == ']') { t->p++; return arr; }
|
||
for (;;) {
|
||
jv_t *v = jv_parse_value(t);
|
||
if (!v) { jv_free(arr); return NULL; }
|
||
*tail = v;
|
||
tail = &v->next;
|
||
jvp_skip_ws(t);
|
||
if (t->p < t->end && *t->p == ',') { t->p++; continue; }
|
||
if (t->p < t->end && *t->p == ']') { t->p++; return arr; }
|
||
t->err = 1;
|
||
jv_free(arr);
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
static jv_t *jv_parse_value(jvp_t *t)
|
||
{
|
||
jvp_skip_ws(t);
|
||
if (t->p >= t->end) { t->err = 1; return NULL; }
|
||
char c = *t->p;
|
||
if (c == '{') return jv_parse_object(t);
|
||
if (c == '[') return jv_parse_array(t);
|
||
if (c == '"') {
|
||
char *s = jv_parse_string(t);
|
||
if (!s) return NULL;
|
||
jv_t *v = jv_alloc(J_STR);
|
||
if (!v) { free(s); t->err = 1; return NULL; }
|
||
v->str = s;
|
||
return v;
|
||
}
|
||
if (!strncmp(t->p, "true", 4)) { t->p += 4; jv_t *v = jv_alloc(J_BOOL); if (v) v->bval = 1; return v; }
|
||
if (!strncmp(t->p, "false", 5)) { t->p += 5; return jv_alloc(J_BOOL); }
|
||
if (!strncmp(t->p, "null", 4)) { t->p += 4; return jv_alloc(J_NULL); }
|
||
/* number */
|
||
const char *q = t->p;
|
||
int isint = 1;
|
||
while (q < t->end && (*q == '-' || *q == '+' || *q == '.' || *q == 'e' || *q == 'E' || isdigit((unsigned char)*q))) {
|
||
if (*q == '.' || *q == 'e' || *q == 'E') isint = 0;
|
||
q++;
|
||
}
|
||
if (q == t->p) { t->err = 1; return NULL; }
|
||
char tmp[64];
|
||
size_t dlen = (size_t)(q - t->p);
|
||
if (dlen >= sizeof tmp) dlen = sizeof tmp - 1;
|
||
memcpy(tmp, t->p, dlen);
|
||
tmp[dlen] = 0;
|
||
t->p = q;
|
||
jv_t *v = jv_alloc(J_NUM);
|
||
if (!v) { t->err = 1; return NULL; }
|
||
v->dnum = strtod(tmp, NULL);
|
||
v->inum = (long long)strtoll(tmp, NULL, 10);
|
||
(void)isint;
|
||
return v;
|
||
}
|
||
|
||
static jv_t *jv_parse(const char *text, int len)
|
||
{
|
||
jvp_t t;
|
||
t.p = text;
|
||
t.end = text + len;
|
||
t.err = 0;
|
||
jv_t *v = jv_parse_value(&t);
|
||
if (!v || t.err) { if (v) jv_free(v); return NULL; }
|
||
return v;
|
||
}
|
||
|
||
static jv_t *jv_obj_get(const jv_t *obj, const char *key)
|
||
{
|
||
if (!obj || obj->type != J_OBJ) return NULL;
|
||
for (const jv_t *p = obj->child; p; p = p->next)
|
||
if (p->str && !strcmp(p->str, key)) return p->child;
|
||
return NULL;
|
||
}
|
||
|
||
/* ---------------- 设置表(权威来源:meow_settings.c g_defs,逐键一致) ----------------
|
||
* 特别说明(与 MEOWPANEL_PROTOCOL/实现出入记录):
|
||
* - ui.style 值域源码为 "md3,miui",但 SYSTEM_SETTINGS §1 定案 + MEOWPANEL_PROTOCOL §5
|
||
* 一致行为 **miuix**(对应 MEOW_STYLE_MIUIX)。前端 data-style 也用 miuix。故本表对齐为
|
||
* "md3,miuix" / "Material You,MIUIx"(实现优先,回改见 README)。
|
||
* - display.brightness 的 g_defs 为 min=0 max=255 def=101,但语义 clamp 到 [brightness_min(默认1),100]
|
||
* (防 101 闪屏)。本表对元数据给出 min=1 max=100(协议 §3.2 期望),实际 set 走 brightness clamp。
|
||
*/
|
||
enum { ST_INT, ST_STR, ST_ENUM, ST_BOOL };
|
||
|
||
typedef struct setdef {
|
||
const char *key;
|
||
int type;
|
||
const char *def;
|
||
int min, max; /* ST_INT 取值域;max<=0 表示无上界 */
|
||
const char *enum_names; /* ST_ENUM 逗号名 */
|
||
const char *enum_labels; /* 对应中文 */
|
||
const char *group; /* 分组标签(协议 groups 用) */
|
||
} setdef_t;
|
||
|
||
static const setdef_t g_sets[] = {
|
||
/* 显示 */
|
||
{ "display.brightness", ST_INT, "101", 1, 100, NULL, NULL, "显示" },
|
||
{ "display.brightness_min", ST_INT, "1", 0, 255, NULL, NULL, "显示" },
|
||
{ "display.screen_off_mode", ST_ENUM, "panel",0,0, "panel,black", "面板下电,背光全黑", "显示" },
|
||
{ "display.screen_off_timeout", ST_INT, "30", 0, 86400,NULL, NULL, "显示" },
|
||
{ "display.refresh_rate", ST_ENUM, "60", 0,0, "60,30,15", "60Hz,30Hz,15Hz", "显示" },
|
||
{ "display.refresh_always_max", ST_BOOL, "0", 0,0, NULL, NULL, "显示" },
|
||
{ "display.theme", ST_ENUM, "dark", 0,0, "dark,light", "深色,浅色", "显示" },
|
||
{ "display.theme_mode", ST_ENUM, "manual",0,0, "manual,auto", "手动,时段自动", "显示" },
|
||
{ "display.theme_dark_start", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
|
||
{ "display.theme_dark_end", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
|
||
{ "display.background", ST_STR, "black",0,0, NULL, NULL, "显示" },
|
||
{ "display.multi_font", ST_INT, "0", 0, 1, NULL, NULL, "显示" },
|
||
/* 电源 */
|
||
{ "power.longpress_menu_ms", ST_INT, "3000", 0,0, NULL, NULL, "电源" },
|
||
{ "power.forceoff_ms", ST_INT, "8000", 0,0, NULL, NULL, "电源" },
|
||
{ "power.defeat_hotkey", ST_STR, "power_voldown",0,0, NULL, NULL, "电源" },
|
||
/* 开机 */
|
||
{ "boot.default", ST_ENUM, "meow", 0,0, "meow,hiby", "MeowOS,HiByOS", "开机" },
|
||
{ "boot.menu_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "开机" },
|
||
{ "boot.timeout_ms", ST_INT, "5000", 0,0, NULL, NULL, "开机" },
|
||
/* 输入 */
|
||
{ "input.hotkey_seq", ST_STR, "volup,volup,volup",0,0, NULL, NULL, "输入" },
|
||
{ "input.hotkey_window_ms", ST_INT, "2500", 0,0, NULL, NULL, "输入" },
|
||
{ "input.touch_arm_delay_ms", ST_INT, "700", 0,0, NULL, NULL, "输入" },
|
||
{ "input.swipe_invert", ST_BOOL, "0", 0,0, NULL, NULL, "输入" },
|
||
/* 字体/主题 */
|
||
{ "font.file", ST_STR, "MeowOS/fonts/default/regular/font.png",0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_cjk", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_cjk_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_num", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_latin", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_latin_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.class_other", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.load_full", ST_INT, "0", 0,1, NULL, NULL, "字体主题" },
|
||
{ "font.size.title", ST_INT, "28", 0,0, NULL, NULL, "字体主题" },
|
||
{ "font.size.body", ST_INT, "20", 0,0, NULL, NULL, "字体主题" },
|
||
{ "ui.style", ST_ENUM, "md3", 0,0, "md3,miuix", "Material You,MIUIx", "字体主题" },
|
||
/* 通知 */
|
||
{ "notify.center_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "通知" },
|
||
{ "notify.app_quota", ST_INT, "16", 0,0, NULL, NULL, "通知" },
|
||
{ "notify.categories", ST_INT, "4", 1,4, NULL, NULL, "通知" },
|
||
{ "notify.category_quota", ST_INT, "4", 1,16, NULL, NULL, "通知" },
|
||
{ "notify.media_quota", ST_INT, "1", 1,1, NULL, NULL, "通知" },
|
||
{ "notify.dnd", ST_BOOL, "0", 0,0, NULL, NULL, "通知" },
|
||
/* 日志 */
|
||
{ "log.level", ST_ENUM, "info", 0,0, "error,warn,info,debug", "错误,警告,信息,调试", "日志" },
|
||
{ "log.to_card", ST_BOOL, "1", 0,0, NULL, NULL, "日志" },
|
||
/* 音频 */
|
||
{ "audio.volume", ST_INT, "60", 0,100, NULL, NULL, "音频" },
|
||
{ "audio.volume_max", ST_INT, "100", 0,100, NULL, NULL, "音频" },
|
||
{ "audio.volume_step", ST_INT, "1", 1,5, NULL, NULL, "音频" },
|
||
{ "audio.output", ST_STR, "hw:0,0",0,0, NULL, NULL, "音频" },
|
||
/* 网络 */
|
||
{ "network.wifi_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "网络" },
|
||
{ "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" },
|
||
/* 系统 */
|
||
{ "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" },
|
||
};
|
||
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
|
||
|
||
static const setdef_t *setdef_find(const char *key) {
|
||
for (int i = 0; i < SETS_COUNT; i++)
|
||
if (!strcmp(g_sets[i].key, key)) return &g_sets[i];
|
||
return NULL;
|
||
}
|
||
|
||
/* ---------------- settings.conf 解析(照 meow_ui_vol_read / settings_get_int_trim) ----------------
|
||
* '=' 切分,key 去首尾空白精确匹配(防 audio.volume 误配 audio.volume_max)。
|
||
*/
|
||
static char *settings_get_value(const char *key)
|
||
{
|
||
FILE *f = fopen(SETTINGS_FILE, "r");
|
||
if (!f) return NULL;
|
||
size_t kl = strlen(key);
|
||
char line[256];
|
||
char *found = NULL;
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *eq = strchr(line, '=');
|
||
if (!eq) continue;
|
||
char *k = line;
|
||
while (*k == ' ' || *k == '\t') k++;
|
||
long n = (long)(eq - k);
|
||
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--;
|
||
if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue;
|
||
found = eq + 1;
|
||
break;
|
||
}
|
||
fclose(f);
|
||
if (!found) return NULL;
|
||
while (*found == ' ' || *found == '\t') found++;
|
||
char *e = found + strlen(found);
|
||
while (e > found && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
|
||
return strdup(found);
|
||
}
|
||
|
||
static int settings_get_int(const char *key, int defv)
|
||
{
|
||
char *v = settings_get_value(key);
|
||
if (!v) return defv;
|
||
int r = atoi(v);
|
||
free(v);
|
||
return r;
|
||
}
|
||
|
||
/* 亮度 clamp:下限取 display.brightness_min(默认 1),上限 100(防 101 闪屏)。
|
||
* 与 meow_settings.c clamp_brightness 同口径。 */
|
||
static int brightness_clamp(int v)
|
||
{
|
||
int mn = settings_get_int("display.brightness_min", 1);
|
||
if (mn < 1) mn = 1;
|
||
if (mn > 100) mn = 100;
|
||
if (v < mn) v = mn;
|
||
if (v > 100) v = 100;
|
||
return v;
|
||
}
|
||
|
||
/* 重写 settings.conf:把 key 写为 "key = value"。保留头与其它行;缺 key 则追加。
|
||
* 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");
|
||
char tmp[2048];
|
||
size_t n = 0;
|
||
int c;
|
||
if (f) {
|
||
while (n < sizeof tmp - 1 && (c = fgetc(f)) != EOF) tmp[n++] = (char)c;
|
||
fclose(f);
|
||
}
|
||
tmp[n] = 0;
|
||
FILE *nf = fopen(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 行替换;其余照抄 */
|
||
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);
|
||
}
|
||
}
|
||
if (!found) fprintf(nf, "%s = %s\n", key, val);
|
||
fclose(nf);
|
||
return 0;
|
||
}
|
||
|
||
/* 设定值→落盘规范字符串(clamp/校验集中在此)。返回 -1=非法(422),0=成功。 */
|
||
static int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap)
|
||
{
|
||
switch (d->type) {
|
||
case ST_INT: {
|
||
int v = atoi(input);
|
||
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
|
||
else {
|
||
if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
|
||
else if (v < d->min) v = d->min;
|
||
}
|
||
snprintf(out, outcap, "%d", v);
|
||
return 0;
|
||
}
|
||
case ST_BOOL: {
|
||
char *q = (char *)input;
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
int b = 0;
|
||
if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1;
|
||
else { long v = strtol(q, NULL, 10); if (q[0] && v != 0) b = 1; }
|
||
snprintf(out, outcap, "%d", b ? 1 : 0);
|
||
return 0;
|
||
}
|
||
case ST_ENUM: {
|
||
const char *names = d->enum_names;
|
||
if (!names || !names[0]) { snprintf(out, outcap, "%s", input); return 0; }
|
||
int cnt = csv_count(names);
|
||
char tok[128];
|
||
/* 接受名字或索引(向后兼容) */
|
||
int idx = -1;
|
||
for (int i = 0; i < cnt; i++) {
|
||
csv_at(names, i, tok, sizeof tok);
|
||
if (!strcmp(tok, input)) { idx = i; break; }
|
||
}
|
||
if (idx < 0) {
|
||
int n = atoi(input);
|
||
if (n >= 0 && n < cnt) idx = n;
|
||
}
|
||
if (idx < 0) return -1; /* 非法枚举 */
|
||
csv_at(names, idx, tok, sizeof tok);
|
||
snprintf(out, outcap, "%s", tok);
|
||
return 0;
|
||
}
|
||
case ST_STR:
|
||
strncpy(out, input, outcap - 1);
|
||
out[outcap - 1] = 0;
|
||
return 0;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* 恢复全部默认:重写整个文件为 头 + 所有键 def。 */
|
||
static int settings_reset_all(void)
|
||
{
|
||
mkdir("/usr/data/meow", 0777);
|
||
FILE *nf = fopen(SETTINGS_FILE, "w");
|
||
if (!nf) return -1;
|
||
fprintf(nf, "%s\n", SETTINGS_HDR);
|
||
for (int i = 0; i < SETS_COUNT; i++)
|
||
fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def);
|
||
fclose(nf);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- IPC 帧读写(同 meow_svc_perf/power 客户端约定,全小端 LE) ---------------- */
|
||
static int x_read_full(int fd, uint8_t *buf, size_t n)
|
||
{
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t r = read(fd, buf + off, n - off);
|
||
if (r <= 0) return -1;
|
||
off += (size_t)r;
|
||
}
|
||
return 0;
|
||
}
|
||
static int x_write_full(int fd, const uint8_t *buf, size_t n)
|
||
{
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t w = write(fd, buf + off, n - off);
|
||
if (w <= 0) return -1;
|
||
off += (size_t)w;
|
||
}
|
||
return 0;
|
||
}
|
||
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
|
||
{
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
uint32_t magic = MEOW_IPC_MAGIC, rl = len;
|
||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||
memcpy(hdr + 0, &magic, 4);
|
||
memcpy(hdr + 4, &ver, 2);
|
||
memcpy(hdr + 6, &t, 2);
|
||
memcpy(hdr + 8, &req_id, 4);
|
||
memcpy(hdr + 12, &rl, 4);
|
||
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
|
||
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* 读一帧:返回 payload 长度(<=cap 时复制到 buf),-1 失败;type/req_id 回传。 */
|
||
static int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap)
|
||
{
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
if (x_read_full(fd, hdr, sizeof hdr) < 0) return -1;
|
||
uint32_t magic, rlen, rid;
|
||
uint16_t ver, t;
|
||
memcpy(&magic, hdr, 4);
|
||
memcpy(&ver, hdr + 4, 2);
|
||
memcpy(&t, hdr + 6, 2);
|
||
memcpy(&rid, hdr + 8, 4);
|
||
memcpy(&rlen, hdr + 12, 4);
|
||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||
if (rlen > MEOW_IPC_MAX) return -1;
|
||
if (type) *type = t;
|
||
if (req_id) *req_id = rid;
|
||
uint32_t n = (rlen < cap) ? rlen : cap;
|
||
if (n && x_read_full(fd, buf, n) < 0) return -1;
|
||
/* 若实际更长,丢弃剩余 */
|
||
for (uint32_t i = n; i < rlen; i++) { uint8_t c; if (x_read_full(fd, &c, 1) < 0) return -1; }
|
||
return (int)n;
|
||
}
|
||
|
||
/* 带超时连 AF_UNIX 流 socket:非阻塞 connect + poll(POLLOUT)。 */
|
||
static int ipc_connect_tmo(const char *sock, int tmo_ms)
|
||
{
|
||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||
if (fd < 0) return -1;
|
||
struct sockaddr_un a;
|
||
memset(&a, 0, sizeof a);
|
||
a.sun_family = AF_UNIX;
|
||
strncpy(a.sun_path, sock, sizeof a.sun_path - 1);
|
||
int fl = fcntl(fd, F_GETFL, 0);
|
||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||
int rc = connect(fd, (struct sockaddr *)&a, sizeof a);
|
||
if (rc < 0 && errno == EINPROGRESS) {
|
||
struct pollfd pf = { fd, POLLOUT, 0 };
|
||
int pr = poll(&pf, 1, tmo_ms);
|
||
if (pr <= 0) { close(fd); return -1; }
|
||
int soerr = 0;
|
||
socklen_t sl = sizeof soerr;
|
||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl);
|
||
if (soerr != 0) { close(fd); return -1; }
|
||
} else if (rc < 0) { close(fd); return -1; }
|
||
fcntl(fd, F_SETFL, fl);
|
||
/* 读/写超时兜底(正常服务应即时响应) */
|
||
struct timeval tv = { tmo_ms / 1000, (tmo_ms % 1000) * 1000 };
|
||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||
return fd;
|
||
}
|
||
|
||
/* 服务端握手(MSG_HELLO → MSG_ACK),app_id 用系统面板身份 os.meow.panel。 */
|
||
static int ipc_hello(int fd)
|
||
{
|
||
m_hello_t h;
|
||
memset(&h, 0, sizeof h);
|
||
strncpy(h.app_id, "os.meow.panel", sizeof h.app_id - 1);
|
||
h.version = 1;
|
||
if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) < 0) return -1;
|
||
uint8_t buf[64];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_ack_t)) return -1;
|
||
if (type != MSG_ACK) return -1;
|
||
m_ack_t a;
|
||
memcpy(&a, buf, sizeof a);
|
||
return (a.code == MEOW_OK) ? 0 : -1;
|
||
}
|
||
|
||
/* ---------------- 读侧:meow_svc_perf IPC ----------------
|
||
* 返回 0=成功(json 已填入 out/cap),否则负错误码,errmsg 填中文说明。
|
||
*/
|
||
static const char *perf_code_str(int code)
|
||
{
|
||
switch (code) {
|
||
case MEOW_OK: return ""; /* 不应使用 */
|
||
case MEOW_ERR_INVAL: return "参数非法";
|
||
case MEOW_ERR_IO: return "读取失败";
|
||
case MEOW_ERR_NODEV: return "设备不存在";
|
||
default: return "服务端错误";
|
||
}
|
||
}
|
||
|
||
static int perf_subset_for_path(const char *path)
|
||
{
|
||
if (!strcmp(path, "/api/perf")) return PERF_SUBSET_PERF;
|
||
if (!strcmp(path, "/api/system")) return 0; /* system 需 uptime/storage,取全量 */
|
||
return 0; /* /api/status */
|
||
}
|
||
|
||
static int ipc_perf_query(int subset, char *out, int cap)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_perf, IPC_TIMEOUT_MS);
|
||
if (fd < 0) return MEOW_ERR_NODEV;
|
||
int rc = MEOW_ERR_INTERNAL;
|
||
char msg[128] = "";
|
||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; snprintf(msg, sizeof msg, "握手中断:性能服务未就绪"); goto done; }
|
||
m_perf_query_t q;
|
||
memset(&q, 0, sizeof q);
|
||
q.subset = (uint32_t)subset;
|
||
if (send_frame(fd, MSG_PERF_QUERY, 2, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "发送查询失败"); goto done; }
|
||
uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_perf_result_t)) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "应答帧过短"); goto done; }
|
||
if (type != MSG_PERF_RESULT) { rc = MEOW_ERR_INVAL; snprintf(msg, sizeof msg, "非预期应答类型 0x%04x", type); goto done; }
|
||
m_perf_result_t r;
|
||
memcpy(&r, buf, sizeof r);
|
||
if (r.code != MEOW_OK) { rc = (int)r.code; snprintf(msg, sizeof msg, "性能服务错误: %s", perf_code_str((int)r.code)); goto done; }
|
||
uint16_t jl = r.json_len;
|
||
if (jl > sizeof r.json - 1) jl = (uint16_t)(sizeof r.json - 1);
|
||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||
memcpy(out, r.json, jl);
|
||
out[jl] = 0;
|
||
rc = MEOW_OK;
|
||
done:
|
||
close(fd);
|
||
/* 服务不可达/非 OK 由调用方据此返回错误;绝不静默失败、绝不降级直连 /proc。 */
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 动作侧:meow_svc_power IPC ----------------
|
||
* 返回 0=受理;否则负错误码(PERM/INVAL/NODEV/…),msg 填服务端说明。
|
||
*/
|
||
static const char *power_code_str(int code)
|
||
{
|
||
switch (code) {
|
||
case MEOW_OK: return "";
|
||
case MEOW_ERR_PERM: return "无权限(需 root)";
|
||
case MEOW_ERR_INVAL: return "未知命令";
|
||
case MEOW_ERR_NODEV: return "设备/能力不支持";
|
||
case MEOW_ERR_NOENT: return "缺少升级文件";
|
||
case MEOW_ERR_IO: return "读写失败";
|
||
default: return "电源服务错误";
|
||
}
|
||
}
|
||
|
||
static int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st)
|
||
{
|
||
int fd = ipc_connect_tmo(g_sock_power, 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_power_action_t a;
|
||
memset(&a, 0, sizeof a);
|
||
a.cmd = cmd;
|
||
a.flag = flag;
|
||
if (arg) snprintf(a.arg, sizeof a.arg, "%s", arg);
|
||
if (send_frame(fd, MSG_POWER_ACTION, 3, &a, sizeof a) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||
uint8_t buf[256];
|
||
uint16_t type; uint32_t rid;
|
||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||
if (n < (int)sizeof(m_power_status_t)) { rc = MEOW_ERR_IO; goto done; }
|
||
if (type != MSG_POWER_STATUS) { rc = MEOW_ERR_INVAL; goto done; }
|
||
m_power_status_t st;
|
||
memcpy(&st, buf, sizeof st);
|
||
if (out_st) *out_st = st;
|
||
rc = (int)st.code;
|
||
done:
|
||
close(fd);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 路径安全 ---------------- */
|
||
static int tf_present(void)
|
||
{
|
||
if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */
|
||
FILE *f = fopen("/proc/mounts", "r");
|
||
if (!f) return 0;
|
||
char line[512];
|
||
int present = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strstr(line, TF_ROOT)) { present = 1; break; }
|
||
}
|
||
fclose(f);
|
||
return present;
|
||
}
|
||
|
||
/* 判断某个规范化路径是否在卡根白名单内(==TF_ROOT 或属于某个白名单一级子目录)。 */
|
||
static int path_in_whitelist(const char *abs)
|
||
{
|
||
size_t rl = strlen(TF_ROOT);
|
||
if (strncmp(abs, TF_ROOT, rl) != 0) return 0;
|
||
if (abs[rl] == 0) return 1; /* 根自身 */
|
||
if (abs[rl] != '/') return 0;
|
||
const char *rest = abs + rl + 1;
|
||
for (int i = 0; i < ALLOWED_COUNT; i++) {
|
||
size_t dl = strlen(g_allowed_dirs[i]);
|
||
if (strncmp(rest, g_allowed_dirs[i], dl) == 0) {
|
||
if (rest[dl] == 0 || rest[dl] == '/') return 1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int is_sensitive_name(const char *base)
|
||
{
|
||
for (int i = 0; i < SENSITIVE_COUNT; i++)
|
||
if (!strcmp(base, g_sensitive_names[i])) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* 规范化输入路径(去除 . / ..;绝对/相对容错)。
|
||
* 输入可能是绝对(`/usr/data/mnt/sd_0/MeowOS/x`,前端下载/删除等用)或相对 root(`/MeowOS/x`)。
|
||
* 输出(buf)为规范化后的绝对路径;若 escape 出卡根,返回 -1。 */
|
||
static int normalize_path(const char *in, char *buf, size_t buflen)
|
||
{
|
||
size_t rl = strlen(TF_ROOT);
|
||
if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */
|
||
char joined[2048];
|
||
if (in[0] == '/') {
|
||
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
|
||
snprintf(joined, sizeof joined, "%s", in);
|
||
} else {
|
||
/* 相对 root 的展示路径,如 /MeowOS/x → 拼到 root */
|
||
if (in[0] == '/' && in[1] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in + 1);
|
||
else if (in[0] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in);
|
||
else snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||
}
|
||
} else {
|
||
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||
}
|
||
/* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。
|
||
* 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */
|
||
char out[2048];
|
||
size_t n = 0;
|
||
out[0] = '/';
|
||
out[1] = 0;
|
||
n = 1;
|
||
char *p = joined;
|
||
while (*p) {
|
||
while (*p == '/') p++;
|
||
if (!*p) break;
|
||
char seg[256];
|
||
int k = 0;
|
||
while (*p && *p != '/') { if (k < 255) seg[k++] = *p; p++; }
|
||
seg[k] = 0;
|
||
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
|
||
if (!strcmp(seg, "..")) {
|
||
/* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */
|
||
if (n <= 1) return -1;
|
||
while (n > 1 && out[n - 1] != '/') n--;
|
||
if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */
|
||
out[n] = 0;
|
||
continue;
|
||
}
|
||
if (k > 0) {
|
||
size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1;
|
||
if (need >= sizeof out) return -1;
|
||
if (n > 1) { out[n++] = '/'; out[n] = 0; }
|
||
memcpy(out + n, seg, (size_t)k);
|
||
n += (size_t)k;
|
||
out[n] = 0;
|
||
}
|
||
}
|
||
/* 空/根(n==1,仅 "/")→ 卡根 */
|
||
if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT);
|
||
size_t ol = strlen(out);
|
||
if (ol >= buflen) return -1;
|
||
memcpy(buf, out, ol + 1); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
|
||
return 0;
|
||
}
|
||
|
||
/* 展示 rel(相对 root):如 /MeowOS/x。 */
|
||
static void make_rel(const char *abs, char *buf, size_t buflen)
|
||
{
|
||
size_t rl = strlen(TF_ROOT);
|
||
if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; }
|
||
const char *rest = abs + rl;
|
||
if (*rest == 0) snprintf(buf, buflen, "/");
|
||
else snprintf(buf, buflen, "%s", rest);
|
||
}
|
||
|
||
/* 安全地打开一个可读文件(白名单内 + 非敏感名)。失败返回 -1。 */
|
||
static int open_safe_for_read(const char *abs, int follow_rel)
|
||
{
|
||
(void)follow_rel;
|
||
if (!path_in_whitelist(abs)) return -1;
|
||
const char *base = strrchr(abs, '/');
|
||
base = base ? base + 1 : abs;
|
||
if (is_sensitive_name(base)) return -1;
|
||
struct stat st;
|
||
if (stat(abs, &st) < 0) return -1;
|
||
if (!S_ISREG(st.st_mode)) return -1;
|
||
return open(abs, O_RDONLY);
|
||
}
|
||
|
||
/* ---------------- 文件浏览 ---------------- */
|
||
static void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime)
|
||
{
|
||
sA(s, "{\"name\":");
|
||
sA_str(s, name);
|
||
sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}",
|
||
isdir ? "dir" : "file", size, mtime);
|
||
}
|
||
|
||
struct fent { char name[256]; int isdir; long long size, mtime; };
|
||
/* 目录优先 + 名称不区分大小写(再按大小写敏感定序保证稳定) */
|
||
static int fent_cmp(const void *a, const void *b)
|
||
{
|
||
const struct fent *fa = a, *fb = b;
|
||
if (fa->isdir != fb->isdir) return fb->isdir - fa->isdir;
|
||
int c = strcasecmp(fa->name, fb->name);
|
||
return c ? c : strcmp(fa->name, fb->name);
|
||
}
|
||
|
||
static int files_list(int sock, const char *relpath)
|
||
{
|
||
char abs[1024], rel[1024];
|
||
if (normalize_path(relpath, abs, sizeof abs) < 0)
|
||
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
if (!path_in_whitelist(abs))
|
||
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
struct stat st;
|
||
if (stat(abs, &st) < 0)
|
||
return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||
if (!S_ISDIR(st.st_mode))
|
||
return send_err(sock, 404, "NOT_FOUND", "非目录");
|
||
DIR *d = opendir(abs);
|
||
if (!d)
|
||
return send_err(sock, 500, "INTERNAL", "读目录失败");
|
||
make_rel(abs, rel, sizeof rel);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"root\":");
|
||
sA_str(&s, TF_ROOT);
|
||
sA(&s, ",\"path\":");
|
||
sA_str(&s, abs);
|
||
sA(&s, ",\"rel\":");
|
||
sA_str(&s, rel);
|
||
sA(&s, ",\"allowed\":[");
|
||
for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); }
|
||
sA(&s, "],\"items\":[");
|
||
/* M2.78:排序后输出(原 readdir 原始顺序混杂难用)——目录优先,同组按名称
|
||
* 不区分大小写排序;条目上限 1024(与 files App 一致,防巨目录爆内存)。 */
|
||
int cap = 1024, n = 0;
|
||
struct fent *ents = malloc((size_t)cap * sizeof *ents);
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1400];
|
||
snprintf(full, sizeof full, "%s/%s", abs, e->d_name);
|
||
struct stat fs;
|
||
if (stat(full, &fs) < 0) continue;
|
||
if (n >= cap) break;
|
||
struct fent *en = &ents[n++];
|
||
snprintf(en->name, sizeof en->name, "%s", e->d_name);
|
||
en->isdir = S_ISDIR(fs.st_mode);
|
||
en->size = en->isdir ? 0 : (long long)fs.st_size;
|
||
en->mtime = (long long)fs.st_mtime;
|
||
}
|
||
closedir(d);
|
||
qsort(ents, (size_t)n, sizeof *ents, fent_cmp);
|
||
for (int i = 0; i < n; i++) {
|
||
if (i) sA(&s, ",");
|
||
fmt_json_item(&s, ents[i].name, ents[i].isdir, ents[i].size, ents[i].mtime);
|
||
}
|
||
free(ents);
|
||
sA(&s, "]}");
|
||
return send_json(sock, 200, s.b ? s.b : "");
|
||
}
|
||
|
||
/* ---------------- HTTP 响应(JSON 信封) ---------------- */
|
||
/* send_json 前置声明 */
|
||
static int send_json(int sock, int status, const char *data);
|
||
|
||
/* 读文本文件(预览上限 TEXT_PREVIEW_MAX,超出截断返回 truncated):
|
||
* 返回 malloc 内容(NUL 结尾);二进制检测 -2;打不开 -1。size=文件真实字节数(非截断后)。 */
|
||
static char *read_text_file_limited(const char *abs, long long *size, int *truncated)
|
||
{
|
||
int fd = open_safe_for_read(abs, 0);
|
||
if (fd < 0) return NULL;
|
||
struct stat st;
|
||
if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; }
|
||
long long total = (long long)st.st_size;
|
||
size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total;
|
||
char *buf = malloc(cap + 1);
|
||
if (!buf) { close(fd); return NULL; }
|
||
ssize_t n = read(fd, buf, cap);
|
||
close(fd);
|
||
if (n < 0) { free(buf); return NULL; }
|
||
buf[n] = 0;
|
||
/* 二进制检测:含 NUL 或大量控制字符 */
|
||
int ctrl = 0;
|
||
for (ssize_t i = 0; i < n; i++) {
|
||
unsigned char c = (unsigned char)buf[i];
|
||
if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */
|
||
if (c < 0x09) ctrl++;
|
||
}
|
||
if (ctrl > n / 10) { free(buf); return (char *)-2; }
|
||
if (size) *size = total;
|
||
if (truncated) *truncated = (total > (long long)cap) ? 1 : 0;
|
||
return buf;
|
||
}
|
||
|
||
/* ---------------- 按键注入(经 /dev/input/event* 写 EV_KEY;uinput_tap inject 同机制) ----------------
|
||
* 设备映射(launcher meow_launcher_internal.h):
|
||
* /dev/input/event0 = md-gpio-keys: KEY_POWER(116), KEY_PREVIOUSSONG(165)
|
||
* /dev/input/event2 = jz adc keyboard: KEY_VOLUMEUP(115), KEY_VOLUMEDOWN(114), KEY_NEXTSONG(163), KEY_PLAYPAUSE(164)
|
||
* 注入 = open(O_WRONLY) 写 EV_KEY press/release + SYN_REPORT(evdev 支持 write 注入,uinput_tap.c inject 先例)。
|
||
* 红线(APPS.md 物理键 / 任务约束):只注入音量/电源/播放/上下曲,**禁止映射 返回(158)/主页(172)/确认/exit**。
|
||
*/
|
||
#define PWR_KEYDEV "/dev/input/event0"
|
||
#define ADC_KEYDEV "/dev/input/event2"
|
||
|
||
static int keycode_to_device(int code, const char **dev)
|
||
{
|
||
switch (code) {
|
||
case KEY_POWER: *dev = PWR_KEYDEV; return KEY_POWER;
|
||
case KEY_PREVIOUSSONG: *dev = PWR_KEYDEV; return KEY_PREVIOUSSONG;
|
||
case KEY_VOLUMEUP: *dev = ADC_KEYDEV; return KEY_VOLUMEUP;
|
||
case KEY_VOLUMEDOWN: *dev = ADC_KEYDEV; return KEY_VOLUMEDOWN;
|
||
case KEY_NEXTSONG: *dev = ADC_KEYDEV; return KEY_NEXTSONG;
|
||
case KEY_PLAYPAUSE: *dev = ADC_KEYDEV; return KEY_PLAYPAUSE;
|
||
default: return -1;
|
||
}
|
||
}
|
||
|
||
static int inject_key_event(const char *dev, int code, int down)
|
||
{
|
||
int fd = open(dev, O_WRONLY | O_NONBLOCK);
|
||
if (fd < 0) return -1;
|
||
struct input_event ev;
|
||
memset(&ev, 0, sizeof ev);
|
||
ev.type = EV_KEY;
|
||
ev.code = code;
|
||
ev.value = down ? 1 : 0;
|
||
int ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev);
|
||
if (ok) {
|
||
memset(&ev, 0, sizeof ev);
|
||
ev.type = EV_SYN;
|
||
ev.code = SYN_REPORT;
|
||
ev.value = 0;
|
||
ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev);
|
||
}
|
||
close(fd);
|
||
return ok ? 0 : -1;
|
||
}
|
||
|
||
/* 注入 tap(按下+抬起)或 long(按住 ms)。返回 0=成功。 */
|
||
static int inject_key_release(int code, int press_ms)
|
||
{
|
||
const char *dev = NULL;
|
||
int kc = keycode_to_device(code, &dev);
|
||
if (kc < 0) return -1;
|
||
int release_ms = press_ms > 0 ? press_ms : 0;
|
||
if (inject_key_event(dev, code, 1) < 0) return -1;
|
||
if (release_ms > 0) usleep((useconds_t)release_ms * 1000);
|
||
else usleep(80 * 1000);
|
||
if (inject_key_event(dev, code, 0) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */
|
||
static int read_small_file(const char *path, char *buf, int cap)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
int n = 0;
|
||
while (n < cap - 1) {
|
||
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
|
||
if (r <= 0) break;
|
||
n += (int)r;
|
||
}
|
||
close(fd);
|
||
buf[n] = 0;
|
||
return n;
|
||
}
|
||
|
||
/* 读某应用目录的 app.json(用 jv 解析提取字段,供扫描/详情)。 */
|
||
static int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail)
|
||
{
|
||
char pj[1024];
|
||
snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name);
|
||
char buf[8192];
|
||
if (read_small_file(pj, buf, sizeof buf) <= 0) {
|
||
/* 解析失败项:invalid */
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, name);
|
||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||
}
|
||
return -1;
|
||
}
|
||
jv_t *root = jv_parse(buf, (int)strlen(buf));
|
||
if (!root) {
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, name);
|
||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||
}
|
||
return -1;
|
||
}
|
||
jv_t *jpkg = jv_obj_get(root, "package");
|
||
jv_t *jname = jv_obj_get(root, "name");
|
||
jv_t *jver = jv_obj_get(root, "version");
|
||
jv_t *jexec = jv_obj_get(root, "exec");
|
||
jv_t *jfw = jv_obj_get(root, "framework");
|
||
jv_t *jim = jv_obj_get(root, "immersive");
|
||
jv_t *jfull = jv_obj_get(root, "fullscreen");
|
||
const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name;
|
||
const char *nm = (jname && jname->type == J_STR) ? jname->str : "";
|
||
const char *ver = (jver && jver->type == J_STR) ? jver->str : "";
|
||
const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : "";
|
||
/* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */
|
||
long long dsize = 0;
|
||
snprintf(pj, sizeof pj, "%s/%s", appdir, name);
|
||
DIR *d = opendir(pj);
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) == 0) dsize += (long long)st.st_size;
|
||
}
|
||
closedir(d);
|
||
}
|
||
if (!want_detail) {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, pkg);
|
||
sA(out, ",\"name\":");
|
||
sA_str(out, nm[0] ? nm : NULL);
|
||
sA(out, ",\"version\":");
|
||
sA_str(out, ver[0] ? ver : NULL);
|
||
sA(out, ",\"exec\":");
|
||
sA_str(out, exec);
|
||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}",
|
||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL));
|
||
} else {
|
||
sA(out, "{\"package\":");
|
||
sA_str(out, pkg);
|
||
sA(out, ",\"name\":");
|
||
sA_str(out, nm[0] ? nm : NULL);
|
||
sA(out, ",\"version\":");
|
||
sA_str(out, ver[0] ? ver : NULL);
|
||
sA(out, ",\"exec\":");
|
||
sA_str(out, exec);
|
||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":",
|
||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize);
|
||
sA_str(out, pj);
|
||
sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}");
|
||
}
|
||
jv_free(root);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- HTTP 请求读取 ---------------- */
|
||
typedef struct {
|
||
char method[8];
|
||
char path[512]; /* 解码后的路径(不含 query) */
|
||
char query[1024]; /* 原始 query(key=value&…,未解码;取值时用 qparam) */
|
||
char *body;
|
||
size_t body_len;
|
||
char content_type[128]; /* 请求 Content-Type(若非空) */
|
||
} req_t;
|
||
|
||
static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
|
||
|
||
static int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
|
||
static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return c - 'A' + 10; }
|
||
|
||
/* URL 解码到 buf。 */
|
||
static void url_decode(const char *in, char *buf, int buflen)
|
||
{
|
||
int o = 0;
|
||
for (int i = 0; in[i] && o < buflen - 1; i++) {
|
||
if (in[i] == '%' && is_hex(in[i + 1]) && is_hex(in[i + 2])) {
|
||
buf[o++] = (char)((hexval(in[i + 1]) << 4) | hexval(in[i + 2]));
|
||
i += 2;
|
||
} else if (in[i] == '+') buf[o++] = ' ';
|
||
else buf[o++] = in[i];
|
||
}
|
||
buf[o] = 0;
|
||
}
|
||
|
||
/* 从 query(key=value&…)取第一个值(url 解码后写入 out),未找到返回 0。 */
|
||
static int qparam(const char *query, const char *key, char *out, int outcap)
|
||
{
|
||
size_t kl = strlen(key);
|
||
const char *p = query;
|
||
while (p && *p) {
|
||
const char *amp = strchr(p, '&');
|
||
size_t seg = amp ? (size_t)(amp - p) : strlen(p);
|
||
if (seg >= kl + 1 && strncmp(p, key, kl) == 0 && p[kl] == '=') {
|
||
char tmp[1024];
|
||
size_t vlen = seg - kl - 1;
|
||
if (vlen >= sizeof tmp) vlen = sizeof tmp - 1;
|
||
memcpy(tmp, p + kl + 1, vlen);
|
||
tmp[vlen] = 0;
|
||
url_decode(tmp, out, outcap);
|
||
return 1;
|
||
}
|
||
if (!amp) break;
|
||
p = amp + 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 从请求 path 中取动态段(如 /api/settings/:key)。返回是否匹配。 */
|
||
static int path_segment(const char *prefix, const char *full, char *out, int outcap)
|
||
{
|
||
size_t pl = strlen(prefix);
|
||
if (strncmp(full, prefix, pl) != 0) return 0;
|
||
const char *rest = full + pl;
|
||
if (*rest == '/') rest++;
|
||
if (!*rest) return 0;
|
||
size_t l = strlen(rest);
|
||
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
|
||
memcpy(out, rest, l);
|
||
out[l] = 0;
|
||
/* 去掉后续 / 段(取第一段) */
|
||
char *slash = strchr(out, '/');
|
||
if (slash) *slash = 0;
|
||
return 1;
|
||
}
|
||
|
||
/* 从 "GET /path?query HTTP/1.1" 的 path 部分切出 path 与 query。 */
|
||
static void split_path_query(const char *src, char *path, int plc, char *query, int qlc)
|
||
{
|
||
const char *q = strchr(src, '?');
|
||
if (q) {
|
||
size_t pl = (size_t)(q - src);
|
||
if (pl >= (size_t)plc) pl = (size_t)plc - 1;
|
||
memcpy(path, src, pl);
|
||
path[pl] = 0;
|
||
snprintf(query, qlc, "%s", q + 1);
|
||
} else {
|
||
snprintf(path, plc, "%s", src);
|
||
query[0] = 0;
|
||
}
|
||
}
|
||
|
||
/* 在 buf[0..n) 中找首个 "\r\n\r\n",返回其起始下标;找不到返回 -1。 */
|
||
static ptrdiff_t find_header_end(const char *buf, size_t n)
|
||
{
|
||
if (n < 4) return -1;
|
||
for (size_t i = 0; i + 3 < n; i++) {
|
||
if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n')
|
||
return (ptrdiff_t)i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* 读请求:先读头(到 \r\n\r\n),解析 Content-Length/Content-Type,再精确读 body。
|
||
* 处理「头+body 同一 TCP 段到达」(小 POST 常见):单次 read 可能把 body 一并读入,
|
||
* 故在缓冲区内**搜索** \r\n\r\n 定位头边界,body 从边界后搬移。成功 0;超时/过大/失败 -1。 */
|
||
static int read_request(int sock, req_t *r)
|
||
{
|
||
char *buf = malloc(MAX_HTTP_HEADER);
|
||
if (!buf) return -1;
|
||
size_t hn = 0;
|
||
int deadline_ms = 5000;
|
||
ptrdiff_t hend = -1;
|
||
for (;;) {
|
||
struct pollfd pf = { sock, POLLIN, 0 };
|
||
int pr = poll(&pf, 1, deadline_ms);
|
||
if (pr <= 0) { free(buf); return -1; }
|
||
ssize_t rd = read(sock, buf + hn, MAX_HTTP_HEADER - 1 - hn);
|
||
if (rd <= 0) { free(buf); return -1; }
|
||
hn += (size_t)rd;
|
||
buf[hn] = 0;
|
||
hend = find_header_end(buf, hn);
|
||
if (hend >= 0) break;
|
||
if (hn >= MAX_HTTP_HEADER - 2) { free(buf); return -1; }
|
||
}
|
||
/* 头结束在 hend,body 起点在 hend+4 */
|
||
char *hdr = buf;
|
||
char *line_end = strstr(hdr, "\r\n");
|
||
if (!line_end) { free(buf); return -1; }
|
||
*line_end = 0;
|
||
char method[8], pathraw[512], proto[16];
|
||
if (sscanf(hdr, "%7s %511s %15s", method, pathraw, proto) < 2) { free(buf); return -1; }
|
||
snprintf(r->method, sizeof r->method, "%s", method);
|
||
split_path_query(pathraw, r->path, sizeof r->path, r->query, sizeof r->query);
|
||
size_t clen = 0;
|
||
char *hp = line_end + 2;
|
||
while (hp < buf + hend && strncmp(hp, "\r\n", 2)) {
|
||
char *he = strstr(hp, "\r\n");
|
||
if (!he || he > buf + hend) break;
|
||
*he = 0;
|
||
if (!strncasecmp(hp, "Content-Length:", 15)) clen = (size_t)strtoul(hp + 15, NULL, 10);
|
||
else if (!strncasecmp(hp, "Content-Type:", 13)) {
|
||
char *v = hp + 13;
|
||
while (*v == ' ') v++;
|
||
snprintf(r->content_type, sizeof r->content_type, "%s", v);
|
||
}
|
||
hp = he + 2;
|
||
}
|
||
char *body_start = buf + hend + 4; /* body 实际起点 */
|
||
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
|
||
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。 */
|
||
size_t cap = MAX_HTTP_BODY;
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0) cap = MAX_UPLOAD_BODY;
|
||
if (clen > cap) { free(buf); return -2; }
|
||
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
|
||
char *body = malloc(clen + 1);
|
||
if (!body) { free(buf); return -1; }
|
||
size_t got = 0;
|
||
if (buffered > 0) {
|
||
size_t take = buffered < clen ? buffered : clen;
|
||
memcpy(body, body_start, take);
|
||
got = take;
|
||
}
|
||
free(buf);
|
||
while (got < clen) {
|
||
struct pollfd pf = { sock, POLLIN, 0 };
|
||
int pr = poll(&pf, 1, 5000);
|
||
if (pr <= 0) { free(body); return -1; }
|
||
ssize_t rd = read(sock, body + got, clen - got);
|
||
if (rd <= 0) { free(body); return -1; }
|
||
got += (size_t)rd;
|
||
}
|
||
body[clen] = 0;
|
||
r->body = body;
|
||
r->body_len = clen;
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- HTTP 响应写入 ---------------- */
|
||
static int write_full(int sock, const void *b, size_t n)
|
||
{
|
||
const char *p = (const char *)b;
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t w = write(sock, p + off, n - off);
|
||
if (w <= 0) return -1;
|
||
off += (size_t)w;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra)
|
||
{
|
||
char h[512];
|
||
const char *sts = "OK";
|
||
switch (status) {
|
||
case 200: sts = "OK"; break;
|
||
case 201: sts = "Created"; break;
|
||
case 202: sts = "Accepted"; break;
|
||
case 400: sts = "Bad Request"; break;
|
||
case 401: sts = "Unauthorized"; break;
|
||
case 403: sts = "Forbidden"; break;
|
||
case 404: sts = "Not Found"; break;
|
||
case 405: sts = "Method Not Allowed"; break;
|
||
case 409: sts = "Conflict"; break;
|
||
case 413: sts = "Payload Too Large"; break;
|
||
case 415: sts = "Unsupported Media Type"; break;
|
||
case 422: sts = "Unprocessable Entity"; break;
|
||
case 500: sts = "Internal Server Error"; break;
|
||
case 501: sts = "Not Implemented"; break;
|
||
case 503: sts = "Service Unavailable"; break;
|
||
default: sts = "OK"; break;
|
||
}
|
||
snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n",
|
||
status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : "");
|
||
return write_full(sock, h, strlen(h));
|
||
}
|
||
|
||
/* 发送原生 bytes(已含 body)。 */
|
||
static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen)
|
||
{
|
||
if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1;
|
||
if (bodylen && write_full(sock, body, bodylen) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
/* 成功 JSON 信封:{"ok":true,"data":<data>}。
|
||
* 前缀 "{"ok":true,"data":" 共 18 字节。用 memcpy 精确构包(sA 的 %s 有 ~511B 单次截断,不能用于大 payload)。 */
|
||
static int send_ok(int sock, const char *data)
|
||
{
|
||
static const char prefix[] = "{\"ok\":true,\"data\":";
|
||
size_t pl = sizeof prefix - 1; /* 18 */
|
||
size_t dl = strlen(data);
|
||
size_t tot = pl + dl + 1;
|
||
char *b = malloc(tot + 1);
|
||
if (!b) {
|
||
const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}";
|
||
return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e));
|
||
}
|
||
memcpy(b, prefix, pl);
|
||
memcpy(b + pl, data, dl);
|
||
b[pl + dl] = '}';
|
||
b[pl + dl + 1] = 0;
|
||
int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot);
|
||
free(b);
|
||
return rc;
|
||
}
|
||
|
||
/* 错误 JSON 信封:{"ok":false,"error":{"code":..,"message":..}} */
|
||
static int send_err(int sock, int status, const char *code, const char *msg)
|
||
{
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"ok\":false,\"error\":{\"code\":");
|
||
sA_str(&s, code ? code : "INTERNAL");
|
||
sA(&s, ",\"message\":");
|
||
sA_str(&s, msg ? msg : "");
|
||
sA(&s, "}}");
|
||
int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n);
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int send_json(int sock, int status, const char *data)
|
||
{
|
||
if (status == 200) return send_ok(sock, data);
|
||
return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data));
|
||
}
|
||
|
||
/* 流式发送磁盘文件(下载)。 */
|
||
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename)
|
||
{
|
||
int fd = open(abspath, O_RDONLY);
|
||
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在");
|
||
struct stat st;
|
||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
|
||
char extra[256];
|
||
if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename);
|
||
else extra[0] = 0;
|
||
if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; }
|
||
char buf[8192];
|
||
ssize_t n;
|
||
while ((n = read(fd, buf, sizeof buf)) > 0) {
|
||
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; }
|
||
}
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
|
||
/* ---------------- multipart/form-data 解析(文件上传) ----------------
|
||
* 解析 body,提取 name=<path> 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。
|
||
* 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。
|
||
*/
|
||
typedef struct { char path[1400]; char filename[320]; } upfile_t;
|
||
|
||
/* 安全文件名:取 basename(去目录分隔符),拒绝空/`.`/`..`/含控制字符或分隔符。
|
||
* 返回 0=ok 写入 out;-1=拒绝。用于防上传文件名逃逸白名单(如 `../../etc/passwd`)。 */
|
||
static int safe_filename(const char *name, char *out, size_t cap)
|
||
{
|
||
if (!name || !name[0]) return -1;
|
||
const char *b = name;
|
||
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
|
||
const char *slash = strrchr(b, '/');
|
||
const char *bsl = strrchr(b, '\\');
|
||
if (bsl && (!slash || bsl > slash)) slash = bsl;
|
||
if (slash) b = slash + 1;
|
||
if (!b[0]) return -1;
|
||
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
|
||
for (const char *p = b; *p; p++) {
|
||
unsigned char c = (unsigned char)*p;
|
||
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
|
||
}
|
||
snprintf(out, cap, "%s", b);
|
||
return 0;
|
||
}
|
||
|
||
static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
||
{
|
||
/* 取 boundary */
|
||
const char *ct = r->content_type;
|
||
const char *b = strstr(ct, "boundary=");
|
||
if (!b) return -1;
|
||
b += 9;
|
||
if (*b == '"') { b++; }
|
||
char boundary[128];
|
||
int bi = 0;
|
||
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
|
||
boundary[bi] = 0;
|
||
char dmark[160];
|
||
snprintf(dmark, sizeof dmark, "--%s", boundary);
|
||
char emark[160];
|
||
snprintf(emark, sizeof emark, "--%s--", boundary);
|
||
/* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */
|
||
const char *p = r->body;
|
||
const char *end = r->body + r->body_len;
|
||
int nfiles = 0;
|
||
char curpath[1024] = "";
|
||
while (p < end) {
|
||
const char *fstart = strstr(p, dmark);
|
||
if (!fstart) break;
|
||
fstart += strlen(dmark);
|
||
if (*fstart == '-') break; /* 结束标记 --boundary-- */
|
||
if (*fstart == '\r' || *fstart == '\n') fstart++;
|
||
/* 解析该 part 的头部(直至空行) */
|
||
const char *headers_end = strstr(fstart, "\r\n\r\n");
|
||
if (!headers_end) break;
|
||
char hdr[512];
|
||
size_t hl = (size_t)(headers_end - fstart);
|
||
if (hl >= sizeof hdr) hl = sizeof hdr - 1;
|
||
memcpy(hdr, fstart, hl);
|
||
hdr[hl] = 0;
|
||
const char *data_start = headers_end + 4;
|
||
const char *name = NULL, *fname = NULL;
|
||
char *save = NULL;
|
||
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||
char *l = ln;
|
||
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||
/* name 与 filename 常同行(name="file"; filename="up.txt")。
|
||
* 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */
|
||
char *fn = strstr(l, "filename=\"");
|
||
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
|
||
char *nm = strstr(l, "name=\"");
|
||
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
|
||
}
|
||
}
|
||
/* 数据结束:下一个 "--boundary" 之前 */
|
||
const char *data_end = strstr(data_start, dmark);
|
||
if (!data_end) data_end = end;
|
||
/* 去掉数据尾部 \r\n */
|
||
while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--;
|
||
if (name && !strcmp(name, "path")) {
|
||
size_t dl = (size_t)(data_end - data_start);
|
||
if (dl >= sizeof curpath) dl = sizeof curpath - 1;
|
||
memcpy(curpath, data_start, dl);
|
||
curpath[dl] = 0;
|
||
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
|
||
size_t dl = (size_t)(data_end - data_start);
|
||
char safe[260];
|
||
if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */
|
||
/* 目标目录规范化 + 白名单校验 */
|
||
char absdir[1024];
|
||
if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; }
|
||
/* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */
|
||
char joined[1400];
|
||
snprintf(joined, sizeof joined, "%s/%s", absdir, safe);
|
||
char abs[1400];
|
||
if (normalize_path(joined, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { p = data_end; continue; }
|
||
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", safe);
|
||
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", abs);
|
||
int fd = open(abs, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) {
|
||
write_full(fd, data_start, dl);
|
||
close(fd);
|
||
nfiles++;
|
||
}
|
||
/* fd<0(不可写)→ 不计数,跳过该文件 */
|
||
}
|
||
p = data_end;
|
||
}
|
||
return nfiles; /* >=0:已保存文件数;-1 边界缺失/解析失败 */
|
||
}
|
||
|
||
/* ---------------- 日志(stderr;svcd supervisor 捕获) ---------------- */
|
||
static void panel_log(const char *fmt, ...)
|
||
{
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
fprintf(stderr, "[MeowPanel] ");
|
||
vfprintf(stderr, fmt, ap);
|
||
fprintf(stderr, "\n");
|
||
va_end(ap);
|
||
}
|
||
|
||
/* ---------------- 设置项 JSON 序列化 ---------------- */
|
||
static int str_truthy(const char *v)
|
||
{
|
||
char q[32];
|
||
snprintf(q, sizeof q, "%s", v);
|
||
char *p = q;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!strcasecmp(p, "true") || !strcasecmp(p, "yes") || !strcasecmp(p, "on")) return 1;
|
||
long n = strtol(p, NULL, 10);
|
||
return (p[0] && n != 0);
|
||
}
|
||
|
||
static int int_meta_max(const setdef_t *d)
|
||
{
|
||
if (d->max > 0) return d->max;
|
||
int dn = atoi(d->def);
|
||
if (dn > 0) return dn * 2 + 100;
|
||
return 100;
|
||
}
|
||
|
||
/* 非改写的 CSV 辅助:取第 idx 个项(抄入 buf,NUL 结尾),未命中返回空串。
|
||
* 用于 enum 名/标签 —— 不再 strtok_r(那会在只读字面量上写 NUL SEGV)。 */
|
||
static const char *csv_at(const char *s, int idx, char *buf, int buflen)
|
||
{
|
||
buf[0] = 0;
|
||
if (!s || !s[0]) return buf;
|
||
const char *p = s;
|
||
int i = 0;
|
||
for (;;) {
|
||
const char *start = p;
|
||
while (*p && *p != ',') p++;
|
||
if (i == idx) {
|
||
size_t k = (size_t)(p - start);
|
||
if (k >= (size_t)buflen) k = (size_t)buflen - 1;
|
||
memcpy(buf, start, k);
|
||
buf[k] = 0;
|
||
break;
|
||
}
|
||
if (!*p) break;
|
||
p++;
|
||
i++;
|
||
}
|
||
return buf;
|
||
}
|
||
|
||
static int csv_count(const char *s)
|
||
{
|
||
if (!s || !s[0]) return 0;
|
||
int n = 1;
|
||
for (const char *p = s; *p; p++) if (*p == ',') n++;
|
||
return n;
|
||
}
|
||
|
||
static void enum_options_json(sbuf_t *s, const char *names, const char *labels)
|
||
{
|
||
sA(s, "[");
|
||
int cnt = csv_count(names);
|
||
for (int i = 0; i < cnt; i++) {
|
||
char nb[128], lb[128];
|
||
csv_at(names, i, nb, sizeof nb);
|
||
csv_at(labels, i, lb, sizeof lb);
|
||
if (i) sA(s, ",");
|
||
sA(s, "{\"name\":");
|
||
sA_str(s, nb);
|
||
sA(s, ",\"label\":");
|
||
sA_str(s, lb[0] ? lb : nb);
|
||
sA(s, "}");
|
||
}
|
||
sA(s, "]");
|
||
}
|
||
|
||
static void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val)
|
||
{
|
||
const char *tn = "str";
|
||
if (d->type == ST_INT) tn = "int";
|
||
else if (d->type == ST_BOOL) tn = "bool";
|
||
else if (d->type == ST_ENUM) tn = "enum";
|
||
sA(s, "{\"key\":");
|
||
sA_str(s, d->key);
|
||
sA(s, ",\"type\":\"%s\",\"value\":", tn);
|
||
if (d->type == ST_INT) {
|
||
int v = atoi(val);
|
||
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
|
||
else if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
|
||
else if (v < d->min) v = d->min;
|
||
sA(s, "%d", v);
|
||
sA(s, ",\"min\":%d,\"max\":%d", d->min, int_meta_max(d));
|
||
} else if (d->type == ST_BOOL) {
|
||
sA(s, "%s", str_truthy(val) ? "true" : "false");
|
||
} else {
|
||
sA_str(s, val);
|
||
}
|
||
sA(s, ",\"def\":");
|
||
sA_str(s, d->def);
|
||
if (d->type == ST_ENUM) {
|
||
sA(s, ",\"options\":");
|
||
enum_options_json(s, d->enum_names, d->enum_labels);
|
||
sA(s, ",\"default_name\":");
|
||
sA_str(s, d->def);
|
||
}
|
||
sA(s, "}");
|
||
}
|
||
|
||
/* 读某键当前值(无则用默认;str 用默认)。返回 malloc 字符串。 */
|
||
static char *settings_current_value(const setdef_t *d)
|
||
{
|
||
char *v = settings_get_value(d->key);
|
||
if (v) return v;
|
||
return strdup(d->def);
|
||
}
|
||
|
||
/* ---------------- 设置端点 ---------------- */
|
||
static int api_settings_get_any(int sock, const char *key)
|
||
{
|
||
const setdef_t *d = setdef_find(key);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
|
||
char *val = settings_current_value(d);
|
||
sbuf_t s = {0};
|
||
settings_item_json(&s, d, val);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
free(val);
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_get_all(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"items\":[");
|
||
char groups[16][32];
|
||
int ngroups = 0;
|
||
for (int i = 0; i < SETS_COUNT; i++) {
|
||
const setdef_t *d = &g_sets[i];
|
||
char *val = settings_current_value(d);
|
||
if (i) sA(&s, ",");
|
||
settings_item_json(&s, d, val);
|
||
free(val);
|
||
if (d->group && d->group[0]) {
|
||
int found = 0;
|
||
for (int g = 0; g < ngroups; g++) if (!strcmp(groups[g], d->group)) { found = 1; break; }
|
||
if (!found && ngroups < 16) snprintf(groups[ngroups++], 32, "%s", d->group);
|
||
}
|
||
}
|
||
sA(&s, "],\"count\":%d,\"groups\":[", SETS_COUNT);
|
||
for (int g = 0; g < ngroups; g++) {
|
||
if (g) sA(&s, ",");
|
||
sA_str(&s, groups[g]);
|
||
}
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_put(int sock, const char *key, const req_t *r)
|
||
{
|
||
const setdef_t *d = setdef_find(key);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *val = jv_obj_get(root, "value");
|
||
if (!val) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 value 字段"); }
|
||
char raw[256];
|
||
switch (val->type) {
|
||
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
|
||
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
|
||
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
|
||
default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持");
|
||
}
|
||
jv_free(root);
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
|
||
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
|
||
}
|
||
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
/* boot.default 同步权威文件(同 meow_settings_save) */
|
||
if (!strcmp(key, "boot.default")) {
|
||
int v = (!strcmp(coerced, "hiby")) ? 1 : 0;
|
||
FILE *bf = fopen(BOOT_DEFAULT_FILE, "w");
|
||
if (bf) { fprintf(bf, "%d\n", v); fclose(bf); }
|
||
}
|
||
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;
|
||
}
|
||
|
||
static int api_settings_batch(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root || root->type != J_OBJ) { if (root) jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON 对象"); }
|
||
sbuf_t ok = {0}, fail = {0};
|
||
int nok = 0, nfail = 0;
|
||
for (const jv_t *p = root->child; p; p = p->next) {
|
||
const setdef_t *d = setdef_find(p->str);
|
||
if (!d) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"NOT_FOUND\",\"message\":\"未知设置项\"}"); nfail++; continue; }
|
||
jv_t *val = p->child;
|
||
char raw[256];
|
||
switch (val->type) {
|
||
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
|
||
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
|
||
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;
|
||
}
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
|
||
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);
|
||
nok++;
|
||
} else {
|
||
if (nfail) sA(&fail, ",");
|
||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}");
|
||
nfail++;
|
||
}
|
||
}
|
||
jv_free(root);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"updated\":[");
|
||
if (ok.b) sA(&s, "%s", ok.b);
|
||
sA(&s, "],\"failed\":[");
|
||
if (fail.b) sA(&s, "%s", fail.b);
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s); sbuf_free(&ok); sbuf_free(&fail);
|
||
return rc;
|
||
}
|
||
|
||
static int api_settings_restore(int sock)
|
||
{
|
||
if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败");
|
||
return send_json(sock, 200, "{\"reset\":true}");
|
||
}
|
||
|
||
/* ---------------- 主题端点 ---------------- */
|
||
static void theme_json(sbuf_t *s)
|
||
{
|
||
const setdef_t *dst = setdef_find("ui.style");
|
||
const setdef_t *dth = setdef_find("display.theme");
|
||
const setdef_t *dbg = setdef_find("display.background");
|
||
char *style = settings_current_value(dst);
|
||
char *theme = settings_current_value(dth);
|
||
char *bg = settings_current_value(dbg);
|
||
/* 当前值 → 中文标签(csv 里同下标查 label) */
|
||
char slabel[128] = "", tlabel[128] = "";
|
||
for (int i = 0; i < csv_count(dst->enum_names); i++) {
|
||
char nb[128], lb[128];
|
||
csv_at(dst->enum_names, i, nb, sizeof nb);
|
||
csv_at(dst->enum_labels, i, lb, sizeof lb);
|
||
if (!strcmp(nb, style)) { snprintf(slabel, sizeof slabel, "%s", lb[0] ? lb : nb); break; }
|
||
}
|
||
for (int i = 0; i < csv_count(dth->enum_names); i++) {
|
||
char nb[128], lb[128];
|
||
csv_at(dth->enum_names, i, nb, sizeof nb);
|
||
csv_at(dth->enum_labels, i, lb, sizeof lb);
|
||
if (!strcmp(nb, theme)) { snprintf(tlabel, sizeof tlabel, "%s", lb[0] ? lb : nb); break; }
|
||
}
|
||
sA(s, "{\"style\":");
|
||
sA_str(s, style);
|
||
sA(s, ",\"theme\":");
|
||
sA_str(s, theme);
|
||
sA(s, ",\"background\":");
|
||
sA_str(s, bg);
|
||
sA(s, ",\"style_label\":");
|
||
sA_str(s, slabel);
|
||
sA(s, ",\"theme_label\":");
|
||
sA_str(s, tlabel);
|
||
sA(s, ",\"style_options\":");
|
||
enum_options_json(s, dst->enum_names, dst->enum_labels);
|
||
sA(s, ",\"theme_options\":");
|
||
enum_options_json(s, dth->enum_names, dth->enum_labels);
|
||
sA(s, ",\"background_options\":[{\"name\":\"black\",\"label\":\"黑\"},{\"name\":\"blue\",\"label\":\"深蓝\"},{\"name\":\"green\",\"label\":\"墨绿\"}]}");
|
||
free(style); free(theme); free(bg);
|
||
}
|
||
|
||
static int api_theme_get(int sock)
|
||
{
|
||
sbuf_t s = {0};
|
||
theme_json(&s);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_theme_put(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
/* 允许 style/theme/background 任意子集 */
|
||
const char *keys[3] = { "style", "theme", "background" };
|
||
const char *setkeys[3] = { "ui.style", "display.theme", "display.background" };
|
||
for (int i = 0; i < 3; i++) {
|
||
jv_t *v = jv_obj_get(root, keys[i]);
|
||
if (!v) continue;
|
||
const setdef_t *d = setdef_find(setkeys[i]);
|
||
if (!d) continue;
|
||
char raw[256];
|
||
if (v->type == J_STR) snprintf(raw, sizeof raw, "%s", v->str);
|
||
else if (v->type == J_NUM) snprintf(raw, sizeof raw, "%lld", v->inum);
|
||
else continue;
|
||
char coerced[256];
|
||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "非法值"); }
|
||
settings_write_one(setkeys[i], coerced);
|
||
}
|
||
jv_free(root);
|
||
return api_theme_get(sock);
|
||
}
|
||
|
||
/* ---------------- 系统信息 / 性能 ---------------- */
|
||
static int api_perf(int sock, const char *path)
|
||
{
|
||
char json[4096];
|
||
int rc = ipc_perf_query(perf_subset_for_path(path), json, sizeof json);
|
||
if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
|
||
return send_json(sock, 200, json);
|
||
}
|
||
|
||
/* 从 meow_svc_perf 聚合 JSON 提取顶层 "network":{...} 段原始文本(含外层 {}),
|
||
* 供 /api/system 透传(单一权威源,后端不再各自采集)。找不到顶层 network 返回 -1。 */
|
||
static int perf_extract_network(const char *json, char *out, int cap)
|
||
{
|
||
const char *p = json;
|
||
for (;;) {
|
||
p = strstr(p, "\"network\"");
|
||
if (!p) return -1;
|
||
const char *q = p + 9; /* strlen("\"network\"") */
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
if (*q == ':') { p = q + 1; break; } /* 顶层 network 键 */
|
||
p += 9; /* 嵌套同名键跳过 */
|
||
}
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (*p != '{') return -1;
|
||
int depth = 0, in_str = 0, esc = 0;
|
||
const char *q;
|
||
for (q = p; *q; q++) {
|
||
char ch = *q;
|
||
if (in_str) {
|
||
if (esc) esc = 0;
|
||
else if (ch == '\\') esc = 1;
|
||
else if (ch == '"') in_str = 0;
|
||
continue;
|
||
}
|
||
if (ch == '"') { in_str = 1; continue; }
|
||
if (ch == '{') depth++;
|
||
else if (ch == '}') { depth--; if (depth == 0) { q++; break; } }
|
||
}
|
||
if (depth != 0) return -1;
|
||
size_t len = (size_t)(q - p);
|
||
if (len == 0) return -1;
|
||
if (len >= (size_t)cap) len = (size_t)cap - 1;
|
||
memcpy(out, p, len); out[len] = 0;
|
||
return 0;
|
||
}
|
||
|
||
/* /api/system:性能服务 SYSTEM 子集 + boot/ota 局部补全。
|
||
* network 段由 meow_svc_perf 聚合(SYSTEM_FRAMEWORK_SERVICES §4.1 已定案),此处**透传 perf 的 network**,
|
||
* 后端不再各自采集 wifi/蓝牙;perf 若缺 network 段才回退 hostname_local(保持不崩)。 */
|
||
static int api_system(int sock)
|
||
{
|
||
char json[4096];
|
||
int rc = ipc_perf_query(0, json, sizeof json);
|
||
if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
|
||
jv_t *root = jv_parse(json, (int)strlen(json));
|
||
if (!root) return send_err(sock, 500, "INTERNAL", "性能服务 JSON 解析失败");
|
||
jv_t *jsys = jv_obj_get(root, "system");
|
||
jv_t *jupt = jv_obj_get(root, "uptime_sec");
|
||
jv_t *jst = jv_obj_get(root, "storage");
|
||
jv_t *jtf = jv_obj_get(root, "tfcard");
|
||
const char *ver = "-", *host = "-", *ker = "-", *arch = "-";
|
||
if (jsys && jsys->type == J_OBJ) {
|
||
jv_t *v = jv_obj_get(jsys, "version"); if (v && v->type == J_STR) ver = v->str;
|
||
v = jv_obj_get(jsys, "hostname"); if (v && v->type == J_STR) host = v->str;
|
||
v = jv_obj_get(jsys, "kernel"); if (v && v->type == J_STR) ker = v->str;
|
||
v = jv_obj_get(jsys, "arch"); if (v && v->type == J_STR) arch = v->str;
|
||
}
|
||
long long upt = jupt ? (long long)jupt->dnum : 0;
|
||
/* boot default:读权威 /data/meow_boot_default(0=meow 默认, 1=hiby),失败回退 meow */
|
||
int bdf = -1;
|
||
{
|
||
char b[16];
|
||
FILE *f = fopen(BOOT_DEFAULT_FILE, "r");
|
||
if (f) { if (fgets(b, sizeof b, f)) bdf = atoi(b); fclose(f); }
|
||
}
|
||
const char *bootname = bdf < 0 ? "meow" : (bdf ? "hiby" : "meow");
|
||
char host_local[80];
|
||
snprintf(host_local, sizeof host_local, "%s.local", host);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"version\":");
|
||
sA_str(&s, ver);
|
||
sA(&s, ",\"hostname\":");
|
||
sA_str(&s, host);
|
||
sA(&s, ",\"kernel\":");
|
||
sA_str(&s, ker);
|
||
sA(&s, ",\"arch\":");
|
||
sA_str(&s, arch);
|
||
sA(&s, ",\"uptime_sec\":%lld,\"boot\":{\"default\":", upt);
|
||
sA_str(&s, bootname);
|
||
sA(&s, "},\"network\":");
|
||
char netbuf[1024];
|
||
if (perf_extract_network(json, netbuf, sizeof netbuf) == 0) {
|
||
sA(&s, "%s", netbuf); /* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local) */
|
||
} else {
|
||
/* 回退:perf 未含 network 段 → 仅 hostname_local(保持原语义,不崩) */
|
||
sA(&s, "{\"hostname_local\":");
|
||
sA_str(&s, host_local);
|
||
sA(&s, "}");
|
||
}
|
||
sA(&s, ",\"storage\":{");
|
||
if (jst && jst->type == J_OBJ) {
|
||
jv_t *t = jv_obj_get(jst, "total"); long long tot = t ? (long long)t->dnum : 0;
|
||
jv_t *u = jv_obj_get(jst, "used"); long long usd = u ? (long long)u->dnum : 0;
|
||
jv_t *a = jv_obj_get(jst, "avail"); long long ava = a ? (long long)a->dnum : 0;
|
||
sA(&s, "\"root\":{\"total\":%lld,\"used\":%lld,\"avail\":%lld}", tot, usd, ava);
|
||
} else {
|
||
sA(&s, "\"root\":{\"total\":0,\"used\":0,\"avail\":0}");
|
||
}
|
||
sA(&s, ",\"tfcard\":{");
|
||
if (jtf && jtf->type == J_OBJ) {
|
||
jv_t *pr = jv_obj_get(jtf, "present"); int present = pr && pr->bval;
|
||
long long tot = 0, usd = 0, ava = 0;
|
||
jv_t *t = jv_obj_get(jtf, "total"); if (t) tot = (long long)t->dnum;
|
||
jv_t *u = jv_obj_get(jtf, "used"); if (u) usd = (long long)u->dnum;
|
||
jv_t *a = jv_obj_get(jtf, "avail"); if (a) ava = (long long)a->dnum;
|
||
sA(&s, "\"present\":%s,\"total\":%lld,\"used\":%lld,\"avail\":%lld}", present ? "true" : "false", tot, usd, ava);
|
||
} else {
|
||
sA(&s, "\"present\":false,\"total\":0,\"used\":0,\"avail\":0}");
|
||
}
|
||
sA(&s, "},\"ota\":{\"current\":");
|
||
sA_str(&s, ver);
|
||
sA(&s, ",\"latest\":");
|
||
sA_str(&s, ver);
|
||
sA(&s, ",\"available\":false,\"url\":null}}");
|
||
jv_free(root);
|
||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return ret;
|
||
}
|
||
|
||
/* ---------------- 系统动作(经 meow_svc_power IPC) ---------------- */
|
||
static int api_system_action(int sock, const char *which, const req_t *r)
|
||
{
|
||
(void)r;
|
||
uint32_t cmd;
|
||
if (!strcmp(which, "reboot")) cmd = POWER_REBOOT;
|
||
else if (!strcmp(which, "shutdown")) cmd = POWER_SHUTDOWN;
|
||
else if (!strcmp(which, "ota")) cmd = POWER_UPGRADE;
|
||
else return send_err(sock, 404, "NOT_FOUND", "未知系统动作");
|
||
m_power_status_t st;
|
||
memset(&st, 0, sizeof st);
|
||
int rc = ipc_power_action(cmd, 0, NULL, &st);
|
||
if (rc == MEOW_OK) return send_json(sock, 200, "{\"accepted\":true}");
|
||
/* 系统动作类服务端通常「先应答受理再执行」;此处若返回非 OK 则说明服务未受理 */
|
||
const char *why = st.msg[0] ? st.msg : power_code_str(rc);
|
||
if (rc == MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", "电源服务拒绝(非 root 调用)");
|
||
if (rc == MEOW_ERR_NODEV) return send_err(sock, 503, "NO_CARD", "升级文件缺失或服务不可达");
|
||
if (rc == MEOW_ERR_NOENT || rc == MEOW_ERR_IO) return send_err(sock, 503, "NO_CARD", why);
|
||
return send_err(sock, 500, "INTERNAL", why);
|
||
}
|
||
|
||
/* ---------------- 远程控制(按键注入) ---------------- */
|
||
static int api_key_single(int sock, int code, const req_t *r)
|
||
{
|
||
int press_ms = 0;
|
||
char pressbuf[16] = "tap";
|
||
const char *press = pressbuf;
|
||
if (r->body && r->body[0]) {
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (root) {
|
||
jv_t *p = jv_obj_get(root, "press");
|
||
if (p && p->type == J_STR) snprintf(pressbuf, sizeof pressbuf, "%s", p->str);
|
||
jv_t *m = jv_obj_get(root, "ms");
|
||
if (m && m->type == J_NUM) press_ms = (int)m->inum;
|
||
jv_free(root);
|
||
}
|
||
}
|
||
if (!strcmp(press, "hold"))
|
||
return send_err(sock, 501, "NOT_IMPLEMENTED", "hold 持续注入需常驻通道(阶段 D),后端未实现");
|
||
if (strcmp(press, "tap") && strcmp(press, "long"))
|
||
return send_err(sock, 400, "BAD_REQUEST", "press 非法");
|
||
if (press_ms <= 0) {
|
||
if (!strcmp(press, "long")) press_ms = 3000;
|
||
}
|
||
/* 物理键红线:禁止映射 返回(158)/主页(172)/确认/exit(APPS.md)。 */
|
||
if (code == 158 || code == 172 || code == 32 || code == 28 || code == 96)
|
||
return send_err(sock, 403, "FORBIDDEN", "该键码(返回/主页/确认)按物理键红线禁止映射");
|
||
const char *tmpdev = NULL;
|
||
int dispatchable = (keycode_to_device(code, &tmpdev) >= 0);
|
||
if (!dispatchable)
|
||
return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/电源/播放/上下曲)");
|
||
if (inject_key_release(code, press_ms) < 0)
|
||
return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press);
|
||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return ret;
|
||
}
|
||
|
||
static int api_key_batch(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *keys = jv_obj_get(root, "keys");
|
||
if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); }
|
||
sbuf_t s = {0};
|
||
int first = 1;
|
||
for (const jv_t *k = keys->child; k; k = k->next) {
|
||
if (k->type != J_OBJ) continue;
|
||
jv_t *c = jv_obj_get(k, "code");
|
||
jv_t *p = jv_obj_get(k, "press");
|
||
jv_t *m = jv_obj_get(k, "ms");
|
||
if (!c || c->type != J_NUM) continue;
|
||
int code = (int)c->inum;
|
||
if (code == 158 || code == 172) continue; /* 红线跳过 */
|
||
int press_ms = (m && m->type == J_NUM) ? (int)m->inum : 0;
|
||
if (p && p->type == J_STR && !strcmp(p->str, "long") && press_ms <= 0) press_ms = 3000;
|
||
const char *tmpdev = NULL;
|
||
if (keycode_to_device(code, &tmpdev) < 0) continue;
|
||
if (inject_key_release(code, press_ms) < 0) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "%d", code);
|
||
}
|
||
jv_free(root);
|
||
sbuf_t o = {0};
|
||
if (first) sA(&o, "{\"dispatched\":[]}");
|
||
else sA(&o, "{\"dispatched\":[%s]}", s.b);
|
||
int ret = send_json(sock, 200, o.b ? o.b : "");
|
||
sbuf_free(&s); sbuf_free(&o);
|
||
return ret;
|
||
}
|
||
|
||
/* ---------------- 日志 ---------------- */
|
||
static int api_logs_list(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
DIR *d = opendir(LOGS_DIR);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"dir\":");
|
||
sA_str(&s, LOGS_DIR);
|
||
sA(&s, ",\"files\":[");
|
||
int first = 1;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
if (is_sensitive_name(e->d_name)) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "{\"name\":");
|
||
sA_str(&s, e->d_name);
|
||
sA(&s, ",\"size\":%lld,\"mtime\":%lld}", (long long)st.st_size, (long long)st.st_mtime);
|
||
}
|
||
closedir(d);
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_logs_read(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char file[512], lines[16];
|
||
if (!qparam(r->query, "file", file, sizeof file))
|
||
return send_err(sock, 400, "BAD_REQUEST", "缺 file 参数");
|
||
int maxlines = 200;
|
||
if (qparam(r->query, "lines", lines, sizeof lines)) {
|
||
int n = atoi(lines);
|
||
if (n > 0) maxlines = n;
|
||
if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES;
|
||
}
|
||
/* 防逃逸:file 相对 LOGS_DIR,禁止 .. 与绝对路径 */
|
||
if (file[0] == '/' || strstr(file, "..")) return send_err(sock, 403, "FORBIDDEN", "文件路径逃逸");
|
||
if (is_sensitive_name(strrchr(file, '/') ? strrchr(file, '/') + 1 : file))
|
||
return send_err(sock, 403, "FORBIDDEN", "敏感文件");
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, file);
|
||
int fd = open(full, O_RDONLY);
|
||
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "日志文件不存在");
|
||
struct stat st;
|
||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
|
||
/* 逐行读,保留末尾 maxlines 行 */
|
||
const char *buf = malloc((size_t)st.st_size + 1);
|
||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||
ssize_t n = read(fd, (char *)buf, (size_t)st.st_size);
|
||
close(fd);
|
||
if (n < 0) { free((void *)buf); return send_err(sock, 500, "INTERNAL", "读日志失败"); }
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"file\":");
|
||
sA_str(&s, file);
|
||
sA(&s, ",\"lines\":[");
|
||
/* 直接扫一遍,保留末尾 maxlines 行文本 */
|
||
const char *p = buf;
|
||
const char *end = buf + n;
|
||
int total = 0, truncated = 0;
|
||
int gotlines = 0;
|
||
int linecnt = 0;
|
||
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
|
||
if (linecnt > 0 && end[-1] != '\n') linecnt++;
|
||
int startrow = linecnt - maxlines;
|
||
if (startrow < 0) startrow = 0;
|
||
if (startrow > 0) truncated = 1;
|
||
int rowidx = 0;
|
||
p = buf;
|
||
while (p < end) {
|
||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||
const char *line_end = nl ? nl : end;
|
||
if (rowidx >= startrow) {
|
||
if (gotlines) sA(&s, ",");
|
||
sbuf_t ln = {0};
|
||
for (const char *c = p; c < line_end; c++) {
|
||
unsigned char cc = (unsigned char)*c;
|
||
if (cc == '"' || cc == '\\') sA(&ln, "\\%c", (int)cc);
|
||
else if (cc < 0x20 && cc != '\t') sA(&ln, "\\u%04x", cc);
|
||
else sA(&ln, "%c", (int)cc);
|
||
}
|
||
/* 用 sA_str(任意长度安全);不要 sA("%s")——单次超 511B 会截断坏 JSON */
|
||
sA(&s, "\"");
|
||
for (const char *c = ln.b ? ln.b : ""; *c; c++) sA(&s, "%c", (int)(unsigned char)*c);
|
||
sA(&s, "\"");
|
||
sbuf_free(&ln);
|
||
gotlines++;
|
||
}
|
||
rowidx++;
|
||
if (!nl) break;
|
||
p = nl + 1;
|
||
}
|
||
total = linecnt;
|
||
free((void *)buf);
|
||
sA(&s, "],\"total\":%d,\"truncated\":%s}", total, truncated ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 通知(数据源 = TF 卡通知转储日志) ----------------
|
||
* 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务):
|
||
* 1) launcher IPC 查询 —— meow_ipc.h 已定义 MSG_NOTIFY_LIST(0x23),但 launcher 侧仅作
|
||
* 守护→App 下发行(util.c:1079「not dispatched here」),**无客户端查询请求类型**,
|
||
* 后端(svcd 独立进程)无法据此取到活跃清单 → 跳过,回退 ②。详见报告「需 launcher 侧配合项」。
|
||
* 2) TF 卡通知转储文件 `<卡根>/MeowOS/notifications/notifications.log`:
|
||
* launcher 把每条通知归档时追加一行 CSV(meow_notif.c tf_line:CSV 追加 + 10MB 滚动,开机清空)。
|
||
* 字段顺序:timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,
|
||
* ongoing,media,dismissed(前 6/7/11/12/13 为裸整数,其余引号包裹 + 内部 " 翻倍)。
|
||
* 注意:**归档发生在置 dismissed=1 之前(meow_notif_dismiss 先 archive_one 后置 dismissed)**,
|
||
* 故日志的 dismissed 列恒 ≈0(不可靠),且日志是「会话级归档」,非活跃快照。
|
||
* 3) 都没有 → 明确空态 {items:[],notifications:[],source:"none"},**不伪造数据**。
|
||
* clear(dismiss_all)语义:保留活动中的媒体/进度通知(ongoing||media||progress>=0||category=="media"),
|
||
* 其余从日志移除。单条删除:按 id 移除该行。 */
|
||
typedef struct {
|
||
long long ms; /* created_ms(epoch ms) */
|
||
unsigned id;
|
||
char app_id[40];
|
||
char app_icon[64];
|
||
char category[40];
|
||
int urgency;
|
||
int progress; /* -1=无 */
|
||
char summary[96];
|
||
char body[128];
|
||
char target[64];
|
||
int ongoing, media, dismissed;
|
||
} notif_rec_t;
|
||
|
||
/* 取一个 CSV 字段(引号包裹 → 解转义;裸字段 → 原样;末后跳过 ',')。返回 0。 */
|
||
static int notif_csv_field(const char **pp, char *out, int outcap)
|
||
{
|
||
const char *p = *pp;
|
||
int o = 0;
|
||
if (*p == '"') {
|
||
p++;
|
||
while (*p && *p != '"') {
|
||
if (*p == '"' && p[1] == '"') {
|
||
if (o < outcap - 1) out[o++] = '"';
|
||
p += 2;
|
||
} else {
|
||
if (o < outcap - 1) out[o++] = *p;
|
||
p++;
|
||
}
|
||
}
|
||
if (*p == '"') p++;
|
||
} else {
|
||
while (*p && *p != ',' && *p != '\n' && *p != '\r') {
|
||
if (o < outcap - 1) out[o++] = *p;
|
||
p++;
|
||
}
|
||
}
|
||
if (o < outcap) { out[o] = 0; }
|
||
else if (outcap > 0) { out[outcap - 1] = 0; }
|
||
if (*p == ',') p++;
|
||
*pp = p;
|
||
return 0;
|
||
}
|
||
|
||
static int notif_parse_line(const char *line, notif_rec_t *r)
|
||
{
|
||
const char *p = line;
|
||
char f1[32], f2[16], f6[8], f7[16], f11[8], f12[8], f13[8];
|
||
char app[40], icon[64], cat[40], sum[96], body[128], tgt[64];
|
||
notif_csv_field(&p, f1, sizeof f1);
|
||
notif_csv_field(&p, f2, sizeof f2);
|
||
notif_csv_field(&p, app, sizeof app);
|
||
notif_csv_field(&p, icon, sizeof icon);
|
||
notif_csv_field(&p, cat, sizeof cat);
|
||
notif_csv_field(&p, f6, sizeof f6);
|
||
notif_csv_field(&p, f7, sizeof f7);
|
||
notif_csv_field(&p, sum, sizeof sum);
|
||
notif_csv_field(&p, body, sizeof body);
|
||
notif_csv_field(&p, tgt, sizeof tgt);
|
||
notif_csv_field(&p, f11, sizeof f11);
|
||
notif_csv_field(&p, f12, sizeof f12);
|
||
notif_csv_field(&p, f13, sizeof f13);
|
||
r->ms = strtoll(f1, NULL, 10);
|
||
r->id = (unsigned)strtoul(f2, NULL, 10);
|
||
r->urgency = atoi(f6);
|
||
r->progress = atoi(f7);
|
||
r->ongoing = atoi(f11);
|
||
r->media = atoi(f12);
|
||
r->dismissed = atoi(f13);
|
||
snprintf(r->app_id, sizeof r->app_id, "%s", app);
|
||
snprintf(r->app_icon, sizeof r->app_icon, "%s", icon);
|
||
snprintf(r->category, sizeof r->category, "%s", cat);
|
||
snprintf(r->summary, sizeof r->summary, "%s", sum);
|
||
snprintf(r->body, sizeof r->body, "%s", body);
|
||
snprintf(r->target, sizeof r->target, "%s", tgt);
|
||
return 0;
|
||
}
|
||
|
||
/* 读通知日志,保留最新 maxrec 条(环形缓冲,避免全量进内存)。返回条数;-1=文件不存在/无卡。 */
|
||
static int notif_load_list(notif_rec_t *recs, int maxrec)
|
||
{
|
||
FILE *f = fopen(g_notif_log, "r");
|
||
if (!f) return -1;
|
||
int total = 0, head = 0;
|
||
char line[1400];
|
||
notif_rec_t r;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (notif_parse_line(line, &r) != 0) continue;
|
||
recs[head] = r;
|
||
head = (head + 1) % maxrec;
|
||
if (total < maxrec) total++;
|
||
}
|
||
fclose(f);
|
||
if (total < maxrec) return total; /* 未满窗:recs[0..total-1] 即 oldest..newest */
|
||
/* 满窗:oldest 在 recs[head],重排为 oldest..newest 连续 */
|
||
notif_rec_t tmp[NOTIF_PREVIEW_MAX];
|
||
for (int i = 0; i < maxrec; i++) tmp[i] = recs[(head + i) % maxrec];
|
||
memcpy(recs, tmp, (size_t)maxrec * sizeof recs[0]);
|
||
return total;
|
||
}
|
||
|
||
/* app_id → 展示名("system" → "系统";其余用 app_id 兜底,前端可回退)。 */
|
||
static const char *notif_app_name(const char *app_id)
|
||
{
|
||
if (!strcmp(app_id, "system")) return "系统";
|
||
return app_id;
|
||
}
|
||
|
||
static void notif_emit_item(sbuf_t *out, const notif_rec_t *r)
|
||
{
|
||
sA(out, "{\"id\":%u,\"app_id\":", r->id);
|
||
sA_str(out, r->app_id);
|
||
sA(out, ",\"app_name\":");
|
||
sA_str(out, notif_app_name(r->app_id));
|
||
sA(out, ",\"app_icon\":");
|
||
sA_str(out, r->app_icon);
|
||
sA(out, ",\"category\":");
|
||
sA_str(out, r->category);
|
||
sA(out, ",\"summary\":");
|
||
sA_str(out, r->summary);
|
||
sA(out, ",\"body\":");
|
||
sA_str(out, r->body);
|
||
sA(out, ",\"urgency\":%d,\"ts\":%lld,\"progress\":", r->urgency, r->ms > 0 ? r->ms / 1000 : 0);
|
||
if (r->progress >= 0) sA(out, "%d", r->progress); else sA(out, "null");
|
||
sA(out, ",\"ongoing\":%s,\"media\":%s,\"target_activity\":",
|
||
r->ongoing ? "true" : "false", r->media ? "true" : "false");
|
||
sA_str(out, r->target);
|
||
sA(out, ",\"dismissed\":%s}", r->dismissed ? "true" : "false");
|
||
}
|
||
|
||
static int api_notifications(int sock)
|
||
{
|
||
notif_rec_t recs[NOTIF_PREVIEW_MAX];
|
||
int n = notif_load_list(recs, NOTIF_PREVIEW_MAX);
|
||
if (n < 0) return send_json(sock, 200, "{\"count\":0,\"items\":[],\"notifications\":[],\"source\":\"none\"}");
|
||
sbuf_t arr = {0};
|
||
int first = 1;
|
||
for (int i = n - 1; i >= 0; i--) { /* newest first */
|
||
if (!first) sA(&arr, ",");
|
||
first = 0;
|
||
notif_emit_item(&arr, &recs[i]);
|
||
}
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"count\":%d,\"source\":\"tf\",\"items\":[", n);
|
||
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||
sA(&s, "],\"notifications\":[");
|
||
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||
sA(&s, "]}");
|
||
sbuf_free(&arr);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 重写通知日志。mode=1:保留活动媒体/进度(ongoing||media||progress>=0||category=="media"),
|
||
* 其余删除(dismiss_all 语义,对应通知中心既定语义);mode=0:删除指定 id(单条删除)。
|
||
* 返回删除行数;-1=IO 错;0=无文件(无可删)。写临时文件后 rename(避免与 launcher append 竞写)。 */
|
||
static int notif_rewrite(int mode, unsigned del_id)
|
||
{
|
||
if (!g_notif_log || !g_notif_log[0]) return -1;
|
||
char tmppath[1400];
|
||
snprintf(tmppath, sizeof tmppath, "%s.panel.tmp", g_notif_log);
|
||
FILE *in = fopen(g_notif_log, "r");
|
||
if (!in) return 0;
|
||
FILE *out = fopen(tmppath, "w");
|
||
if (!out) { fclose(in); return -1; }
|
||
int removed = 0;
|
||
char line[1400];
|
||
while (fgets(line, sizeof line, in)) {
|
||
notif_rec_t r;
|
||
int parsed = (notif_parse_line(line, &r) == 0);
|
||
int drop = 0;
|
||
if (mode == 1) {
|
||
if (!parsed) drop = 1;
|
||
else drop = !(r.ongoing || r.media || r.progress >= 0 || !strcmp(r.category, "media"));
|
||
} else {
|
||
if (parsed && r.id == del_id) drop = 1;
|
||
}
|
||
if (drop) removed++;
|
||
else fputs(line, out);
|
||
}
|
||
fclose(in);
|
||
if (fclose(out) != 0) return -1;
|
||
if (rename(tmppath, g_notif_log) < 0) { unlink(tmppath); return -1; }
|
||
return removed;
|
||
}
|
||
|
||
static int api_notification_delete(int sock, unsigned id)
|
||
{
|
||
int removed = notif_rewrite(0, id);
|
||
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||
if (removed == 0) return send_err(sock, 404, "NOT_FOUND", "通知不存在");
|
||
char d[64];
|
||
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
|
||
return send_json(sock, 200, d);
|
||
}
|
||
|
||
static int api_notification_dismiss_all(int sock)
|
||
{
|
||
int removed = notif_rewrite(1, 0);
|
||
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||
char d[64];
|
||
snprintf(d, sizeof d, "{\"dismissed\":%d}", removed);
|
||
return send_json(sock, 200, d);
|
||
}
|
||
|
||
/* ---------------- 文件操作 ---------------- */
|
||
static int api_files_list(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024] = "/";
|
||
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
|
||
return files_list(sock, path);
|
||
}
|
||
|
||
static int api_files_read(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024];
|
||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||
char abs[1024];
|
||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
long long size; int trunc;
|
||
char *txt = read_text_file_limited(abs, &size, &trunc);
|
||
if (txt == (char *)-2) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "二进制文件不支持文本读取");
|
||
if (!txt) return send_err(sock, 404, "NOT_FOUND", "文件不存在或过大");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"path\":");
|
||
sA_str(&s, abs);
|
||
sA(&s, ",\"content\":");
|
||
sA_str(&s, txt);
|
||
sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false");
|
||
free(txt);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_files_download(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024];
|
||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||
char abs[1024];
|
||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
const char *base = strrchr(abs, '/');
|
||
base = base ? base + 1 : abs;
|
||
if (is_sensitive_name(base)) return send_err(sock, 403, "FORBIDDEN", "敏感文件");
|
||
return send_file(sock, 200, "application/octet-stream", abs, base);
|
||
}
|
||
|
||
static int api_files_upload(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0)
|
||
return send_err(sock, 415, "UNSUPPORTED_MEDIA", "需 multipart/form-data");
|
||
upfile_t files[16];
|
||
memset(files, 0, sizeof files);
|
||
int n = parse_multipart(r, files, 16);
|
||
if (n < 0) return send_err(sock, 400, "BAD_REQUEST", "multipart 解析失败");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"item\":[");
|
||
int first = 1;
|
||
for (int i = 0; i < n && i < 16; i++) {
|
||
if (!files[i].filename[0]) continue;
|
||
if (!strstr(files[i].path, "/")) continue; /* 无目标目录的跳过 */
|
||
struct stat st;
|
||
if (stat(files[i].path, &st) < 0) continue;
|
||
const char *base = strrchr(files[i].path, '/');
|
||
base = base ? base + 1 : files[i].path;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
sA(&s, "{\"name\":");
|
||
sA_str(&s, base);
|
||
sA(&s, ",\"type\":\"file\",\"size\":%lld,\"mtime\":%lld}",
|
||
(long long)st.st_size, (long long)st.st_mtime);
|
||
}
|
||
sA(&s, "]}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* 递归删除目录/文件。 */
|
||
static int rm_recursive(const char *path)
|
||
{
|
||
struct stat st;
|
||
if (lstat(path, &st) < 0) return -1;
|
||
if (S_ISDIR(st.st_mode)) {
|
||
DIR *d = opendir(path);
|
||
if (!d) return -1;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", path, e->d_name);
|
||
rm_recursive(full);
|
||
}
|
||
closedir(d);
|
||
return rmdir(path);
|
||
}
|
||
return unlink(path);
|
||
}
|
||
|
||
static int api_files_mkdir(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
if (!pj || pj->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path"); }
|
||
char abs[1024];
|
||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||
struct stat st;
|
||
if (stat(abs, &st) == 0) { jv_free(root); return send_err(sock, 409, "CONFLICT", "目录已存在"); }
|
||
if (mkdir(abs, 0755) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "创建目录失败"); }
|
||
const char *base = strrchr(abs, '/');
|
||
const char *b = base ? base + 1 : abs;
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"item\":{\"name\":");
|
||
sA_str(&s, b);
|
||
sA(&s, ",\"type\":\"dir\",\"size\":0,\"mtime\":%lld}}", (long long)time(NULL));
|
||
jv_free(root);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_files_rename(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
jv_t *pnn = jv_obj_get(root, "new_name");
|
||
if (!pj || pj->type != J_STR || !pnn || pnn->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path/new_name"); }
|
||
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
|
||
char abs[1024], newabs[1400];
|
||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||
/* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */
|
||
char parent[1024];
|
||
snprintf(parent, sizeof parent, "%s", abs);
|
||
char *sl = strrchr(parent, '/');
|
||
if (!sl || sl == parent) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "无法重命名根路径"); }
|
||
*sl = 0;
|
||
if (!parent[0]) snprintf(parent, sizeof parent, "%s", TF_ROOT); /* abs 顶层(如 /tmp/mptf/x)→ 父=根 */
|
||
snprintf(newabs, sizeof newabs, "%s/%s", parent, pnn->str);
|
||
/* newabs 复校白名单(new_name 无 /,父目录已校验;防御) */
|
||
if (!path_in_whitelist(newabs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"item\":{\"name\":");
|
||
sA_str(&s, pnn->str);
|
||
sA(&s, ",\"type\":\"");
|
||
struct stat st;
|
||
if (stat(newabs, &st) == 0 && S_ISDIR(st.st_mode)) sA(&s, "dir"); else sA(&s, "file");
|
||
sA(&s, "\",\"size\":%lld,\"mtime\":%lld}}", (long long)st.st_size, (long long)st.st_mtime);
|
||
jv_free(root);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_files_delete(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
char path[1024];
|
||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||
char recursive[8] = "0";
|
||
qparam(r->query, "recursive", recursive, sizeof recursive);
|
||
char abs[1024];
|
||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||
if (!strcmp(abs, TF_ROOT)) return send_err(sock, 403, "FORBIDDEN", "禁止删除卡根");
|
||
struct stat st;
|
||
if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||
if (S_ISDIR(st.st_mode) && atoi(recursive) != 1) {
|
||
DIR *d = opendir(abs);
|
||
int nonempty = 0;
|
||
if (d) {
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
nonempty = 1; break;
|
||
}
|
||
closedir(d);
|
||
}
|
||
if (nonempty) return send_err(sock, 409, "CONFLICT", "目录非空(需 recursive=1)");
|
||
}
|
||
if (rm_recursive(abs) < 0) return send_err(sock, 500, "INTERNAL", "删除失败");
|
||
return send_json(sock, 200, "{\"removed\":true}");
|
||
}
|
||
|
||
/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */
|
||
/* 执行命令(sh -c),捕获 stdout(上限 cap-1),返回退出状态(-1=无法启动)。 */
|
||
static int run_cmd(const char *cmd, char *out, int outcap)
|
||
{
|
||
FILE *f = popen(cmd, "r");
|
||
if (!f) return -1;
|
||
size_t off = 0;
|
||
while (off < (size_t)outcap - 1) {
|
||
size_t rd = fread(out + off, 1, (size_t)outcap - 1 - off, f);
|
||
if (rd == 0) break;
|
||
off += rd;
|
||
}
|
||
out[off] = 0;
|
||
int st = pclose(f);
|
||
int code = -1;
|
||
if (WIFEXITED(st)) code = WEXITSTATUS(st);
|
||
return code;
|
||
}
|
||
|
||
static int api_apps_list(int sock)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
DIR *d = opendir(MEOWAPPS_DIR);
|
||
if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"count\":0,\"apps\":[");
|
||
int first = 1, count = 0;
|
||
struct dirent *e;
|
||
while ((e = readdir(d))) {
|
||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||
if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue;
|
||
if (strstr(e->d_name, ".bak")) continue;
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name);
|
||
struct stat st;
|
||
if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue;
|
||
if (!first) sA(&s, ",");
|
||
first = 0;
|
||
apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0);
|
||
count++;
|
||
}
|
||
closedir(d);
|
||
sA(&s, "]}");
|
||
/* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */
|
||
/* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
(void)count;
|
||
return rc;
|
||
}
|
||
|
||
static int api_apps_info(int sock, const char *pkg)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char appdir[1024];
|
||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg);
|
||
struct stat st;
|
||
if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装");
|
||
sbuf_t s = {0};
|
||
if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0)
|
||
return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* multipart 提取第一个 file part 并写入 outpath。返回 0=成功。 */
|
||
static int multipart_extract_file(const req_t *r, const char *outpath)
|
||
{
|
||
const char *b = strstr(r->content_type, "boundary=");
|
||
if (!b) return -1;
|
||
b += 9;
|
||
if (*b == '"') b++;
|
||
char boundary[128]; int bi = 0;
|
||
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
|
||
boundary[bi] = 0;
|
||
char dmark[160];
|
||
snprintf(dmark, sizeof dmark, "--%s", boundary);
|
||
const char *p = r->body;
|
||
while (p < r->body + r->body_len) {
|
||
const char *fs = strstr(p, dmark);
|
||
if (!fs) break;
|
||
fs += strlen(dmark);
|
||
if (*fs == '-') break;
|
||
if (*fs == '\r' || *fs == '\n') fs++;
|
||
const char *he = strstr(fs, "\r\n\r\n");
|
||
if (!he) break;
|
||
char hdr[512];
|
||
size_t hl = (size_t)(he - fs); if (hl >= sizeof hdr) hl = sizeof hdr - 1;
|
||
memcpy(hdr, fs, hl); hdr[hl] = 0;
|
||
int isfile = 0;
|
||
char *save = NULL;
|
||
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||
char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||
if (strstr(l, "name=\"file\"")) isfile = 1;
|
||
}
|
||
}
|
||
const char *ds = he + 4;
|
||
const char *de = strstr(ds, dmark);
|
||
if (!de) de = r->body + r->body_len;
|
||
while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--;
|
||
if (isfile) {
|
||
int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd < 0) return -1;
|
||
write_full(fd, ds, (size_t)(de - ds));
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
p = de;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static const char *nyai_bin(void)
|
||
{
|
||
/* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */
|
||
if (!access(NYAI_PATH, X_OK)) return NYAI_PATH;
|
||
return NYAI_PATH_CARD;
|
||
}
|
||
|
||
static int api_apps_install(int sock, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (access(nyai_bin(), X_OK) < 0) return send_err(sock, 500, "INTERNAL", "nyai 不可用(固件与卡上均无)");
|
||
char mpkpath[1024];
|
||
int ispath = 0;
|
||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) {
|
||
/* JSON {path} */
|
||
if (r->body && r->body[0]) {
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *pj = jv_obj_get(root, "path");
|
||
if (pj && pj->type == J_STR) {
|
||
snprintf(mpkpath, sizeof mpkpath, "%s", pj->str);
|
||
ispath = 1;
|
||
}
|
||
jv_free(root);
|
||
}
|
||
if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file");
|
||
} else {
|
||
/* 保存上传 file 到临时文件 */
|
||
snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR);
|
||
mkdir(mpkpath, 0777);
|
||
char tmp[1024];
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR);
|
||
if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败");
|
||
snprintf(mpkpath, sizeof mpkpath, "%s", tmp);
|
||
}
|
||
if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在");
|
||
char cmd[1400];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' install '%s' 2>&1", nyai_bin(), mpkpath);
|
||
char out[4096];
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败");
|
||
/* 包名 = 文件名茎;版本从 app.json 读 */
|
||
char stem[256];
|
||
const char *base = strrchr(mpkpath, '/');
|
||
base = base ? base + 1 : mpkpath;
|
||
snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base);
|
||
char *dot = strrchr(stem, '.');
|
||
if (dot) *dot = 0;
|
||
char appdir[1024], pj[1100];
|
||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem);
|
||
jv_t *approot = NULL;
|
||
char version[16] = "";
|
||
int upgraded = 0;
|
||
snprintf(pj, sizeof pj, "%s/app.json", appdir);
|
||
char jbuf[8192];
|
||
if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0)
|
||
approot = jv_parse(jbuf, (int)strlen(jbuf));
|
||
if (approot) {
|
||
jv_t *v = jv_obj_get(approot, "version");
|
||
if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str);
|
||
jv_free(approot);
|
||
}
|
||
if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1;
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"package\":");
|
||
sA_str(&s, stem);
|
||
sA(&s, ",\"version\":");
|
||
sA_str(&s, version[0] ? version : NULL);
|
||
sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
static int api_apps_export(int sock, const char *pkg)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char tmp[1100];
|
||
char card_tmp[1200];
|
||
snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR);
|
||
mkdir(card_tmp, 0777);
|
||
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
|
||
unlink(tmp);
|
||
char cmd[1400];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' export '%s' '%s' 2>&1", nyai_bin(), pkg, tmp);
|
||
char out[1024];
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败");
|
||
char dn[256];
|
||
snprintf(dn, sizeof dn, "%s.mpk", pkg);
|
||
return send_file(sock, 200, "application/octet-stream", tmp, dn);
|
||
}
|
||
|
||
static int api_apps_remove(int sock, const char *pkg, const req_t *r)
|
||
{
|
||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||
char purgebuf[8] = "0";
|
||
if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf);
|
||
int use_purge = atoi(purgebuf);
|
||
char cmd[1400];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' remove '%s'%s 2>&1", nyai_bin(), pkg, use_purge ? " --purge" : "");
|
||
char out[1024];
|
||
int code = run_cmd(cmd, out, sizeof out);
|
||
if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败");
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
/* ---------------- 静态文件托管(web_root = 二进制所在目录) ---------------- */
|
||
static char g_webroot[512];
|
||
static const char *web_root(void)
|
||
{
|
||
if (g_webroot[0]) return g_webroot;
|
||
char exe[512];
|
||
ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1);
|
||
if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; }
|
||
exe[n] = 0;
|
||
char *slash = strrchr(exe, '/');
|
||
if (slash) *slash = 0;
|
||
snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : ".");
|
||
return g_webroot;
|
||
}
|
||
|
||
static const char *mime_for(const char *path)
|
||
{
|
||
const char *ext = strrchr(path, '.');
|
||
if (!ext) return "application/octet-stream";
|
||
if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8";
|
||
if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8";
|
||
if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8";
|
||
if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8";
|
||
if (!strcasecmp(ext, ".png")) return "image/png";
|
||
if (!strcasecmp(ext, ".ico")) return "image/x-icon";
|
||
if (!strcasecmp(ext, ".svg")) return "image/svg+xml";
|
||
return "application/octet-stream";
|
||
}
|
||
|
||
static int serve_static(int sock, const char *path)
|
||
{
|
||
const char *root = web_root();
|
||
char rel[512];
|
||
/* / → index.html;/assets/<f> → <f>;/<f> → <f>(meowpanel.css/js 等) */
|
||
if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html");
|
||
else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8);
|
||
else snprintf(rel, sizeof rel, "%s", path + 1);
|
||
if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸");
|
||
/* 只允许安全文件(防读到二进制/任意文件) */
|
||
char full[1024];
|
||
snprintf(full, sizeof full, "%s/%s", root, rel);
|
||
/* 敏感名拒绝 */
|
||
const char *base = strrchr(full, '/');
|
||
base = base ? base + 1 : full;
|
||
if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行文件");
|
||
FILE *f = fopen(full, "rb");
|
||
if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在");
|
||
fclose(f);
|
||
return send_file(sock, 200, mime_for(full), full, NULL);
|
||
}
|
||
|
||
/* ---------------- 路由分发 ---------------- */
|
||
static int handle(int sock, req_t *r)
|
||
{
|
||
const char *p = r->path;
|
||
if (strncmp(p, "/api/", 5) != 0) {
|
||
if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0)
|
||
return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET");
|
||
return serve_static(sock, p);
|
||
}
|
||
|
||
/* ---- 性能/状态 ---- */
|
||
if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf")))
|
||
return api_perf(sock, p);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system"))
|
||
return api_system(sock);
|
||
|
||
/* ---- 系统动作 ---- */
|
||
if (!strcmp(r->method, "POST")) {
|
||
if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r);
|
||
if (!strcmp(p, "/api/system/shutdown")) return api_system_action(sock, "shutdown", r);
|
||
if (!strcmp(p, "/api/system/ota")) return api_system_action(sock, "ota", r);
|
||
if (!strcmp(p, "/api/settings")) return api_settings_batch(sock, r);
|
||
if (!strcmp(p, "/api/settings/restore")) return api_settings_restore(sock);
|
||
if (!strcmp(p, "/api/theme")) return api_theme_put(sock, r);
|
||
if (!strcmp(p, "/api/files/upload")) return api_files_upload(sock, r);
|
||
if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r);
|
||
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
|
||
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
||
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
|
||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||
char seg[256];
|
||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||
return api_key_single(sock, atoi(seg), r);
|
||
}
|
||
|
||
/* ---- 设置 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/settings")) return api_settings_get_all(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/theme")) return api_theme_get(sock);
|
||
if (!strcmp(r->method, "PUT")) {
|
||
char seg[256];
|
||
if (path_segment("/api/settings/", p, seg, sizeof seg))
|
||
return api_settings_put(sock, seg, r);
|
||
}
|
||
if (!strcmp(r->method, "GET")) {
|
||
char seg[256];
|
||
if (path_segment("/api/settings/", p, seg, sizeof seg))
|
||
return api_settings_get_any(sock, seg);
|
||
}
|
||
|
||
/* ---- 文件 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
||
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
||
|
||
/* ---- 应用 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/apps")) return api_apps_list(sock);
|
||
if (!strcmp(r->method, "GET")) {
|
||
char seg[256];
|
||
if (path_segment("/api/apps/", p, seg, sizeof seg)) {
|
||
/* :pkg/export 结尾(path_segment 已把 seg 截为 pkg,这里检查原 path 尾缀) */
|
||
size_t pl = strlen(p);
|
||
int is_export = (pl >= 7 && !strcmp(p + pl - 7, "/export"));
|
||
if (is_export) return api_apps_export(sock, seg);
|
||
return api_apps_info(sock, seg);
|
||
}
|
||
}
|
||
if (!strcmp(r->method, "DELETE")) {
|
||
char seg[256];
|
||
if (path_segment("/api/apps/", p, seg, sizeof seg))
|
||
return api_apps_remove(sock, seg, r);
|
||
}
|
||
|
||
/* ---- 日志 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs")) return api_logs_list(sock);
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/read")) return api_logs_read(sock, r);
|
||
|
||
/* ---- 通知 ---- */
|
||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock);
|
||
if (!strcmp(r->method, "DELETE")) {
|
||
char seg[256];
|
||
if (path_segment("/api/notifications/", p, seg, sizeof seg)) {
|
||
unsigned nid = (unsigned)strtoul(seg, NULL, 10);
|
||
return api_notification_delete(sock, nid);
|
||
}
|
||
}
|
||
|
||
return send_err(sock, 404, "NOT_FOUND", "端点不存在");
|
||
}
|
||
|
||
/* ---------------- 主循环 ---------------- */
|
||
static int g_port = DEFAULT_PORT;
|
||
|
||
static int read_port_config(void)
|
||
{
|
||
/* 配置文件 meowpanel.conf(web 根目录),格式 key=value,取 port */
|
||
char path[1024];
|
||
snprintf(path, sizeof path, "%s/meowpanel.conf", web_root());
|
||
FILE *f = fopen(path, "r");
|
||
if (f) {
|
||
char line[160];
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *eq = strchr(line, '=');
|
||
if (!eq) continue;
|
||
char *k = line; while (*k == ' ' || *k == '\t') k++;
|
||
if (!strncmp(k, "port", 4) && k[4] == '=') g_port = atoi(eq + 1);
|
||
}
|
||
fclose(f);
|
||
}
|
||
const char *env = getenv("MEOWPORT");
|
||
if (env && env[0]) g_port = atoi(env);
|
||
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
|
||
return g_port;
|
||
}
|
||
|
||
static void handle_conn(int sock)
|
||
{
|
||
req_t r;
|
||
memset(&r, 0, sizeof r);
|
||
int rr = read_request(sock, &r);
|
||
if (rr != 0) {
|
||
if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)");
|
||
close(sock);
|
||
return;
|
||
}
|
||
handle(sock, &r);
|
||
req_free(&r);
|
||
close(sock);
|
||
}
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
/* 参数:-p PORT(可选)。默认端口 8080。 */
|
||
int port = DEFAULT_PORT;
|
||
for (int i = 1; i < argc; i++) {
|
||
if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i];
|
||
else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = 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]\n");
|
||
return 0;
|
||
}
|
||
}
|
||
init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */
|
||
const char *env = getenv("MEOW_IPC_PERF_SOCK");
|
||
if (env && env[0]) g_sock_perf = env;
|
||
env = getenv("MEOW_IPC_POWER_SOCK");
|
||
if (env && env[0]) g_sock_power = env;
|
||
|
||
(void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */
|
||
/* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */
|
||
if (port != DEFAULT_PORT) g_port = port;
|
||
else g_port = read_port_config();
|
||
/* 再按 env MEOWPORT 覆盖 conf(-p 最高) */
|
||
if (port == DEFAULT_PORT) {
|
||
const char *ep = getenv("MEOWPORT");
|
||
if (ep && ep[0]) g_port = atoi(ep);
|
||
}
|
||
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
|
||
|
||
signal(SIGPIPE, SIG_IGN);
|
||
web_root();
|
||
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);
|
||
if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; }
|
||
int on = 1;
|
||
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
|
||
struct sockaddr_in addr;
|
||
memset(&addr, 0, sizeof addr);
|
||
addr.sin_family = AF_INET;
|
||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||
addr.sin_port = htons((uint16_t)g_port);
|
||
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) {
|
||
panel_log("bind :%d fail: %s", g_port, strerror(errno));
|
||
close(lfd);
|
||
return 1;
|
||
}
|
||
if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; }
|
||
|
||
for (;;) {
|
||
int c = accept(lfd, NULL, NULL);
|
||
if (c < 0) { if (errno == EINTR) continue; continue; }
|
||
handle_conn(c);
|
||
}
|
||
return 0;
|
||
}
|