/* 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= '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; }