Files
ECAP_Server/module/Local_Server/server/server.cpp
T
2026-06-16 11:18:22 +08:00

680 lines
25 KiB
C++

#include "Global.h"
#include "Performance_Monitor.h"
#include <algorithm>
#include <cctype>
#include <string_view>
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<const Invalid_Http_Param*>(&e) != nullptr
|| dynamic_cast<const Psc::json_assign_error*>(&e) != nullptr
|| dynamic_cast<const std::invalid_argument*>(&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 extension, std::string fallback = "zip") {
while (!extension.empty() && extension.front() == '.') {
extension.erase(extension.begin());
}
if (extension.empty()) {
extension = std::move(fallback);
}
if (!Is_Safe_Archive_Extension(extension)) {
throw std::invalid_argument("invalid archive extension: " + extension);
}
return extension;
}
std::optional<std::string> Try_Get_String_Field(const JSON& object, const std::string& key) {
const JSON* value = object.get(key);
if (value == nullptr) {
return std::nullopt;
}
if (value->valueType != Psc::String) {
throw std::invalid_argument(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 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& params = 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 = [](const std::string &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](const std::string &path) {
drogon::app().registerHandlerViaRegex(
path,
[return_file](const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&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<void(const drogon::HttpResponsePtr &)> &&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<void(const drogon::HttpResponsePtr &)> &&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<void(const drogon::HttpResponsePtr &)> &&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<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
auto export_request = Parse_Archive_Export_Request(req, Global::instance()->version);
const 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." + 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<void(const drogon::HttpResponsePtr&)>&& cb) {
try {
auto export_request = Parse_Archive_Export_Request(req, Global::instance()->version);
const 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 + "." + 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 + "." + 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<void(const drogon::HttpResponsePtr&)>&& 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<void(const drogon::HttpResponsePtr&)>&& 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<const Invalid_Archive_Error*>(&e) != nullptr
|| dynamic_cast<const std::invalid_argument*>(&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_Type>("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<void(const drogon::HttpResponsePtr&)>&& 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<int>(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<void(const drogon::HttpResponsePtr&)>&& 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);
}
});
}