refactor(panel): 前后端按模块拆分+屏幕/控制合并页 — 后端meowpanel.c 6372行拆src/*.c+mp.h(235函数原型,20端点output byte-identical,0warning)+前端拆mp_shared/mp_pages/meowpanel三层;屏幕·遥控合并(左屏实时画面+右MD3 SVG矢量按钮无emoji);重编后热更TF卡

This commit is contained in:
AskaEth committed 2026-09-04 00:56:13 +08:00
1 parent f3ae06d7cd
commit 2dc4cad454
26 files changed
+9317 -2755

No files matched your search

+4 -3
View File
@@ -7,7 +7,8 @@
# 用法:
# ./build_meowpanel.sh # 交叉编译 → patch_v2/meowpanel/meowpanel(mipsel 静态)
# ./build_meowpanel.sh host # 宿主编译 → patch_v2/meowpanel/meowpanel_host
# 源码:meowpanel.c + mp_jpeg.c(最小 baseline JPEG 编码器,阶段 C 实时画面)。
# 源码:src/*.c(模块化拆分,见 mp.h 共享内部头)+ mp_jpeg.c(最小 baseline JPEG 编码器,阶段 C 实时画面)。
# 说明:meowpanel.c 为拆分前的单体源码(保留作参考,**不在构建源列表**;构建以 src/ 模块为准)。
# 字体库导入/导出走 busybox `gzip -dc | tar` 管道(与 nyai .mpk 同构),零额外源文件。
set -e
cd "$(dirname "$0")"
@@ -17,14 +18,14 @@ I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/m_perf_query_t/m_perf
if [ "$1" = "host" ]; then
HOST_CC="${CC_HOST:-cc}"
"$HOST_CC" -Os -Wall -Wextra -I"$I_HDR" meowpanel.c mp_jpeg.c -o meowpanel_host
"$HOST_CC" -Os -Wall -Wextra -I. -I"$I_HDR" src/*.c mp_jpeg.c -o meowpanel_host
echo "== host build =="
ls -la meowpanel_host
file meowpanel_host
exit 0
fi
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meowpanel.c mp_jpeg.c -o meowpanel
"$CC" -Os -Wall -Wextra -static -I. -I"$I_HDR" src/*.c mp_jpeg.c -o meowpanel
echo "== meowpanel (mipsel static) =="
ls -la meowpanel
md5sum meowpanel
+453
View File
@@ -0,0 +1,453 @@
/* mp.h — MeowPanel 后端共享内部头(模块化拆分后各 .c 共同包含)。
* 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件
* (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 services.d 启动。**不进固件**。
* 这是拆分后的单一共享内部头:共享常量/类型/外部全局 + 所有跨模块函数原型。
* 各 api_* 域 / store / ipc / util / json / server 均以 `#include "mp.h"` 起步。
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。 */
#ifndef MP_H
#define MP_H
#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 内核头),零第三方库(mp_jpeg.c 自研最小 JPEG;
* 字体库导入/导出走 busybox `gzip -dc | tar` 管道——与 nyai .mpk 同构,设备必带 tar/gzip)。
* 设备无 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_* 枚举 */
#include "mp_jpeg.h" /* 实时画面:RGB565 → 最小 baseline JPEG(自包含,零依赖) */
/* 实时画面 / 触摸注入(阶段 C)。fb ioctl 常量按 launcher fb_compositor 同款手工定义
* (避免 libc/kernel struct ABI 差异,读入裸字节缓冲再按偏移取字段)。 */
#ifndef FBIOGET_VSCREENINFO
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOGET_FSCREENINFO 0x4602
#endif
#ifndef FB_DEV
#define FB_DEV "/dev/fb0" /* 16bpp RGB565,480x720,双层虚拟缓冲 */
#endif
#ifndef TOUCH_DEV
#define TOUCH_DEV "/dev/input/event1" /* goodix-ts:接收 write() 注入(本机实测可投递) */
#endif
/* ---------------- 常量 ---------------- */
#define PANEL_VERSION "0.1"
#define DEFAULT_PORT 8080
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
#define SETTINGS_HDR "# meow-settings 1"
#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 LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */
#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */
#define NET_SOCK "/usr/data/meow_net.sock" /* meow_netmgrd 网络管理(MSG_NET_*,期2 观测层/DNS 代理) */
#define PM_SOCK "/usr/data/meow_pm.sock" /* meow_powermgr 功耗管理(MSG_PM_QUERY→RESULT) */
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
#define AMS_BIN "/usr/bin/am" /* 固件内置 amsd 用户侧 CLI(权威) */
#define AMS_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/am" /* 旧卡上路径(回退) */
#define SVCD_BIN "/usr/bin/meow_svcd" /* 固件内置 svcd 服务表 CLI(权威) */
#define SVCD_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/meow_svcd" /* 旧卡上路径(回退) */
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
#define VERSION_FILE "/etc/meowos_version"
#define HOSTNAME_FILE ""
#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_FONT_UPLOAD (32 * 1024 * 1024) /* 字体库 tar.gz 上传上限(gbk 字库 png+idx 可能 10-20MB) */
#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 缓冲安全) */
#define PANEL_VOL_MAX_DEFAULT 100 /* 音频音量上限默认(对齐 meow_launcher MEOW_VOL_MAX_DEFAULT;设备可经 settings.conf audio.volume_max 覆盖) */
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
#define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0]))
#define SCREEN_BLOCK 8 /* 比对块边长(像素) */
#ifndef MSG_PM_POLICY_GET
#define MSG_PM_POLICY_GET 0x00D2 /* ↑ 策略读(空负载 → MSG_PM_RESULT policy JSON;读级任意调用者) */
#define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份) */
#define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count,0=默认) */
#define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON */
#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count,0=默认) */
#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON */
#define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(动作级;本期面板不暴露,预留) */
#define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(动作级;预留) */
#endif
#define AUTH_TOKEN_BYTES 16 /* 128bit → 32 hex */
#define AUTH_TOKEN_TTL (3 * 86400) /* 3 天(固定,不滑动续期) */
#define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */
#define AUTH_HASH_LEN 64 /* sha256 hex 长度 */
/* ---------------- 有界动态字符串(sbuf) ---------------- */
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
/* ---------------- 最简 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;
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;
/* 期 3 · 策略写负载(item="profile"/"low-pct"/"crit-pct"/"bri-cap"/"scr-cap")。 */
typedef struct m_pm_policy_set { char item[16]; char val[32]; } m_pm_policy_set_t; /* 48 B */
struct fent { char name[256]; int isdir; long long size, mtime; };
/* ---------------- 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(若非空) */
char authorization[256]; /* Authorization 头(Bearer <token>) */
char x_meow_token[256]; /* X-MeowToken 头 */
char cookie[512]; /* Cookie 头(meow_token=<token> 等) */
} req_t;
typedef struct { char path[1400]; char filename[320]; } upfile_t;
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;
typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; size_t buflen; } mp_sha256_ctx;
/* ---- 会话(**纯内存**:重启自然失效,无卡上持久化) ---- */
typedef struct { char token[65]; time_t expires; } auth_sess_t;
/* ---------------- 跨模块全局(extern;定义见各拥有模块 .c) ---------------- */
extern const char *g_settings_file;
extern char g_tf_root_buf[1024];
extern char g_tf_card_buf[2048], g_apps_dir_buf[2048], g_logs_dir_buf[2048], g_notif_log_buf[2048];
extern const char *g_tf_card, *g_apps_dir, *g_logs_dir, *g_notif_log;
extern const char *g_allowed_dirs[];
extern const char *g_sensitive_names[];
extern const char *g_sock_perf, *g_sock_power, *g_sock_launcher, *g_sock_audio, *g_sock_net, *g_sock_pm;
extern time_t g_auth_ttl;
/* ---------------- 函数原型(全部跨模块;无 static,避免 -Wunused-function) ---------------- */
void init_tf_paths(void);
int sb_reserve(sbuf_t *s, size_t extra);
void sA(sbuf_t *s, const char *fmt, ...);
int utf8_seq_len(unsigned char lead);
int json_strict_balanced(const char *b, size_t n);
int utf8_seq_ok(const char *p, int len);
int utf8_valid_n(const char *s, size_t n);
int utf8_sanitize(char *dst, const char *src, size_t n);
void sA_str_n(sbuf_t *s, const char *v, size_t n);
void sA_str(sbuf_t *s, const char *v);
void sA_append(sbuf_t *s, const char *b, size_t n);
void sbuf_free(sbuf_t *s);
jv_t *jv_alloc(int type);
void jv_free(jv_t *v);
void jvp_skip_ws(jvp_t *t);
char *jv_parse_string(jvp_t *t);
jv_t *jv_parse_object(jvp_t *t);
jv_t *jv_parse_array(jvp_t *t);
jv_t *jv_parse_value(jvp_t *t);
jv_t *jv_parse(const char *text, int len);
jv_t *jv_obj_get(const jv_t *obj, const char *key);
void jv_to_json(sbuf_t *s, const jv_t *v);
const setdef_t *setdef_find(const char *key);
void settings_mkdir_p(void);
char *settings_get_value(const char *key);
int settings_get_int(const char *key, int defv);
int brightness_clamp(int v);
int settings_write_one(const char *key, const char *val);
int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap);
int settings_reset_all(void);
int x_read_full(int fd, uint8_t *buf, size_t n);
int x_write_full(int fd, const uint8_t *buf, size_t n);
int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len);
int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap);
int ipc_connect_tmo(const char *sock, int tmo_ms);
int ipc_hello(int fd);
const char *perf_code_str(int code);
int perf_subset_for_path(const char *path);
int ipc_perf_query(int subset, char *out, int cap);
const char *power_code_str(int code);
int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st);
void svc_msg_kv(const char *msg, const char *key, char *out, size_t cap);
const char *audio_kv(const char *txt, const char *key, char *out, size_t cap);
long long audio_kv_ll(const char *txt, const char *key, long long defv);
int audio_vol_max(void);
void audio_fill(sbuf_t *s);
int api_audio(int sock);
int audio_write_cmd(uint16_t type, const void *pl, uint32_t len, m_ack_t *out);
int api_audio_route(int sock, const char *body, size_t body_len);
int api_audio_volume(int sock, const char *body, size_t body_len);
int api_audio_monitor(int sock, const char *query);
int ipc_pm_query(uint32_t subset, char *out, int cap);
int api_power(int sock);
int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen, uint16_t *rtype, char *out, int cap);
int api_power_policy_get(int sock);
int api_power_policy_set(int sock, const char *body, size_t body_len);
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail);
int api_power_history(int sock);
int api_power_events(int sock);
const char *ams_bin(void);
const char *svcd_bin(void);
int net_ipc(uint16_t req_type, const void *req, uint32_t req_len, uint16_t *ret_type, uint32_t *code, uint16_t *meta, char *out, size_t cap);
int api_net_status(int sock);
int net_conns_parse_body(sbuf_t *s, const char *txt);
int api_net_conns(int sock, const char *query);
int net_dns_obj_fill(sbuf_t *s);
int api_net_dns(int sock);
int net_block_fill(sbuf_t *s);
int api_net_dns_flush(int sock);
int api_net_log(int sock);
int api_net_ifaces(int sock);
void net_policy_parse(const char *txt, sbuf_t *s);
int api_net_policy_get(int sock);
int api_net_policy_set(int sock, const char *body, size_t body_len);
void panel_strip_ansi(char *s);
int txt_tokens(char *buf, char **toks, int maxtoks);
void join_tokens(char **toks, int from, int to, char *out, size_t cap);
void ams_fill(sbuf_t *s);
void ams_fg_fill(sbuf_t *s);
int api_ams(int sock);
int ams_pkg_ok(const char *pkg);
void ams_history_parse(char *out, sbuf_t *s, const char *pkg);
void ams_history_fill(sbuf_t *s, const char *pkg);
int api_ams_history(int sock, const char *pkg);
int api_ams_ctl(int sock, const char *pkg, int kill);
void svcd_fill(sbuf_t *s);
int api_svcd(int sock);
int api_overview(int sock);
int tf_present(void);
int path_in_whitelist(const char *abs);
int is_sensitive_name(const char *base);
int normalize_path(const char *in, char *buf, size_t buflen);
void make_rel(const char *abs, char *buf, size_t buflen);
int open_safe_for_read(const char *abs, int follow_rel);
void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime);
int nat_strcmp(const char *a, const char *b);
int fent_cmp(const void *a, const void *b);
int files_list(int sock, const char *relpath);
char *read_text_file_limited(const char *abs, long long *size, int *truncated);
int keycode_to_device(int code, const char **dev);
int inject_key_event(const char *dev, int code, int down);
int inject_key_release(int code, int press_ms);
int read_small_file(const char *path, char *buf, int cap);
int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail);
void req_free(req_t *r);
int is_hex(char c);
int hexval(char c);
void url_decode(const char *in, char *buf, int buflen);
int qparam(const char *query, const char *key, char *out, int outcap);
int path_segment(const char *prefix, const char *full, char *out, int outcap);
void split_path_query(const char *src, char *path, int plc, char *query, int qlc);
ptrdiff_t find_header_end(const char *buf, size_t n);
int read_request(int sock, req_t *r);
void mp_sha256_init(mp_sha256_ctx *c);
void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p);
void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len);
void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32]);
int auth_read_random(unsigned char *buf, size_t n);
void auth_hex(const unsigned char *raw, size_t n, char *out);
int auth_token_gen(char *out, int outcap);
int auth_make_salt(char *out, int outcap);
int auth_pass_hash(const char *salt, const char *password, char *out, int outcap);
int auth_make_hash(const char *password, char *out, int outcap);
int hash_matches(const char *stored, const char *password);
int pin_set(void);
int auth_pass_set(void);
int auth_verify(const char *password);
int pin_verify(const char *pin);
int pin_validate(const char *pin);
long long pin_lock_remaining(void);
void pin_lock_reset(void);
void pin_record_fail(void);
int sess_find(const char *token);
int sess_valid(const char *token);
void sess_add(const char *token, time_t expires);
void sess_remove(const char *token);
void auth_cp(char *dst, int dcap, const char *src);
int extract_token(const req_t *r, char *out, int outcap);
int require_auth(int sock, const req_t *r);
int api_auth_status(int sock, const req_t *r);
int api_auth_login(int sock, const req_t *r);
int api_auth_setup(int sock, const req_t *r);
int api_auth_logout(int sock, const req_t *r);
int write_full(int sock, const void *b, size_t n);
int send_head(int sock, int status, const char *ctype, long long clen, const char *extra);
int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen);
int send_ok(int sock, const char *data);
int send_err(int sock, int status, const char *code, const char *msg);
int send_json(int sock, int status, const char *data);
int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
const char *mem_find(const void *hay, size_t hlen, const char *needle);
int safe_filename(const char *name, char *out, size_t cap);
int parse_multipart(const req_t *r, upfile_t *files, int maxfiles);
void panel_log(const char *fmt, ...);
int str_truthy(const char *v);
int int_meta_max(const setdef_t *d);
const char *csv_at(const char *s, int idx, char *buf, int buflen);
int csv_count(const char *s);
void enum_options_json(sbuf_t *s, const char *names, const char *labels);
void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val);
char *settings_current_value(const setdef_t *d);
int api_settings_get_any(int sock, const char *key);
int api_settings_get_all(int sock);
int api_settings_put(int sock, const char *key, const req_t *r);
int api_settings_batch(int sock, const req_t *r);
int api_settings_restore(int sock);
void theme_json(sbuf_t *s);
int api_theme_get(int sock);
int api_theme_put(int sock, const req_t *r);
int api_perf(int sock, const char *path);
int perf_extract_network(const char *json, char *out, int cap);
int api_system(int sock);
int api_pin_unlock_once(int sock);
int api_system_action(int sock, const char *which, const req_t *r);
int api_key_single(int sock, int code, const req_t *r);
int api_key_batch(int sock, const req_t *r);
const char *log_group(const char *name);
int api_logs_list(int sock);
int api_logs_read(int sock, const req_t *r);
int api_logs_crash(int sock);
int notif_csv_field(const char **pp, char *out, int outcap);
int notif_parse_line(const char *line, notif_rec_t *r);
int notif_load_list(notif_rec_t *recs, int maxrec);
const char *notif_app_name(const char *app_id);
void notif_emit_item(sbuf_t *out, const notif_rec_t *r);
int notif_ipc_query(notif_rec_t *recs, int maxrec);
int notif_ipc_dismiss(unsigned id);
int notif_ipc_dismiss_all(int *out_count);
int api_notifications(int sock);
int notif_rewrite(int mode, unsigned del_id);
int api_notification_delete(int sock, unsigned id);
int api_notification_dismiss_all(int sock);
int api_files_list(int sock, const req_t *r);
int api_files_read(int sock, const req_t *r);
int api_files_download(int sock, const req_t *r);
int api_files_upload(int sock, const req_t *r);
int rm_recursive(const char *path);
int api_files_mkdir(int sock, const req_t *r);
int api_files_rename(int sock, const req_t *r);
int api_files_delete(int sock, const req_t *r);
void fonts_root(char *buf, size_t cap);
int safe_dir_segment(const char *s, char *out, size_t cap);
int unsafe_path(const char *s);
int has_dotdot_comp(const char *p);
int sanitize_member(const char *in, char *out, int n);
int mkdir_p(const char *path);
const jv_t *font_json_str(const jv_t *root, const char *key);
int api_fonts_list(int sock);
int sha256_file_hex(const char *path, char out[65]);
int api_fonts_import(int sock, const req_t *r);
int api_fonts_export(int sock, const req_t *r);
int run_cmd(const char *cmd, char *out, int outcap);
int api_apps_list(int sock);
int api_apps_info(int sock, const char *pkg);
int multipart_extract_file(const req_t *r, const char *outpath);
const char *nyai_bin(void);
int api_apps_install(int sock, const req_t *r);
int api_apps_export(int sock, const char *pkg);
int api_apps_remove(int sock, const char *pkg, const req_t *r);
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h);
int fb_dims(const char *dev, int *w, int *h);
void screen_prev_store(const uint8_t *frame, int w, int h);
int screen_diff_blocks(const uint8_t *cur, const uint8_t *prev, int w, int h, int *bx, int *by, int *bw, int *bh, int *total_blocks);
int api_screen_jpeg(int sock, const req_t *r);
int64_t now_mono_ms(void);
int64_t touch_last_local_ms(const char *dev);
int inject_tap(const char *dev, int x, int y);
int api_input_tap(int sock, const req_t *r);
const char *web_root(void);
const char *mime_for(const char *path);
int serve_static(int sock, const char *path);
int handle(int sock, req_t *r);
int read_port_config(void);
void handle_conn(int sock);
int panel_selftest(void);
int main(int argc, char **argv);
#endif /* MP_H */
+196
View File
@@ -0,0 +1,196 @@
/* mp_ams.c — App 运行表域:am list 解析 + amd 历史 + start/kill 控制。 */
#include "mp.h"
const char *ams_bin(void)
{
const char *e = getenv("MEOW_AMS_BIN");
if (e && e[0]) return e;
if (!access(AMS_BIN, X_OK)) return AMS_BIN;
return AMS_BIN_CARD;
}
void ams_fill(sbuf_t *s)
{
char out[16384];
char cmd[300];
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
int code = run_cmd(cmd, out, sizeof out);
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
panel_strip_ansi(out);
char *toks[256];
sbuf_t apps = {0};
int first = 1, count = 0;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
if (strncmp(ln, "APP ", 4) == 0) {
int n = txt_tokens(ln, toks, 256);
if (n >= 9) {
/* 布局:APP <pkg> <name...> <state> <pid> <launches> <crashes> <last_kind> <last_code>
* tok[0]="APP" 前缀;自右向左固定:state=tok[n-6], pid=n-5, launches=n-4, crashes=n-3,
* last_kind=n-2, last_code=n-1;pkg=tok[1],name=join(tok[2..n-6))。 */
char nm[128];
join_tokens(toks, 2, n - 6, nm, sizeof nm);
if (!first) sA(&apps, ",");
first = 0; count++;
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
sA(&apps, ",\"state\":"); sA_str(&apps, toks[n-6]);
sA(&apps, ",\"pid\":%d", atoi(toks[n-5]));
sA(&apps, ",\"launches\":%d", atoi(toks[n-4]));
sA(&apps, ",\"crashes\":%d", atoi(toks[n-3]));
sA(&apps, ",\"last_kind\":"); sA_str(&apps, toks[n-2]);
sA(&apps, ",\"last_code\":%d}", atoi(toks[n-1]));
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : "");
sbuf_free(&apps);
ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */
sA(s, "}");
}
void ams_fg_fill(sbuf_t *s)
{
char out[16384];
char cmd[300];
snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin());
int code = run_cmd(cmd, out, sizeof out);
if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; }
panel_strip_ansi(out);
char *toks[256];
char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0;
sbuf_t run = {0}; int first = 1;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
if (strncmp(ln, "RUN ", 4) == 0) {
int n = txt_tokens(ln, toks, 256);
if (n >= 6) { /* RUN pkg state pid dur activity... */
char act[160];
join_tokens(toks, 5, n, act, sizeof act);
const char *st = toks[2];
if (!first) sA(&run, ",");
first = 0;
sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]);
sA(&run, ",\"state\":"); sA_str(&run, st);
sA(&run, ",\"pid\":%d", atoi(toks[3]));
sA(&run, ",\"activity\":"); sA_str(&run, act);
sA(&run, "}");
if (strcmp(st, "fg") == 0 && !fg_pkg[0]) {
snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]);
snprintf(fg_act, sizeof fg_act, "%s", act);
fg_pid = atoi(toks[3]);
}
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, ",\"fg\":");
if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); }
else sA(s, "null");
sA(s, ",\"running\":[%s]", run.b ? run.b : "");
sbuf_free(&run);
}
int api_ams(int sock)
{
sbuf_t s = {0};
ams_fill(&s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
int ams_pkg_ok(const char *pkg)
{
if (!pkg || !pkg[0]) return 0;
if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0;
for (const char *p = pkg; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0;
if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */
}
return 1;
}
void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */
{
if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */
panel_strip_ansi(out);
char *toks[256];
sbuf_t ev = {0};
int first = 1, count = 0;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
if (strncmp(ln, "EVT ", 4) == 0) {
int n = txt_tokens(ln, toks, 256);
if (n >= 5) { /* EVT epoch up kind pkg detail... */
char det[256];
join_tokens(toks, 5, n, det, sizeof det);
if (!first) sA(&ev, ",");
first = 0; count++;
sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2]));
sA_str(&ev, toks[3]);
sA(&ev, ",\"detail\":"); sA_str(&ev, det);
sA(&ev, "}");
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg);
sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : "");
sbuf_free(&ev);
}
void ams_history_fill(sbuf_t *s, const char *pkg)
{
char out[16384];
char cmd[320];
snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg);
int code = run_cmd(cmd, out, sizeof out);
if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; }
ams_history_parse(out, s, pkg);
}
int api_ams_history(int sock, const char *pkg)
{
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
sbuf_t s = {0};
ams_history_fill(&s, pkg);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
int api_ams_ctl(int sock, const char *pkg, int kill)
{
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
char out[256];
char cmd[320];
snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg);
int code = run_cmd(cmd, out, sizeof out);
if (code == 0 && strstr(out, "OK ")) {
sbuf_t s = {0};
sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start");
sA_str(&s, pkg);
sA(&s, ",\"message\":"); sA_str(&s, out);
sA(&s, "}");
int rc2 = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc2;
}
/* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */
const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败");
if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail);
if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail);
if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail);
if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达");
return send_err(sock, 500, "INTERNAL", detail);
}
+242
View File
@@ -0,0 +1,242 @@
/* mp_apps.c — 应用域:MeowAPPs 扫描 + nyai 安装/导出/移除。 */
#include "mp.h"
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;
}
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;
}
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;
}
const char *nyai_bin(void)
{
/* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */
if (!access(NYAI_PATH, X_OK)) return NYAI_PATH;
return NYAI_PATH_CARD;
}
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;
}
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);
}
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;
}
+232
View File
@@ -0,0 +1,232 @@
/* mp_audio.c — 音频域:meow_audiod IPC(状态快照 + 写命令 route/volume)。 */
#include "mp.h"
const char *audio_kv(const char *txt, const char *key, char *out, size_t cap)
{
size_t kl = strlen(key);
const char *p = txt;
while (p && *p) {
if (!strncmp(p, key, kl) && p[kl] == '=') {
p += kl + 1;
size_t n = strcspn(p, " \t\n");
if (n >= cap) n = cap - 1;
memcpy(out, p, n);
out[n] = 0;
return out;
}
p = strchr(p, ' ');
if (p) p++;
}
out[0] = 0;
return NULL;
}
long long audio_kv_ll(const char *txt, const char *key, long long defv)
{
char b[64];
if (!audio_kv(txt, key, b, sizeof b) || !b[0]) return defv;
return strtoll(b, NULL, 10);
}
int audio_vol_max(void)
{
int v = settings_get_int("audio.volume_max", PANEL_VOL_MAX_DEFAULT);
if (v < 0) v = 0;
if (v > 100) v = 100;
return v;
}
void audio_fill(sbuf_t *s)
{
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
if (fd < 0) { sA(s, "{\"available\":false}"); return; }
uint8_t buf[MEOW_IPC_MAX];
uint16_t type = 0; uint32_t rid = 0;
int n = -1;
if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0)
n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1);
close(fd);
if (n <= 0 || type != MSG_AUDIO_STATE) { sA(s, "{\"available\":false}"); return; }
buf[n] = 0;
const char *txt = (const char *)buf;
char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16], target[16], volctl[16], codec[32];
audio_kv(txt, "app", app, sizeof app);
audio_kv(txt, "fmt", fmt, sizeof fmt);
audio_kv(txt, "route_mode", rmode, sizeof rmode);
/* 输出目标/音量控制/蓝牙编解码器(M6.32 面板透传:audiod 已发 target/volctl/codec,
* 前端蓝牙卡据此点亮「输出目标 DAC|蓝牙 / 音量控制 avrcp|dac / 编解码器」)。缺失时前端优雅降级「—」。 */
audio_kv(txt, "target", target, sizeof target);
audio_kv(txt, "volctl", volctl, sizeof volctl);
audio_kv(txt, "codec", codec, sizeof codec);
sA(s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0));
sA_str(s, app);
sA(s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":",
audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0));
sA_str(s, fmt);
sA(s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":",
audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0),
audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0));
sA_str(s, rmode);
sA(s, ",\"port\":%lld,\"vol\":%lld",
audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1));
sA(s, ",\"target\":");
sA_str(s, target);
sA(s, ",\"volctl\":");
sA_str(s, volctl);
sA(s, ",\"codec\":");
sA_str(s, codec);
/* 电平监控(261):rms/peak deci-dB(-990=-inf),lvlseq=快照代际;透传时别漏(ABI 追加键)。 */
sA(s, ",\"rms\":%lld,\"peak\":%lld,\"lvlseq\":%lld,\"vol_max\":%d}",
audio_kv_ll(txt, "rms", -990), audio_kv_ll(txt, "peak", -990), audio_kv_ll(txt, "lvlseq", 0), audio_vol_max());
}
int api_audio(int sock)
{
sbuf_t s = {0};
audio_fill(&s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
int audio_write_cmd(uint16_t type, const void *pl, uint32_t len, m_ack_t *out)
{
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
if (fd < 0) return -1;
uint8_t buf[64];
uint16_t rtype = 0; uint32_t rid = 0;
int n = -1;
if (send_frame(fd, type, 0x22, pl, len) == 0)
n = recv_frame(fd, &rtype, &rid, buf, sizeof buf);
close(fd);
if (n < (int)sizeof(m_ack_t) || rtype != MSG_ACK) return -1;
if (out) memcpy(out, buf, sizeof *out);
return 0;
}
int api_audio_route(int sock, const char *body, size_t body_len)
{
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(body, (int)body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jp = jv_obj_get(root, "port");
if (!jp || jp->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 port 数字"); }
long port = (long)jp->inum;
jv_free(root);
if (port != 0 && port != 2 && port != 3)
return send_err(sock, 422, "UNSUPPORTED", "非法路由口(仅允许 0=auto/2=3.5/3=4.4;4=SPDIF、5=audiod 内部值禁止)");
m_audio_route_t a; a.port = (uint32_t)port;
m_ack_t st;
if (audio_write_cmd(MSG_AUDIO_ROUTE, &a, sizeof a, &st) < 0)
return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)");
sbuf_t s = {0};
sA(&s, "{\"accepted\":true,\"port\":%ld}", port);
int out = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return out;
}
int api_audio_volume(int sock, const char *body, size_t body_len)
{
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(body, (int)body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jv = jv_obj_get(root, "value");
if (!jv || jv->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 value 数字"); }
long v = (long)jv->inum;
jv_free(root);
int vmax = audio_vol_max();
if (v < 0) v = 0;
if (v > vmax) v = vmax; /* 关键 clamp 红线 */
m_audio_volume_t a; a.pct = (int32_t)v;
m_ack_t st;
if (audio_write_cmd(MSG_AUDIO_VOLUME, &a, sizeof a, &st) < 0)
return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)");
sbuf_t s = {0};
sA(&s, "{\"accepted\":true,\"value\":%ld,\"applied\":%u,\"vol_max\":%d}", v, st.status, vmax);
int out = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return out;
}
/* ---- /api/audio/monitor(A3 面板音频页增强):读 audiod「全面监控日志」最新
* audio_monitor_<bootts>.log 尾部 N 行(默认 200,?lines=N 覆盖)。监控日志由 audiod 主线程 1s
* 节拍写(流活跃才写):时间戳+app+参数+rms/peak dBFS+帧计数+underrun/stall/dropped/txdrop+
* 路由口+FPGA+音量%+hwrate+dsd 标记(见 meow_audiod.c mon_tick)。面板只读展示,写口一律不开放
* (A3 定案);audiod 不写/无卡/无文件 → {"available":false}(HTTP 200,不报错)。
* 复用 mp_logs 的「找最新 <前缀> 文件 + 尾读」模式,分页=尾部 N 行(前端滚动翻页)。 ---- */
#define AUD_MON_TAIL_MAX (256 * 1024) /* 监控日志单次读取上限(内存约束,不整读大文件) */
int api_audio_monitor(int sock, const char *query)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
int maxlines = 200;
char ql[16];
if (query && query[0] && qparam(query, "lines", ql, sizeof ql)) {
int n = atoi(ql);
if (n > 0) maxlines = n;
if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES;
}
DIR *d = opendir(LOGS_DIR);
if (!d) return send_json(sock, 200, "{\"available\":false}");
char best[512] = ""; long best_mt = -1;
struct dirent *e;
while ((e = readdir(d))) {
if (strncmp(e->d_name, "audio_monitor_", 14) != 0) continue;
if (e->d_name[0] == '.') 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 (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
}
closedir(d);
if (!best[0]) return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
char full[1024];
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
int fd = open(full, O_RDONLY);
struct stat st;
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
if (fd >= 0) close(fd);
return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
}
off_t off = st.st_size > AUD_MON_TAIL_MAX ? st.st_size - AUD_MON_TAIL_MAX : 0;
size_t buflen = (size_t)(st.st_size - off);
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
if (off) (void)lseek(fd, off, SEEK_SET);
char *buf = malloc(buflen + 1);
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
ssize_t n = read(fd, buf, buflen);
close(fd);
if (n <= 0) { free(buf); return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
sbuf_t s = {0};
sA(&s, "{\"available\":true,\"file\":");
sA_str(&s, best);
sA(&s, ",\"lines\":[");
const char *p = buf, *end = buf + n;
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;
int truncated = (off > 0 || startrow > 0) ? 1 : 0;
int rowidx = 0, gotlines = 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, ",");
sA_str_n(&s, p, (size_t)(line_end - p)); /* 转义 + UTF-8 消毒 */
gotlines++;
}
rowidx++;
if (!nl) break;
p = nl + 1;
}
free(buf);
sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
+426
View File
@@ -0,0 +1,426 @@
/* mp_auth.c — 访问级 PIN 鉴权(panel.pin)+ 内联 SHA-256 + 内存会话。 */
#include "mp.h"
time_t g_auth_ttl = AUTH_TOKEN_TTL;
#define PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
#define PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
static const uint32_t mp_sha256_k[64] = {
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
};
static int g_pin_fails = 0;
static time_t g_pin_locked_until = 0;
static auth_sess_t g_sess[AUTH_MAX_SESS];
static int g_sess_n = 0;
#define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
#define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
#define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22))
#define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25))
#define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3))
#define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10))
void mp_sha256_init(mp_sha256_ctx *c)
{
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
c->count = 0; c->buflen = 0;
}
void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p)
{
uint32_t w[64];
int i;
for (i = 0; i < 16; i++)
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
for (i = 16; i < 64; i++)
w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16];
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
for (i = 0; i < 64; i++) {
uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i];
uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc);
h = g; g = f; f = e; e = d + t1;
d = cc; cc = b; b = a; a = t1 + t2;
}
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
}
void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len)
{
c->count += (uint64_t)len;
if (c->buflen) {
size_t need = 64 - c->buflen;
size_t take = len < need ? len : need;
memcpy(c->buf + c->buflen, data, take);
c->buflen += take; data += take; len -= take;
if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; }
}
while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; }
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
}
void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32])
{
uint64_t bitlen = c->count * 8;
uint8_t *b = c->buf;
size_t bl = c->buflen;
int i;
b[bl++] = 0x80;
if (bl > 56) {
while (bl < 64) b[bl++] = 0;
mp_sha256_block(c, b);
bl = 0;
}
while (bl < 56) b[bl++] = 0;
for (i = 0; i < 8; i++)
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
mp_sha256_block(c, b);
for (i = 0; i < 8; i++) {
out[i*4] = (uint8_t)(c->state[i] >> 24);
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
out[i*4+3] = (uint8_t)(c->state[i]);
}
}
int auth_read_random(unsigned char *buf, size_t n)
{
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
size_t got = 0;
while (got < n) {
ssize_t rd = read(fd, buf + got, n - got);
if (rd <= 0) { close(fd); return -1; }
got += (size_t)rd;
}
close(fd);
return 0;
}
void auth_hex(const unsigned char *raw, size_t n, char *out)
{
static const char hex[] = "0123456789abcdef";
for (size_t i = 0; i < n; i++) {
out[i*2] = hex[raw[i] >> 4];
out[i*2+1] = hex[raw[i] & 0xf];
}
out[n*2] = 0;
}
int auth_token_gen(char *out, int outcap) /* 16B → 32 hex(128bit) */
{
unsigned char raw[AUTH_TOKEN_BYTES];
if (outcap < AUTH_TOKEN_BYTES * 2 + 1) return -1;
if (auth_read_random(raw, sizeof raw) < 0) return -1;
auth_hex(raw, sizeof raw, out);
return 0;
}
int auth_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
{
unsigned char raw[16];
if (outcap < 33) return -1;
if (auth_read_random(raw, sizeof raw) < 0) return -1;
auth_hex(raw, sizeof raw, out);
return 0;
}
int auth_pass_hash(const char *salt, const char *password, char *out, int outcap)
{
mp_sha256_ctx c;
uint8_t digest[32];
if (outcap < AUTH_HASH_LEN + 1) return -1;
mp_sha256_init(&c);
mp_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
mp_sha256_update(&c, (const uint8_t *)password, strlen(password));
mp_sha256_final(&c, digest);
auth_hex(digest, sizeof digest, out);
return 0;
}
int auth_make_hash(const char *password, char *out, int outcap) /* salt:hex */
{
char salt[33], h[AUTH_HASH_LEN + 1];
if (auth_make_salt(salt, sizeof salt) < 0) return -1;
if (auth_pass_hash(salt, password, h, sizeof h) < 0) return -1;
snprintf(out, outcap, "%s:%s", salt, h);
return 0;
}
int hash_matches(const char *stored, const char *password)
{
if (!stored || !stored[0] || !password) return 0;
char tmp[160];
snprintf(tmp, sizeof tmp, "%s", stored);
char *colon = strchr(tmp, ':');
if (!colon) return 0;
*colon = 0;
char want[AUTH_HASH_LEN + 1];
if (auth_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
if (strlen(colon + 1) != AUTH_HASH_LEN) return 0;
return memcmp(want, colon + 1, AUTH_HASH_LEN) == 0;
}
int pin_set(void) /* panel.pin 是否已设(访问屏障开启) */
{
char *v = settings_get_value("panel.pin");
int s = (v && v[0]) ? 1 : 0;
free(v);
return s;
}
int auth_pass_set(void) /* 旧密码是否已设(兼容迁移用) */
{
char *v = settings_get_value("panel.auth_pass");
int s = (v && v[0]) ? 1 : 0;
free(v);
return s;
}
int auth_verify(const char *password) /* 旧密码校验(迁移期;见 login) */
{
char *stored = settings_get_value("panel.auth_pass");
int ok = hash_matches(stored, password);
free(stored);
return ok;
}
int pin_verify(const char *pin) /* PIN 校验 */
{
char *stored = settings_get_value("panel.pin");
int ok = hash_matches(stored, pin);
free(stored);
return ok;
}
int pin_validate(const char *pin)
{
if (!pin) return 0;
size_t l = strlen(pin);
if (l != 4) return 0;
for (size_t i = 0; i < l; i++)
if (pin[i] < '0' || pin[i] > '9') return 0;
return 1;
}
long long pin_lock_remaining(void) /* 剩余锁定秒;0=未锁 */
{
time_t now = time(NULL);
if (g_pin_locked_until <= now) return 0;
return (long long)(g_pin_locked_until - now);
}
void pin_lock_reset(void)
{
g_pin_fails = 0;
g_pin_locked_until = 0;
}
void pin_record_fail(void)
{
if (++g_pin_fails >= PIN_MAX_FAILS) {
g_pin_locked_until = time(NULL) + PIN_LOCK_SEC;
g_pin_fails = 0;
}
}
int sess_find(const char *token)
{
for (int i = 0; i < g_sess_n; i++)
if (!strcmp(g_sess[i].token, token)) return i;
return -1;
}
int sess_valid(const char *token)
{
int i = sess_find(token);
if (i < 0) return 0;
if (g_sess[i].expires > 0 && time(NULL) >= g_sess[i].expires) return 0;
return 1;
}
void sess_add(const char *token, time_t expires)
{
int i = sess_find(token);
if (i >= 0) { g_sess[i].expires = expires; return; }
if (g_sess_n >= AUTH_MAX_SESS) { /* 满:淘汰最早过期的一条 */
int e = 0;
for (int k = 1; k < g_sess_n; k++)
if (g_sess[k].expires < g_sess[e].expires) e = k;
if (e < g_sess_n - 1)
memmove(&g_sess[e], &g_sess[e + 1], (size_t)(g_sess_n - 1 - e) * sizeof(auth_sess_t));
g_sess_n--;
}
snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", token);
g_sess[g_sess_n].expires = expires;
g_sess_n++;
}
void sess_remove(const char *token)
{
int i = sess_find(token);
if (i < 0) return;
if (i < g_sess_n - 1)
memmove(&g_sess[i], &g_sess[i + 1], (size_t)(g_sess_n - 1 - i) * sizeof(auth_sess_t));
g_sess_n--;
}
void auth_cp(char *dst, int dcap, const char *src) /* 有界拷贝(防 -Wformat-truncation) */
{
size_t l = strlen(src);
if (l >= (size_t)dcap) l = (size_t)dcap - 1;
memcpy(dst, src, l);
dst[l] = 0;
}
int extract_token(const req_t *r, char *out, int outcap)
{
out[0] = 0;
if (r->x_meow_token[0]) { auth_cp(out, outcap, r->x_meow_token); return out[0] ? 1 : 0; }
if (!strncasecmp(r->authorization, "Bearer ", 7)) {
auth_cp(out, outcap, r->authorization + 7);
return out[0] ? 1 : 0;
}
if (r->cookie[0]) {
const char *p = strstr(r->cookie, "meow_token=");
if (p) {
p += 11;
const char *e = p;
while (*e && *e != ';' && *e != ' ' && *e != '\t') e++;
size_t l = (size_t)(e - p);
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
memcpy(out, p, l);
out[l] = 0;
return out[0] ? 1 : 0;
}
}
return 0;
}
int require_auth(int sock, const req_t *r)
{
const char *p = r->path;
if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") ||
!strcmp(p, "/api/auth/status") || !strcmp(p, "/api/auth/setup")) return 1;
char t[65];
if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1;
return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效");
}
int api_auth_status(int sock, const req_t *r)
{
int ps = pin_set();
int lg = auth_pass_set();
int sess = 0;
char t[65];
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
long long rem = pin_lock_remaining();
sbuf_t s = {0};
sA(&s, "{\"auth_enabled\":true,\"session\":%s,\"pin_set\":%s,\"legacy\":%s,\"locked\":%s",
sess ? "true" : "false", ps ? "true" : "false", lg ? "true" : "false", rem > 0 ? "true" : "false");
if (rem > 0) sA(&s, ",\"lock_remaining_ms\":%lld", rem * 1000LL);
sA(&s, "}");
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
int api_auth_login(int sock, const req_t *r)
{
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jpin = jv_obj_get(root, "pin");
jv_t *jpw = jv_obj_get(root, "password");
char pin[8], pw[256];
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0;
if (jpw && jpw->type == J_STR && jpw->str) snprintf(pw, sizeof pw, "%s", jpw->str); else pw[0] = 0;
jv_free(root);
if (pin_lock_remaining() > 0) { /* 已在锁定期 */
sbuf_t e = {0};
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"已锁定,请稍后重试\"},\"lock_remaining_ms\":%lld}",
pin_lock_remaining() * 1000LL);
int rc = send_json(sock, 429, e.b ? e.b : "");
sbuf_free(&e);
return rc;
}
int use_legacy = 0;
const char *cred = NULL;
if (pin_set()) { cred = pin; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 pin 字段"); }
else if (auth_pass_set()) { use_legacy = 1; cred = pw; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); }
else return send_err(sock, 400, "BAD_REQUEST", "请先设置面板 PIN(/api/auth/setup)");
int ok = use_legacy ? auth_verify(cred) : pin_verify(cred);
if (!ok) {
pin_record_fail();
if (pin_lock_remaining() > 0) { /* 本错已触发锁定 */
sbuf_t e = {0};
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"连续错误次数过多,已锁定 5 分钟\"},\"lock_remaining_ms\":%lld}",
pin_lock_remaining() * 1000LL);
int rc = send_json(sock, 429, e.b ? e.b : "");
sbuf_free(&e);
return rc;
}
return send_err(sock, 401, "UNAUTHORIZED", "PIN 错误");
}
pin_lock_reset();
char token[65];
if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败");
time_t exp = time(NULL) + g_auth_ttl; /* 固定 3 天(MEOW_AUTH_TTL 可覆盖测试用),不滑动续期 */
sess_add(token, exp);
sbuf_t s = {0};
sA(&s, "{\"token\":");
sA_str(&s, token);
sA(&s, ",\"expires_at\":%lld,\"auth_enabled\":true}", (long long)exp);
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
int api_auth_setup(int sock, const req_t *r)
{
if (pin_set()) return send_err(sock, 409, "CONFLICT", "面板 PIN 已设置,请直接登录");
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
jv_t *root = jv_parse(r->body, (int)r->body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jpin = jv_obj_get(root, "pin");
char pin[8];
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str);
else pin[0] = 0;
jv_free(root);
if (!pin_validate(pin)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
char h[200];
if (auth_make_hash(pin, h, sizeof h) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
if (settings_write_one("panel.pin", h) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
pin_lock_reset();
return send_json(sock, 200, "{\"pin_set\":true}");
}
int api_auth_logout(int sock, const req_t *r)
{
char t[65];
if (extract_token(r, t, sizeof t) && t[0]) sess_remove(t);
return send_json(sock, 200, "{\"logged_out\":true}");
}
#undef MP_ROR
#undef MP_CH
#undef MP_MAJ
#undef MP_BSIG0
#undef MP_BSIG1
#undef MP_SSIG0
#undef MP_SSIG1
+619
View File
@@ -0,0 +1,619 @@
/* mp_files.c — 文件域:路径安全/白名单 + /api/files* + multipart 上传解析 + 卡根派生路径。 */
#include "mp.h"
char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
char g_tf_card_buf[2048], g_apps_dir_buf[2048], g_logs_dir_buf[2048], g_notif_log_buf[2048];
const char *g_tf_card = NULL, *g_apps_dir = NULL, *g_logs_dir = NULL, *g_notif_log = NULL;
const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
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]))
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;
}
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;
}
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;
}
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;
}
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;
}
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);
}
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);
}
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);
}
int nat_strcmp(const char *a, const char *b)
{
const unsigned char *pa = (const unsigned char *)a;
const unsigned char *pb = (const unsigned char *)b;
while (*pa && *pb) {
unsigned char ca = *pa, cb = *pb;
if (ca >= '0' && ca <= '9' && cb >= '0' && cb <= '9') {
const unsigned char *na = pa, *nb = pb;
while (*na == '0') na++;
while (*nb == '0') nb++;
const unsigned char *da = na, *db = nb;
while (*da >= '0' && *da <= '9') da++;
while (*db >= '0' && *db <= '9') db++;
int la = (int)(da - na), lb = (int)(db - nb); /* 有效数字位数(去前导0) */
if (la != lb) return la - lb; /* 位数小者数小 */
int c = memcmp(na, nb, (size_t)la);
if (c) return c;
int ta = (int)(da - pa), tb = (int)(db - pb); /* 原始数字串长(含前导0) */
if (ta != tb) return ta - tb;
pa = da; pb = db;
continue; /* 数字段完全相同,继续 */
}
unsigned char la = (ca >= 'A' && ca <= 'Z') ? (unsigned char)(ca + 32) : ca;
unsigned char lbc = (cb >= 'A' && cb <= 'Z') ? (unsigned char)(cb + 32) : cb;
if (la != lbc) return (int)la - (int)lbc;
if (ca != cb) return (int)ca - (int)cb; /* 大小写 tie-break:大写在前 */
pa++; pb++;
}
return (int)*pa - (int)*pb;
}
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;
return nat_strcmp(fa->name, fb->name);
}
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 : "");
}
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;
}
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;
}
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 = mem_find(p, (size_t)(end - 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 = mem_find(data_start, (size_t)(end - 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 边界缺失/解析失败 */
}
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);
}
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;
}
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);
}
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;
}
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);
}
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;
}
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;
}
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}");
}
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 = mem_find(ds, (size_t)((r->body + r->body_len) - 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;
}
+374
View File
@@ -0,0 +1,374 @@
/* mp_fonts.c — 字体库域:tar.gz 导入/导出 + 列表(与 .mpk 同构,走 busybox gzip|tar 管道)。 */
#include "mp.h"
#define FONT_EXTRACT_MAX (32u * 1024u * 1024u) /* 三件套解压后总大小上限(防 gzip 炸弹) */
void fonts_root(char *buf, size_t cap) { snprintf(buf, cap, "%s/MeowOS/fonts", TF_ROOT); }
int safe_dir_segment(const char *s, char *out, size_t cap)
{
if (!s || !s[0]) return -1;
if (!strcmp(s, ".") || !strcmp(s, "..")) return -1;
for (const char *p = s; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1;
}
if (strlen(s) + 1 > cap) return -1;
snprintf(out, cap, "%s", s);
return 0;
}
int unsafe_path(const char *s)
{
return s && strpbrk(s, " ;`$'\"&|<>") != NULL;
}
int has_dotdot_comp(const char *p)
{
const char *s = p;
while (*s) {
if (s[0] == '.' && s[1] == '.' &&
(s == p || s[-1] == '/') && (s[2] == 0 || s[2] == '/'))
return 1;
s++;
}
return 0;
}
int sanitize_member(const char *in, char *out, int n)
{
if (!in || !*in) return -1;
const char *p = in;
while (*p == '/' || (p[0] == '.' && p[1] == '/')) p++;
if (*p == '/' || !*p) return -1;
if (has_dotdot_comp(p)) return -1;
if (unsafe_path(p)) return -1;
size_t l = strlen(p);
if (l + 1 > (size_t)n) return -1;
memcpy(out, p, l + 1); /* memcpy 而非 snprintf:p 可能来自长行缓冲,防 -Wformat-truncation 误报 */
return 0;
}
int mkdir_p(const char *path)
{
char buf[1600];
snprintf(buf, sizeof buf, "%s", path);
for (char *p = buf + 1; *p; p++) {
if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; }
}
return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1;
}
const jv_t *font_json_str(const jv_t *root, const char *key)
{
const jv_t *v = jv_obj_get(root, key);
if (v && v->type == J_STR && v->str) return v;
return NULL;
}
int api_fonts_list(int sock)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char root[1100];
fonts_root(root, sizeof root);
DIR *fd = opendir(root);
if (!fd) return send_json(sock, 200, "{\"fonts\":[]}");
sbuf_t s = {0};
sA(&s, "{\"fonts\":[");
int first = 1;
struct dirent *fe;
while ((fe = readdir(fd))) {
if (fe->d_name[0] == '.') continue;
char famdir[1700];
snprintf(famdir, sizeof famdir, "%s/%s", root, fe->d_name);
struct stat fst;
if (stat(famdir, &fst) < 0 || !S_ISDIR(fst.st_mode)) continue;
DIR *wd = opendir(famdir);
if (!wd) continue;
struct dirent *we;
while ((we = readdir(wd))) {
if (we->d_name[0] == '.') continue;
char wdir[2200];
snprintf(wdir, sizeof wdir, "%s/%s", famdir, we->d_name);
struct stat wst;
if (stat(wdir, &wst) < 0 || !S_ISDIR(wst.st_mode)) continue;
char pp[2560], pi[2560], pj[2560];
snprintf(pp, sizeof pp, "%s/font.png", wdir);
snprintf(pi, sizeof pi, "%s/font.idx", wdir);
snprintf(pj, sizeof pj, "%s/font.json", wdir);
if (access(pp, R_OK) || access(pi, R_OK) || access(pj, R_OK)) continue;
/* 解析 font.json(可选增强字段) */
char name[256] = "", tier[64] = "";
long long gc = 0;
char jbuf[4096];
if (read_small_file(pj, jbuf, sizeof jbuf) >= 0) {
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
if (jr) {
const jv_t *v = font_json_str(jr, "name");
if (v) snprintf(name, sizeof name, "%s", v->str);
v = font_json_str(jr, "tier");
if (v) snprintf(tier, sizeof tier, "%s", v->str);
else { v = font_json_str(jr, "charset"); if (v) snprintf(tier, sizeof tier, "%s", v->str); }
v = jv_obj_get(jr, "glyph_count");
if (v && v->type == J_NUM) gc = v->inum;
jv_free(jr);
}
}
if (!name[0]) snprintf(name, sizeof name, "%s", fe->d_name);
if (!first) sA(&s, ",");
sA(&s, "{\"family\":");
sA_str(&s, fe->d_name);
sA(&s, ",\"weight\":");
sA_str(&s, we->d_name);
sA(&s, ",\"name\":");
sA_str(&s, name);
sA(&s, ",\"tier\":");
sA_str(&s, tier);
sA(&s, ",\"glyph_count\":%lld,\"mtime\":%lld}", gc, (long long)wst.st_mtime);
first = 0;
}
closedir(wd);
}
closedir(fd);
sA(&s, "]}");
return send_json(sock, 200, s.b ? s.b : "{\"fonts\":[]}");
}
int sha256_file_hex(const char *path, char out[65])
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
mp_sha256_ctx c;
mp_sha256_init(&c);
uint8_t buf[8192];
ssize_t n;
while ((n = read(fd, buf, sizeof buf)) > 0) mp_sha256_update(&c, buf, (size_t)n);
close(fd);
if (n < 0) return -1;
uint8_t h[32];
mp_sha256_final(&c, h);
auth_hex(h, sizeof h, out);
out[64] = 0;
return 0;
}
int api_fonts_import(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char force[8] = "0";
qparam(r->query, "force", force, sizeof force);
long uni = (long)time(NULL);
char tmpdir[1500], tmpgz[1500], stg[1500];
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
snprintf(tmpgz, sizeof tmpgz, "%s/tmp/mp_font_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
snprintf(stg, sizeof stg, "%s/tmp/mp_font_stg_%d_%ld", TF_CARD_DIR, (int)getpid(), uni);
/* ① 写 multipart file 到临时 tar.gz */
if (multipart_extract_file(r, tmpgz) < 0)
return send_err(sock, 400, "BAD_REQUEST", "缺字体库 tar.gz(multipart 字段 file)");
/* ② 成员名预校验:gzip -dc | tar -t → 每名 sanitize,且恰为三件套平铺(拒绝对/.. / 多出条目) */
{
char cmd[3200];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null", tmpgz);
FILE *pf = popen(cmd, "r");
if (!pf) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "无法列 tar 成员"); }
int bad = 0, have_png = 0, have_idx = 0, have_json = 0, nent = 0;
char line[512];
while (fgets(line, sizeof line, pf)) {
char *e = strchr(line, '\n'); if (e) *e = 0;
if (!line[0]) continue;
char san[128];
if (sanitize_member(line, san, sizeof san) < 0) { bad = 1; break; }
if (!strcmp(san, "font.png")) have_png = 1;
else if (!strcmp(san, "font.idx")) have_idx = 1;
else if (!strcmp(san, "font.json")) have_json = 1;
else { bad = 1; break; }
nent++;
}
pclose(pf);
if (bad || nent != 3 || !have_png || !have_idx || !have_json) {
unlink(tmpgz);
return send_err(sock, 400, "BAD_REQUEST", "tar 缺三件套或含非法成员(拒绝绝对路径/../多出条目)");
}
}
/* ③ 解压到临时目录 */
rm_recursive(stg);
if (mkdir_p(stg) != 0) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "建临时目录失败"); }
{
char cmd[3200];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null", tmpgz, stg);
if (system(cmd) != 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 解压失败(非 gzip/tar?)"); }
}
/* ④ 目录锁:恰为 3 个普通文件(lstat 拒软链/目录/设备),总大小 ≤ FONT_EXTRACT_MAX(防炸弹) */
{
static const char *names[3] = { "font.png", "font.idx", "font.json" };
int seen[3] = {0,0,0}, cnt = 0, ok = 1;
DIR *d = opendir(stg);
if (!d) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "读临时目录失败"); }
struct dirent *de;
while ((de = readdir(d))) {
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue;
int idx = -1;
for (int i = 0; i < 3; i++) if (!strcmp(de->d_name, names[i])) { idx = i; break; }
if (idx < 0) { ok = 0; break; }
char fp[1800]; snprintf(fp, sizeof fp, "%s/%s", stg, de->d_name);
struct stat st;
if (lstat(fp, &st) != 0 || !S_ISREG(st.st_mode)) { ok = 0; break; } /* 拒软链/目录/设备 */
seen[idx] = 1; cnt++;
}
closedir(d);
long long tot = 0;
for (int i = 0; i < 3; i++) {
if (!seen[i]) { ok = 0; break; }
char fp[1600]; snprintf(fp, sizeof fp, "%s/%s", stg, names[i]);
struct stat st;
if (stat(fp, &st) != 0) { ok = 0; break; }
tot += (long long)st.st_size;
}
if (!ok || cnt != 3 || tot > (long long)FONT_EXTRACT_MAX) {
rm_recursive(stg); unlink(tmpgz);
return send_err(sock, 400, "BAD_REQUEST", "tar 内容非法(缺件/软链/多出条目/解压后超大)");
}
}
/* ⑤ 读 font.json → family/weight(可解析硬失败) */
char jpath[1600]; snprintf(jpath, sizeof jpath, "%s/font.json", stg);
char jbuf[8192];
if (read_small_file(jpath, jbuf, sizeof jbuf) < 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 缺失"); }
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
if (!jr) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 解析失败"); }
char family[128] = "font", weight[64] = "regular";
const jv_t *v = font_json_str(jr, "family");
if (v) snprintf(family, sizeof family, "%s", v->str);
else { v = font_json_str(jr, "name"); if (v) snprintf(family, sizeof family, "%s", v->str); }
v = font_json_str(jr, "weight");
if (v) snprintf(weight, sizeof weight, "%s", v->str);
char fam_s[128], wt_s[64];
if (safe_dir_segment(family, fam_s, sizeof fam_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json family 非法"); }
if (safe_dir_segment(weight, wt_s, sizeof wt_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json weight 非法"); }
snprintf(family, sizeof family, "%s", fam_s);
snprintf(weight, sizeof weight, "%s", wt_s);
/* ⑥ 目标目录 + 覆盖冲突(409,除非 force=1) */
char root[1100], target[1500];
fonts_root(root, sizeof root);
snprintf(target, sizeof target, "%s/%s/%s", root, family, weight);
if (strcmp(force, "1") != 0) {
static const char *names3[] = { "font.png", "font.idx", "font.json" };
char tp[1600]; int conflicted = 0;
for (int i = 0; i < 3; i++) { snprintf(tp, sizeof tp, "%s/%s", target, names3[i]); if (access(tp, F_OK) == 0) { conflicted = 1; break; } }
if (conflicted) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 409, "CONFLICT", "目标字库已存在,请确认覆盖"); }
}
/* ⑦ sha256 仅 WARN(对齐 font.json 约定,不硬失败) */
char warn[256] = "";
{
const jv_t *wp = font_json_str(jr, "sha256_png");
const jv_t *wi = font_json_str(jr, "sha256_idx");
char need[65], got[65];
if (wp && wp->str[0]) {
snprintf(need, sizeof need, "%s", wp->str);
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.png", stg);
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
snprintf(warn, sizeof warn, "sha256_png 不匹配(源库校验失败,但已导入)");
}
if (wi && wi->str[0] && !warn[0]) {
snprintf(need, sizeof need, "%s", wi->str);
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.idx", stg);
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
snprintf(warn, sizeof warn, "sha256_idx 不匹配(源库校验失败,但已导入)");
}
}
/* ⑧ 建目标目录 + rename 三件套(原子替换) */
{
char p1[1400];
snprintf(p1, sizeof p1, "%s/%s", root, family);
mkdir(p1, 0755);
mkdir(target, 0755);
}
{
static const char *names3[] = { "font.png", "font.idx", "font.json" };
int ok = 1;
for (int i = 0; i < 3; i++) {
char sp[1600], tp[1600];
snprintf(sp, sizeof sp, "%s/%s", stg, names3[i]);
snprintf(tp, sizeof tp, "%s/%s", target, names3[i]);
if (rename(sp, tp) < 0) ok = 0;
}
if (!ok) { rm_recursive(stg); unlink(tmpgz); jv_free(jr); return send_err(sock, 500, "INTERNAL", "写入字体文件失败(磁盘满?)"); }
}
rm_recursive(stg);
unlink(tmpgz);
sbuf_t s = {0};
sA(&s, "{\"family\":");
sA_str(&s, family);
sA(&s, ",\"weight\":");
sA_str(&s, weight);
sA(&s, ",\"imported\":true,\"hint\":\"字体已导入。请进设备「设置 → 字体」页,字体列表在进入时自动重扫生效。\",\"warning\":");
sA_str(&s, warn[0] ? warn : "");
sA(&s, "}");
jv_free(jr);
return send_json(sock, 200, s.b ? s.b : "");
}
int api_fonts_export(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char family[128], weight[64];
if (!qparam(r->query, "family", family, sizeof family)) family[0] = 0;
if (!qparam(r->query, "weight", weight, sizeof weight)) weight[0] = 0;
char f[128], w[64];
if (safe_dir_segment(family, f, sizeof f) < 0) return send_err(sock, 400, "BAD_REQUEST", "family 非法");
if (!weight[0]) snprintf(w, sizeof w, "%s", "regular");
else if (safe_dir_segment(weight, w, sizeof w) < 0) return send_err(sock, 400, "BAD_REQUEST", "weight 非法");
char root[1100], dir[1500];
fonts_root(root, sizeof root);
snprintf(dir, sizeof dir, "%s/%s/%s", root, f, w);
static const char *names3[] = { "font.png", "font.idx", "font.json" };
for (int i = 0; i < 3; i++) {
char ap[1600];
snprintf(ap, sizeof ap, "%s/%s", dir, names3[i]);
struct stat st;
if (stat(ap, &st) < 0) {
sbuf_t s = {0};
sA(&s, "font."); sA_str(&s, names3[i]);
sA(&s, " 缺失(字库 <");
sA_str(&s, f); sA(&s, "/"); sA_str(&s, w); sA(&s, "> 不完整)");
int rc = send_err(sock, 404, "NOT_FOUND", s.b ? s.b : "字体三件套缺失");
sbuf_free(&s);
return rc;
}
}
/* 打包:tar -cf - -C <dir> 三件套 | gzip -c > <tmp>,落临时文件求 Content-Length 再流式下发 */
long uni = (long)time(NULL);
char tmpdir[1500], tmp[1500];
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
snprintf(tmp, sizeof tmp, "%s/tmp/mp_font_exp_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
char cmd[3600];
snprintf(cmd, sizeof cmd, "tar -cf - -C '%s' font.png font.idx font.json 2>/dev/null | gzip -c > '%s' 2>/dev/null", dir, tmp);
if (system(cmd) != 0) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包失败"); }
struct stat st;
if (stat(tmp, &st) < 0 || !S_ISREG(st.st_mode)) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包文件不可读"); }
char fname[220];
snprintf(fname, sizeof fname, "%s-%s.font.tar.gz", f, w);
char extra[300];
snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", fname);
if (send_head(sock, 200, "application/gzip", (long long)st.st_size, extra) < 0) { unlink(tmp); return -1; }
int fd = open(tmp, O_RDONLY);
if (fd < 0) { unlink(tmp); 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); unlink(tmp); return -1; }
close(fd);
unlink(tmp);
return 0;
}
+211
View File
@@ -0,0 +1,211 @@
/* mp_ipc.c — IPC 帧读写 + meow_svc_perf/power/netmgr 客户端(单点 IPC 收发)。 */
#include "mp.h"
const char *g_sock_perf = PERF_SOCK;
const char *g_sock_power = POWER_SOCK;
const char *g_sock_launcher = LAUNCHER_SOCK;
const char *g_sock_audio = AUDIO_SOCK;
const char *g_sock_net = NET_SOCK;
const char *g_sock_pm = PM_SOCK;
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;
}
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;
}
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;
}
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;
}
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;
}
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;
}
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 "服务端错误";
}
}
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 */
}
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;
}
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 "电源服务错误";
}
}
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;
}
void svc_msg_kv(const char *msg, const char *key, char *out, size_t cap)
{
out[0] = 0;
if (!msg || !key) return;
size_t kl = strlen(key);
if (strncmp(msg, key, kl) == 0 && msg[kl] == '=' && msg[kl + 1])
snprintf(out, cap, "%s", msg + kl + 1);
}
+232
View File
@@ -0,0 +1,232 @@
/* mp_json.c — 最简 JSON 解析器/序列化器(jv_t 树;零第三方库)。
* 模块化拆分自 meowpanel.c(接口/行为不变)。 */
#include "mp.h"
int json_strict_balanced(const char *b, size_t n)
{
int depth = 0, instr = 0, esc = 0;
for (size_t i = 0; i < n; i++) {
char c = b[i];
if (instr) {
if (esc) esc = 0;
else if (c == '\\') esc = 1;
else if (c == '"') instr = 0;
continue;
}
if (c == '"') { instr = 1; continue; }
if (c == '{' || c == '[') depth++;
else if (c == '}' || c == ']') { if (--depth < 0) return 0; }
}
return depth == 0 && !instr;
}
jv_t *jv_alloc(int type)
{
jv_t *v = calloc(1, sizeof *v);
if (v) v->type = type;
return v;
}
void jv_free(jv_t *v)
{
if (!v) return;
jv_t *c = v->child;
while (c) {
jv_t *nx = c->next;
if (v->type == J_ARR) { jv_free(c); } /* 数组元素是完整值(可为对象/嵌套数组),须整体递归释放 */
else { jv_free(c->child); free(c->str); free(c); } /* 对象:pair 链 → 释放 value + key + pair 节点 */
c = nx;
}
free(v->str);
free(v);
}
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++;
}
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;
}
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;
}
}
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;
}
}
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;
}
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;
}
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;
}
void jv_to_json(sbuf_t *s, const jv_t *v)
{
if (!v) { sA(s, "null"); return; }
switch (v->type) {
case J_NULL: sA(s, "null"); break;
case J_BOOL: sA(s, v->bval ? "true" : "false"); break;
case J_NUM:
if (v->dnum == (double)v->inum) sA(s, "%lld", v->inum);
else sA(s, "%g", v->dnum);
break;
case J_STR: sA_str(s, v->str ? v->str : ""); break;
case J_ARR:
sA(s, "[");
{ int f = 1; for (const jv_t *e = v->child; e; e = e->next) { if (!f) sA(s, ","); f = 0; jv_to_json(s, e); } }
sA(s, "]");
break;
case J_OBJ:
sA(s, "{");
{ int f2 = 1;
for (const jv_t *p = v->child; p; p = p->next) {
if (!f2) { sA(s, ","); }
f2 = 0;
sA_str(s, p->str); sA(s, ":"); jv_to_json(s, p->child);
} }
sA(s, "}");
break;
}
}
int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
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; }
+126
View File
@@ -0,0 +1,126 @@
/* mp_key.c — 远程控制域:按键注入(写 /dev/input/event* EV_KEY,物理键红线内)。 */
#include "mp.h"
#define PWR_KEYDEV "/dev/input/event0"
#define ADC_KEYDEV "/dev/input/event2"
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;
}
}
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;
}
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;
}
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;
}
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;
}
+241
View File
@@ -0,0 +1,241 @@
/* mp_logs.c — 日志域:/api/logs 列表 + 读 + 崩溃汇总(meow_crash 落盘格式)。 */
#include "mp.h"
#define CRASH_TAIL_MAX (256u * 1024u) /* 崩溃日志单次读取上限 */
#define CRASH_ENT_MAX 64 /* 实体汇总上限 */
#define CRASH_DMESG_TAIL 5 /* dmesg 保留最近行数 */
const char *log_group(const char *name)
{
if (!strncmp(name, "crash_", 6)) return "crash";
if (!strncmp(name, "ams_", 4)) return "ams";
if (!strncmp(name, "audio_monitor_", 14)) return "audio_monitor";
if (!strncmp(name, "net_", 4)) return "net";
if (!strncmp(name, "power_", 6)) return "power";
if (!strncmp(name, "meow_", 5)) return "system";
return "app";
}
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,\"group\":", (long long)st.st_size, (long long)st.st_mtime);
sA_str(&s, log_group(e->d_name));
sA(&s, "}");
}
closedir(d);
sA(&s, "]}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
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, ",");
/* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */
sA_str_n(&s, p, (size_t)(line_end - p));
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;
}
int api_logs_crash(int sock)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
/* 找最新 crash_*.log(按 mtime);无 → 空汇总(不 404,不谎报)。 */
DIR *d = opendir(LOGS_DIR);
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}");
char best[512] = ""; long best_mt = -1;
struct dirent *e;
while ((e = readdir(d))) {
if (strncmp(e->d_name, "crash_", 6) != 0 || e->d_name[0] == '.') 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 (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
}
closedir(d);
if (!best[0])
return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}");
char full[1024];
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
int fd = open(full, O_RDONLY);
struct stat st;
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { if (fd >= 0) close(fd); return send_err(sock, 500, "INTERNAL", "崩溃日志不可读"); }
off_t off = st.st_size > (off_t)CRASH_TAIL_MAX ? st.st_size - (off_t)CRASH_TAIL_MAX : 0;
size_t buflen = (size_t)(st.st_size - off);
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}"); }
if (off) (void)lseek(fd, off, SEEK_SET);
char *buf = malloc(buflen + 1);
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
ssize_t n = read(fd, buf, buflen);
close(fd);
if (n < 0) { free(buf); return send_err(sock, 500, "INTERNAL", "读崩溃日志失败"); }
buf[n] = 0;
/* 每实体聚合(svc 行);dmesg 单独计数 + 留最近几行。
* 有界缓冲(det ≤ 127 字符)保证 snprintf 到 name/detail 不越界且 -Wformat-truncation 零告警。 */
struct { char name[128]; int count; long long last_epoch; char kind[8]; int code; char detail[160]; } ents[CRASH_ENT_MAX];
int entn = 0, svc_count = 0, dmesg_count = 0;
char dmesg_tail[CRASH_DMESG_TAIL][128]; int dmesg_tail_n = 0;
char *ln = buf;
while (*ln && (*ln == '\n')) ln++;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
long long epoch = 0, up = 0; char src[16] = "", det[128] = "";
int f = sscanf(ln, "%lld %lld %15s %127[^\n]", &epoch, &up, src, det);
if (f >= 3) {
if (!strcmp(src, "svc")) {
svc_count++;
/* det = `<name> exit=<code>` 或 `<name> signal=<sig>`;name 为第一 token(就地切分) */
char name[128] = "", kind[8] = ""; int code = 0, k2 = 0;
char *sp = strchr(det, ' ');
if (sp) {
*sp = 0;
snprintf(name, sizeof name, "%s", det); /* det ≤127 → name[128] 不截断 */
char *kp = sp + 1;
if (!strncmp(kp, "exit=", 5)) { snprintf(kind, sizeof kind, "exit"); code = atoi(kp + 5); k2 = 1; }
else if (!strncmp(kp, "signal=", 7)) { snprintf(kind, sizeof kind, "sig"); code = atoi(kp + 7); k2 = 1; }
else { snprintf(kind, sizeof kind, "crash"); k2 = 0; }
} else {
snprintf(name, sizeof name, "%s", det);
snprintf(kind, sizeof kind, "crash");
}
/* 聚合到 ents(按 name 匹配;超限则丢弃剩余陌生实体,仅计入总数) */
int found = -1;
for (int i = 0; i < entn; i++) if (!strcmp(ents[i].name, name)) { found = i; break; }
if (found < 0 && entn < CRASH_ENT_MAX) {
found = entn++;
snprintf(ents[found].name, sizeof ents[found].name, "%s", name);
ents[found].count = 0; ents[found].last_epoch = 0;
ents[found].kind[0] = 0; ents[found].code = 0; ents[found].detail[0] = 0;
}
if (found >= 0) {
ents[found].count++;
if (epoch >= ents[found].last_epoch) {
ents[found].last_epoch = epoch;
snprintf(ents[found].kind, sizeof ents[found].kind, "%s", k2 ? kind : "crash");
ents[found].code = code;
snprintf(ents[found].detail, sizeof ents[found].detail, "%s", det);
}
}
} else if (!strcmp(src, "dmesg")) {
dmesg_count++;
if (dmesg_tail_n < CRASH_DMESG_TAIL) snprintf(dmesg_tail[dmesg_tail_n++], sizeof dmesg_tail[0], "%s", det);
else { /* 前移一行,末尾放最新 */ for (int i = 0; i < dmesg_tail_n - 1; i++) memcpy(dmesg_tail[i], dmesg_tail[i + 1], sizeof dmesg_tail[0]); snprintf(dmesg_tail[dmesg_tail_n - 1], sizeof dmesg_tail[0], "%s", det); }
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
while (*ln == '\n') ln++;
}
free(buf);
sbuf_t s = {0};
sA(&s, "{\"file\":");
sA_str(&s, best);
sA(&s, ",\"count\":%d,\"dmesg_count\":%d,\"dmesg_tail\":[", svc_count, dmesg_count);
for (int i = 0; i < dmesg_tail_n; i++) { if (i) sA(&s, ","); sA_str(&s, dmesg_tail[i]); }
sA(&s, "],\"entities\":[");
int first = 1;
for (int i = 0; i < entn; i++) {
if (!ents[i].count) continue;
if (!first) sA(&s, ",");
first = 0;
sA(&s, "{\"name\":");
sA_str(&s, ents[i].name);
sA(&s, ",\"source\":\"svc\",\"count\":%d,\"last_epoch\":%lld,\"last_kind\":", ents[i].count, ents[i].last_epoch);
sA_str(&s, ents[i].kind[0] ? ents[i].kind : "crash");
sA(&s, ",\"last_code\":%d,\"last_detail\":", ents[i].code);
sA_str(&s, ents[i].detail);
/* 最近崩溃时间(epoch → 可读);strftime 本地时区,失败回退 epoch */
char tbuf[32] = ""; time_t t = (time_t)ents[i].last_epoch;
struct tm tmv;
if (ents[i].last_epoch > 0 && localtime_r(&t, &tmv)) strftime(tbuf, sizeof tbuf, "%Y-%m-%d %H:%M:%S", &tmv);
sA(&s, ",\"last_time\":");
sA_str(&s, tbuf);
sA(&s, "}");
}
sA(&s, "]}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
+440
View File
@@ -0,0 +1,440 @@
/* mp_net.c — 网络管理域:meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3)。 */
#include "mp.h"
#define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限 */
int net_ipc(uint16_t req_type, const void *req, uint32_t req_len,
uint16_t *ret_type, uint32_t *code, uint16_t *meta,
char *out, size_t cap)
{
int fd = ipc_connect_tmo(g_sock_net, IPC_TIMEOUT_MS);
if (fd < 0) return -1;
uint8_t buf[MEOW_IPC_MAX];
uint16_t type = 0; uint32_t rid = 0;
int rlen = -1;
if (send_frame(fd, req_type, 0x1020, req, req_len) == 0)
rlen = recv_frame(fd, &type, &rid, buf, sizeof buf);
close(fd);
if (rlen <= 0) return -1;
if (ret_type) *ret_type = type;
/* m_net_*_result 统一布局:code(u32)|len(u16)|meta/pad(u16)|content[len],content 偏移 8 */
uint32_t c = 0; uint16_t ln = 0, mv = 0;
memcpy(&c, buf, 4); memcpy(&ln, buf + 4, 2); memcpy(&mv, buf + 6, 2);
if (code) *code = c;
if (meta) *meta = mv;
if ((size_t)ln >= cap) ln = (uint16_t)(cap - 1);
if ((size_t)(8 + ln) < sizeof buf) buf[8 + ln] = 0;
memcpy(out, buf + 8, ln);
out[ln] = 0;
return (int)ln;
}
int api_net_status(int sock)
{
uint16_t t; uint32_t code; char json[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &t, &code, NULL, json, sizeof json);
if (n <= 0 || t != MSG_NET_STATUS_RESULT || code != MEOW_OK || !json[0])
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
if (!utf8_valid_n(json, strlen(json))) /* 上游坏字节(含 ssid)→ 不返回,降级 */
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务应答包含非法字节");
return send_json(sock, 200, json);
}
int net_conns_parse_body(sbuf_t *s, const char *txt)
{
const char *p = txt;
int first = 1, n = 0;
while (p && *p) {
const char *nl = strchr(p, '\n');
size_t ll = nl ? (size_t)(nl - p) : strlen(p);
if (ll == 0) { p = nl ? nl + 1 : p + 1; continue; }
char line[384];
if (ll >= sizeof line) ll = sizeof line - 1;
memcpy(line, p, ll); line[ll] = 0;
char *f[6]; int fn = 0; char *cur = line;
while (fn < 6) {
f[fn++] = cur;
char *bar = strchr(cur, '|');
if (!bar) break;
*bar = 0; cur = bar + 1;
}
if (fn >= 6) {
if (!first) sA(s, ",");
first = 0;
sA(s, "{\"entity\":"); sA_str(s, f[0]);
sA(s, ",\"local\":"); sA_str(s, f[1]);
sA(s, ",\"remote\":"); sA_str(s, f[2]);
sA(s, ",\"state\":"); sA_str(s, f[3]);
sA(s, ",\"age\":%lld,\"domain\":", atoll(f[4]));
sA_str(s, strcmp(f[5], "-") == 0 ? "" : f[5]);
sA(s, "}");
n++;
}
p = nl ? nl + 1 : p + ll;
}
return n;
}
int api_net_conns(int sock, const char *query)
{
m_net_conns_req_t q; memset(&q, 0, sizeof q);
char ent[128];
if (qparam(query, "entity", ent, sizeof ent))
strncpy(q.entity, ent, sizeof q.entity - 1);
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_CONNS_REQ, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_CONNS_RESULT || code != MEOW_OK)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
sbuf_t a = {0};
int count = net_conns_parse_body(&a, txt);
sbuf_t s = {0};
sA(&s, "{\"truncated\":%s,\"count\":%d,\"conns\":[", meta ? "true" : "false", count);
if (a.n) sA_append(&s, a.b, a.n);
sA(&s, "]}");
sbuf_free(&a);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int net_dns_obj_fill(sbuf_t *s)
{
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
strcpy(q.cmd, "status");
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_DNS_RESULT) return -1;
if (code != MEOW_OK) return -2;
/* txt: `dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d` */
char mode[32] = "", upstream[64] = "";
int enabled = 0, ch = 0, ct = 0, rmod = 0;
unsigned long long qq = 0, deny = 0, first = 0;
(void)sscanf(txt, "dns mode=%31s enabled=%d upstream=%63s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d",
mode, &enabled, upstream, &ch, &ct, &qq, &deny, &first, &rmod);
sA(s, "{\"mode\":");
sA_str(s, mode);
sA(s, ",\"enabled\":%s,\"upstream\":", enabled ? "true" : "false");
sA_str(s, upstream);
sA(s, ",\"cache_hits\":%d,\"cache_total\":%d,\"queries\":%llu,\"denies\":%llu,\"first_seen\":%llu,\"resolv_modified\":%s}",
ch, ct, qq, deny, first, rmod ? "true" : "false");
return 0;
}
int api_net_dns(int sock)
{
sbuf_t s = {0};
int rc = net_dns_obj_fill(&s);
if (rc == -1) { sbuf_free(&s); return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); }
if (rc == -2) { sbuf_free(&s); return send_err(sock, 503, "NET_NODEV", "DNS 代理不可用"); }
int out = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return out;
}
int net_block_fill(sbuf_t *s)
{
uint16_t nt; uint32_t ncode; char njson[MEOW_IPC_MAX];
int nn = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &nt, &ncode, NULL, njson, sizeof njson);
if (nn <= 0 || nt != MSG_NET_STATUS_RESULT || ncode != MEOW_OK || !njson[0]) return -1;
/* 状态 JSON 为对象:剥外层大括号,拼入 DNS 子键(DNS 不可用则省略该键,仍回 net 状态,不谎报)。 */
size_t l = strlen(njson);
while (l > 0 && (njson[l - 1] == ' ' || njson[l - 1] == '\t' || njson[l - 1] == '\n' || njson[l - 1] == '\r')) njson[--l] = 0;
if (l < 2 || njson[0] != '{' || njson[l - 1] != '}') return -1;
if (!utf8_valid_n(njson, l)) return -1; /* 任一上游(netmgrd 含 ssid)产出非法 UTF-8 → 整块降级,不拖垮整页 */
njson[l - 1] = 0; /* 去尾部 } */
sbuf_t dns = {0};
int drc = net_dns_obj_fill(&dns);
sA(s, ",\"net\":{");
sA(s, "%s", njson + 1); /* 去首 { 的状态内层 */
if (drc == 0 && dns.b) sA(s, ",\"dns\":%s", dns.b);
sA(s, "}"); /* net 块只开了一个 {,一个 } 收尾(批2 曾写 }} 多一括号,浏览器 Extra data 破页 2026-09-02) */
sbuf_free(&dns);
return 0;
}
int api_net_dns_flush(int sock)
{
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
strcpy(q.cmd, "flush");
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_DNS_RESULT)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
if (code == (uint32_t)MEOW_ERR_PERM)
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "DNS flush 需系统身份");
if (code != MEOW_OK)
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS flush 失败");
/* txt: `dns cache flushed (N entries)` */
int entries = 0;
(void)sscanf(txt, "dns cache flushed (%d entries)", &entries);
sbuf_t s = {0};
sA(&s, "{\"flushed\":true,\"entries\":%d}", entries);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_net_log(int sock)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
DIR *d = opendir(LOGS_DIR);
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
char best[512] = ""; long best_mt = -1;
struct dirent *e;
while ((e = readdir(d))) {
if (strncmp(e->d_name, "net_", 4) != 0) continue;
if (e->d_name[0] == '.') 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 (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
}
closedir(d);
if (!best[0])
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
char full[1024];
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
int fd = open(full, O_RDONLY);
struct stat st;
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
if (fd >= 0) close(fd);
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
}
off_t off = st.st_size > NET_LOG_TAIL_MAX ? st.st_size - NET_LOG_TAIL_MAX : 0;
size_t buflen = (size_t)(st.st_size - off);
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
if (off) (void)lseek(fd, off, SEEK_SET);
char *buf = malloc(buflen + 1);
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
ssize_t n = read(fd, buf, buflen);
close(fd);
if (n <= 0) { free(buf); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
sbuf_t s = {0};
sA(&s, "{\"file\":");
sA_str(&s, best);
sA(&s, ",\"lines\":[");
const char *p = buf, *end = buf + n;
int linecnt = 0;
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
if (linecnt > 0 && end[-1] != '\n') linecnt++;
int maxlines = LOG_MAX_LINES;
int startrow = linecnt - maxlines;
if (startrow < 0) startrow = 0;
int truncated = (off > 0 || startrow > 0) ? 1 : 0;
int rowidx = 0, gotlines = 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, ",");
/* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */
sA_str_n(&s, p, (size_t)(line_end - p));
gotlines++;
}
rowidx++;
if (!nl) break;
p = nl + 1;
}
free(buf);
sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
int api_net_ifaces(int sock)
{
m_net_iface_req_t q; memset(&q, 0, sizeof q);
uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr);
if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0])
return send_json(sock, 200, "{\"available\":false}");
/* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */
if (!utf8_valid_n(arr, strlen(arr)))
return send_json(sock, 200, "{\"available\":false}");
/* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */
uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX];
int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj);
long long rssi = -9999; int got_rssi = 0;
sbuf_t rfkill = {0}, dns = {0};
if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) {
jv_t *jr = jv_parse(sj, (int)strlen(sj));
if (jr) {
jv_t *w = jv_obj_get(jr, "wifi");
jv_t *r = w ? jv_obj_get(w, "rssi") : NULL;
if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; }
jv_t *rf = jv_obj_get(jr, "rfkill");
if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf);
jv_t *bt = jv_obj_get(jr, "bt");
jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL;
if (dn && dn->type == J_ARR) jv_to_json(&dns, dn);
jv_free(jr);
}
}
sbuf_t s = {0};
sA(&s, "{\"available\":true,\"rssi\":");
if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null");
sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr);
sbuf_free(&rfkill); sbuf_free(&dns);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
void net_policy_parse(const char *txt, sbuf_t *s)
{
char lvl[24] = "", enf[24] = "dns_only";
const char *p = txt;
sA(s, "{\"available\":true,\"enforce\":");
/* 首行找 enforce=... */
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
const char *en = strstr(p, "enforce=");
if (en) {
en += 8;
int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; }
enf[i] = 0;
}
const char *lg = strstr(p, "log_level=");
if (lg) {
lg += 10;
int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; }
lvl[i] = 0;
}
sA_str(s, enf);
sA(s, ",\"log_level\":");
sA_str(s, lvl);
sA(s, ",\"rules\":[");
int first = 1;
const char *ln = txt;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; }
/* 跳注释行(以 # 开头)与空;数据行为 `<entity> net=... domain=...`(entity 含冒号无空格) */
const char *c = ln;
while (c < ln + ll && (*c == ' ' || *c == '\t')) c++;
int is_comment = (c < ln + ll && *c == '#');
int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0);
if (!is_comment && !is_none && c < ln + ll) {
/* entity = 首个空白符之前 */
char entity[64] = ""; size_t ei = 0;
const char *e = c;
while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; }
entity[ei] = 0;
/* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */
char net[8] = "allow"; int has_net = 0;
char allow_h[512] = "", deny_h[512] = "";
const char *k = e;
if (k < ln + ll) {
const char *seg = k;
while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++;
const char *nh = strstr(seg, "net=");
if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; }
const char *dh = strstr(seg, "domain=");
if (dh) {
dh += 7;
/* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */
size_t ao = 0, do2 = 0;
const char *h = dh;
while (*h && *h != ' ' && *h != '\n' && *h != '\r') {
if (*h == '+') {
h++;
if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ',';
while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++;
} else if (*h == '-') {
h++;
if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ',';
while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++;
} else h++;
if (*h == ',') h++;
}
allow_h[ao] = 0; deny_h[do2] = 0;
}
if (!first) sA(s, ",");
first = 0;
sA(s, "{\"entity\":"); sA_str(s, entity);
sA(s, ",\"net\":"); sA_str(s, has_net ? net : "");
sA(s, ",\"has_net\":%s", has_net ? "true" : "false");
sA(s, ",\"allow_hosts\":[");
{ const char *hp = allow_h; int hf = 1;
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
if (!hf) { sA(s, ","); }
hf = 0; sA_str_n(s, hp, hl);
hp = comma ? comma + 1 : hp + hl; } }
sA(s, "],\"deny_hosts\":[");
{ const char *hp = deny_h; int hf = 1;
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
if (!hf) { sA(s, ","); }
hf = 0; sA_str_n(s, hp, hl);
hp = comma ? comma + 1 : hp + hl; } }
sA(s, "]}");
}
}
ln = nl ? nl + 1 : ln + ll;
}
sA(s, "]}");
}
int api_net_policy_get(int sock)
{
m_net_policy_t q; memset(&q, 0, sizeof q);
q.op = NET_POLICY_SHOW;
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK)
return send_json(sock, 200, "{\"available\":false}");
if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */
return send_json(sock, 200, "{\"available\":false}");
sbuf_t s = {0};
net_policy_parse(txt, &s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
int api_net_policy_set(int sock, const char *body, size_t body_len)
{
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(body, (int)body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
char op[16] = "set_net", entity[64] = "", arg[64] = "";
if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str);
if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str);
if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str);
jv_free(root);
if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:<pkg>/svc:<svc>/proc:<comm>)");
if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法");
int nop = 0;
if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET;
else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN;
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
m_net_policy_t q; memset(&q, 0, sizeof q);
q.op = (uint32_t)nop;
snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */
snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_POLICY_RESULT)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
if (code == (uint32_t)MEOW_ERR_PERM)
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?");
if (code == (uint32_t)MEOW_ERR_INVAL)
return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法");
if (code != MEOW_OK)
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败");
sbuf_t s = {0};
sA(&s, "{\"accepted\":true,\"message\":");
sA_str(&s, txt);
sA(&s, "}");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
+298
View File
@@ -0,0 +1,298 @@
/* mp_notif.c — 通知域:优先 launcher IPC 活跃集,回退 TF 归档(查询/撤/清)。 */
#include "mp.h"
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;
}
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;
}
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;
}
const char *notif_app_name(const char *app_id)
{
if (!strcmp(app_id, "system")) return "系统";
return app_id;
}
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");
}
int notif_ipc_query(notif_rec_t *recs, int maxrec)
{
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
if (fd < 0) return -1;
int cnt = -1;
if (ipc_hello(fd) < 0) goto done;
if (send_frame(fd, MSG_NOTIFY_QUERY, 8, NULL, 0) < 0) 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_notify_query_result_t)) goto done;
if (type != MSG_NOTIFY_QUERY_RESULT) goto done;
m_notify_query_result_t qr;
memcpy(&qr, buf, sizeof qr);
if (qr.code != MEOW_OK) goto done;
uint16_t tl = qr.text_len;
if (tl >= sizeof qr.text) tl = (uint16_t)(sizeof qr.text - 1);
if (tl == 0) { cnt = 0; goto done; } /* 无活跃通知 */
jv_t *arr = jv_parse(qr.text, tl);
if (!arr) goto done;
if (arr->type != J_ARR) { jv_free(arr); goto done; }
int c = 0;
for (jv_t *e = arr->child; e && c < maxrec; e = e->next) {
if (e->type != J_OBJ) continue;
notif_rec_t r; memset(&r, 0, sizeof r);
r.progress = -1;
r.urgency = 0;
jv_t *v;
if ((v = jv_obj_get(e, "id")) && v->type == J_NUM) r.id = (unsigned)v->inum;
if ((v = jv_obj_get(e, "app_id")) && v->type == J_STR) snprintf(r.app_id, sizeof r.app_id, "%s", v->str);
if ((v = jv_obj_get(e, "app_icon")) && v->type == J_STR) snprintf(r.app_icon, sizeof r.app_icon, "%s", v->str);
if ((v = jv_obj_get(e, "category")) && v->type == J_STR) snprintf(r.category, sizeof r.category, "%s", v->str);
if ((v = jv_obj_get(e, "summary")) && v->type == J_STR) snprintf(r.summary, sizeof r.summary, "%s", v->str);
if ((v = jv_obj_get(e, "body")) && v->type == J_STR) snprintf(r.body, sizeof r.body, "%s", v->str);
if ((v = jv_obj_get(e, "target_activity")) && v->type == J_STR) snprintf(r.target, sizeof r.target, "%s", v->str);
if ((v = jv_obj_get(e, "urgency")) && v->type == J_NUM) r.urgency = (int)v->inum;
/* ts 为 epoch 毫秒 → 存入 r.ms(emit 时 /1000 出秒,与 TF 同口径) */
if ((v = jv_obj_get(e, "ts")) && v->type == J_NUM) r.ms = (long long)v->inum;
if ((v = jv_obj_get(e, "progress")) && v->type == J_NUM) r.progress = (int)v->inum; /* <0=无;json 无该键则 -1 */
if ((v = jv_obj_get(e, "ongoing")) && v->type == J_NUM) r.ongoing = (int)v->inum;
if ((v = jv_obj_get(e, "media")) && v->type == J_NUM) r.media = (int)v->inum;
recs[c++] = r;
}
jv_free(arr);
/* 按 ms 升序(oldest..newest),与 TF 路径一致(emit 时倒序遍历= newest first)。 */
for (int i = 1; i < c; i++) { notif_rec_t key = recs[i]; int j = i - 1;
while (j >= 0 && recs[j].ms > key.ms) { recs[j + 1] = recs[j]; j--; } recs[j + 1] = key; }
cnt = c;
done:
close(fd);
return cnt;
}
int notif_ipc_dismiss(unsigned id)
{
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
if (fd < 0) return -1;
int rc = -1;
if (ipc_hello(fd) < 0) goto done;
uint32_t nid = (uint32_t)id;
if (send_frame(fd, MSG_NOTIFY_DISMISS, 9, &nid, sizeof nid) < 0) goto done;
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)) goto done;
if (type != MSG_ACK) goto done;
m_ack_t a; memcpy(&a, buf, sizeof a);
rc = (a.code == MEOW_OK) ? 0 : -1;
done:
close(fd);
return rc;
}
int notif_ipc_dismiss_all(int *out_count)
{
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
if (fd < 0) return -1;
int rc = -1;
if (ipc_hello(fd) < 0) goto done;
if (send_frame(fd, MSG_NOTIFY_DISMISS_ALL, 10, NULL, 0) < 0) goto done;
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)) goto done;
if (type != MSG_ACK) goto done;
m_ack_t a; memcpy(&a, buf, sizeof a);
if (a.code == MEOW_OK) { if (out_count) *out_count = (int)a.status; rc = 0; }
done:
close(fd);
return rc;
}
int api_notifications(int sock)
{
notif_rec_t recs[NOTIF_PREVIEW_MAX];
const char *source = "ipc";
int n = notif_ipc_query(recs, NOTIF_PREVIEW_MAX);
if (n < 0) { /* IPC 不可达 → 回退 TF 归档 */
n = notif_load_list(recs, NOTIF_PREVIEW_MAX);
source = (n < 0) ? "none" : "tf";
if (n < 0) n = 0;
}
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\":\"%s\",\"items\":[", n, source);
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;
}
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;
}
int api_notification_delete(int sock, unsigned id)
{
/* 优先 IPC 联动 launcher 内存(launcher 撤下时自动归档留档);IPC 不可达/未受理回退 TF 重写。 */
if (notif_ipc_dismiss(id) == 0) {
char d[64];
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
return send_json(sock, 200, d);
}
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);
}
int api_notification_dismiss_all(int sock)
{
int cleared = 0;
if (notif_ipc_dismiss_all(&cleared) == 0) { /* 联动 launcher 内存,返回清除数 */
char d[64];
snprintf(d, sizeof d, "{\"dismissed\":%d}", cleared);
return send_json(sock, 200, d);
}
int removed = notif_rewrite(1, 0); /* IPC 不可达 → 回退重写 TF 日志 */
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);
}
+192
View File
@@ -0,0 +1,192 @@
/* mp_perf.c — 性能/系统域:/api/perf,/api/system,action,pin-unlock(经 meow_svc_perf IPC 聚合)。 */
#include "mp.h"
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 || !utf8_valid_n(json, strlen(json))) /* 上游坏字节 → 不返回,降级 */
return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
return send_json(sock, 200, json);
}
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;
}
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");
/* 当前运行模式:读 /tmp/meow_boot_mode(meow/hiby),缺失回退 default(PANEL_ENHANCE_PLAN 批1 前端读 boot.mode) */
char modebuf[16] = {0};
{
FILE *f = fopen("/tmp/meow_boot_mode", "r");
if (f) {
if (fgets(modebuf, sizeof modebuf, f)) {
char *p = modebuf;
while (*p) { if (*p == '\n' || *p == '\r' || *p == ' ') { *p = 0; break; } p++; }
}
fclose(f);
}
}
const char *bootmode = (modebuf[0] == 'm' || modebuf[0] == 'h') ? modebuf : bootname;
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, ",\"mode\":");
sA_str(&s, bootmode);
sA(&s, "},\"network\":");
char netbuf[1024];
/* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local);含非法 UTF-8(如坏 ssid)→ 回退,不嵌坏字节 */
if (perf_extract_network(json, netbuf, sizeof netbuf) == 0 && utf8_valid_n(netbuf, strlen(netbuf))) {
sA(&s, "%s", netbuf);
} 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}");
/* 重启原因 / 升级状态:svc_power 读级查询(POWER_REBOOT_REASON(8)/POWER_UPGRADE_STATUS(9),只读、
* 任意调用者)。st.msg 分别为 "coldboot=reboot|shutdown|unknown" 与 "upgrade=yes|no"。
* 前端批1-B 已留位(读 d.reboot_reason / d.upgrade_status),此处点亮。查询失败保持空串(不谎报)。 */
char rreason[48] = "", ustatus[32] = "";
{
m_power_status_t st;
memset(&st, 0, sizeof st);
if (ipc_power_action(POWER_REBOOT_REASON, 0, NULL, &st) == MEOW_OK)
svc_msg_kv(st.msg, "coldboot", rreason, sizeof rreason);
memset(&st, 0, sizeof st);
if (ipc_power_action(POWER_UPGRADE_STATUS, 0, NULL, &st) == MEOW_OK)
svc_msg_kv(st.msg, "upgrade", ustatus, sizeof ustatus);
}
sA(&s, ",\"reboot_reason\":");
sA_str(&s, rreason);
sA(&s, ",\"upgrade_status\":");
sA_str(&s, ustatus);
sA(&s, "}");
jv_free(root);
int ret = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return ret;
}
int api_pin_unlock_once(int sock)
{
const char *path = "/tmp/meow_pin_unlock_once";
FILE *f = fopen(path, "w");
if (!f) return send_err(sock, 500, "INTERNAL", "无法写入一次性解锁标志");
fputs("1\n", f);
fclose(f);
return send_json(sock, 200, "{\"unlock_once\":true}");
}
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);
}
+140
View File
@@ -0,0 +1,140 @@
/* mp_power.c — 功耗管理域:meow_powermgr IPC(/api/power*,policy,window,events)。 */
#include "mp.h"
int ipc_pm_query(uint32_t subset, char *out, int cap)
{
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
if (fd < 0) return MEOW_ERR_NODEV;
int rc = MEOW_ERR_INTERNAL;
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
m_pm_query_t q;
memset(&q, 0, sizeof q);
q.subset = subset;
if (send_frame(fd, MSG_PM_QUERY, 0x31, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; goto done; }
uint8_t buf[MEOW_IPC_MAX];
uint16_t type; uint32_t rid;
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
if (type != MSG_PM_RESULT) { rc = MEOW_ERR_INVAL; goto done; }
uint32_t c; memcpy(&c, buf, 4);
if (c != MEOW_OK) { rc = (int)c; goto done; }
/* powermgr 发**紧凑**负载:json 从偏移 12 起(json_len 在偏移 8);同 m_pm_result_t 布局,兼容全量/紧凑。 */
uint16_t jl; memcpy(&jl, buf + 8, 2);
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
memcpy(out, buf + 12, jl);
out[jl] = 0;
rc = MEOW_OK;
done:
close(fd);
return rc;
}
int api_power(int sock)
{
char js[MEOW_IPC_MAX];
int rc = ipc_pm_query(0, js, sizeof js);
if (rc != MEOW_OK || !utf8_valid_n(js, strlen(js))) /* 上游坏字节 → 不返回,降级 */
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用");
return send_json(sock, 200, js);
}
int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen,
uint16_t *rtype, char *out, int cap)
{
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
if (fd < 0) return MEOW_ERR_NODEV;
int rc = MEOW_ERR_INTERNAL;
if (rtype) *rtype = 0;
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
if (send_frame(fd, req_type, 0x41, pl, plen) < 0) { rc = MEOW_ERR_IO; goto done; }
uint8_t buf[MEOW_IPC_MAX];
uint16_t type; uint32_t rid;
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
if (n <= 0) { rc = MEOW_ERR_IO; goto done; }
if (rtype) *rtype = type;
if (type == MSG_ERR) {
m_err_t e; memcpy(&e, buf, sizeof e);
rc = (int)e.code; goto done;
}
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
uint32_t c; memcpy(&c, buf, 4);
if (c != MEOW_OK) { rc = (int)c; goto done; }
uint16_t jl; memcpy(&jl, buf + 8, 2);
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
memcpy(out, buf + 12, jl); out[jl] = 0;
rc = (int)jl;
done:
close(fd);
return rc;
}
int api_power_policy_get(int sock)
{
char js[MEOW_IPC_MAX];
uint16_t rt = 0;
int rc = ipc_pm_send(MSG_PM_POLICY_GET, NULL, 0, &rt, js, sizeof js);
if (rc <= 0)
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
return send_json(sock, 200, js);
}
int api_power_policy_set(int sock, const char *body, size_t body_len)
{
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
jv_t *root = jv_parse(body, (int)body_len);
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
char item[20] = "", val[40] = "";
if (ji && ji->type == J_STR && ji->str) snprintf(item, sizeof item, "%s", ji->str);
if (jv && jv->type == J_STR && jv->str) snprintf(val, sizeof val, "%s", jv->str);
else if (jv && jv->type == J_NUM) snprintf(val, sizeof val, "%lld", jv->inum);
jv_free(root);
if (!item[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 item(profile/low-pct/crit-pct/bri-cap/scr-cap)");
if (!val[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 value");
m_pm_policy_set_t s; memset(&s, 0, sizeof s);
snprintf(s.item, sizeof s.item, "%.15s", item); /* item[16] 留一个 NUL;防越界截断告警 */
snprintf(s.val, sizeof s.val, "%.31s", val); /* val[32] 同理 */
char js[MEOW_IPC_MAX];
uint16_t rt = 0;
int rc = ipc_pm_send(MSG_PM_POLICY_SET, &s, sizeof s, &rt, js, sizeof js);
if (rc <= 0) {
if (rt == MSG_ERR) /* 权限不足:powermgr 拒绝(面板 root 仅当被白名单才放行;如实回传) */
return send_err(sock, 403, "FORBIDDEN", rc == MEOW_ERR_PERM ? "设备身份不允许写电源策略" : "电源策略写入被拒绝");
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
}
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
/* 服务端对非法值回 ok:false(HTTP 层 200,code 仍 OK);按其 why 映射 422。 */
if (strstr(js, "\"ok\":false")) {
char why[96] = "";
jv_t *jr = jv_parse(js, (int)strlen(js));
if (jr) { jv_t *w = jv_obj_get(jr, "why"); if (w && w->type == J_STR && w->str) snprintf(why, sizeof why, "%s", w->str); jv_free(jr); }
return send_err(sock, 422, "UNSUPPORTED", why[0] ? why : "非法电源策略值");
}
return send_json(sock, 200, js);
}
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail)
{
uint32_t count = 0;
char js[MEOW_IPC_MAX];
uint16_t rt = 0;
int rc = ipc_pm_send(req_type, &count, sizeof count, &rt, js, sizeof js);
if (rc <= 0)
return send_err(sock, 503, "POWER_UNAVAILABLE", unavail);
if (rt != want_type || !utf8_valid_n(js, strlen(js)))
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
return send_json(sock, 200, js);
}
int api_power_history(int sock)
{ return api_power_window(sock, MSG_PM_HISTORY, MSG_PM_HISTORY_RESULT, "电源历史采样不可用(powermgr 未运行)"); }
int api_power_events(int sock)
{ return api_power_window(sock, MSG_PM_EVENTS, MSG_PM_EVENTS_RESULT, "电源事件不可用(powermgr 未运行)"); }
+277
View File
@@ -0,0 +1,277 @@
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap。 */
#include "mp.h"
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
static int g_screen_pw = 0, g_screen_ph = 0;
static int64_t g_last_self_inject_ms = 0;
#define TOUCH_ACTIVE_MS 5000
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
{
int fd = open(dev, O_RDONLY);
if (fd < 0) return -1;
unsigned char vinfo[160], finfo[68];
memset(vinfo, 0, sizeof vinfo);
memset(finfo, 0, sizeof finfo);
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { close(fd); return -1; }
uint32_t xres = *(uint32_t *)(vinfo + 0);
uint32_t yres = *(uint32_t *)(vinfo + 4);
uint32_t yoff = *(uint32_t *)(vinfo + 20);
uint32_t bpp = *(uint32_t *)(vinfo + 24);
uint32_t line = *(uint32_t *)(finfo + 44);
if (!line) line = xres * (bpp / 8);
if (bpp != 16 || xres == 0 || yres == 0 || line < xres * 2) { close(fd); return -1; }
size_t framelen = (size_t)line * (size_t)yres;
uint8_t *frame = (uint8_t *)malloc(framelen);
if (!frame) { close(fd); return -1; }
ssize_t got = pread(fd, frame, framelen, (off_t)line * (off_t)yoff);
close(fd);
if (got != (ssize_t)framelen) { free(frame); return -1; }
/* 若 line != xres*2(行尾填充),压缩为紧凑 RGB565(xres*yres*2) */
if (line != xres * 2) {
size_t cplen = (size_t)xres * 2;
for (uint32_t y = 0; y < yres; y++)
memmove(frame + (size_t)y * cplen, frame + (size_t)y * line, cplen);
}
*out = frame; *w = (int)xres; *h = (int)yres;
return 0;
}
int fb_dims(const char *dev, int *w, int *h)
{
int fd = open(dev, O_RDONLY);
if (fd < 0) return -1;
unsigned char vinfo[160];
memset(vinfo, 0, sizeof vinfo);
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0) { close(fd); return -1; }
uint32_t xres = *(uint32_t *)(vinfo + 0);
uint32_t yres = *(uint32_t *)(vinfo + 4);
close(fd);
if (!xres || !yres) return -1;
*w = (int)xres; *h = (int)yres;
return 0;
}
void screen_prev_store(const uint8_t *frame, int w, int h)
{
size_t need = (size_t)w * (size_t)h * 2;
if (w != g_screen_pw || h != g_screen_ph) {
free(g_screen_prev);
g_screen_prev = NULL;
g_screen_pw = w; g_screen_ph = h;
g_screen_prev = (uint8_t *)malloc(need);
}
if (g_screen_prev) memcpy(g_screen_prev, frame, need);
}
int screen_diff_blocks(const uint8_t *cur, const uint8_t *prev, int w, int h,
int *bx, int *by, int *bw, int *bh, int *total_blocks)
{
const int B = SCREEN_BLOCK;
int nbx = (w + B - 1) / B, nby = (h + B - 1) / B;
if (total_blocks) *total_blocks = nbx * nby;
int changed = 0;
int minx = nbx, miny = nby, maxx = -1, maxy = -1;
for (int byi = 0; byi < nby; byi++) {
for (int bxi = 0; bxi < nbx; bxi++) {
int x0 = bxi * B, y0 = byi * B;
int wdt = B, hgt = B;
if (x0 + wdt > w) wdt = w - x0;
if (y0 + hgt > h) hgt = h - y0;
int diff = 0;
for (int y = 0; y < hgt && !diff; y++) {
const uint8_t *a = cur + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2;
const uint8_t *b = prev + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2;
if (memcmp(a, b, (size_t)wdt * 2) != 0) diff = 1;
}
if (diff) {
changed++;
if (bxi < minx) minx = bxi;
if (byi < miny) miny = byi;
if (bxi > maxx) maxx = bxi;
if (byi > maxy) maxy = byi;
}
}
}
if (changed) { *bx = minx; *by = miny; *bw = maxx - minx + 1; *bh = maxy - miny + 1; }
return changed;
}
int api_screen_jpeg(int sock, const req_t *r)
{
int q = 70; /* 默认质量(体积/清晰折中) */
char qv[8];
if (qparam(r->query, "q", qv, sizeof qv)) {
int t = atoi(qv);
if (t >= 1 && t <= 100) q = t;
}
char dv[8];
int delta = (qparam(r->query, "delta", dv, sizeof dv) && atoi(dv) == 1);
const char *dev = getenv("MEOW_FB_DEV"); if (!dev || !dev[0]) dev = FB_DEV;
uint8_t *frame = NULL; int w = 0, h = 0;
if (screen_read_frame(dev, &frame, &w, &h) != 0)
return send_err(sock, 500, "INTERNAL", "读取帧缓冲失败(/dev/fb0)");
if (!delta) { /* 默认整帧(不破坏原行为) */
uint8_t *jpeg = NULL; size_t jlen = 0;
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
screen_prev_store(frame, w, h); /* 维护 delta 基线 */
int ret = send_raw(sock, 200, "image/jpeg", jpeg, jlen);
free(jpeg);
free(frame);
return ret;
}
/* ---- delta 模式 ---- */
const int B = SCREEN_BLOCK;
int changed = 0, bx = 0, by = 0, bw = 0, bh = 0, total = 0;
int force_full = (!g_screen_prev || g_screen_pw != w || g_screen_ph != h);
if (!force_full) {
changed = screen_diff_blocks(frame, g_screen_prev, w, h, &bx, &by, &bw, &bh, &total);
if (bw * B >= w && bh * B >= h) force_full = 1; /* 全屏变化 → 直接发整帧 */
} else {
total = ((w + B - 1) / B) * ((h + B - 1) / B);
}
screen_prev_store(frame, w, h); /* 基线更新为当前帧 */
char hdr[224];
int ret = -1;
if (changed == 0 && !force_full) { /* 无变化:空 body,前端跳过叠加 */
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
"X-Meow-Changed: 0\r\nX-Meow-Blocks: %d", w, h, total);
ret = send_head(sock, 200, "image/jpeg", 0, hdr);
free(frame);
return ret;
}
if (force_full) { /* 冷藏起/几何变化/全屏变化 → 整帧 */
uint8_t *jpeg = NULL; size_t jlen = 0;
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 1\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
"X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, changed ? changed : total, total);
ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr);
if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1;
free(jpeg);
free(frame);
return ret;
}
/* 子图 = 变化块 bbox 外接矩形(8 对齐,钳到屏内),重编码为独立 JPEG 并以响应头回传偏移。 */
int x0 = bx * B, y0 = by * B;
int sw = bw * B, sh = bh * B;
if (x0 + sw > w) sw = w - x0;
if (y0 + sh > h) sh = h - y0;
uint8_t *sub = (uint8_t *)malloc((size_t)sw * (size_t)sh * 2);
if (!sub) { free(frame); return send_err(sock, 500, "INTERNAL", "内存不足"); }
for (int y = 0; y < sh; y++)
memcpy(sub + (size_t)y * (size_t)sw * 2,
frame + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2,
(size_t)sw * 2);
uint8_t *jpeg = NULL; size_t jlen = 0;
int rc = mp_jpeg_encode_rgb565(sub, sw, sh, q, &jpeg, &jlen);
free(sub);
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
"X-Meow-Sub-X: %d\r\nX-Meow-Sub-Y: %d\r\nX-Meow-Sub-W: %d\r\nX-Meow-Sub-H: %d\r\n"
"X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, x0, y0, sw, sh, changed, total);
ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr);
if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1;
free(jpeg);
free(frame);
return ret;
}
int64_t touch_last_local_ms(const char *dev)
{
int fd = open(dev, O_RDONLY | O_NONBLOCK);
if (fd < 0) return 0;
int64_t last = 0;
unsigned char ev[16];
int64_t inject_deadline = g_last_self_inject_ms + 200; /* 注入事件时间 ≈ 注入时刻 */
while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
int64_t sec = (int64_t)(*(int32_t *)(ev + 0));
int64_t usec = (int64_t)(*(int32_t *)(ev + 4));
int64_t t = sec * 1000 + usec / 1000;
if (t <= inject_deadline) continue; /* 自注入,跳过 */
if (t > last) last = t;
}
close(fd);
return last;
}
int inject_tap(const char *dev, int x, int y)
{
int fd = open(dev, O_WRONLY);
if (fd < 0) return -1;
struct input_event ev;
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
/* 事件顺序:先 BTN_TOUCH=1(按下边沿),后 ABS 坐标 —— launcher/meow_touch 在按下边沿
* 会重置坐标 (-1),只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)。若先发
* ABS 再发 BTN_TOUCH,原点永不锁定 → 抬手表被判为无效 tap(只高亮不启动)。 */
__WE(EV_KEY, BTN_TOUCH, 1);
__WE(EV_ABS, ABS_MT_SLOT, 0);
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
__WE(EV_ABS, ABS_MT_POSITION_X, x);
__WE(EV_ABS, ABS_MT_POSITION_Y, y);
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
__WE(EV_ABS, ABS_X, x);
__WE(EV_ABS, ABS_Y, y);
__WE(EV_SYN, SYN_REPORT, 0);
usleep(100000); /* 按住 100ms 再抬起,抬起到按下的边沿被 launcher 视为一次 tap */
__WE(EV_KEY, BTN_TOUCH, 0);
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
__WE(EV_SYN, SYN_REPORT, 0);
#undef __WE
close(fd);
return 0;
}
int api_input_tap(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 *jx = jv_obj_get(root, "x");
jv_t *jy = jv_obj_get(root, "y");
if (!jx || jx->type != J_NUM || !jy || jy->type != J_NUM) {
jv_free(root);
return send_err(sock, 400, "BAD_REQUEST", "缺 x/y(数值)");
}
int x = (int)jx->inum, y = (int)jy->inum;
jv_free(root);
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
/* 坐标 clamp 到屏幕范围(前端已按 480x720 映射,此处再兜底) */
int sw = 480, sh = 720;
{ int fx = 0, fy = 0; const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
if (x < 0) x = 0;
if (x >= sw) x = sw - 1;
if (y < 0) y = 0;
if (y >= sh) y = sh - 1;
/* 仲裁:本机最近触摸后 5s 内忽略注入 */
int64_t now = now_mono_ms();
int64_t last = touch_last_local_ms(dev);
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
sbuf_t s = {0};
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
(long long)(TOUCH_ACTIVE_MS - (now - last)));
int ret = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return ret;
}
g_last_self_inject_ms = now_mono_ms();
if (inject_tap(dev, x, y) < 0)
return send_err(sock, 500, "INTERNAL", "触摸注入失败(无法打开触摸设备)");
sbuf_t s = {0};
sA(&s, "{\"dispatched\":true,\"x\":%d,\"y\":%d}", x, y);
int ret = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return ret;
}
+618
View File
@@ -0,0 +1,618 @@
/* mp_server.c — HTTP 服务器/路由 + 静态托管 + main(单线程 + poll 串行)。 */
#include "mp.h"
static char g_webroot[512];
static int g_port = DEFAULT_PORT;
void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
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);
}
else if (!strncasecmp(hp, "Authorization:", 14)) {
char *v = hp + 14;
while (*v == ' ') v++;
snprintf(r->authorization, sizeof r->authorization, "%s", v);
}
else if (!strncasecmp(hp, "X-MeowToken:", 12)) {
char *v = hp + 12;
while (*v == ' ') v++;
snprintf(r->x_meow_token, sizeof r->x_meow_token, "%s", v);
}
else if (!strncasecmp(hp, "Cookie:", 7)) {
char *v = hp + 7;
while (*v == ' ') v++;
snprintf(r->cookie, sizeof r->cookie, "%s", v);
}
hp = he + 2;
}
char *body_start = buf + hend + 4; /* body 实际起点 */
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。
* 字体库 tar.gz 端点例外:gbk/full 字库 png+idx 可能 10-20MB,放宽到 MAX_FONT_UPLOAD(32MB)。 */
size_t cap = MAX_HTTP_BODY;
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0)
cap = (!strcmp(r->path, "/api/fonts/import")) ? MAX_FONT_UPLOAD : 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;
}
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;
}
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));
}
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;
}
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;
}
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;
}
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));
}
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;
}
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;
}
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";
}
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")
|| !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件");
FILE *f = fopen(full, "rb");
if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在");
fclose(f);
return send_file(sock, 200, mime_for(full), full, NULL);
}
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);
}
/* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */
if (require_auth(sock, r) != 1) return 0;
/* ---- 鉴权端点(登录/设置/登出/状态:公开——访问面板即需登录,须先经此建立会话) ---- */
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r);
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/setup")) return api_auth_setup(sock, r);
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/logout")) return api_auth_logout(sock, r);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/auth/status")) return api_auth_status(sock, r);
/* ---- 性能/状态 ---- */
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") && !strcmp(p, "/api/fonts/import")) return api_fonts_import(sock, r);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r);
/* ---- 音频状态(meow_audiod 快照,只读;含 rms/peak/lvlseq 电平键;monitor=实时监控日志尾读) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio/monitor")) return api_audio_monitor(sock, r->query);
/* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock);
if (!strcmp(r->method, "GET")) {
char seg[256];
if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0])
return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */
}
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock);
/* ---- 实时画面(阶段 C) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
return api_screen_jpeg(sock, r);
/* ---- 系统动作 ---- */
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/system/pin-unlock-once")) return api_pin_unlock_once(sock);
if (!strcmp(p, "/api/debug/report_bad_json")) {
/* 前端 NOT_JSON 自动回传浏览器收到的原始字节(抓间歇性破包) */
FILE *df = fopen("/tmp/overview_client_bad.txt", "w");
if (df && r->body && r->body_len) fwrite(r->body, 1, r->body_len, df);
if (df) fclose(df);
panel_log("debug: client bad json captured %zu bytes -> /tmp/overview_client_bad.txt", (size_t)r->body_len);
return send_json(sock, 200, "{\"captured\":true}");
}
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/net/dns/flush")) return api_net_dns_flush(sock);
if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len);
if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len);
if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len);
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
char seg[256];
if (path_segment("/api/key/", p, seg, sizeof seg))
return api_key_single(sock, atoi(seg), r);
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
if (!strncmp(p, "/api/ams/", 9)) {
size_t pl = strlen(p);
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
char pkg[256];
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
return api_ams_ctl(sock, pkg, 0);
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
char pkg[256];
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
return api_ams_ctl(sock, pkg, 1);
}
}
}
/* ---- 设置 ---- */
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/crash")) return api_logs_crash(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", "端点不存在");
}
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;
}
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 panel_selftest(void)
{
const char *trunc = "流媒体客户\xE7"; /* 事故里被腰斩的显示名(16B,末字节为「端」首字节) */
const char *oknat = "流媒体客户端"; /* 完整合法名(18B,回归确认不误伤) */
size_t n = strlen(trunc);
/* 1a) utf8_sanitize:截断串必被改写,长度保持,产物合法 UTF-8 */
char san[64];
if (!utf8_sanitize(san, trunc, n)) { printf("SELFTEST FAIL: utf8_sanitize 未改写截断串\n"); return 1; }
if (strlen(san) != n) { printf("SELFTEST FAIL: utf8_sanitize 长度变化 %zu->%zu\n", n, strlen(san)); return 1; }
if (!utf8_valid_n(san, strlen(san))) { printf("SELFTEST FAIL: utf8_sanitize 产物含非法字节\n"); return 1; }
/* 1b) 完整合法名不得被改写(误伤检测) */
char san2[64];
if (utf8_sanitize(san2, oknat, strlen(oknat)) != 0 || strcmp(san2, oknat) != 0) {
printf("SELFTEST FAIL: utf8_sanitize 误改合法 UTF-8\n"); return 1;
}
/* 2) sA_str 嵌入:产物合法 UTF-8 且 jv_parse 完整解析 */
sbuf_t s = {0};
sA(&s, "{\"pkg\":");
sA_str(&s, "com.example.trunc");
sA(&s, ",\"name\":");
sA_str(&s, trunc);
sA(&s, ",\"state\":");
sA_str(&s, "running");
sA(&s, "}");
if (!s.b) { printf("SELFTEST FAIL: sA_str 输出空\n"); return 1; }
if (!utf8_valid_n(s.b, s.n)) { printf("SELFTEST FAIL: sA_str 产物含非法字节\n"); sbuf_free(&s); return 1; }
jv_t *r = jv_parse(s.b, (int)s.n);
if (!r) { printf("SELFTEST FAIL: sA_str 产物 JSON 无法解析\n"); sbuf_free(&s); return 1; }
jv_t *nm = jv_obj_get(r, "name");
if (!nm || nm->type != J_STR) { printf("SELFTEST FAIL: sA_str 产物缺 name\n"); jv_free(r); sbuf_free(&s); return 1; }
jv_free(r);
sbuf_free(&s);
/* 3) ams_fill 桩:模拟 APP 行 parse 出的截断名(join_tokens 后)→ apps 数组可解析 */
sbuf_t a = {0};
sA(&a, "{\"available\":true,\"count\":1,\"apps\":[{\"pkg\":");
sA_str(&a, "com.example.trunc");
sA(&a, ",\"name\":");
sA_str(&a, trunc);
sA(&a, ",\"state\":");
sA_str(&a, "running");
sA(&a, ",\"pid\":123}]}");
if (!a.b || !utf8_valid_n(a.b, a.n)) { printf("SELFTEST FAIL: ams_fill 桩产物含非法字节/为空\n"); sbuf_free(&a); return 1; }
jv_t *ar = jv_parse(a.b, (int)a.n);
if (!ar) { printf("SELFTEST FAIL: ams_fill 桩产物 JSON 无法解析\n"); sbuf_free(&a); return 1; }
jv_free(ar);
sbuf_free(&a);
/* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。
* 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */
{
static const char ptext[] =
"# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n"
"app:com.example.trunc net=block domain=+good.example,-bad\xE7\n"
"svc:meowpanel net=allow domain=+svc.local\n";
sbuf_t p = {0};
net_policy_parse(ptext, &p);
if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; }
jv_t *pr = jv_parse(p.b, (int)p.n);
if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; }
jv_t *rules = jv_obj_get(pr, "rules");
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
jv_t *enf = jv_obj_get(pr, "enforce");
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
jv_free(pr);
sbuf_free(&p);
}
/* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。
* detail 合法时 → 合法 JSON 可解析。 */
{
char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n";
sbuf_t h = {0};
ams_history_parse(bad, &h, "com.example.trunc");
if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; }
sbuf_free(&h);
char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n";
sbuf_t h2 = {0};
ams_history_parse(good, &h2, "com.example.trunc");
if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; }
sbuf_free(&h2);
}
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
return 0;
}
int main(int argc, char **argv)
{
/* 参数:-p PORT(可选)。默认端口 8080。 */
int port = DEFAULT_PORT;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--selftest")) return panel_selftest();
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-launcher") && i + 1 < argc) g_sock_launcher = argv[++i];
else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i];
else if (!strcmp(argv[i], "--sock-net") && i + 1 < argc) g_sock_net = argv[++i];
else if (!strcmp(argv[i], "--sock-pm") && i + 1 < argc) g_sock_pm = argv[++i];
else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; }
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
printf("MeowPanel backend %s\n", PANEL_VERSION);
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P] [--sock-net P] [--sock-pm 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;
env = getenv("MEOW_IPC_LAUNCHER_SOCK");
if (env && env[0]) g_sock_launcher = env;
env = getenv("MEOW_AUDIO_SOCK");
if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */
env = getenv("MEOW_NET_SOCK");
if (env && env[0]) g_sock_net = env; /* 与 netmgrd/meow_net 同 env(宿主/qemu 测试用) */
env = getenv("MEOW_PM_SOCK");
if (env && env[0]) g_sock_pm = env; /* 与 powermgr/meow_power 同 env(宿主/qemu 测试用) */
env = getenv("MEOW_AUTH_TTL");
if (env && env[0]) g_auth_ttl = (time_t)atol(env); /* 会话 TTL 覆盖(宿主测试用) */
env = getenv("MEOW_SETTINGS_FILE");
if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */
(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;
}
+596
View File
@@ -0,0 +1,596 @@
/* mp_store.c — 设置镜像(settings.conf)+ 主题 + /api/settings*,/api/theme*。权威来源:meow_settings.c g_defs(逐键一致)。 */
#include "mp.h"
const char *g_settings_file = SETTINGS_FILE;
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, "系统" },
/* 面板(MeowPanel 自身,访问级 PIN 鉴权)
* panel.pin = 4 位数字 PIN 的 sha256+salt 哈希(`<salt_hex>:<sha256_hex(salt||pin)>`,绝不存明文)。
* 与未来 MeowOS 锁屏 PIN 同源:锁屏键 `system.pin` 预留——本后端只读面板键 panel.pin;将来锁屏启用时
* 以 system.pin 为准并做同源迁移(panel.pin 视为其缓存/别名),此处仅留兼容注记,不实现。
* panel.auth_enabled/panel.auth_pass 为旧密码机制,保留兼容迁移(见鉴权段注释),新代码以 PIN 为准。 */
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
};
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
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;
}
void settings_mkdir_p(void)
{
/* 逐级创建 settings.conf 所在目录(mkdir -p 语义,容忍已存在)。 */
char tmp[1024];
snprintf(tmp, sizeof tmp, "%s", g_settings_file);
char *slash = strrchr(tmp, '/');
if (!slash || slash == tmp) return; /* 无目录或根部 */
*slash = 0;
if (!tmp[0]) return;
char buf[1024]; buf[0] = 0;
size_t i = 0;
if (tmp[0] == '/') buf[i++] = '/';
const char *s = tmp + (tmp[0] == '/' ? 1 : 0);
while (*s) {
const char *tom = strchr(s, '/');
size_t l = tom ? (size_t)(tom - s) : strlen(s);
if (l) {
if (i && buf[i - 1] != '/') buf[i++] = '/';
if (i + l >= sizeof buf) break;
memcpy(buf + i, s, l);
i += l;
buf[i] = 0;
mkdir(buf, 0777);
}
if (!tom) break;
s = tom + 1;
}
}
char *settings_get_value(const char *key)
{
FILE *f = fopen(g_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);
}
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;
}
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;
}
int settings_write_one(const char *key, const char *val)
{
settings_mkdir_p(); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
FILE *f = fopen(g_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(g_settings_file, "w");
if (!nf) return -1;
size_t klen = strlen(key);
int found = 0;
if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR);
/* 逐行重写:头照抄;命中 key 行替换;其余照抄。匹配容忍 `key = value` 两侧空格(与 settings_get_value 同口径)。 */
char *save = NULL;
for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
char *p = ln;
while (*p == ' ' || *p == '\t') p++;
char *eq = strchr(p, '=');
if (eq) {
char *k = p;
size_t km = (size_t)(eq - k);
while (km > 0 && (k[km - 1] == ' ' || k[km - 1] == '\t')) km--;
if (km == klen && strncmp(k, key, klen) == 0) {
fprintf(nf, "%s = %s\n", key, val);
found = 1;
continue;
}
}
fprintf(nf, "%s\n", ln);
}
if (!found) fprintf(nf, "%s = %s\n", key, val);
fclose(nf);
return 0;
}
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;
}
int settings_reset_all(void)
{
settings_mkdir_p();
FILE *nf = fopen(g_settings_file, "w");
if (!nf) return -1;
fprintf(nf, "%s\n", SETTINGS_HDR);
for (int i = 0; i < SETS_COUNT; i++)
fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def);
fclose(nf);
return 0;
}
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, "]");
}
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 {
if (!strcmp(d->key, "panel.auth_pass") || !strcmp(d->key, "panel.pin")) {
/* 密码/PIN 哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设) */
sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false");
} else {
sA_str(s, val);
}
}
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, "}");
}
char *settings_current_value(const setdef_t *d)
{
char *v = settings_get_value(d->key);
if (v) return v;
return strdup(d->def);
}
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;
}
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;
}
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 hashbuf[200];
/* panel.pin:4 位数字 PIN,只落带盐哈希;空串=清除(回未设状态);非法(非 4 位数字)→ 422 */
if (!strcmp(key, "panel.pin")) {
if (raw[0]) {
if (!pin_validate(raw)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
} else {
hashbuf[0] = 0; /* 清空 → 回未设状态 */
}
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
pin_lock_reset();
sbuf_t s = {0};
settings_item_json(&s, d, hashbuf);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */
if (!strcmp(key, "panel.auth_pass")) {
if (raw[0]) {
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
return send_err(sock, 500, "INTERNAL", "生成密码哈希失败");
} else {
hashbuf[0] = 0; /* 清空 → 禁用密码 */
}
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
sbuf_t s = {0};
settings_item_json(&s, d, hashbuf);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* panel.auth_enabled(旧机制,兼容迁移):置 1 前须已有访问屏障(PIN 或旧密码) */
if (!strcmp(key, "panel.auth_enabled")) {
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0)
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
if (!strcmp(coerced, "1") && !pin_set() && !auth_pass_set())
return send_err(sock, 422, "UNSUPPORTED", "请先设置面板 PIN 或密码");
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
sbuf_t s = {0};
settings_item_json(&s, d, coerced);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
}
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;
}
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;
}
/* panel.pin:只落带盐哈希(空=清除);非法(非 4 位数字)→ 422 */
if (!strcmp(p->str, "panel.pin")) {
char hashbuf[200];
if (raw[0]) {
if (!pin_validate(raw)) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"PIN 必须为 4 位数字\"}"); nfail++; continue; }
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成 PIN 哈希失败\"}"); nfail++; continue; }
} else hashbuf[0] = 0;
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
continue;
}
/* panel.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */
if (!strcmp(p->str, "panel.auth_pass")) {
char hashbuf[200];
if (raw[0]) { if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成密码哈希失败\"}"); nfail++; continue; } }
else hashbuf[0] = 0;
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
continue;
}
char coerced[256];
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"值域/枚举非法\"}");
nfail++;
continue;
}
/* panel.auth_enabled(迁移):启用前须已有访问屏障(PIN 或旧密码) */
if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) {
if (nfail) sA(&fail, ",");
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板 PIN 或密码\"}");
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;
}
int api_settings_restore(int sock)
{
if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败");
return send_json(sock, 200, "{\"reset\":true}");
}
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);
}
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;
}
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);
}
+130
View File
@@ -0,0 +1,130 @@
/* mp_svcd.c — 服务表域:svcd 状态解析 + /api/svcd + 首页聚合 /api/overview。 */
#include "mp.h"
const char *svcd_bin(void)
{
const char *e = getenv("MEOW_SVCD_BIN");
if (e && e[0]) return e;
if (!access(SVCD_BIN, X_OK)) return SVCD_BIN;
return SVCD_BIN_CARD;
}
void svcd_fill(sbuf_t *s)
{
char out[16384];
char cmd[300];
snprintf(cmd, sizeof cmd, "%s status all 2>/dev/null", svcd_bin());
int code = run_cmd(cmd, out, sizeof out);
if (code != 0) { sA(s, "{\"available\":false}"); return; }
panel_strip_ansi(out);
char *toks[256];
sbuf_t svcs = {0};
int first = 1, count = 0;
char *ln = out;
while (*ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
int n = txt_tokens(ln, toks, 256);
/* 数据行:NAME STATE PID RESTART BACKOFF TRIGGER SRC EXIT EXIT_TIME EXEC...
* state=tok[1] 须为合法状态;exec=join(tok[9..n))。 */
if (n >= 10) {
const char *st = toks[1];
int is_known = (!strcmp(st,"running") || !strcmp(st,"stopped") ||
!strcmp(st,"stopping") || !strcmp(st,"disabled") || !strcmp(st,"failed"));
if (is_known) {
char exec[512];
join_tokens(toks, 9, n, exec, sizeof exec);
int restarts = 0;
{ const char *slash = strchr(toks[3], '/'); (void)slash; restarts = atoi(toks[3]); }
if (!first) sA(&svcs, ",");
first = 0; count++;
sA(&svcs, "{\"name\":"); sA_str(&svcs, toks[0]);
sA(&svcs, ",\"state\":"); sA_str(&svcs, toks[1]);
sA(&svcs, ",\"pid\":%d", atoi(toks[2]));
sA(&svcs, ",\"restart\":%d", restarts);
sA(&svcs, ",\"restart_str\":");
sA_str(&svcs, toks[3]);
sA(&svcs, ",\"backoff_ms\":%d", atoi(toks[4]));
sA(&svcs, ",\"trigger\":");
sA_str(&svcs, toks[5]);
sA(&svcs, ",\"src\":");
sA_str(&svcs, toks[6]);
sA(&svcs, ",\"exit\":");
sA_str(&svcs, toks[7]);
sA(&svcs, ",\"exit_time\":");
sA_str(&svcs, toks[8]);
sA(&svcs, ",\"exec\":");
sA_str(&svcs, exec);
sA(&svcs, "}");
}
}
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
}
sA(s, "{\"available\":true,\"count\":%d,\"services\":[%s]}", count, svcs.b ? svcs.b : "");
sbuf_free(&svcs);
}
int api_svcd(int sock)
{
sbuf_t s = {0};
svcd_fill(&s);
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
sbuf_free(&s);
return rc;
}
int api_overview(int sock)
{
sbuf_t s = {0};
sA(&s, "{\"ts\":%lld,", (long long)time(NULL));
char pb[4096];
if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK && utf8_valid_n(pb, strlen(pb)))
sA(&s, "\"perf\":%s,", pb);
else sA(&s, "\"perf\":{\"available\":false},");
char pw[MEOW_IPC_MAX];
if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw)))
sA(&s, "\"power\":%s,", pw);
else sA(&s, "\"power\":{\"available\":false},");
/* 最近异常耗电/充放电/亮灭屏/背光跳变事件(摘要 top2 进 overview,完整列表走 /api/power/events)。 */
{
uint32_t ecnt = 2;
char ej[MEOW_IPC_MAX];
uint16_t ert = 0;
int ert_rc = ipc_pm_send(MSG_PM_EVENTS, &ecnt, sizeof ecnt, &ert, ej, sizeof ej);
if (ert_rc > 0 && ert == MSG_PM_EVENTS_RESULT && utf8_valid_n(ej, strlen(ej)))
sA(&s, "\"events\":%s,", ej);
else
sA(&s, "\"events\":{\"available\":false},");
}
sA(&s, "\"audio\":");
audio_fill(&s);
if (net_block_fill(&s) == 0)
; /* net 块已追加(状态 + 合并的 dns 子键) */
else
sA(&s, ",\"net\":{\"available\":false}");
sA(&s, ",\"ams\":");
ams_fill(&s);
sA(&s, ",\"svcd\":");
svcd_fill(&s);
sA(&s, "}");
/* 服务端自校验:发出前 jv_parse 自查,坏了留证+回 500(防「非 JSON 响应」破页,2026-09-02 间歇性事故)
* 加严:内嵌 NUL(strlen 截断浏览器收残体)/非法 UTF-8(浏览器 JSON.parse 拒收,宽松解析器放行)*/
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
size_t slen = s.b ? strlen(s.b) : 0;
int nul_inside = s.b && (slen != s.n); /* 内嵌 NUL:strlen < s.n */
int utf8_bad = s.b && !utf8_valid_n(s.b, s.n);
int unbalanced = s.b && !json_strict_balanced(s.b, s.n); /* 尾随垃圾/多括号(jv_parse 容忍,浏览器不容忍) */
if (s.b && (nul_inside || utf8_bad || unbalanced)) { chk = NULL; }
if (!chk && s.b) {
FILE *df = fopen("/tmp/overview_bad.json", "w");
if (df) { fwrite(s.b, 1, s.n, df); fclose(df); }
panel_log("overview: SELF-CHECK fail (nul=%d utf8_bad=%d unbalanced=%d n=%zu strlen=%zu), raw dumped /tmp/overview_bad.json", nul_inside, utf8_bad, unbalanced, s.n, slen);
sbuf_free(&s);
return send_err(sock, 500, "INTERNAL", "overview 聚合输出未通过 JSON 自校验(原始数据已留 /tmp/overview_bad.json)");
}
if (chk) jv_free(chk);
int rc = send_json(sock, 200, s.b ? s.b : "{}");
sbuf_free(&s);
return rc;
}
+368
View File
@@ -0,0 +1,368 @@
/* mp_util.c — 通用工具:有界字符串(sbuf)/JSON 字符串消毒(UTF-8)/内存搜索/命令管道/路径与 token 工具。 */
#include "mp.h"
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;
}
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;
}
int utf8_seq_len(unsigned char lead)
{
if (lead >= 0xC2 && lead <= 0xDF) return 2; /* C2+:规避 overlong C0/C1 */
if (lead >= 0xE0 && lead <= 0xEF) return 3; /* 3 字节(含代理区,由 seq_ok 剔除) */
if (lead >= 0xF0 && lead <= 0xF4) return 4; /* F5+ 超 U+10FFFF,剔除 */
return 0;
}
int utf8_seq_ok(const char *p, int len)
{
unsigned char c0 = (unsigned char)p[0];
for (int k = 1; k < len; k++) {
unsigned char ck = (unsigned char)p[k];
if (ck == 0 || (ck & 0xC0) != 0x80) return 0; /* 截断/非续字节 */
}
unsigned long cp;
if (len == 2) cp = ((unsigned long)(c0 & 0x1F) << 6) | ((unsigned char)p[1] & 0x3F);
else if (len == 3) cp = ((unsigned long)(c0 & 0x0F) << 12) | (((unsigned char)p[1] & 0x3F) << 6) | ((unsigned char)p[2] & 0x3F);
else cp = ((unsigned long)(c0 & 0x07) << 18) | (((unsigned char)p[1] & 0x3F) << 12) | (((unsigned char)p[2] & 0x3F) << 6) | ((unsigned char)p[3] & 0x3F);
if (len == 2 && cp < 0x80) return 0; /* overlong(C0/C1 已由 seq_len 拒,双保险) */
if (len == 3 && cp < 0x800) return 0; /* overlong E0 80.. */
if (len == 4 && cp < 0x10000) return 0; /* overlong F0 80.. */
if (cp >= 0xD800 && cp <= 0xDFFF) return 0; /* 代理对(JS 字符串不允许孤立) */
if (cp > 0x10FFFF) return 0;
return 1;
}
int utf8_valid_n(const char *s, size_t n)
{
size_t i = 0;
while (i < n) {
unsigned char c = (unsigned char)s[i];
if (c < 0x80) { i++; continue; }
int len = utf8_seq_len(c);
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(s + i, len)) return 0;
i += (size_t)len;
}
return 1;
}
int utf8_sanitize(char *dst, const char *src, size_t n)
{
size_t i = 0, j = 0;
int changed = 0;
while (i < n) {
unsigned char c = (unsigned char)src[i];
if (c < 0x80) { dst[j++] = (char)c; i++; continue; }
int len = utf8_seq_len(c);
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(src + i, len)) {
dst[j++] = '?'; changed = 1; i++; continue; /* 逐字节替换,前进 1 重对齐 */
}
memcpy(dst + j, src + i, (size_t)len);
j += (size_t)len; i += (size_t)len;
}
dst[j] = 0;
return changed;
}
void sA_str_n(sbuf_t *s, const char *v, size_t n)
{
sA(s, "\"");
size_t i = 0;
while (i < n) {
unsigned char c = (unsigned char)v[i];
if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); i++; }
else if (c < 0x20) { sA(s, "\\u%04x", c); i++; }
else if (c < 0x80) { sA(s, "%c", (int)c); i++; }
else {
int len = utf8_seq_len(c);
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(v + i, len)) {
sA(s, "?"); i++; /* 非法/截断序列 → 逐字节替换为 '?',前进 1 重对齐 */
} else {
for (int k = 0; k < len; k++) sA(s, "%c", (int)(unsigned char)v[i + k]);
i += (size_t)len;
}
}
}
sA(s, "\"");
}
void sA_str(sbuf_t *s, const char *v)
{
if (!v) v = "";
sA_str_n(s, v, strlen(v));
}
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;
}
void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
void panel_strip_ansi(char *s)
{
char *r = s, *w = s;
while (*r) {
if (*r == '\033' && r[1] == '[') {
r += 2;
while (*r && !(*r >= '@' && *r <= '~')) r++;
if (*r) r++;
} else { *w++ = *r++; }
}
*w = 0;
}
int txt_tokens(char *buf, char **toks, int maxtoks)
{
int n = 0;
char *p = buf;
while (*p && n < maxtoks) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (!*p) break;
toks[n++] = p;
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
if (*p) *p++ = 0;
}
return n;
}
void join_tokens(char **toks, int from, int to, char *out, size_t cap)
{
size_t o = 0;
for (int i = from; i < to; i++) {
size_t need = strlen(toks[i]) + (o ? 1 : 0);
if (o + need >= cap) { out[cap - 1] = 0; return; }
if (o) out[o++] = ' ';
memcpy(out + o, toks[i], strlen(toks[i]));
o += strlen(toks[i]);
}
out[o] = 0;
}
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;
}
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;
}
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;
}
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;
}
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;
}
}
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;
}
const char *mem_find(const void *hay, size_t hlen, const char *needle)
{
size_t nl = strlen(needle);
if (!nl || hlen < nl) return NULL;
const char *h = (const char *)hay;
for (size_t i = 0; i + nl <= hlen; i++) {
if (h[i] == needle[0] && memcmp(h + i, needle, nl) == 0)
return h + i;
}
return NULL;
}
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);
}
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);
}
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;
}
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;
}
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;
}
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;
}
int64_t now_mono_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
+6 -7
View File
@@ -61,13 +61,9 @@
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2" fill="var(--bg)"/><circle cx="15" cy="12" r="2" fill="var(--bg)"/><circle cx="7" cy="18" r="2" fill="var(--bg)"/></svg></span>
<span class="nav-label">设置</span>
</button>
<button class="nav-item" data-route="remote" title="远程控制">
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="7" y="3" width="10" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="15" r="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M10 7h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
<span class="nav-label">远程</span>
</button>
<button class="nav-item" data-route="screen" title="实时画面 / 点击遥控">
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8 21h8M12 17v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="8.5" cy="11" r="1.2" fill="currentColor"/><path d="M12 11l3.5 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
<span class="nav-label">屏幕</span>
<button class="nav-item" data-route="screen" title="屏幕 · 遥控(实时画面 + 遥控器)">
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="14" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M7 21h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="7.5" cy="10.5" r="1.1" fill="currentColor"/><path d="M10 10.5l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" transform="rotate(-45 11.5 12)"/><rect x="18" y="4" width="4.5" height="4.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="18" y="11" width="4.5" height="4.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="18" y="18" width="4.5" height="2.5" rx="1" fill="currentColor"/></svg></span>
<span class="nav-label">屏幕·遥控</span>
</button>
<button class="nav-item" data-route="proc" title="进程 / 服务(meow_svcd + meow_amsd)">
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2.2" fill="var(--bg)"/><circle cx="15" cy="12" r="2.2" fill="var(--bg)"/><circle cx="7" cy="18" r="2.2" fill="var(--bg)"/></svg></span>
@@ -171,6 +167,9 @@
<div id="toast" class="toast" hidden></div>
<!-- 前端模块化拆分(共享框架 → 各页渲染 → 入口路由),按顺序加载 -->
<script src="mp_shared.js"></script>
<script src="mp_pages.js"></script>
<script src="meowpanel.js"></script>
</body>
</html>
+22
View File
@@ -293,6 +293,28 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
.key-btn:hover{ background:var(--hover); }
.key-btn .k-ico{ font-size:20px; display:block; margin-bottom:4px; }
/* 屏幕·遥控 合并页:左screen右遥控板 */
.screen-remote{ display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; }
.screen-col{ flex:1 1 340px; min-width:280px; max-width:520px; }
.remote-col{ flex:0 1 260px; min-width:220px; }
.remote-col .card{ padding:14px 16px; }
.remote-pad{ display:flex; flex-direction:column; gap:12px; }
.remote-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:10px; }
.remote-btn{
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px;
padding:12px 6px; border:none; border-radius:var(--radius-m);
background:var(--surface-variant); color:var(--on-surface);
border:var(--outline-w) solid var(--outline-variant);
cursor:pointer; font:inherit; text-align:center; transition:background var(--speed);
}
.remote-btn:hover{ background:var(--hover); }
.remote-btn .r-ico{ width:26px; height:26px; display:block; }
.remote-btn .r-ico svg{ width:26px; height:26px; display:block; }
.remote-btn .r-lbl{ font-size:12px; line-height:1.2; }
.remote-bright{ display:flex; align-items:center; gap:8px; }
.remote-bright .r-lbl{ font-size:12px; color:var(--on-surface-variant); white-space:nowrap; }
.remote-bright input{ flex:1; }
/* 危险区 */
.danger-box{ border:1px solid var(--error); border-radius:var(--radius-l); padding:16px 18px; }
.danger-box .card-title{ color:var(--error); }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+467
View File
@@ -0,0 +1,467 @@
/* mp_shared.js — MeowPanel 前端共享框架:全局状态 + API 客户端 + 公共工具 + 主题/弹窗/登录/滑条。
* 由 meowpanel.js 拆分而来(接口/行为不变)。作为经典脚本在 meowpanel.js 之前加载,暴露 window.MP。 */
(function () {
'use strict';
var MP = window.MP = window.MP || {};
const $ = (sel, root) => (root || document).querySelector(sel);
const $$ = (sel, root) => Array.prototype.slice.call((root || document).querySelectorAll(sel));
const sparkRms = [], sparkPeak = [];
const SPARK_MAX = 30;
const state = {
style: 'md3', mode: 'dark', // 已解析的风格/明暗(应用到 <html> data-style/data-mode)
devStyle: null, devTheme: null, // 设备 settings.conf 的 ui.style / display.theme(默认来源)
conn: 'unknown',
filesRel: '/', filesAbs: null,
perfTimer: null, // 性能页轮询
route: 'perf',
unauth: false, // 鉴权未通过(弹登录页)
downloadNonce: 0
};
let toastTimer = null;
let authMode = 'login'; // 'login' | 'setup'
let volTimer = null;
const saveSetting = debounce(async function (key, value, type) {
try {
await api('PUT', '/api/settings/' + key, { value: value });
toast('已保存 ' + key + ' = ' + value);
} catch (e) { toast('保存失败:' + errText(e)); }
}, 400);
const LOG_GROUP_NAME = { all: '全部', system: '系统服务', app: 'App', audio_monitor: 'audio_monitor', power: 'power', net: 'net', crash: 'crash', ams: 'ams' };
function storageGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
function storageSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
function storageDel(k) { try { localStorage.removeItem(k); } catch (e) {} }
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, m => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[m]));
}
function fmtBytes(n) {
if (n == null || isNaN(n)) return '—';
const u = ['B', 'KB', 'MB', 'GB', 'TB']; let v = Number(n), i = 0;
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
return (i === 0 ? v : v.toFixed(1)) + ' ' + u[i];
}
function fmtUptime(sec) {
if (sec == null || isNaN(sec)) return '—';
sec = Math.floor(sec);
const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60);
return (d > 0 ? d + ' 天 ' : '') + h + ' 小时 ' + m + ' 分';
}
function fmtTime(ts) { return ts ? new Date(ts * 1000).toLocaleString() : '—'; }
function debounce(fn, ms) { let t = null; return function () { const a = arguments, c = this; clearTimeout(t); t = setTimeout(() => fn.apply(c, a), ms); }; }
function pct(p) { return Math.max(0, Math.min(100, p)); }
function deciToDb(v) { return (v == null || v <= -990) ? null : v / 10; }
function fmtDb(v) {
const db = deciToDb(v);
if (db == null) return '−∞';
return (db > 0 ? '+' : '') + db.toFixed(1);
}
function dbPct(v) { // -60..0 dBFS 映射到 0..100% 条宽
const db = deciToDb(v);
if (db == null) return 0;
return Math.max(0, Math.min(100, (db + 60) / 60 * 100));
}
function isAvail(b) { return !!(b && b.available !== false); }
function sparklineHtml() {
return '<svg class="lvl-spark" viewBox="0 0 100 46" preserveAspectRatio="none" aria-hidden="true">' +
'<line class="grid" x1="0" y1="46" x2="100" y2="46"/>' +
'<line class="grid" x1="0" y1="23" x2="100" y2="23"/>' +
'<polyline class="spk-rms" points="" fill="none" stroke="#00bfa5" stroke-width="1.6" vector-effect="non-scaling-stroke"/>' +
'<polyline class="spk-peak" points="" fill="none" stroke="#ffb300" stroke-width="1.6" vector-effect="non-scaling-stroke"/>' +
'</svg>';
}
function sparklinePoints(arr) {
const n = arr.length;
if (!n) return '';
return arr.map((v, i) => {
const x = (i / (SPARK_MAX - 1)) * 100;
return x.toFixed(1) + ',' + (46 - Math.max(0, Math.min(100, v)) / 100 * 40).toFixed(1);
}).join(' ');
}
function pushSpark(v, arr) { // deci-dB → 0..100 百分比入队(<=30 点)
arr.push(dbPct(v));
if (arr.length > SPARK_MAX) arr.shift();
}
function drawSpark(rootEl) {
const svg = rootEl && rootEl.querySelector('.lvl-spark');
if (!svg) return;
const r = svg.querySelector('.spk-rms'), p = svg.querySelector('.spk-peak');
if (r) r.setAttribute('points', sparklinePoints(sparkRms));
if (p) p.setAttribute('points', sparklinePoints(sparkPeak));
}
function resetSpark() { sparkRms.length = 0; sparkPeak.length = 0; }
function levelMeterHtml() {
return '<div class="lvl">' +
'<div class="lvl-row"><span class="lvl-lbl">RMS</span>' +
'<div class="lvl-meter"><div class="lvl-fill rms" style="width:0%"></div></div>' +
'<span class="lvl-val" data-val="rms">−∞</span></div>' +
'<div class="lvl-row"><span class="lvl-lbl">PEAK</span>' +
'<div class="lvl-meter"><div class="lvl-fill peak" style="width:0%"></div></div>' +
'<span class="lvl-val" data-val="peak">−∞</span></div>' +
'<div class="lvl-scale"><span>-60</span><span>-40</span><span>-20</span><span>0 dBFS</span></div>' +
sparklineHtml() + '</div>';
}
function updateLevelMeter(rootEl, d) {
if (!rootEl) return;
const rms = (d && d.available) ? d.rms : null;
const peak = (d && d.available) ? d.peak : null;
const fr = rootEl.querySelector('.lvl-fill.rms');
const fp = rootEl.querySelector('.lvl-fill.peak');
const vr = rootEl.querySelector('.lvl-val[data-val="rms"]');
const vp = rootEl.querySelector('.lvl-val[data-val="peak"]');
if (fr) fr.style.width = dbPct(rms) + '%';
if (fp) fp.style.width = dbPct(peak) + '%';
if (vr) vr.textContent = fmtDb(rms);
if (vp) vp.textContent = fmtDb(peak);
pushSpark(rms, sparkRms); pushSpark(peak, sparkPeak);
drawSpark(rootEl);
}
function homeKv(k, v) {
return '<div class="home-kv"><span class="k">' + esc(k) + '</span><span class="v">' + v + '</span></div>';
}
function homeUnavailable() { return '<div class="home-sub">— 数据不可用</div>'; }
function homeCard(title, body) { return '<div class="card"><h3 class="card-title">' + esc(title) + '</h3>' + body + '</div>'; }
function modePill(m) {
if (!m) return '<span class="home-chip mut">—</span>';
const cls = (m === 'meow') ? 'ok' : ((m === 'hiby') ? 'warn' : 'mut');
return '<span class="home-chip ' + cls + '">' + esc(m) + '</span>';
}
function setConn(ok) {
state.conn = ok ? 'ok' : 'bad';
const dot = $('#conn-dot');
dot.dataset.state = state.conn;
$('#conn-text').textContent = ok ? '在线' : '离线';
$('#offline-banner').hidden = !!ok;
}
function dbg() {
const a = Array.from(arguments);
console.log.apply(console, ['[mp]'].concat(a));
}
function dbgErr() {
const a = Array.from(arguments);
console.warn.apply(console, ['[mp][ERR]'].concat(a));
}
async function api(method, path, body) {
let res;
const headers = {};
const token = storageGet('meowpanel.token');
if (token) headers['X-MeowToken'] = token;
const init = { method: method || 'GET' };
if (body instanceof FormData) {
init.body = body;
} else if (body !== undefined && body !== null) {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(body);
}
if (Object.keys(headers).length) init.headers = headers;
const t0 = performance.now();
try {
res = await fetch(path, init);
} catch (e) {
dbgErr(method || 'GET', path, '网络层失败:', e && e.message);
setConn(false);
throw { code: 'NETWORK', message: '后端未连接', status: 0 };
}
const ms = Math.round(performance.now() - t0);
/* 先读原文再解析:非 JSON 时能拿到原始内容前 300 字节进控制台 */
let text = '';
try { text = await res.text(); } catch (e) { /* 读体失败走 NOT_JSON */ }
let json = null;
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
if (!json || typeof json.ok !== 'boolean') {
// 静态服务器返回 HTML / 404 等
dbgErr(method || 'GET', path, 'HTTP', res.status, ms + 'ms', '非 JSON 响应, len=' + text.length, '头:', text.slice(0, 200), '尾:', text.slice(-200));
try { fetch('/api/debug/report_bad_json', { method: 'POST', headers: { 'Content-Type': 'text/plain', 'X-MeowToken': storageGet('meowpanel.token') || '' }, body: text }); } catch (e2) {}
setConn(res.ok ? true : false);
throw { code: 'NOT_JSON', message: '后端返回了非 JSON 响应(HTTP ' + res.status + ')', status: res.status };
}
if (!json.ok) {
setConn(true);
const code = (json.error && json.error.code) || 'ERROR';
dbgErr(method || 'GET', path, 'HTTP', res.status, ms + 'ms', '后端错误:', code, (json.error && json.error.message) || '');
if (code === 'UNAUTHORIZED') authRequired(); // 会话失效/未认证 → 弹登录页
throw { code: code, message: (json.error && json.error.message) || ('后端错误 ' + code), status: res.status };
}
if (ms > 800) dbg(method || 'GET', path, '慢响应', ms + 'ms');
setConn(true);
return json.data;
}
async function apiDownload(path) {
let res;
const headers = {};
const token = storageGet('meowpanel.token');
if (token) headers['X-MeowToken'] = token;
try { res = await fetch(path, Object.keys(headers).length ? { headers: headers } : undefined); }
catch (e) { setConn(false); throw { code: 'NETWORK', message: '后端未连接' }; }
if (!res.ok) { setConn(true); throw { code: 'HTTP_' + res.status, message: '下载失败(HTTP ' + res.status + ')' }; }
setConn(true);
return URL.createObjectURL(await res.blob());
}
function toast(msg, ms) {
const t = $('#toast');
t.textContent = msg; t.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { t.hidden = true; }, ms || 2600);
}
function confirmDlg(title, body, danger) {
return new Promise(res => {
$('#modal-title').textContent = title || '确认';
$('#modal-body').textContent = body || '';
const yes = $('#modal-yes'), no = $('#modal-no');
yes.className = 'btn btn-primary' + (danger ? ' btn-danger' : '');
const scrim = $('#modal'); scrim.hidden = false;
const done = v => { scrim.hidden = true; yes.onclick = no.onclick = scrim.onclick = null; res(v); };
yes.onclick = () => done(true);
no.onclick = () => done(false);
scrim.onclick = e => { if (e.target === scrim) done(false); };
});
}
function loginErr(msg) { const err = $('#login-err'); if (err) { err.textContent = msg; err.hidden = false; } }
function authRequired() { // 任一路由收到 401 → 弹登录页(会话失效/未认证)
state.unauth = true;
refreshAuthGate();
}
async function refreshAuthGate() { // 查状态决定「首次设置」还是「登录」
let st = null;
try { st = await api('GET', '/api/auth/status'); } catch (e) { st = null; }
if (st && st.session) { state.unauth = false; hideLogin(); return; }
if (st && !st.pin_set && !st.legacy) showLogin('setup');
else showLogin('login');
}
function showLogin(mode) {
state.unauth = true;
authMode = mode || 'login';
const lg = $('#login'); if (lg) lg.hidden = false;
const sub = $('#login-sub');
const pin2 = $('#login-pin2');
const btn = $('#login-btn');
if (authMode === 'setup') {
if (sub) sub.textContent = '首次使用:请设置一个 4 位数字 PIN 作为本面板访问口令。';
if (pin2) pin2.hidden = false;
if (btn) btn.textContent = '设 置';
} else {
if (sub) sub.textContent = '此面板已开启访问鉴权,请输入 4 位 PIN。';
if (pin2) pin2.hidden = true;
if (btn) btn.textContent = '登 录';
}
const ip = $('#login-pin'); if (ip) { ip.value = ''; ip.focus(); }
const ip2 = $('#login-pin2'); if (ip2 && authMode !== 'setup') ip2.value = '';
const err = $('#login-err'); if (err) err.hidden = true;
}
function hideLogin() { const lg = $('#login'); if (lg) lg.hidden = true; }
async function doLogin() {
const pin = ($('#login-pin').value || '').trim();
const err = $('#login-err'); if (err) err.hidden = true;
if (!pin) { loginErr('请输入 PIN'); return; }
if (authMode === 'setup') {
const pin2 = ($('#login-pin2').value || '').trim();
if (pin.length !== 4 || /[^0-9]/.test(pin)) { loginErr('PIN 必须为 4 位数字'); return; }
if (pin !== pin2) { loginErr('两次输入的 PIN 不一致'); return; }
const btn = $('#login-btn'); if (btn) btn.disabled = true;
try {
await api('POST', '/api/auth/setup', { pin: pin });
toast('PIN 已设置,请登录');
showLogin('login');
} catch (e2) { loginErr((e2 && e2.message) ? e2.message : '设置失败'); }
finally { if (btn) btn.disabled = false; }
return;
}
const btn = $('#login-btn'); if (btn) btn.disabled = true;
try {
const d = await api('POST', '/api/auth/login', { pin: pin });
storageSet('meowpanel.token', d.token);
state.unauth = false;
hideLogin();
toast('已登录');
if (started) route(); else startApp();
} catch (e) {
if (e && (e.code === 'LOCKED' || e.status === 429)) loginErr(e.message || '错误次数过多,已锁定,请稍后重试');
else loginErr((e && e.message) ? e.message : '登录失败');
} finally {
const b = $('#login-btn'); if (b) b.disabled = false;
}
}
function bindLogin() {
const ok = $('#login-btn'); if (ok) ok.addEventListener('click', doLogin);
const ip = $('#login-pin'); if (ip) ip.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
// PIN 输入:只允许数字,最长 4 位
[ip, $('#login-pin2')].forEach(el => { if (el) el.addEventListener('input', () => { el.value = el.value.replace(/[^0-9]/g, '').slice(0, 4); }); });
}
function passwordDlg(title, body) {
return new Promise(res => {
$('#pwdlg-title').textContent = title || '设置面板密码';
$('#pwdlg-body').textContent = body || '请输入新密码(留空则清除密码)。';
const inp = $('#pwdlg-input'); inp.value = ''; inp.focus();
const scrim = $('#pwdlg'); scrim.hidden = false;
const okb = $('#pwdlg-ok'), cancel = $('#pwdlg-cancel');
const done = v => { scrim.hidden = true; okb.onclick = cancel.onclick = scrim.onclick = inp.onkeydown = null; res(v); };
okb.onclick = () => done(inp.value);
cancel.onclick = () => done(null);
scrim.onclick = e => { if (e.target === scrim) done(null); };
inp.onkeydown = e => { if (e.key === 'Enter') done(inp.value); if (e.key === 'Escape') done(null); };
});
}
function bindPwDlg() { /* #content 点击委托在 bindSettings 里处理 pass 按钮;此处无需额外绑定 */ }
function browserMode() {
try { return (window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; }
catch (e) { return 'dark'; }
}
function storedStyle() { const v = storageGet('meowpanel.style'); return (v === 'md3' || v === 'miuix') ? v : null; }
function storedMode() { const v = storageGet('meowpanel.mode'); return (v === 'system' || v === 'browser' || v === 'light' || v === 'dark') ? v : null; }
function resolveStyle() { return storedStyle() || state.devStyle || 'md3'; }
function resolveMode() {
const m = storedMode() || 'system';
if (m === 'light' || m === 'dark') return m;
if (m === 'system') return state.devTheme || browserMode(); // 设备未知时回退浏览器偏好
return browserMode();
}
function devStyleLabel() { return state.devStyle ? (state.devStyle === 'miuix' ? 'MIUIx' : 'Material You') : '未知(后端离线)'; }
function devThemeLabel() { return state.devTheme ? (state.devTheme === 'light' ? '浅色' : '深色') : '未知(后端离线)'; }
function applyTheme() {
const style = resolveStyle(), mode = resolveMode();
state.style = style; state.mode = mode;
const root = document.documentElement;
root.dataset.style = style; root.dataset.mode = mode;
$$('#style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style));
$$('#theme-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
// 设置页「主题」卡(仅在该页渲染时存在)
$$('#set-style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style));
$$('#set-mode-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === (storedMode() || 'system')));
const rst = $('#set-style-reset'); if (rst) rst.disabled = !storedStyle();
}
async function initTheme() {
// 旧版键迁移:meowpanel.theme(dark/light)→ meowpanel.mode
const old = storageGet('meowpanel.theme');
if (old) {
if (!storedMode()) storageSet('meowpanel.mode', old === 'light' ? 'light' : 'dark');
storageDel('meowpanel.theme');
}
applyTheme();
// 浏览器日/夜切换:模式为 browser(或 system 且设备主题未知)时实时跟随
try {
if (window.matchMedia) {
const mq = matchMedia('(prefers-color-scheme: dark)');
const onChange = function () {
const m = storedMode() || 'system';
if (m === 'browser' || (m === 'system' && !state.devTheme)) applyTheme();
};
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
}
} catch (e) {}
// 设备默认值(ui.style/display.theme)——未覆盖时的默认来源
try {
const t = await api('GET', '/api/theme');
state.devStyle = (t.style === 'md3' || t.style === 'miuix') ? t.style : null;
state.devTheme = (t.theme === 'light' || t.theme === 'dark') ? t.theme : null;
applyTheme();
if (state.route === 'settings') renderSettings(); // 刷新「主题」卡的设备当前值提示
} catch (e) { /* 后端离线,保持本地/浏览器偏好 */ }
}
function bindThemeUi() {
$('#style-seg').addEventListener('click', function (e) {
const b = e.target.closest('.seg-btn'); if (!b) return;
storageSet('meowpanel.style', b.dataset.style);
applyTheme();
sendTheme({ style: b.dataset.style });
});
$('#theme-seg').addEventListener('click', function (e) {
const b = e.target.closest('.seg-btn'); if (!b) return;
storageSet('meowpanel.mode', b.dataset.mode); // 顶栏=显式明/暗
applyTheme();
sendTheme({ theme: b.dataset.mode });
});
}
function emptyState(msg, code) {
const t = code === 'NO_CARD' ? '未识别到 TF 卡' : (code === 'NOT_IMPLEMENTED' ? '该功能后端尚未实现' : msg);
return '<div class="empty">' + esc(t) + '</div>';
}
function errText(e) {
if (e && e.code === 'NO_CARD') return '未识别到 TF 卡';
if (e && e.code === 'NOT_IMPLEMENTED') return '该功能后端尚未实现';
return (e && e.message) ? e.message : '未知错误';
}
function barHtml(p) {
p = pct(p);
const c = p >= 90 ? 'danger' : (p >= 70 ? 'warn' : '');
return '<div class="bar ' + c + '"><span style="width:' + p + '%"></span></div>';
}
function stat(label, valueHtml, sub, barPct) {
return '<div class="stat"><div class="stat-label">' + esc(label) + '</div>' + valueHtml +
(sub ? '<div class="stat-sub">' + sub + '</div>' : '') +
(barPct != null ? barHtml(barPct) : '') + '</div>';
}
function slur(step) { // step: {key,label,min,max,val}
return '<div class="card"><h3 class="card-title">' + esc(step.label) + '</h3>' +
'<div class="field-row"><span class="muted" style="width:44px">' + step.val + '</span>' +
'<input type="range" min="' + step.min + '" max="' + step.max + '" value="' + step.val + '" data-slider data-key="' + esc(step.key) + '">' +
'<span class="muted" style="width:44px;text-align:right">' + step.max + '</span></div></div>';
}
function bindSlider() {
$('#content').addEventListener('input', function (e) {
const r = e.target.closest('[data-slider]');
if (!r) return;
const row = r.closest('.field-row');
if (row) row.querySelector('.muted').textContent = r.value;
saveSetting(r.dataset.key, +r.value, 'int');
});
}
Object.assign(MP, { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, pushSpark, refreshAuthGate, resetSpark, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, updateLevelMeter, volTimer });
})();