#include "Global.h" #include "Performance_Monitor.h" #include #include #include namespace { constexpr std::size_t kMaxLogRequestBodySize = 8 * 1024; void Log_Request_Body_If_Small(const drogon::HttpRequestPtr &request) { const auto &body = request->getBody(); if (body.empty()) { return; } if (body.size() <= kMaxLogRequestBodySize) { LOG_INFO << "[request body] " << "path=" << request->path() << ", size=" << body.size() << ", body=" << std::string(body.data(), body.size()); } else { LOG_INFO << "[request body skipped] " << "path=" << request->path() << ", size=" << body.size() << ", max=" << kMaxLogRequestBodySize; } } bool Is_Invalid_Http_Param(const std::exception &e) { return dynamic_cast(&e) != nullptr || dynamic_cast(&e) != nullptr || dynamic_cast(&e) != nullptr; } bool Is_Safe_Version_Directory_Name(std::string_view name) { if (name.empty() || name == "." || name == "..") { return false; } return std::ranges::all_of(name, [](unsigned char ch) { return std::isalnum(ch) != 0 || ch == '.' || ch == '_' || ch == '-'; }); } bool Is_Safe_Archive_Extension(std::string_view extension) { if (extension.empty() || extension.front() == '.' || extension.back() == '.') { return false; } return std::ranges::all_of(extension, [](unsigned char ch) { return std::isalnum(ch) != 0 || ch == '.' || ch == '_' || ch == '-'; }); } std::string Normalize_Archive_Extension(std::string_view extension, std::string_view fallback = "zip") { auto normalized = std::string(extension); while (!normalized.empty() && normalized.front() == '.') { normalized.erase(normalized.begin()); } if (normalized.empty()) { normalized = std::string(fallback); } if (!Is_Safe_Archive_Extension(normalized)) { throw std::invalid_argument("invalid archive extension: " + normalized); } return normalized; } std::optional Try_Get_String_Field(const JSON &object, std::string_view key) { const JSON *value = object.get(key); if (value == nullptr) { return std::nullopt; } if (value->valueType != Psc::String) { throw std::invalid_argument(std::string(key) + " must be a string"); } return value->val; } struct Archive_Export_Request { std::string version; Archive_Write_Options archive; }; Archive_Export_Request Parse_Archive_Export_Request(const drogon::HttpRequestPtr &req, std::string_view default_version) { Archive_Export_Request result; result.version = std::move(default_version); const auto &body = req->getBody(); if (body.empty()) { return result; } auto parsed = Psc::try_parse_json(body.data()); if (!parsed.has_value()) { throw_invalid_http_param("request body is not valid JSON"); } const JSON ¶ms = parsed.value(); if (auto version = Try_Get_String_Field(params, "version")) { if (!version->empty()) { result.version = *version; } } if (!Is_Safe_Version_Directory_Name(result.version)) { throw std::invalid_argument("invalid version name: " + result.version); } if (auto format = Try_Get_String_Field(params, "format")) { if (!format->empty()) { result.archive.format = *format; } } if (auto filter = Try_Get_String_Field(params, "filter")) { result.archive.filter = *filter; } if (auto extension = Try_Get_String_Field(params, "extension")) { if (!extension->empty()) { result.archive.extension = Normalize_Archive_Extension(*extension); } } else { result.archive.extension = Normalize_Archive_Extension(result.archive.extension); } return result; } const Archive_Layout_Rules &Upgrade_Zip_Layout_Rules() { static const Archive_Layout_Rules rules = [] { Archive_Layout_Rules value; value.directories = { { "", {"adsb_cfg.ini", "config.json", "ecap_server", "lib9002config.json"}, {"lib9002", "wwwroot"}, {}, {} }, {"lib9002", {"lib9002.so", "testserver"}, {"env"}, {}, {}} }; return value; }(); return rules; } } // namespace void setupCors(); void Global::init_web_server() { register_http_performance_monitor( http_monitor_config.access_log, http_monitor_config.slow_request_threshold_ms); setupCors(); static auto &web_server = web_server_config; Web_Server &svr = this->svr; auto &da = drogon::app(); auto return_file = [](std::string_view path) { auto res = drogon::HttpResponse::newHttpResponse(); if (std::filesystem::exists(path)) { std::string content; httplib::detail::read_file(path, content); auto content_type = httplib::detail::find_content_type( path, {}, "application/octet-stream"); res->setContentTypeString(content_type); res->setBody(content); // res.set_file_content(path); } else { res->setStatusCode(drogon::k404NotFound); JSON ret = JSON::object(); ret.append({"LOG_POS", LOG_POS}); ret.append({"not_found_path", path}); res->setContentTypeCode(drogon::CT_TEXT_PLAIN); res->setBody(ret.to_json_string()); } return res; }; auto add_path = [return_file](std::string_view path) { drogon::app().registerHandlerViaRegex( std::string(path), [return_file]( const drogon::HttpRequestPtr &req, std::function &&callback) { callback(return_file(web_server.get_true_webapp() + "/index.html")); }, {drogon::Get}); }; drogon::app().registerHandlerViaRegex( R"(^/tiles/.*$)", [return_file]( const drogon::HttpRequestPtr &req, std::function &&callback) { auto path = web_server.get_true_tiles() + req->getPath().substr(6); callback(return_file(path)); }, {drogon::Get}); drogon::app().registerHandlerViaRegex( "/", [return_file]( const drogon::HttpRequestPtr &req, std::function &&callback) { callback(return_file(web_server.get_true_webapp() + "/index.html")); }, {drogon::Get}); add_path(R"(^/settings)"); add_path(R"(^/map)"); add_path(R"(^/dsp)"); add_path(R"(^/aircraftlist)"); drogon::app().registerHandlerViaRegex( R"(^/ui(/.*)?$)", [return_file]( const drogon::HttpRequestPtr &req, std::function &&callback) { std::cout << req->path() << std::endl; auto res = drogon::HttpResponse::newHttpResponse(); auto path = web_server.get_true_webapp(); auto rp = "/" + req->path().substr(3); if (rp == "/") { path += "/index.html"; } else { path += rp; } callback(return_file(path)); }); State_Report::instance(); svr.Get(R"(/get_debug_info)", [](HTTP_Param) { JSON arr = JSON::array(); auto &sp = Global::instance()->mode_acs.data_feed_config.pool_; arr.append(JSON::object( {{"remain_size", sp.remain_size()}, {"free_size", sp.free_size()}})); res->setBody(arr.to_json_string("", "\n", "")); }); mlat.server(this); database_server(this); external_resources_manager.server(this); device_config.server(this); mode_acs.server(this); dsp_config.server(this); // s.w.init(); drogon::app().registerHandler( api + "export_logs", [](const drogon::HttpRequestPtr &req, std::function &&cb) { try { auto export_request = Parse_Archive_Export_Request(req, Global::instance()->version); std::string name = export_request.version; auto version_dir = std::filesystem::path(get_exe_dir() + "/.."); std::string data = archive_directory_to_memory( version_dir / name / "logs", {}, export_request.archive); auto resp = drogon::HttpResponse::newHttpResponse(); resp->setBody(data); resp->setContentTypeString("application/octet-stream"); resp->addHeader("Content-Disposition", "attachment; filename*=UTF-8''" + name + "_logs." + std::string(export_request.archive.extension)); resp->addHeader("ok", "true"); cb(resp); } catch (const std::exception &e) { auto resp = drogon::HttpResponse::newHttpResponse(); resp->addHeader("ok", "false"); Psc::JSON json = JSON::object({{"message", Psc::platform_2_utf8(e.what())}}); resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); resp->setBody(json.to_json_string()); cb(resp); } }); drogon::app().registerHandler( api + "export_version", [](const drogon::HttpRequestPtr &req, std::function &&cb) { try { auto export_request = Parse_Archive_Export_Request(req, Global::instance()->version); std::string name = export_request.version; auto version_dir = std::filesystem::path(get_exe_dir() + "/.."); std::cout << "准备压缩:" << std::endl; std::string data = archive_directory_to_memory( version_dir / name, { "logs/", "uploads/", "drogon_uploads/", "backups/", // ghw的什么文件路径 "stack_trace.txt", "pt.log", "ecap_server.log", "Core", "core", name + ".zip", name + "." + std::string(export_request.archive.extension) }, export_request.archive); std::cout << "压缩成功:" << data.size() << std::endl; auto resp = drogon::HttpResponse::newHttpResponse(); resp->setBody(data); resp->setContentTypeString("application/octet-stream"); resp->addHeader("Content-Disposition", "attachment; filename*=UTF-8''" + name + "." + std::string(export_request.archive.extension)); resp->addHeader("ok", "true"); cb(resp); } catch (const std::exception &e) { auto message = Psc::platform_2_utf8(e.what()); std::cout << "导出异常触发:" << message << " " << LOG_POS << std::endl; auto resp = drogon::HttpResponse::newHttpResponse(); Psc::JSON json = JSON::object({{"message", message}}); resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); resp->setBody(json.to_json_string()); resp->addHeader("ok", "false"); cb(resp); } }); drogon::app().registerHandler(api + "restart_device", [](const drogon:: HttpRequestPtr &req, std::function &&cb) { #ifdef __linux__ std::string root_dir = Psc::get_exe_dir() + "/.."; std::string log_file = fmt::format("{}/restart_device.log", root_dir); std::string cmd = fmt::format( R"(systemd-run --scope --slice=system.slice sudo bash "{}/restart_device.bash" > {} 2>&1 &)", root_dir, log_file); std::cout << "Generated Command: " << cmd << std::endl; std::system(cmd.c_str()); #endif auto resp = drogon::HttpResponse::newHttpResponse(); resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); resp->setBody(R"({"ok":true})"); cb(resp); }); drogon::app().registerHandler( api + "import_version", [](const drogon::HttpRequestPtr &req, std::function &&cb) { try { // ---------- 参数校验 ---------- auto dir = req->getHeader("dir"); if (!Is_Safe_Version_Directory_Name(dir)) { throw std::invalid_argument("invalid or missing header: dir"); } const auto &body = req->getBody(); auto archive_layout_result = validate_archive_layout_from_memory( body.data(), body.size(), Upgrade_Zip_Layout_Rules()); if (!archive_layout_result) { throw Invalid_Archive_Error(archive_layout_result.error()); } auto version_dir = std::filesystem::path(get_exe_dir() + "/.."); // ---------- 路径准备 ---------- auto targetDir = version_dir / dir; std::cout << "========dir====" << dir << std::endl; std::cout << "========version_dir====" << version_dir << std::endl; std::cout << "========targetDir====" << targetDir << std::endl; std::filesystem::create_directories(targetDir); const auto archive_extension = Normalize_Archive_Extension(req->getHeader("archive-extension")); auto archivePath = targetDir / (dir + "." + archive_extension); // ---------- 写 zip ---------- std::ofstream ofs(archivePath, std::ios::binary); if (!ofs) { throw std::runtime_error("open archive failed: " + archivePath.string()); } ofs.write(body.data(), body.size()); if (!ofs) { throw std::runtime_error("write archive failed: " + archivePath.string()); } ofs.close(); if (!ofs) { throw std::runtime_error("close archive failed: " + archivePath.string()); } #ifdef _WIN32 std::string root_dir = version_dir.string(); std::string old_version = Global::instance()->version; // Global version info std::string new_version = dir; std::string log_file = fmt::format( "{}/upgrade_{}_to_{}.log", root_dir, old_version, new_version); std::string cmd = fmt::format( R"(powershell -NoProfile -ExecutionPolicy Bypass -Command "& {{ . '{}/upgrade.legacy' '{}' '{}' > '{}' 2>&1 }}" )", root_dir, old_version, new_version, log_file); #else std::string root_dir = version_dir.string(); std::string old_version = Global::instance()->version; // Global version info std::string new_version = dir; std::string log_file = fmt::format( "{}/upgrade_{}_to_{}.log", root_dir, old_version, new_version); std::string cmd = fmt::format( R"(systemd-run --scope --slice=system.slice sudo bash "{}/upgrade.bash" "{}" "{}" > {} 2>&1 &)", root_dir, old_version, new_version, log_file); #endif std::cout << "Generated Command: " << cmd << std::endl; std::system(cmd.c_str()); // ---------- 成功响应 ---------- auto resp = drogon::HttpResponse::newHttpResponse(); resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); resp->setBody(R"({"ok":true})"); cb(resp); } catch (const std::exception &e) { auto resp = drogon::HttpResponse::newHttpResponse(); const bool invalid_input = dynamic_cast(&e) != nullptr || dynamic_cast(&e) != nullptr; resp->setStatusCode(invalid_input ? drogon::k400BadRequest : drogon::k500InternalServerError); resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); auto reason = Psc::platform_2_utf8(e.what()); std::cout << "异常" << reason << std::endl; Psc::JSON json = JSON::object({{"ok", false}, {"error", reason}}); resp->setBody(json.to_json_string()); cb(resp); } }, {drogon::Post}); svr.Post(api + "get_debug_log", [](HTTP_Param) { CHECK_JSON_PARAM HTTP_REQUIRE_VALUE( start_serial_number, params.try_get_number("serial_number")) auto log = base64_encode(debug_logger_info_manager.get_info(start_serial_number)); JSON ret = JSON::object(); ret.append({"log", log}); ret.append( {"serial_number", debug_logger_info_manager.get_cur_serial_number()}); ret.append({"start_serial_number", start_serial_number}); res->setBody(warp(ret).to_json_string()); }); svr.Post(api + "get_general_info", [](HTTP_Param) { auto g = Global::instance(); // auto &db = *Global::instance()->total_source(); auto ret = JSON::object(); // ret.append({"total_source key", db.key}); // ret.append({"飞机总数", db.get_aircraft_num()}); // ret.append({"解出位置的数量", db.have_pos_aircraft_num}); JSON arr = JSON::object(); for (auto &ds: g->mode_acs.data_source_config.map.list()) { if (!ds->enable) continue; arr.append({ds->key, ds->statistic_json()}); } ret.append({"统计信息", arr}); // ret.append({"基站信息", base_station.to_Json()}); res->setBody(warp(ret).to_json_string()); }); svr.Post(api + "version", [this](HTTP_Param) { auto ret = JSON::object(); ret.append({"version", version}); res->setBody(warp(ret).to_json_string()); }); auto now = std::chrono::system_clock::now(); start_server_time = std::chrono::system_clock::to_time_t(now); svr.Post(api + "start_device_time", [this](HTTP_Param) { auto ret = JSON::object(); ret.append({"start_device_time", start_server_time}); res->setBody(warp(ret).to_json_string()); }); da.setDefaultHandler( [](const drogon::HttpRequestPtr &req, std::function &&callback) { LOG_WARN << "[http route not found] " << "path=" << req->path(); auto res = drogon::HttpResponse::newHttpResponse(); res->setStatusCode(drogon::k404NotFound); res->setBody("not found"); res->addHeader("Content-Type", "text/plain; charset=utf-8"); callback(res); }); da.setCustomErrorHandler( [](drogon::HttpStatusCode status, const drogon::HttpRequestPtr &req) -> drogon::HttpResponsePtr { LOG_WARN << "[http error] " << "path=" << req->path() << ", status=" << static_cast(status); auto res = drogon::HttpResponse::newHttpResponse(); res->setStatusCode(status); res->setBody("http error"); res->addHeader("Content-Type", "text/plain; charset=utf-8"); return res; }); da.setExceptionHandler( [](const std::exception &e, const drogon::HttpRequestPtr &req, std::function &&callback) { auto res = drogon::HttpResponse::newHttpResponse(); if (Is_Invalid_Http_Param(e)) { LOG_WARN << "[invalid http parameters] " << "path=" << req->path() << ", detail=" << e.what(); Log_Request_Body_If_Small(req); res->setStatusCode(drogon::k400BadRequest); res->setContentTypeCode(drogon::CT_APPLICATION_JSON); res->setBody(R"({"code":400,"message":"invalid http parameters"})"); } else { LOG_ERROR << "[handler exception] " << "path=" << req->path() << ", what=" << e.what(); res->setStatusCode(drogon::k500InternalServerError); res->setContentTypeCode(drogon::CT_APPLICATION_JSON); res->setBody(R"({"code":500,"message":"internal server error"})"); } callback(res); }); da.setUploadPath(get_exe_dir() + "/drogon_uploads"); std::cout << "上传路径: " << da.getUploadPath() << std::endl; init_ok = true; } void setupCors() { // Register sync advice to handle CORS preflight (OPTIONS) requests drogon::app().registerSyncAdvice( [](const drogon::HttpRequestPtr &req) -> drogon::HttpResponsePtr { if (req->method() == drogon::HttpMethod::Options) { auto resp = drogon::HttpResponse::newHttpResponse(); // Set Access-Control-Allow-Origin header based on the Origin // request header const auto &origin = req->getHeader("Origin"); if (!origin.empty()) { resp->addHeader("Access-Control-Allow-Origin", origin); } // Set Access-Control-Allow-Methods based on the requested method const auto &requestMethod = req->getHeader("Access-Control-Request-Method"); if (!requestMethod.empty()) { resp->addHeader("Access-Control-Allow-Methods", requestMethod); } // Allow credentials to be included in cross-origin requests resp->addHeader("Access-Control-Allow-Credentials", "true"); // Set allowed headers from the Access-Control-Request-Headers // header const auto &requestHeaders = req->getHeader("Access-Control-Request-Headers"); if (!requestHeaders.empty()) { resp->addHeader("Access-Control-Allow-Headers", requestHeaders); } return std::move(resp); } return {}; }); // Register post-handling advice to add CORS headers to all responses drogon::app().registerPostHandlingAdvice( [](const drogon::HttpRequestPtr &req, const drogon::HttpResponsePtr &resp) -> void { // Set Access-Control-Allow-Origin based on the Origin request // header const auto &origin = req->getHeader("Origin"); if (!origin.empty()) { resp->addHeader("Access-Control-Allow-Origin", origin); } // Reflect the requested Access-Control-Request-Method back in the // response const auto &requestMethod = req->getHeader("Access-Control-Request-Method"); if (!requestMethod.empty()) { resp->addHeader("Access-Control-Allow-Methods", requestMethod); } // Allow credentials to be included in cross-origin requests resp->addHeader("Access-Control-Allow-Credentials", "true"); // Reflect the requested Access-Control-Request-Headers back const auto &requestHeaders = req->getHeader("Access-Control-Request-Headers"); if (!requestHeaders.empty()) { resp->addHeader("Access-Control-Allow-Headers", requestHeaders); } }); }