620 lines
26 KiB
C
620 lines
26 KiB
C
/* 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;
|
||
}
|