diff --git a/confs/configs/gateway.ini b/confs/configs/gateway.ini index 8f3ead1..8b79a5b 100644 --- a/confs/configs/gateway.ini +++ b/confs/configs/gateway.ini @@ -9,7 +9,7 @@ tls = off [Proxy] default = http://127.0.0.1:9200 closed = http://127.0.0.1:8005;http://127.0.0.1:8007; -opened = http://127.0.0.1:9200;http://127.0.0.1:8006; +opened = http://127.0.0.1:9200;http://127.0.0.1:8006;http://127.0.0.1:5200; [Token] # Token 有效期(秒),默认 3600 = 1 小时 diff --git a/core/Makefile b/core/Makefile index 399c883..8f44c44 100644 --- a/core/Makefile +++ b/core/Makefile @@ -16,7 +16,7 @@ CPP:= g++ INCS:= -I include/ -MY_LIBS := -L/usr/lib -lus_th -lus_http +MY_LIBS := -L/usr/lib -lus_th -lus_http -lus_httpclient -lcurl LIBS := -lpthread -lssl -lcrypto -lboost_program_options -lboost_json -lboost_thread -lboost_filesystem CPPFLAGS := --std=c++20 -O3 -pipe #-Wall -Werror diff --git a/core/src/main/HttpTask.cpp b/core/src/main/HttpTask.cpp index acae6de..2a0ad55 100644 --- a/core/src/main/HttpTask.cpp +++ b/core/src/main/HttpTask.cpp @@ -1,6 +1,9 @@ #include +#include #include #include +#include +#include #include "main/HttpTask.hh" #include "main/pmc_mtd.hh" #include "main/subsys_call.hh" @@ -126,6 +129,78 @@ static std::string url_decode(const std::string& str) { namespace process_http_server { + /* 从 Cookie 头中解析指定名称的值(找不到返回空串) */ + static std::string get_cookie_value(const std::string& cookie_header, const std::string& name) { + size_t pos = 0; + while (pos <= cookie_header.size()) { + size_t end = cookie_header.find(';', pos); + if (end == std::string::npos) end = cookie_header.size(); + std::string pair = cookie_header.substr(pos, end - pos); + size_t eq = pair.find('='); + if (eq != std::string::npos) { + std::string k = pair.substr(0, eq); + size_t b = k.find_first_not_of(" \t"); + size_t e = k.find_last_not_of(" \t"); + if (b != std::string::npos) k = k.substr(b, e - b + 1); + if (k == name) return pair.substr(eq + 1); + } + if (end == cookie_header.size()) break; + pos = end + 1; + } + return ""; + } + + /* 管理员验证:调用 gateway 的 /admin/verify 验签端点,验证请求携带的 Token 是否为有效管理员。 + * gateway 是 Token 签发与验证的唯一权威(RSA 验签 + is_admin + 过期检查)。 + * gateway 地址由环境变量 GATEWAY_HOST/GATEWAY_PORT 配置,默认 127.0.0.1:8443。 + * gateway 不可用或响应异常一律视为未授权(fail-closed)。 */ + static bool verify_admin_token(const http::request& req) { + try { + /* 提取 Token:Authorization 头优先,回退 Cookie */ + std::string raw; + auto it = req.base().find(http::field::authorization); + if (it != req.base().end()) { + raw = std::string(it->value()); + } else { + auto cit = req.base().find(http::field::cookie); + if (cit != req.base().end()) { + std::string cookie_token = get_cookie_value(std::string(cit->value()), "q4_token"); + if (!cookie_token.empty()) + raw = "Bearer " + cookie_token; + } + } + if (raw.empty() || raw.substr(0, 7) != "Bearer ") return false; + + /* gateway 地址 */ + std::string host = "127.0.0.1"; + int port = 8443; + if (const char* h = std::getenv("GATEWAY_HOST")) host = h; + if (const char* p = std::getenv("GATEWAY_PORT")) { + int v = std::atoi(p); + if (v > 0) port = v; + } + + std::string url = "http://" + host + ":" + std::to_string(port) + "/admin/verify"; + + pmc::net::HttpClient client; + client.set_header("Authorization: " + raw); + std::string body; + client.post_stream_sync(url, "", [&](const std::string& chunk) { body += chunk; }); + + auto jv = boost::json::parse(body); + if (!jv.is_object()) return false; + auto& obj = jv.as_object(); + auto it_success = obj.find("success"); + auto it_admin = obj.find("is_admin"); + if (it_success == obj.end() || it_admin == obj.end()) return false; + if (!it_success->value().is_bool() || !it_admin->value().is_bool()) return false; + return it_success->value().as_bool() && it_admin->value().as_bool(); + } + catch (const std::exception&) { + return false; + } + } + /* ---- 注册POST路由 ---- */ void registerRoutes(pmc::net::HttpServer& server) { server.post("/list", [](const auto& req, const auto& params) { @@ -199,6 +274,14 @@ namespace process_http_server { res.version(req.version()); res.set(http::field::content_type, "application/json"); + /* 管理员认证:调用 gateway 验签,未授权直接拒绝 */ + if (!verify_admin_token(req)) { + res.result(http::status::unauthorized); + res.body() = buildJsonResponse(false, "Unauthorized: admin token required"); + res.prepare_payload(); + return res; + } + try { std::string target = getTarget(params); @@ -226,6 +309,14 @@ namespace process_http_server { res.version(req.version()); res.set(http::field::content_type, "application/json"); + /* 管理员认证:调用 gateway 验签,未授权直接拒绝 */ + if (!verify_admin_token(req)) { + res.result(http::status::unauthorized); + res.body() = buildJsonResponse(false, "Unauthorized: admin token required"); + res.prepare_payload(); + return res; + } + try { std::string target = getTarget(params); std::string command = getExecCommand(params); @@ -271,6 +362,14 @@ namespace process_http_server { res.version(req.version()); res.set(http::field::content_type, "application/json"); + /* 管理员认证:调用 gateway 验签,未授权直接拒绝 */ + if (!verify_admin_token(req)) { + res.result(http::status::unauthorized); + res.body() = buildJsonResponse(false, "Unauthorized: admin token required"); + res.prepare_payload(); + return res; + } + try { std::string target = getTarget(params); int pid = getPid(params); diff --git a/gateway/src/main.cpp b/gateway/src/main.cpp index 8c7422b..4e1d13c 100644 --- a/gateway/src/main.cpp +++ b/gateway/src/main.cpp @@ -321,6 +321,11 @@ http::response handleWhoami( const http::request& req, const std::unordered_map& params); +/* 内部服务间管理员验签视图:供 core 等内部服务调用,验证 Token 是否为有效管理员 */ +http::response handleAdminVerify( + const http::request& req, + const std::unordered_map& params); + /* 代理 captcha 图形验证码:pymsg 调 captcha 服务,返回 PNG 图片 */ http::response handleCaptchaImage( const http::request& req, @@ -871,6 +876,12 @@ void registerRoutes(HttpServer &server) { [](const auto& req, const auto& params) { return handleWhoami(req, params); }); }); + /* 内部服务间管理员验签视图(供 core 等调用,须在 post("*") 通配符之前注册) */ + server.post("/admin/verify", [](const auto& req, const auto& params) -> http::response { + return wrapperHttp(req, params, + [](const auto& req, const auto& params) { return handleAdminVerify(req, params); }); + }); + /* 代理 captcha 图形验证码(须在 get("*") 通配符之前注册) */ server.get("/captcha/image", [](const auto& req, const auto& params) -> http::response { return wrapperHttp(req, params, @@ -1323,6 +1334,132 @@ http::response handleWhoami( return res; } +/* 内部服务间管理员验签视图:POST /admin/verify + * + * 供 core 等内部服务调用,验证请求携带的 Token 是否为有效且未过期的管理员。 + * Token 来源:Authorization: Bearer 优先,回退 Cookie q4_token。 + * gateway 是 Token 签发与验证的唯一权威,core 通过本接口完成鉴权,避免密钥共享。 + * + * 响应:{ success, is_admin, user } + * - success=true, is_admin=true → 有效管理员(放行) + * - success=true, is_admin=false → 非管理员(拒绝) + * - success=false → 无 Token / 验签失败 / 过期(拒绝) + * 验签失败一律返回 HTTP 401(fail-closed),供内部服务快速判拒绝。 */ +http::response handleAdminVerify( + const http::request& req, + const std::unordered_map& params) +{ + http::response res; + res.version(req.version()); + res.set(http::field::content_type, "application/json"); + res.set("Connection", "close"); + + auto _PUBL = MyRSA::Public_Key::from_pem(_KEYGEN->get_public_key_pem()); + + /* 获取 Token:Authorization 头优先,回退 Cookie */ + std::string raw; + auto it_auth = req.base().find("Authorization"); + if (it_auth != req.base().end()) { + raw = std::string(it_auth->value()); + } else { + auto cit = req.base().find("Cookie"); + if (cit != req.base().end()) { + std::string cookie_token = get_cookie_value(std::string(cit->value()), "q4_token"); + if (!cookie_token.empty()) + raw = "Bearer " + cookie_token; + } + } + + /* 无 Token 或格式错误:按未授权处理 */ + if (raw.empty() || raw.substr(0, 7) != "Bearer ") { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + + raw = raw.substr(7); + size_t idx = raw.find("_"); + if (idx == std::string::npos) { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + + std::string data = raw.substr(0, idx); + std::string sign = raw.substr(idx + 1); + std::string auth; + try { auth = Base64::base64_decode_to_string(data); } + catch (const std::exception&) { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + + /* RSA 验签:失败按未授权处理 */ + if (!_PUBL.Verify(auth, sign)) { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + + /* 解析 Token payload */ + json::value jv_t; + try { jv_t = json::parse(auth); } + catch (const std::exception&) { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + if (!jv_t.is_object()) { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + json::object& obj = jv_t.as_object(); + + /* 过期检查:created_at + keep_time 超过当前时间按无效处理 */ + auto t = json::value_to(obj["created_at"]); + auto keep_time_val = json::value_to(obj["keep_time"]); + auto created_tp = LocalTime::parse(t); + if (created_tp) { + auto now = std::chrono::system_clock::now(); + auto expire_tp = *created_tp + std::chrono::seconds(keep_time_val); + if (now > expire_tp) { + res.result(http::status::unauthorized); + res.body() = R"({"success": false, "is_admin": false})"; + res.prepare_payload(); + return res; + } + } + + /* 管理员判定:旧 Token 未携带 is_admin 时默认为 false */ + bool is_admin = false; + if (obj.contains("is_admin")) { + auto& v = obj["is_admin"]; + if (v.is_bool()) is_admin = v.as_bool(); + } + + std::string username; + try { username = json::value_to(obj["user"]); } + catch (...) {} + + json::value jv_r = { + {"success", true}, + {"is_admin", is_admin}, + {"user", username} + }; + res.result(http::status::ok); + res.body() = json::serialize(jv_r); + res.prepare_payload(); + return res; +} + /* 刷新 Token:POST /refresh_token * * 接受当前有效 Token(即使已过期但在宽限期内),签发新 Token。 diff --git a/static/public/mgmt/features/process.html b/static/public/mgmt/features/process.html index 27241ee..7adad73 100644 --- a/static/public/mgmt/features/process.html +++ b/static/public/mgmt/features/process.html @@ -33,7 +33,7 @@
-
输入模块名:
+
输入命令:
@@ -41,25 +41,124 @@ diff --git a/static/public/mgmt/mgmt.html b/static/public/mgmt/mgmt.html index dd54a8a..f8403ae 100644 --- a/static/public/mgmt/mgmt.html +++ b/static/public/mgmt/mgmt.html @@ -9,7 +9,39 @@