Files
blackbird/dnfs2/src/dnfs2.cpp
T
qing 2e0805eb05 fix(dnfs2): 第2轮审计修复 db 错误处理不一致
- P2-N6: _note_exists db 业务错误误判为节点不存在,改抛 500
- P2-N7: delete_note/update_note 所有权检查分离 db 错误(500)和节点不存在(404)
- P3-N8: CONFS_PORT 添加 [1,65535] 范围校验
- P3-N9: main() 添加 curl_global_cleanup,配对 curl_global_init
- 更新审计文档为第 2 轮,评级 A- → A
2026-08-08 01:44:04 +08:00

1201 lines
46 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** dnfs2 — C++20 树状网络文件系统(对齐 Python dnfs 模块)
*
* 基于 libhttp + libmsg + boost::json 实现,接口与 Python dnfs 完全兼容:
* POST /get_notes 获取子节点列表(无需鉴权)
* POST /add_note 添加节点(需 Token + AI 审核)
* POST /delete_note 删除节点(需 Token + 所有权)
* POST /update_note 更新节点(需 Token + 所有权 + AI 审核)
*
* 安全修复(相比 Python dnfs):
* P0-4 统一封装 _call_db() 检查 success 后再访问 data
* P1-6 server.setMaxBodySize(10MB) 限制请求体大小
* P1-7 日志客户端复用主配置(confs 拉取的 db host/port)
* P1-8 update_note 改 pid≠0 时校验父节点存在
* P1-9 RpcClient 启用 retry_on_refused
* P1-11 note_id 显式 as_int64() + 类型校验
*
* 依赖: libhttp (HttpServer), libmsg (RpcClient/AsyncQueue), boost::json,
* OpenSSL, libcurl
*/
#include <us/HttpServer.hpp>
#include <us/HttpClient.hpp>
#include <us/RpcClient.hpp>
#include <us/AsyncQueue.hpp>
#include <boost/json.hpp>
#include <curl/curl.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
namespace json = boost::json;
namespace http = boost::beast::http;
using namespace pmc::net;
// ============================================================================
// 常量
// ============================================================================
// 请求体大小上限(对齐 WAF,防 OOM)
static constexpr size_t kMaxBodySize = 10 * 1024 * 1024;
// 特殊节点
static constexpr size_t kSpMaxUrl = 2048;
static constexpr size_t kSpMaxCode = 512 * 1024;
static constexpr size_t kSpMaxData = 3500 * 1024;
static constexpr size_t kSpReviewCodePrefix = 4000;
// 日志
static constexpr size_t kLogMaxMessage = 8192;
static constexpr size_t kLogMaxExtra = 4096;
static constexpr size_t kLogBatchSize = 50;
static constexpr double kLogFlushInterval = 2.0;
static constexpr size_t kLogQueueMax = 10000;
static constexpr double kLogRpcTimeout = 5.0;
// HTTP 响应大小上限(对齐 WAF)
static constexpr size_t kHttpMaxResponse = 10 * 1024 * 1024;
// ============================================================================
// 工具函数
// ============================================================================
static std::string utc_now() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
struct tm tmbuf;
gmtime_r(&t, &tmbuf);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tmbuf);
return buf;
}
static int safe_stoi(const std::string& s, const char* field_name) {
try {
size_t pos = 0;
int val = std::stoi(s, &pos);
if (pos != s.size()) {
throw std::runtime_error(std::string("invalid ") + field_name
+ ": '" + s + "' (trailing characters)");
}
return val;
} catch (const std::runtime_error&) {
throw;
} catch (const std::exception& e) {
throw std::runtime_error(std::string("invalid ") + field_name
+ ": '" + s + "' (" + e.what() + ")");
}
}
// ============================================================================
// INI 解析(白名单模式,与 db2 一致)
// ============================================================================
using IniMap = std::map<std::string, std::map<std::string, std::string>>;
static IniMap parse_ini(const std::string& text) {
IniMap result;
std::string section;
std::istringstream ss(text);
std::string line;
while (std::getline(ss, line)) {
auto b = line.find_first_not_of(" \t\r\n");
if (b == std::string::npos) continue;
line = line.substr(b);
auto e = line.find_last_not_of(" \t\r\n");
if (e != std::string::npos) line = line.substr(0, e + 1);
if (line.empty() || line[0] == '#' || line[0] == ';') continue;
if (line.front() == '[' && line.back() == ']') {
section = line.substr(1, line.size() - 2);
continue;
}
auto eq = line.find('=');
if (eq == std::string::npos || section.empty()) continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
auto trim = [](std::string& s) {
auto bb = s.find_first_not_of(" \t");
auto ee = s.find_last_not_of(" \t");
s = (bb == std::string::npos) ? "" : s.substr(bb, ee - bb + 1);
};
trim(key); trim(val);
result[section][key] = val;
}
return result;
}
// ============================================================================
// 配置
// ============================================================================
struct Config {
std::string listen_host = "127.0.0.1";
int listen_port = 8006;
int threads = 4;
std::string db_host = "127.0.0.1";
int db_port = 8005;
double db_timeout = 10.0;
std::string review_url = "http://127.0.0.1:8007/review";
double review_timeout = 10.0;
std::string gateway_url = "http://127.0.0.1:8443/whoami";
double gateway_timeout = 5.0;
// confs 引导
std::string confs_host = "127.0.0.1";
int confs_port = 8010;
};
static Config load_config(int argc, char* argv[]) {
Config cfg;
// 1. confs 引导地址
if (const char* h = std::getenv("CONFS_HOST")) cfg.confs_host = h;
if (const char* p = std::getenv("CONFS_PORT")) {
cfg.confs_port = safe_stoi(p, "CONFS_PORT");
if (cfg.confs_port < 1 || cfg.confs_port > 65535) {
std::fprintf(stderr, "[dnfs2] invalid CONFS_PORT: %d (must be 1-65535)\n", cfg.confs_port);
std::exit(1);
}
}
// 2. 从 confs 拉取 dnfs.ini(retry_on_refused=true,等待 confs 就绪)
std::string ini_text;
try {
RpcClient confs_client(cfg.confs_host, cfg.confs_port, "dnfs2",
/*timeout=*/5.0, /*retry_on_refused=*/true);
json::value data = confs_client.call("get", {{"name", "dnfs.ini"}});
if (data.is_object() && data.as_object().contains("content")) {
ini_text = std::string(data.as_object().at("content").as_string());
}
} catch (const std::exception& e) {
std::fprintf(stderr, "[dnfs2] 从 confs 获取 dnfs.ini 失败: %s\n", e.what());
std::exit(1);
}
// 3. 解析 INI
IniMap ini = parse_ini(ini_text);
// 校验必需配置项
static const std::pair<const char*, const char*> required_keys[] = {
{"server", "host"},
{"server", "port"},
{"database", "host"},
{"database", "port"},
{"database", "timeout"},
{"ai_review", "url"},
{"ai_review", "timeout"},
{"gateway", "url"},
{"gateway", "timeout"},
};
for (auto& [sec, key] : required_keys) {
auto si = ini.find(sec);
if (si == ini.end() || si->second.find(key) == si->second.end()) {
std::fprintf(stderr, "[dnfs2] 缺少必需配置: [%s] %s\n", sec, key);
std::exit(1);
}
}
auto get = [&](const std::string& sec, const std::string& key,
const std::string& def = "") -> std::string {
auto si = ini.find(sec);
if (si == ini.end()) return def;
auto ki = si->second.find(key);
return (ki == si->second.end()) ? def : ki->second;
};
cfg.listen_host = get("server", "host", cfg.listen_host);
cfg.listen_port = safe_stoi(get("server", "port", std::to_string(cfg.listen_port)), "[server] port");
cfg.db_host = get("database", "host", cfg.db_host);
cfg.db_port = safe_stoi(get("database", "port", std::to_string(cfg.db_port)), "[database] port");
cfg.db_timeout = std::stod(get("database", "timeout", std::to_string(cfg.db_timeout)));
cfg.review_url = get("ai_review", "url", cfg.review_url);
cfg.review_timeout = std::stod(get("ai_review", "timeout", std::to_string(cfg.review_timeout)));
cfg.gateway_url = get("gateway", "url", cfg.gateway_url);
cfg.gateway_timeout = std::stod(get("gateway", "timeout", std::to_string(cfg.gateway_timeout)));
// 4. 环境变量覆盖(Docker 兼容)
if (const char* v = std::getenv("LISTEN_ADDR")) cfg.listen_host = v;
if (const char* v = std::getenv("LISTEN_PORT")) cfg.listen_port = safe_stoi(v, "LISTEN_PORT");
if (const char* v = std::getenv("DB_HOST")) cfg.db_host = v;
if (const char* v = std::getenv("DB_PORT")) cfg.db_port = safe_stoi(v, "DB_PORT");
if (const char* v = std::getenv("REVIEW_URL")) cfg.review_url = v;
if (const char* v = std::getenv("GATEWAY_URL")) cfg.gateway_url = v;
// 5. 命令行参数(最高优先级)
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if ((arg == "--addr" || arg == "-a") && i + 1 < argc) cfg.listen_host = argv[++i];
else if ((arg == "--port" || arg == "-p") && i + 1 < argc) cfg.listen_port = safe_stoi(argv[++i], "--port");
else if ((arg == "--threads" || arg == "-t") && i + 1 < argc) cfg.threads = safe_stoi(argv[++i], "--threads");
}
// 6. 数值范围校验
if (cfg.listen_port < 1 || cfg.listen_port > 65535) {
std::fprintf(stderr, "[dnfs2] invalid listen_port: %d (must be 1-65535)\n", cfg.listen_port);
std::exit(1);
}
if (cfg.db_port < 1 || cfg.db_port > 65535) {
std::fprintf(stderr, "[dnfs2] invalid db_port: %d (must be 1-65535)\n", cfg.db_port);
std::exit(1);
}
if (cfg.threads < 1 || cfg.threads > 256) {
std::fprintf(stderr, "[dnfs2] invalid threads: %d (must be 1-256)\n", cfg.threads);
std::exit(1);
}
if (cfg.db_timeout <= 0 || cfg.db_timeout > 60.0) {
std::fprintf(stderr, "[dnfs2] invalid db_timeout: %f (must be in (0, 60])\n", cfg.db_timeout);
std::exit(1);
}
if (cfg.review_timeout <= 0 || cfg.review_timeout > 60.0) {
std::fprintf(stderr, "[dnfs2] invalid review_timeout: %f (must be in (0, 60])\n", cfg.review_timeout);
std::exit(1);
}
if (cfg.gateway_timeout <= 0 || cfg.gateway_timeout > 60.0) {
std::fprintf(stderr, "[dnfs2] invalid gateway_timeout: %f (must be in (0, 60])\n", cfg.gateway_timeout);
std::exit(1);
}
return cfg;
}
// ============================================================================
// 异常类
// ============================================================================
class DnfsExp : public std::runtime_error {
public:
DnfsExp(const std::string& msg, int code)
: std::runtime_error(msg), status_(code) {}
int status() const noexcept { return status_; }
private:
int status_;
};
// ============================================================================
// 特殊节点校验
// ============================================================================
static const std::vector<std::string> kSpTypes = {"link", "js", "image", "file"};
// 识别特殊节点信封:合法信封返回 object;普通内容返回 nullptr;
// 以 {"_sp" 开头但 JSON 非法视为格式错误(抛异常)。
static json::value parse_sp(const std::string& content) {
if (content.empty()) return nullptr;
size_t start = content.find_first_not_of(" \t\r\n");
if (start == std::string::npos) return nullptr;
std::string s = content.substr(start);
if (s.size() < 6 || s.substr(0, 6) != "{\"_sp\"") return nullptr;
try {
json::value obj = json::parse(s);
if (obj.is_object()) {
const auto& o = obj.as_object();
if (o.contains("_sp") && o.at("_sp").is_string()) {
std::string sp_type = std::string(o.at("_sp").as_string());
for (const auto& t : kSpTypes) {
if (sp_type == t) return obj;
}
}
}
return nullptr;
} catch (const std::exception&) {
throw DnfsExp("特殊节点格式非法(JSON 解析失败)", 400);
}
}
static void validate_sp(const json::object& sp) {
std::string t = std::string(sp.at("_sp").as_string());
if (t == "link") {
if (!sp.contains("url") || !sp.at("url").is_string())
throw DnfsExp("链接节点缺少 url", 400);
std::string url = std::string(sp.at("url").as_string());
if (url.rfind("http://", 0) != 0 && url.rfind("https://", 0) != 0)
throw DnfsExp("链接节点必须是 http/https URL", 400);
if (url.size() > kSpMaxUrl)
throw DnfsExp("链接过长", 400);
} else if (t == "js") {
if (!sp.contains("code") || !sp.at("code").is_string())
throw DnfsExp("前端代码不能为空", 400);
std::string code = std::string(sp.at("code").as_string());
if (code.empty() || code.find_first_not_of(" \t\r\n") == std::string::npos)
throw DnfsExp("前端代码不能为空", 400);
if (code.size() > kSpMaxCode)
throw DnfsExp("前端代码过大(上限 512KB)", 400);
} else if (t == "image") {
if (!sp.contains("data") || !sp.at("data").is_string())
throw DnfsExp("图片节点缺少 data", 400);
std::string data = std::string(sp.at("data").as_string());
if (data.rfind("data:image/", 0) != 0)
throw DnfsExp("图片节点 data 必须是 data:image/ 开头的 DataURL", 400);
if (data.size() > kSpMaxData)
throw DnfsExp("图片过大(上限约 2.6MB)", 400);
} else if (t == "file") {
if (!sp.contains("name") || !sp.at("name").is_string())
throw DnfsExp("文件节点缺少文件名", 400);
std::string name = std::string(sp.at("name").as_string());
if (name.empty() || name.find_first_not_of(" \t\r\n") == std::string::npos)
throw DnfsExp("文件节点缺少文件名", 400);
if (name.size() > 255)
throw DnfsExp("文件名过长", 400);
if (!sp.contains("data") || !sp.at("data").is_string())
throw DnfsExp("文件节点缺少数据", 400);
std::string data = std::string(sp.at("data").as_string());
if (data.empty())
throw DnfsExp("文件节点缺少数据", 400);
if (data.size() > kSpMaxData)
throw DnfsExp("文件过大(上限约 2.6MB)", 400);
}
}
// 特殊节点送审文本:剔除 base64 负载
static std::string sp_review_text(const std::string& title, const json::object& sp) {
std::vector<std::string> parts;
parts.push_back(title);
if (sp.contains("note") && sp.at("note").is_string()) {
std::string note = std::string(sp.at("note").as_string());
if (!note.empty()) parts.push_back(note);
}
std::string t = std::string(sp.at("_sp").as_string());
if (t == "link") {
if (sp.contains("url") && sp.at("url").is_string())
parts.push_back(std::string(sp.at("url").as_string()));
} else if (t == "js") {
if (sp.contains("code") && sp.at("code").is_string()) {
std::string code = std::string(sp.at("code").as_string());
if (code.size() > kSpReviewCodePrefix) code = code.substr(0, kSpReviewCodePrefix);
parts.push_back(code);
}
} else {
if (sp.contains("name") && sp.at("name").is_string())
parts.push_back(std::string(sp.at("name").as_string()));
}
std::string result;
for (size_t i = 0; i < parts.size(); ++i) {
if (i > 0) result += "\n";
result += parts[i];
}
return result;
}
// ============================================================================
// 日志
// ============================================================================
struct LogEntry {
std::string ts;
std::string service;
std::string level;
std::string message;
std::string trace_id;
std::string extra;
};
// ============================================================================
// HTTP 响应辅助
// ============================================================================
static http::response<http::string_body> make_response(
http::status status,
const std::string& body,
const std::string& content_type,
const http::request<http::string_body>& req)
{
http::response<http::string_body> res;
res.result(status);
res.version(req.version());
res.set(http::field::content_type, content_type);
res.keep_alive(req.keep_alive());
// 安全响应头
res.set("X-Frame-Options", "SAMEORIGIN");
res.set("X-XSS-Protection", "1; mode=block");
res.set("X-Content-Type-Options", "nosniff");
res.set("Referrer-Policy", "strict-origin-when-cross-origin");
res.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
res.set("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; "
"style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; "
"font-src 'self'; connect-src 'self'; frame-ancestors 'self'");
res.body() = body;
res.prepare_payload();
return res;
}
static http::response<http::string_body> make_json(
http::status status,
const std::string& json_str,
const http::request<http::string_body>& req)
{
return make_response(status, json_str, "application/json", req);
}
static http::response<http::string_body> make_json_obj(
http::status status,
const json::object& obj,
const http::request<http::string_body>& req)
{
return make_json(status, json::serialize(obj), req);
}
static http::response<http::string_body> make_error(
http::status status,
const std::string& error_msg,
const http::request<http::string_body>& req)
{
json::object obj;
obj["error"] = error_msg;
return make_json_obj(status, obj, req);
}
// ============================================================================
// Dnfs2Server
// ============================================================================
class Dnfs2Server {
public:
explicit Dnfs2Server(Config cfg)
: _cfg(std::move(cfg)),
_rpc(_cfg.db_host, _cfg.db_port, "dnfs2", _cfg.db_timeout,
/*retry_on_refused=*/true),
_log_rpc(_cfg.db_host, _cfg.db_port, "dnfs2-log", kLogRpcTimeout,
/*retry_on_refused=*/false) {
// 初始化异步日志队列
_log_queue = std::make_unique<AsyncQueue<LogEntry>>(
[this](std::vector<LogEntry>&& batch) { _send_log_batch(std::move(batch)); },
AsyncQueueConfig{kLogBatchSize, kLogFlushInterval, kLogQueueMax, 0.5},
[](const std::exception& e, std::vector<LogEntry>&&) {
std::fprintf(stderr, "[dnfs2] 日志队列异常: %s\n", e.what());
}
);
_log_queue->start();
std::printf("[dnfs2] 配置来源: confs (dnfs.ini @ %s:%d)\n",
_cfg.confs_host.c_str(), _cfg.confs_port);
std::printf("[dnfs2] Token 验签: gateway %s\n", _cfg.gateway_url.c_str());
}
~Dnfs2Server() {
if (_log_queue) _log_queue->shutdown();
}
void run() {
HttpServer server(_cfg.listen_host, static_cast<unsigned short>(_cfg.listen_port),
static_cast<unsigned int>(_cfg.threads));
// 请求体大小限制(P1-6 修复)
server.setMaxBodySize(kMaxBodySize);
// 注册路由
server.post("/get_notes", [this](const auto& req, const auto&) { return _handle_get_notes(req); });
server.post("/add_note", [this](const auto& req, const auto&) { return _handle_add_note(req); });
server.post("/delete_note", [this](const auto& req, const auto&) { return _handle_delete_note(req); });
server.post("/update_note", [this](const auto& req, const auto&) { return _handle_update_note(req); });
// 信号处理(优雅关闭)
_server = &server;
std::signal(SIGINT, &Dnfs2Server::_signal_handler);
std::signal(SIGTERM, &Dnfs2Server::_signal_handler);
std::thread stop_watcher([this]() {
while (!_should_stop.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
if (_server) _server->stop();
});
_log("INFO", "dnfs2", "服务启动,监听 http://" + _cfg.listen_host + ":"
+ std::to_string(_cfg.listen_port)
+ "(db=" + _cfg.db_host + ":" + std::to_string(_cfg.db_port)
+ ", ai_review=" + _cfg.review_url + ")");
server.run();
// 确保停止标志置位(处理 server.run() 非信号退出的情况)
_should_stop.store(true);
if (stop_watcher.joinable()) stop_watcher.join();
_log("INFO", "dnfs2", "服务停止");
if (_log_queue) _log_queue->shutdown();
}
private:
Config _cfg;
RpcClient _rpc; // db 调用(retry_on_refused=true,P1-9 修复)
RpcClient _log_rpc; // 日志写入(不重试,失败即丢弃)
std::unique_ptr<AsyncQueue<LogEntry>> _log_queue;
HttpServer* _server = nullptr;
static std::atomic<bool> _should_stop;
static void _signal_handler(int) {
_should_stop.store(true);
}
// ---- 日志 --------------------------------------------------------------
void _log(const std::string& level, const std::string& service,
const std::string& message, const std::string& trace_id = "",
const std::string& extra = "") {
LogEntry e;
e.ts = utc_now();
e.service = service;
e.level = level;
e.message = (message.size() > kLogMaxMessage) ? message.substr(0, kLogMaxMessage) : message;
e.trace_id = trace_id;
e.extra = (extra.size() > kLogMaxExtra) ? extra.substr(0, kLogMaxExtra) : extra;
if (_log_queue) _log_queue->push(std::move(e));
}
void _send_log_batch(std::vector<LogEntry>&& batch) {
for (auto& e : batch) {
try {
json::object values;
values["ts"] = e.ts;
values["service"] = e.service;
values["level"] = e.level;
values["message"] = e.message;
if (!e.trace_id.empty()) values["trace_id"] = e.trace_id;
else values["trace_id"] = nullptr;
if (!e.extra.empty()) values["extra"] = e.extra;
else values["extra"] = nullptr;
json::object payload;
payload["action"] = "insert";
payload["table"] = "Logs";
payload["values"] = values;
_log_rpc.call("execute", payload);
} catch (const std::exception& ex) {
std::fprintf(stderr, "[dnfs2] 日志写入失败: %s\n", ex.what());
}
}
}
// ---- db 调用封装(P0-4 修复:统一检查 success)--------------------------
struct DbResult {
bool success = false;
json::value data; // 成功时为 data 字段
std::string error; // 失败时为错误信息
};
DbResult _call_db(const json::object& payload) {
try {
json::value data = _rpc.call("execute", payload);
return {true, data, ""};
} catch (const RpcError& e) {
return {false, nullptr, e.what()};
} catch (const std::exception& e) {
// 网络错误(连接失败、超时等)
_log("ERROR", "dnfs2", std::string("db 调用失败: ") + e.what());
throw DnfsExp("数据库服务暂时不可用", 502);
}
}
// 查询节点是否存在(P2-N6 修复:db 业务错误抛 500,不误判为"不存在")
bool _note_exists(int64_t id) {
json::object payload;
payload["action"] = "select";
payload["table"] = "Notes";
payload["where"] = json::object{{"id", id}};
payload["limit"] = 1;
DbResult r = _call_db(payload);
if (!r.success) {
_log("ERROR", "dnfs2", "查询节点存在性失败 id=" + std::to_string(id)
+ " err=" + r.error);
throw DnfsExp("查询节点失败", 500);
}
return r.data.is_array() && !r.data.as_array().empty();
}
// ---- Token 验签(通过 gateway /whoami)---------------------------------
struct UserInfo {
std::string user;
bool is_admin = false;
};
// 返回 UserInfo;验签失败返回 nullopt;网络异常抛 DnfsExp(503)
std::optional<UserInfo> decode_token(const std::string& token) {
if (token.empty()) return std::nullopt;
HttpClient http;
http.set_max_response_size(kHttpMaxResponse);
http.set_timeout(static_cast<long>(_cfg.gateway_timeout));
http.set_header("Content-Type: application/json");
http.set_header("Authorization: Bearer " + token);
std::string response_body;
try {
http.post_stream_sync(_cfg.gateway_url, "{}",
[&response_body](const std::string& chunk) {
response_body += chunk;
});
} catch (const std::exception& e) {
_log("WARNING", "dnfs2", std::string("Token 验签失败(gateway 异常): ") + e.what());
throw DnfsExp("认证服务暂时不可用", 503);
}
long code = http.get_response_code();
if (code != 200) {
_log("WARNING", "dnfs2", "Token 验签失败(gateway 返回 " + std::to_string(code) + ")");
throw DnfsExp("认证服务暂时不可用", 503);
}
json::value data;
try {
data = json::parse(response_body);
} catch (const std::exception&) {
_log("WARNING", "dnfs2", "Token 验签失败(gateway 响应非 JSON)");
throw DnfsExp("认证服务暂时不可用", 503);
}
if (!data.is_object()) return std::nullopt;
const auto& obj = data.as_object();
if (!obj.contains("login") || !obj.at("login").is_bool() || !obj.at("login").as_bool()) {
return std::nullopt;
}
UserInfo info;
if (obj.contains("user") && obj.at("user").is_string()) {
info.user = std::string(obj.at("user").as_string());
}
if (obj.contains("is_admin") && obj.at("is_admin").is_bool()) {
info.is_admin = obj.at("is_admin").as_bool();
}
return info;
}
// 从请求头提取 Token
static std::string extract_token(const http::request<http::string_body>& req) {
auto it = req.find(http::field::authorization);
if (it == req.end()) return "";
std::string auth = std::string(it->value());
if (auth.rfind("Bearer ", 0) == 0) {
return auth.substr(7);
}
return "";
}
// ---- AI 内容审核(fail-closed)-----------------------------------------
bool review_content(const std::string& content) {
HttpClient http;
http.set_max_response_size(kHttpMaxResponse);
http.set_timeout(static_cast<long>(_cfg.review_timeout));
http.set_header("Content-Type: application/json");
json::object payload;
payload["content"] = content;
std::string body = json::serialize(payload);
std::string response_body;
try {
http.post_stream_sync(_cfg.review_url, body,
[&response_body](const std::string& chunk) {
response_body += chunk;
});
} catch (const std::exception& e) {
// fail-closed:网络异常拒绝
_log("WARNING", "dnfs2", std::string("AI 审核失败: ") + e.what());
return false;
}
long code = http.get_response_code();
if (code != 200) {
_log("WARNING", "dnfs2", "AI 审核返回 " + std::to_string(code));
return false;
}
json::value data;
try {
data = json::parse(response_body);
} catch (const std::exception&) {
return false; // fail-closed
}
if (!data.is_object()) return false;
const auto& obj = data.as_object();
// invalid 为 false 或 "false" 时表示内容安全
if (auto it = obj.find("invalid"); it != obj.end()) {
const auto& v = it->value();
if (v.is_bool()) return !v.as_bool();
if (v.is_string()) return std::string(v.as_string()) == "false";
}
return false; // 未知响应,fail-closed
}
// ---- JSON 请求体解析 ---------------------------------------------------
static json::object parse_request_body(const http::request<http::string_body>& req) {
const std::string& body = req.body();
if (body.empty()) {
throw DnfsExp("请求体不能为空", 400);
}
json::value val;
try {
val = json::parse(body);
} catch (const std::exception&) {
throw DnfsExp("请求体不是合法 JSON", 400);
}
if (!val.is_object()) {
throw DnfsExp("请求体必须是 JSON 对象", 400);
}
return val.as_object();
}
// 从 JSON 对象获取整数(P1-11 修复:严格类型校验)
static int64_t get_int(const json::object& obj, const std::string& key) {
auto it = obj.find(key);
if (it == obj.end() || !it->value().is_int64()) {
throw DnfsExp("非法参数: " + key + " 必须是整数", 400);
}
return it->value().as_int64();
}
static std::string get_string(const json::object& obj, const std::string& key) {
auto it = obj.find(key);
if (it == obj.end() || !it->value().is_string()) {
throw DnfsExp("非法参数: " + key + " 必须是字符串", 400);
}
std::string s = std::string(it->value().as_string());
if (s.empty()) {
throw DnfsExp("非法参数: " + key + " 不能为空", 400);
}
return s;
}
// ---- 路由 handler ------------------------------------------------------
http::response<http::string_body> _handle_get_notes(
const http::request<http::string_body>& req)
{
try {
json::object body = parse_request_body(req);
// pid 参数校验(P2-15:严格类型校验,非空字符串/None 报错)
int64_t pid = 0;
if (auto it = body.find("pid"); it != body.end()) {
if (!it->value().is_int64()) {
throw DnfsExp("非法参数: pid 必须是整数", 400);
}
pid = it->value().as_int64();
if (pid < 0) {
throw DnfsExp("非法参数: pid 不能为负数", 400);
}
}
json::object payload;
payload["action"] = "select";
payload["table"] = "Notes";
payload["where"] = json::object{{"pid", pid}};
DbResult r = _call_db(payload);
if (!r.success) {
_log("ERROR", "dnfs2", "get_notes 查询失败: " + r.error);
json::object err;
err["error"] = "查询失败";
return make_json_obj(http::status::internal_server_error, err, req);
}
// 解析当前用户(可选):为每个节点附带 can_operate 标记
std::optional<UserInfo> info = std::nullopt;
std::string token = extract_token(req);
if (!token.empty()) {
try {
info = decode_token(token);
} catch (const DnfsExp&) {
info = std::nullopt; // Token 无效时,所有节点无权
}
}
if (r.data.is_array()) {
json::array result = r.data.as_array();
for (auto& item : result) {
if (item.is_object()) {
json::object& note = item.as_object();
bool can_operate = false;
if (info) {
if (info->is_admin) {
can_operate = true;
} else if (note.contains("owner") && note.at("owner").is_string()) {
can_operate = std::string(note.at("owner").as_string()) == info->user;
}
}
note["can_operate"] = can_operate;
}
}
_log("INFO", "dnfs2", "get_notes 查询 pid=" + std::to_string(pid)
+ " 返回 " + std::to_string(result.size()) + " 条");
return make_json(http::status::ok, json::serialize(result), req);
}
json::array empty;
return make_json(http::status::ok, json::serialize(empty), req);
} catch (const DnfsExp& e) {
_log("WARNING", "dnfs2", std::string("get_notes: ") + e.what());
return make_error(static_cast<http::status>(e.status()), e.what(), req);
} catch (const std::exception& e) {
_log("ERROR", "dnfs2", std::string("get_notes 内部错误: ") + e.what());
return make_error(http::status::internal_server_error, "内部错误", req);
}
}
http::response<http::string_body> _handle_add_note(
const http::request<http::string_body>& req)
{
try {
json::object body = parse_request_body(req);
int64_t pid = get_int(body, "pid");
std::string title = get_string(body, "title");
std::string content = get_string(body, "content");
if (pid < 0) {
throw DnfsExp("非法参数: pid 不能为负数", 400);
}
// Token 验签
std::string token = extract_token(req);
auto info = decode_token(token);
if (!info) {
throw DnfsExp("未授权访问", 401);
}
// 父节点是否存在
if (pid != 0 && !_note_exists(pid)) {
throw DnfsExp("父节点不存在", 400);
}
// 特殊节点校验(管理员专属)
json::value sp = parse_sp(content);
if (!sp.is_null()) {
if (!info->is_admin) {
throw DnfsExp("特殊节点仅管理员可发布", 403);
}
validate_sp(sp.as_object());
}
// 内容审核(特殊节点剔除 base64 负载)
std::string review_text_val;
if (!sp.is_null()) {
review_text_val = sp_review_text(title, sp.as_object());
} else {
review_text_val = title + "\n" + content;
}
if (!review_content(review_text_val)) {
_log("WARNING", "dnfs2", "add_note 内容审核不通过 title=" + title);
throw DnfsExp("内容审核不通过", 400);
}
// 执行插入
json::object values;
values["pid"] = pid;
values["owner"] = info->user;
values["title"] = title;
values["content"] = content;
json::object payload;
payload["action"] = "insert";
payload["table"] = "Notes";
payload["values"] = values;
DbResult r = _call_db(payload);
if (!r.success) {
throw DnfsExp("插入节点失败", 400);
}
_log("INFO", "dnfs2", "add_note 成功 title=" + title + " pid=" + std::to_string(pid),
"", json::serialize(json::object{{"user", info->user}}));
json::object result;
result["success"] = true;
return make_json_obj(http::status::ok, result, req);
} catch (const DnfsExp& e) {
_log("WARNING", "dnfs2", std::string("add_note: ") + e.what());
return make_error(static_cast<http::status>(e.status()), e.what(), req);
} catch (const std::exception& e) {
_log("ERROR", "dnfs2", std::string("add_note 内部错误: ") + e.what());
return make_error(http::status::internal_server_error, "内部错误", req);
}
}
http::response<http::string_body> _handle_delete_note(
const http::request<http::string_body>& req)
{
try {
json::object body = parse_request_body(req);
// P1-11 修复:note_id 显式 as_int64() + 类型校验
int64_t note_id = get_int(body, "id");
if (note_id <= 0) {
throw DnfsExp("Note ID is required", 400);
}
// Token 验签
std::string token = extract_token(req);
auto info = decode_token(token);
if (!info) {
throw DnfsExp("Warning: no Authorization", 401);
}
// 检查是否有子节点
json::object child_payload;
child_payload["action"] = "select";
child_payload["table"] = "Notes";
child_payload["where"] = json::object{{"pid", note_id}};
DbResult child_r = _call_db(child_payload);
if (!child_r.success || !child_r.data.is_array()) {
_log("ERROR", "dnfs2", "delete_note 查询子节点失败 id=" + std::to_string(note_id)
+ " err=" + child_r.error);
throw DnfsExp("查询子节点失败", 500);
}
if (!child_r.data.as_array().empty()) {
_log("WARNING", "dnfs2", "delete_note 存在子节点 id=" + std::to_string(note_id));
throw DnfsExp("this node has a child", 400);
}
// 检查所有权
json::object owner_payload;
owner_payload["action"] = "select";
owner_payload["table"] = "Notes";
owner_payload["where"] = json::object{{"id", note_id}};
DbResult owner_r = _call_db(owner_payload);
if (!owner_r.success) {
_log("ERROR", "dnfs2", "delete_note 查询节点失败 id=" + std::to_string(note_id)
+ " err=" + owner_r.error);
throw DnfsExp("查询节点失败", 500);
}
if (!owner_r.data.is_array() || owner_r.data.as_array().empty()) {
throw DnfsExp("节点不存在", 404);
}
if (!info->is_admin) {
const auto& note = owner_r.data.as_array()[0];
std::string owner;
if (note.is_object() && note.as_object().contains("owner")
&& note.as_object().at("owner").is_string()) {
owner = std::string(note.as_object().at("owner").as_string());
}
if (owner != info->user) {
_log("WARNING", "dnfs2", "delete_note 所有权拒绝 id=" + std::to_string(note_id));
throw DnfsExp("It's not your note", 403);
}
}
// 执行删除
json::object delete_payload;
delete_payload["action"] = "delete";
delete_payload["table"] = "Notes";
delete_payload["where"] = json::object{{"id", note_id}};
DbResult r = _call_db(delete_payload);
if (!r.success) {
json::object result;
result["success"] = false;
return make_json_obj(http::status::bad_request, result, req);
}
_log("INFO", "dnfs2", "delete_note 成功 id=" + std::to_string(note_id),
"", json::serialize(json::object{{"user", info->user}}));
json::object result;
result["success"] = true;
return make_json_obj(http::status::ok, result, req);
} catch (const DnfsExp& e) {
_log("WARNING", "dnfs2", std::string("delete_note: ") + e.what());
return make_error(static_cast<http::status>(e.status()), e.what(), req);
} catch (const std::exception& e) {
_log("ERROR", "dnfs2", std::string("delete_note 内部错误: ") + e.what());
return make_error(http::status::internal_server_error, "内部错误", req);
}
}
http::response<http::string_body> _handle_update_note(
const http::request<http::string_body>& req)
{
try {
json::object body = parse_request_body(req);
int64_t id = get_int(body, "id");
int64_t pid = get_int(body, "pid");
std::string title = get_string(body, "title");
std::string content = get_string(body, "content");
if (id <= 0) {
throw DnfsExp("Invalid param", 400);
}
if (pid < 0) {
throw DnfsExp("Invalid param", 400);
}
// Token 验签
std::string token = extract_token(req);
auto info = decode_token(token);
if (!info) {
throw DnfsExp("Warning: no Authorization", 401);
}
// 检查所有权
json::object owner_payload;
owner_payload["action"] = "select";
owner_payload["table"] = "Notes";
owner_payload["where"] = json::object{{"id", id}};
DbResult owner_r = _call_db(owner_payload);
if (!owner_r.success) {
_log("ERROR", "dnfs2", "update_note 查询节点失败 id=" + std::to_string(id)
+ " err=" + owner_r.error);
throw DnfsExp("查询节点失败", 500);
}
if (!owner_r.data.is_array() || owner_r.data.as_array().empty()) {
throw DnfsExp("节点不存在", 404);
}
if (!info->is_admin) {
const auto& note = owner_r.data.as_array()[0];
std::string owner;
if (note.is_object() && note.as_object().contains("owner")
&& note.as_object().at("owner").is_string()) {
owner = std::string(note.as_object().at("owner").as_string());
}
if (owner != info->user) {
_log("WARNING", "dnfs2", "update_note 所有权拒绝 id=" + std::to_string(id));
throw DnfsExp("It's not your note", 403);
}
}
// P1-8 修复:pid≠0 时校验父节点存在
if (pid != 0 && !_note_exists(pid)) {
throw DnfsExp("父节点不存在", 400);
}
// 特殊节点校验
json::value sp = parse_sp(content);
if (!sp.is_null()) {
if (!info->is_admin) {
throw DnfsExp("特殊节点仅管理员可发布", 403);
}
validate_sp(sp.as_object());
}
// 内容审核
std::string review_text_val;
if (!sp.is_null()) {
review_text_val = sp_review_text(title, sp.as_object());
} else {
review_text_val = title + "\n" + content;
}
if (!review_content(review_text_val)) {
_log("WARNING", "dnfs2", "update_note 内容审核不通过 id=" + std::to_string(id)
+ " title=" + title);
throw DnfsExp("review failed", 400);
}
// 执行更新
json::object values;
values["pid"] = pid;
values["title"] = title;
values["content"] = content;
json::object payload;
payload["action"] = "update";
payload["table"] = "Notes";
payload["values"] = values;
payload["where"] = json::object{{"id", id}};
DbResult r = _call_db(payload);
if (!r.success) {
json::object result;
result["success"] = false;
return make_json_obj(http::status::bad_request, result, req);
}
_log("INFO", "dnfs2", "update_note 成功 id=" + std::to_string(id) + " title=" + title,
"", json::serialize(json::object{{"user", info->user}}));
json::object result;
result["success"] = true;
return make_json_obj(http::status::ok, result, req);
} catch (const DnfsExp& e) {
_log("WARNING", "dnfs2", std::string("update_note: ") + e.what());
return make_error(static_cast<http::status>(e.status()), e.what(), req);
} catch (const std::exception& e) {
_log("ERROR", "dnfs2", std::string("update_note 内部错误: ") + e.what());
return make_error(http::status::internal_server_error, "内部错误", req);
}
}
};
// 静态成员定义
std::atomic<bool> Dnfs2Server::_should_stop{false};
// ============================================================================
// main
// ============================================================================
int main(int argc, char* argv[]) {
// curl_global_init 必须在多线程启动前调用(libcurl 要求,防首次 curl_easy_init 竞态)
curl_global_init(CURL_GLOBAL_DEFAULT);
int ret = 0;
try {
Config cfg = load_config(argc, argv);
std::printf("[dnfs2] listen=%s:%d threads=%d\n",
cfg.listen_host.c_str(), cfg.listen_port, cfg.threads);
std::printf("[dnfs2] db=%s:%d ai_review=%s gateway=%s\n",
cfg.db_host.c_str(), cfg.db_port,
cfg.review_url.c_str(), cfg.gateway_url.c_str());
std::fflush(stdout);
Dnfs2Server server(std::move(cfg));
server.run();
} catch (const std::exception& e) {
std::fprintf(stderr, "[dnfs2] fatal: %s\n", e.what());
ret = 1;
}
curl_global_cleanup();
return ret;
}