bonus-edge-proxy/src/web/web_server.cc

690 lines
24 KiB
C++
Raw Normal View History

2025-10-15 14:27:21 +08:00
#include "web_server.h"
2025-11-03 09:37:54 +08:00
#include <fstream>
#include <sstream>
#include "config/config_manager.h"
2025-11-04 19:46:01 +08:00
#include "nlohmann/json.hpp"
2025-10-15 14:27:21 +08:00
#include "spdlog/spdlog.h"
2025-11-04 14:52:43 +08:00
WebServer::WebServer(SystemMonitor::SystemMonitor &monitor,
DeviceManager &deviceManager, LiveDataCache &liveDataCache,
AlarmService &alarm_service, uint16_t port)
: crow::Crow<crow::CORSHandler>(), m_monitor(monitor),
m_device_manager(deviceManager), m_live_data_cache(liveDataCache),
m_alarm_service(alarm_service), m_port(port) {
auto &cors = this->get_middleware<crow::CORSHandler>();
2025-11-03 09:37:54 +08:00
cors.global()
.origin("*")
.headers("Content-Type", "Authorization")
.methods("GET"_method, "POST"_method, "OPTIONS"_method);
this->loglevel(crow::LogLevel::Warning);
setup_routes();
2025-10-15 14:27:21 +08:00
}
2025-11-03 09:37:54 +08:00
WebServer::~WebServer() { stop(); }
2025-10-15 14:27:21 +08:00
void WebServer::start() {
2025-11-03 09:37:54 +08:00
if (m_thread.joinable()) {
spdlog::warn("Web server is already running.");
return;
}
m_thread = std::thread([this]() {
spdlog::info("Starting Web server on port {}", m_port);
this->bindaddr("0.0.0.0").port(m_port).run();
spdlog::info("Web server has stopped.");
});
2025-10-15 14:27:21 +08:00
}
void WebServer::stop() {
2025-11-03 09:37:54 +08:00
crow::Crow<crow::CORSHandler>::stop();
if (m_thread.joinable()) {
m_thread.join();
}
2025-10-15 14:27:21 +08:00
}
2025-11-04 19:46:01 +08:00
void WebServer::set_shutdown_handler(std::function<void()> handler) {
m_shutdown_handler = handler;
}
bool WebServer::validate_video_config(const std::string &json_string,
std::string &error_message) {
try {
auto config = nlohmann::json::parse(json_string);
if (!config.is_object()) {
error_message = "Root is not an object.";
return false;
}
// 1. 验证 'video_service'
if (!config.contains("video_service") ||
!config["video_service"].is_object()) {
error_message = "Missing 'video_service' object.";
return false;
}
if (!config["video_service"].contains("enabled") ||
!config["video_service"]["enabled"].is_boolean()) {
error_message = "'video_service' must have 'enabled' (boolean).";
return false;
}
// 2. 验证 'video_streams'
if (!config.contains("video_streams") ||
!config["video_streams"].is_array()) {
error_message = "Missing 'video_streams' array.";
return false;
}
// 3. 验证 'video_streams' 中的每个元素
for (const auto &stream : config["video_streams"]) {
if (!stream.is_object()) {
error_message = "Item in 'video_streams' is not an object.";
return false;
}
// 检查必需的键
for (const char *key : {"id", "input_url", "module_type"}) {
if (!stream.contains(key) || !stream[key].is_string() ||
stream[key].get<std::string>().empty()) {
error_message =
"Stream missing or invalid key: '" + std::string(key) + "'.";
return false;
}
}
if (!stream.contains("enabled") || !stream["enabled"].is_boolean()) {
error_message = "Stream missing or invalid key: 'enabled' (boolean).";
return false;
}
if (!stream.contains("module_config") ||
!stream["module_config"].is_object()) {
error_message = "Stream missing 'module_config' (object).";
return false;
}
// 4. 验证 'module_config' 的关键字段
const auto &mod_cfg = stream["module_config"];
for (const char *key : {"model_path", "label_path"}) {
if (!mod_cfg.contains(key) || !mod_cfg[key].is_string() ||
mod_cfg[key].get<std::string>().empty()) {
error_message =
"module_config missing or empty key: '" + std::string(key) + "'.";
return false;
}
}
if (!mod_cfg.contains("class_num") ||
!mod_cfg["class_num"].is_number_integer() ||
mod_cfg["class_num"].get<int>() <= 0) {
error_message = "module_config missing or invalid 'class_num' (must be "
"integer > 0).";
return false;
}
if (!mod_cfg.contains("rknn_thread_num") ||
!mod_cfg["rknn_thread_num"].is_number_integer()) {
error_message = "module_config missing 'rknn_thread_num' (integer).";
return false;
}
}
return true; // 所有检查通过
} catch (const nlohmann::json::parse_error &e) {
error_message = "Invalid JSON: " + std::string(e.what());
return false;
} catch (const std::exception &e) {
error_message = "Validation error: " + std::string(e.what());
return false;
}
}
2025-11-05 14:50:25 +08:00
bool WebServer::validate_main_config(const std::string &json_string,
std::string &error_message) {
try {
auto config = nlohmann::json::parse(json_string);
if (!config.is_object()) {
error_message = "Root is not an object.";
return false;
}
// 1. 必需的字符串类型
for (const char *key :
{"device_id", "config_base_path", "mqtt_broker",
"mqtt_client_id_prefix", "data_storage_db_path", "data_cache_db_path",
"log_level", "alarm_rules_path", "piper_executable_path",
"piper_model_path", "video_config_path"}) {
if (!config.contains(key) || !config[key].is_string() ||
config[key].get<std::string>().empty()) {
error_message =
"Missing or invalid/empty string key: '" + std::string(key) + "'.";
return false;
}
}
// 2. 必需的整数类型
if (!config.contains("web_server_port") ||
!config["web_server_port"].is_number_integer()) {
error_message = "Missing or invalid 'web_server_port' (must be integer).";
return false;
}
// 3. 必需的数组类型
if (!config.contains("tcp_server_ports") ||
!config["tcp_server_ports"].is_array()) {
error_message = "Missing or invalid 'tcp_server_ports' (must be array).";
return false;
}
for (const auto &port : config["tcp_server_ports"]) {
if (!port.is_number_integer()) {
error_message = "Item in 'tcp_server_ports' is not an integer.";
return false;
}
}
return true; // 所有检查通过
} catch (const nlohmann::json::parse_error &e) {
error_message = "Invalid JSON: " + std::string(e.what());
return false;
} catch (const std::exception &e) {
error_message = "Validation error: " + std::string(e.what());
return false;
}
}
2025-11-04 19:46:01 +08:00
2025-11-05 14:50:25 +08:00
void WebServer::setup_routes() {
2025-11-04 19:46:01 +08:00
2025-11-03 09:37:54 +08:00
CROW_ROUTE((*this), "/api/system/id").methods("GET"_method)([this] {
auto deviceID = ConfigManager::getInstance().getDeviceID();
crow::json::wvalue response;
response["deviceID"] = deviceID;
return response;
});
CROW_ROUTE((*this), "/api/system/status").methods("GET"_method)([this] {
auto cpu_util = m_monitor.getCpuUtilization();
auto mem_info = m_monitor.getMemoryInfo();
crow::json::wvalue response;
response["cpu_usage_percentage"] = cpu_util.totalUsagePercentage;
response["memory_total_kb"] = mem_info.total_kb;
response["memory_free_kb"] = mem_info.available_kb;
response["memory_usage_percentage"] =
(mem_info.total_kb > 0)
? (1.0 -
static_cast<double>(mem_info.available_kb) / mem_info.total_kb) *
100.0
2025-10-15 14:27:21 +08:00
: 0.0;
2025-10-16 17:05:24 +08:00
2025-11-03 09:37:54 +08:00
return response;
});
2025-10-24 14:19:33 +08:00
2025-11-03 09:37:54 +08:00
CROW_ROUTE((*this), "/api/devices").methods("GET"_method)([this] {
auto devices_info = m_device_manager.get_all_device_info();
2025-10-16 17:05:24 +08:00
2025-11-03 09:37:54 +08:00
std::vector<crow::json::wvalue> devices_json;
2025-11-04 14:52:43 +08:00
for (const auto &info : devices_info) {
2025-11-03 09:37:54 +08:00
crow::json::wvalue device_obj;
2025-10-16 17:05:24 +08:00
2025-11-03 09:37:54 +08:00
device_obj["id"] = info.id;
device_obj["type"] = info.type;
device_obj["is_running"] = info.is_running;
2025-10-24 14:19:33 +08:00
2025-11-03 09:37:54 +08:00
crow::json::wvalue details_obj;
2025-11-04 14:52:43 +08:00
for (const auto &pair : info.connection_details) {
2025-11-03 09:37:54 +08:00
details_obj[pair.first] = pair.second;
}
device_obj["connection_details"] = std::move(details_obj);
2025-10-24 14:19:33 +08:00
2025-11-03 09:37:54 +08:00
devices_json.push_back(std::move(device_obj));
}
2025-11-04 17:51:37 +08:00
auto res = crow::response(crow::json::wvalue(devices_json));
res.set_header("Content-Type", "application/json");
return res;
2025-11-03 09:37:54 +08:00
});
2025-10-24 14:19:33 +08:00
2025-11-03 09:37:54 +08:00
CROW_ROUTE((*this), "/api/data/latest").methods("GET"_method)([this] {
auto latest_data_map = m_live_data_cache.get_all_data();
crow::json::wvalue response;
2025-11-04 14:52:43 +08:00
for (const auto &pair : latest_data_map) {
2025-11-03 09:37:54 +08:00
response[pair.first] = crow::json::load(pair.second);
}
2025-11-04 17:51:37 +08:00
auto res = crow::response(response);
res.set_header("Content-Type", "application/json");
return res;
2025-11-03 09:37:54 +08:00
});
CROW_ROUTE((*this), "/api/alarms/active").methods("GET"_method)([this] {
try {
auto json_string = m_alarm_service.getActiveAlarmsJson().dump();
auto res = crow::response(200, json_string);
res.set_header("Content-Type", "application/json");
return res;
2025-11-04 14:52:43 +08:00
} catch (const std::exception &e) {
2025-11-03 09:37:54 +08:00
spdlog::error("Error processing /api/alarms/active: {}", e.what());
crow::json::wvalue error_resp;
error_resp["error"] = "Failed to retrieve active alarms.";
auto res = crow::response(500, error_resp.dump());
res.set_header("Content-Type", "application/json");
return res;
}
});
CROW_ROUTE((*this), "/api/alarms/history")
2025-11-04 14:52:43 +08:00
.methods("GET"_method)([this](const crow::request &req) {
2025-11-03 09:37:54 +08:00
int limit = 100;
2025-10-24 14:19:33 +08:00
if (req.url_params.get("limit")) {
2025-11-03 09:37:54 +08:00
try {
limit = std::stoi(req.url_params.get("limit"));
2025-11-04 14:52:43 +08:00
} catch (const std::exception &) { /* ignore invalid */
2025-11-03 09:37:54 +08:00
}
2025-10-24 14:19:33 +08:00
}
2025-11-04 14:52:43 +08:00
if (limit <= 0)
limit = 100;
2025-10-24 14:19:33 +08:00
try {
2025-11-03 09:37:54 +08:00
auto json_string = m_alarm_service.getAlarmHistoryJson(limit).dump();
2025-10-24 14:19:33 +08:00
2025-11-03 09:37:54 +08:00
auto res = crow::response(200, json_string);
res.set_header("Content-Type", "application/json");
return res;
2025-10-24 14:19:33 +08:00
2025-11-04 14:52:43 +08:00
} catch (const std::exception &e) {
2025-11-03 09:37:54 +08:00
spdlog::error("Error processing /api/alarms/history: {}", e.what());
crow::json::wvalue error_resp;
error_resp["error"] = "Failed to retrieve alarm history.";
2025-10-24 14:19:33 +08:00
2025-11-03 09:37:54 +08:00
auto res = crow::response(500, error_resp.dump());
res.set_header("Content-Type", "application/json");
return res;
2025-10-24 14:19:33 +08:00
}
2025-11-03 09:37:54 +08:00
});
CROW_ROUTE((*this), "/api/alarms/reload").methods("POST"_method)([this]() {
spdlog::info("Web API: Received request to reload alarm rules...");
bool success = m_alarm_service.reload_rules();
if (success) {
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] = "Alarm rules reloaded successfully.";
auto res = crow::response(200, response_json.dump());
res.set_header("Content-Type", "application/json");
return res;
} else {
crow::json::wvalue error_json;
error_json["status"] = "error";
error_json["message"] =
"Failed to reload alarm rules. Check service logs for details.";
auto res = crow::response(500, error_json.dump());
res.set_header("Content-Type", "application/json");
return res;
}
});
CROW_ROUTE((*this), "/api/alarms/clear")
2025-11-04 14:52:43 +08:00
.methods("POST"_method)([this](const crow::request &req) {
crow::json::rvalue j_body;
try {
j_body = crow::json::load(req.body);
2025-11-04 14:52:43 +08:00
} catch (const std::exception &e) {
spdlog::warn("Failed to parse request body for /api/alarms/clear: {}",
e.what());
auto res = crow::response(400, "{\"error\":\"Invalid JSON body.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
if (!j_body.has("rule_id") || !j_body.has("device_id")) {
auto res = crow::response(
400, "{\"error\":\"Missing 'rule_id' or 'device_id'.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
std::string rule_id = j_body["rule_id"].s();
std::string device_id = j_body["device_id"].s();
bool success = m_alarm_service.manually_clear_alarm(rule_id, device_id);
if (success) {
auto res = crow::response(
200, "{\"status\":\"success\", \"message\":\"Alarm cleared.\"}");
res.set_header("Content-Type", "application/json");
return res;
} else {
2025-11-04 14:52:43 +08:00
auto res = crow::response(
404, "{\"status\":\"error\", \"message\":\"Alarm not "
"found or not active.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
});
CROW_ROUTE((*this), "/api/alarms/config").methods("GET"_method)([this]() {
std::string rules_json_string = m_alarm_service.get_rules_as_json_string();
auto res = crow::response(200, rules_json_string);
res.set_header("Content-Type", "application/json");
return res;
});
CROW_ROUTE((*this), "/api/alarms/config")
.methods("POST"_method)([this](const crow::request &req) {
const std::string &new_rules_content = req.body;
bool success =
m_alarm_service.save_rules_from_json_string(new_rules_content);
if (success) {
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] =
"Rules saved successfully. A reload is required to apply.";
auto res = crow::response(200, response_json.dump());
res.set_header("Content-Type", "application/json");
return res;
} else {
crow::json::wvalue error_json;
error_json["status"] = "error";
error_json["message"] =
"Failed to save rules. Invalid JSON format or server error.";
auto res = crow::response(400, error_json.dump());
res.set_header("Content-Type", "application/json");
return res;
}
});
2025-11-04 17:51:37 +08:00
/**
* @brief GET /api/devices/config
* (devices.json) JSON
*/
CROW_ROUTE((*this), "/api/devices/config").methods("GET"_method)([this]() {
std::string rules_json_string =
m_device_manager.get_config_as_json_string();
auto res = crow::response(200, rules_json_string);
res.set_header("Content-Type", "application/json");
return res;
});
/**
* @brief POST /api/devices/config
* (devices.json) JSON
*/
CROW_ROUTE((*this), "/api/devices/config")
.methods("POST"_method)([this](const crow::request &req) {
const std::string &new_rules_content = req.body;
// save_config_from_json_string 内部包含 JSON 格式和 Schema 校验
bool success =
m_device_manager.save_config_from_json_string(new_rules_content);
if (success) {
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] = "Device config saved successfully. A "
"reload is required to apply.";
auto res = crow::response(200, response_json.dump());
res.set_header("Content-Type", "application/json");
return res;
} else {
crow::json::wvalue error_json;
error_json["status"] = "error";
error_json["message"] = "Failed to save rules. Invalid JSON format "
"or schema. Check service logs.";
auto res = crow::response(400, error_json.dump());
res.set_header("Content-Type", "application/json");
return res;
}
});
/**
* @brief POST /api/devices/reload
* devices.json
*/
CROW_ROUTE((*this), "/api/devices/reload").methods("POST"_method)([this]() {
spdlog::info("Web API: Received request to reload device rules...");
bool success = m_device_manager.reload_config_from_file();
if (success) {
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] = "Device rules reload posted successfully.";
auto res = crow::response(200, response_json.dump());
res.set_header("Content-Type", "application/json");
return res;
} else {
crow::json::wvalue error_json;
error_json["status"] = "error";
error_json["message"] =
"Failed to post device rules reload. Check service logs.";
auto res = crow::response(500, error_json.dump());
res.set_header("Content-Type", "application/json");
return res;
}
});
2025-11-04 19:46:01 +08:00
/**
* @brief GET /api/video_config
* video_config.json JSON
*/
CROW_ROUTE((*this), "/api/video_config").methods("GET"_method)([this]() {
std::string config_path;
try {
2025-11-05 14:50:25 +08:00
2025-11-04 19:46:01 +08:00
config_path = ConfigManager::getInstance().getVideoConfigPath();
} catch (const std::exception &e) {
spdlog::error("Failed to get video config path from ConfigManager: {}",
e.what());
auto res = crow::response(500, "{\"error\":\"Server configuration error: "
"cannot determine config path.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
std::ifstream ifs(config_path);
if (!ifs.is_open()) {
spdlog::error("Failed to open video config file for reading: {}",
config_path);
auto res =
crow::response(404, "{\"error\":\"Video config file not found.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
auto res = crow::response(200, content);
res.set_header("Content-Type", "application/json");
return res;
});
/**
* @brief POST /api/video_config
* video_config.json
*/
CROW_ROUTE((*this), "/api/video_config")
.methods("POST"_method)([this](const crow::request &req) {
const std::string &new_config_content = req.body;
std::string error_msg;
if (!validate_video_config(new_config_content, error_msg)) {
spdlog::warn("Web API: Failed to save video_config: {}", error_msg);
crow::json::wvalue error_json;
error_json["status"] = "error";
error_json["message"] = "Invalid JSON format or schema: " + error_msg;
auto res = crow::response(400, error_json.dump());
res.set_header("Content-Type", "application/json");
return res;
}
std::string config_path =
ConfigManager::getInstance().getVideoConfigPath();
std::ofstream ofs(config_path);
if (!ofs.is_open()) {
spdlog::error("Failed to open video config file for writing: {}",
config_path);
auto res = crow::response(
500, "{\"error\":\"Failed to write config file on server.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
auto json_data = nlohmann::json::parse(new_config_content);
ofs << json_data.dump(4); // 格式化写入
ofs.close();
// 4. 成功响应
spdlog::info("Video config successfully updated via API at {}",
config_path);
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] =
"Video config saved. Send POST to /api/service/reload to apply.";
auto res = crow::response(200, response_json.dump());
res.set_header("Content-Type", "application/json");
return res;
});
CROW_ROUTE((*this), "/api/service/reload").methods("POST"_method)([this]() {
if (m_shutdown_handler) {
spdlog::info("Web API: Received request to reload service...");
m_shutdown_handler();
auto res =
crow::response(202, "{\"status\":\"restarting\", "
"\"message\":\"Service is restarting...\"}");
res.set_header("Content-Type", "application/json");
return res;
}
spdlog::error("Web API: /api/service/reload called, but shutdown handler "
"is not set!");
auto res = crow::response(
500, "{\"error\":\"Shutdown handler not configured on server.\"}");
res.set_header("Content-Type", "application/json");
return res;
});
2025-11-05 14:50:25 +08:00
/**
* @brief GET /api/config
* config.json JSON
*/
CROW_ROUTE((*this), "/api/config").methods("GET"_method)([this]() {
std::string config_path;
try {
// 1. 从 ConfigManager 获取路径
config_path = ConfigManager::getInstance().getConfigFilePath();
if (config_path.empty()) {
throw std::runtime_error("ConfigManager returned an empty path.");
}
} catch (const std::exception &e) {
spdlog::error("Failed to get main config path from ConfigManager: {}",
e.what());
auto res = crow::response(500, "{\"error\":\"Server configuration error: "
"cannot determine config path.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
// 2. 读取文件
std::ifstream ifs(config_path);
if (!ifs.is_open()) {
spdlog::error("Failed to open main config file for reading: {}",
config_path);
auto res =
crow::response(404, "{\"error\":\"Main config file not found.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
// 3. 返回内容
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
auto res = crow::response(200, content);
res.set_header("Content-Type", "application/json");
return res;
});
/**
* @brief POST /api/config
* config.json
*/
CROW_ROUTE((*this), "/api/config")
.methods("POST"_method)([this](const crow::request &req) {
const std::string &new_config_content = req.body;
std::string error_msg;
if (!validate_main_config(new_config_content, error_msg)) {
spdlog::warn("Web API: Failed to save main config: {}", error_msg);
crow::json::wvalue error_json;
error_json["status"] = "error";
error_json["message"] = "Invalid JSON format or schema: " + error_msg;
auto res = crow::response(400, error_json.dump());
res.set_header("Content-Type", "application/json");
return res;
}
std::string config_path =
ConfigManager::getInstance().getConfigFilePath();
if (config_path.empty()) {
auto res = crow::response(
500, "{\"error\":\"Failed to get config file path on server.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
std::ofstream ofs(config_path);
if (!ofs.is_open()) {
spdlog::error("Failed to open main config file for writing: {}",
config_path);
auto res = crow::response(
500, "{\"error\":\"Failed to write config file on server.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
try {
auto json_data = nlohmann::json::parse(new_config_content);
ofs << json_data.dump(4);
ofs.close();
} catch (const std::exception &e) {
spdlog::error("Failed to re-parse and dump main config: {}",
e.what());
auto res = crow::response(
500, "{\"error\":\"Failed to serialize config for writing.\"}");
res.set_header("Content-Type", "application/json");
return res;
}
spdlog::info("Main config successfully updated via API at {}",
config_path);
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] =
"Main config saved. A full service restart (e.g., via "
"/api/service/reload) is required to apply changes.";
auto res = crow::response(200, response_json.dump());
res.set_header("Content-Type", "application/json");
return res;
});
2025-10-15 14:27:21 +08:00
}