From f5d6fca6e4fa60db6a3d2db3ca3b0f531ca57a78 Mon Sep 17 00:00:00 2001 From: lichunjun Date: Sat, 19 Apr 2025 18:02:39 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(http):1.12.6,=20=E5=AE=8C=E6=88=90Mult?= =?UTF-8?q?partFromMiddleware=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E7=BC=96=E5=86=99=E4=BA=86=E5=AF=B9=E5=BA=94=E7=9A=84example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/http/server/form_data/Makefile | 37 ++ .../http/server/form_data/file_upload.cpp | 263 +++++++++++++ examples/http/server/router/router.cpp | 4 +- modules/http/CMakeLists.txt | 13 +- modules/http/Makefile | 8 +- modules/http/server/middlewares/form_data.cpp | 86 +++++ modules/http/server/middlewares/form_data.h | 105 ++++++ .../middlewares/multipart_form_middleware.cpp | 356 ++++++++++++++++++ .../middlewares/multipart_form_middleware.h | 87 +++++ modules/http/server/middlewares/route_key.h | 26 ++ .../router_middleware.cpp} | 58 ++- .../router_middleware.h} | 24 +- modules/http/url.cpp | 13 +- modules/http/url_test.cpp | 3 +- version.mk | 2 +- 15 files changed, 1029 insertions(+), 56 deletions(-) create mode 100644 examples/http/server/form_data/Makefile create mode 100644 examples/http/server/form_data/file_upload.cpp create mode 100644 modules/http/server/middlewares/form_data.cpp create mode 100644 modules/http/server/middlewares/form_data.h create mode 100644 modules/http/server/middlewares/multipart_form_middleware.cpp create mode 100644 modules/http/server/middlewares/multipart_form_middleware.h create mode 100644 modules/http/server/middlewares/route_key.h rename modules/http/server/{router.cpp => middlewares/router_middleware.cpp} (39%) rename modules/http/server/{router.h => middlewares/router_middleware.h} (58%) diff --git a/examples/http/server/form_data/Makefile b/examples/http/server/form_data/Makefile new file mode 100644 index 0000000..348fe46 --- /dev/null +++ b/examples/http/server/form_data/Makefile @@ -0,0 +1,37 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/http/server/file_upload +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := file_upload.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/http/server/form_data/file_upload.cpp b/examples/http/server/form_data/file_upload.cpp new file mode 100644 index 0000000..57a722b --- /dev/null +++ b/examples/http/server/form_data/file_upload.cpp @@ -0,0 +1,263 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * 文件上传服务器示例 + * + * 这个示例展示了如何使用 MultipartFormData 中间件处理文件上传 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::http; +using namespace tbox::http::server; + +// HTML表单页面 +const char *kHtmlForm = R"( + + + + + 文件上传示例 + + + +

文件上传示例

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +)"; + +// 显示成功页面 +std::string GenSuccessHtml(const std::string& name, const std::string& email, const std::string& filename, const std::string& description) { + std::string html = R"( + + + + + 上传成功 + + + +

文件上传成功!

+
+

姓名:)"; + html += name; + html += R"(

+

邮箱:)"; + html += email; + html += R"(

+

文件名:)"; + html += filename; + html += R"(

+

描述:)"; + html += description; + html += R"(

+
+ + + + )"; + return html; +} + +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:8080"; + std::string upload_dir = "./uploads"; + + if (argc >= 2) + bind_addr = argv[1]; + + if (argc >= 3) + upload_dir = argv[2]; + + // 启用日志输出 + LogOutput_Enable(); + LogInfo("Starting file upload server"); + + // 创建上传目录 + if (!tbox::util::fs::MakeDirectory(upload_dir)) { + LogErr("Cannot create upload directory, exiting"); + return 1; + } + + // 创建事件循环和信号处理 + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + // 确保资源被正确释放 + SetScopeExitAction( + [=] { + delete sp_sig_event; + delete sp_loop; + } + ); + + // 初始化信号处理 + sp_sig_event->initialize({SIGINT, SIGTERM}, Event::Mode::kPersist); + sp_sig_event->enable(); + + // 创建HTTP服务器 + Server srv(sp_loop); + if (!srv.initialize(SockAddr::FromString(bind_addr), 5)) { + LogErr("HTTP server initialization failed"); + return 1; + } + + srv.start(); + srv.setContextLogEnable(true); + + // 创建路由和表单数据处理器 + RouterMiddleware router; + MultipartFormMiddleware form_data_handler; + + // 添加首页路由 + router.get("/", [](ContextSptr ctx, const NextFunc& next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = kHtmlForm; + }); + + // 添加文件上传处理 + form_data_handler.post("/upload", [&upload_dir](ContextSptr ctx, const FormData& form_data, const NextFunc& next) { + // 获取表单字段 + std::string name, email, description, filename, file_content; + + if (!form_data.getField("name", name)) + name = "未提供"; + + if (!form_data.getField("email", email)) + email = "未提供"; + + if (!form_data.getField("description", description)) + description = "未提供"; + + // 获取上传的文件 + if (form_data.getFile("file", filename, file_content)) { + // 保存文件 + if (!filename.empty()) { + std::string file_path = upload_dir + "/" + filename; + std::ofstream outfile(file_path, std::ios::binary); + + if (outfile.is_open()) { + outfile.write(file_content.c_str(), file_content.size()); + outfile.close(); + + LogInfo("File saved: %s", file_path.c_str()); + + // 返回成功页面 + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = GenSuccessHtml(name, email, filename, description); + } else { + LogWarn("Cannot create file: %s", file_path.c_str()); + ctx->res().status_code = StatusCode::k500_InternalServerError; + ctx->res().body = "Server Error: Cannot save file"; + } + } else { + LogNotice("Filename is empty"); + ctx->res().status_code = StatusCode::k400_BadRequest; + ctx->res().body = "Error: No filename provided"; + } + } else { + LogNotice("File not found"); + ctx->res().status_code = StatusCode::k400_BadRequest; + ctx->res().body = "Error: No file uploaded"; + } + }); + + // 添加中间件到服务器 + srv.use(&router); + srv.use(&form_data_handler); + + // 设置信号处理回调 + sp_sig_event->setCallback( + [&] (int sig) { + LogInfo("Received signal %d, preparing to exit...", sig); + srv.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("File upload server started, listening on http://%s/", bind_addr.c_str()); + + // 运行事件循环 + sp_loop->runLoop(); + + LogInfo("Server stopped"); + srv.cleanup(); + + LogInfo("Exiting"); + return 0; +} \ No newline at end of file diff --git a/examples/http/server/router/router.cpp b/examples/http/server/router/router.cpp index 3fc8115..3a5d413 100644 --- a/examples/http/server/router/router.cpp +++ b/examples/http/server/router/router.cpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include using namespace tbox; using namespace tbox::event; @@ -63,7 +63,7 @@ int main(int argc, char **argv) srv.start(); srv.setContextLogEnable(true); - Router router; + RouterMiddleware router; srv.use(&router); router diff --git a/modules/http/CMakeLists.txt b/modules/http/CMakeLists.txt index e5e88d7..e119bc3 100644 --- a/modules/http/CMakeLists.txt +++ b/modules/http/CMakeLists.txt @@ -38,7 +38,9 @@ set(TBOX_HTTP_SOURCES server/server.cpp server/server_imp.cpp server/context.cpp - server/router.cpp + server/middlewares/router_middleware.cpp + server/middlewares/form_data.cpp + server/middlewares/multipart_form_middleware.cpp client/client.cpp) set(TBOX_HTTP_TEST_SOURCES @@ -89,10 +91,17 @@ install( server/server.h server/context.h server/middleware.h - server/router.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/http/server ) +install( + FILES + server/middlewares/router_middleware.h + server/middlewares/form_data.h + server/middlewares/multipart_form_middleware.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/http/server/middlewares +) + install( FILES client/client.h diff --git a/modules/http/Makefile b/modules/http/Makefile index 0bb197c..c449c42 100644 --- a/modules/http/Makefile +++ b/modules/http/Makefile @@ -33,7 +33,9 @@ HEAD_FILES = \ server/server.h \ server/context.h \ server/middleware.h \ - server/router.h \ + server/middlewares/router_middleware.h \ + server/middlewares/form_data.h \ + server/middlewares/multipart_form_middleware.h \ client/client.h \ CPP_SRC_FILES = \ @@ -45,7 +47,9 @@ CPP_SRC_FILES = \ server/server.cpp \ server/server_imp.cpp \ server/context.cpp \ - server/router.cpp \ + server/middlewares/router_middleware.cpp \ + server/middlewares/form_data.cpp \ + server/middlewares/multipart_form_middleware.cpp \ client/client.cpp \ CXXFLAGS := -DMODULE_ID='"tbox.http"' $(CXXFLAGS) diff --git a/modules/http/server/middlewares/form_data.cpp b/modules/http/server/middlewares/form_data.cpp new file mode 100644 index 0000000..a7ec7b5 --- /dev/null +++ b/modules/http/server/middlewares/form_data.cpp @@ -0,0 +1,86 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "form_data.h" + +namespace tbox { +namespace http { +namespace server { + +void FormData::addItem(const FormItem& item) { + items_.push_back(item); + size_t index = items_.size() - 1; + name_index_[item.name].push_back(index); +} + +const std::vector& FormData::items() const { + return items_; +} + +std::vector FormData::getItems(const std::string& name) const { + std::vector result; + auto it = name_index_.find(name); + if (it != name_index_.end()) { + for (auto index : it->second) { + result.push_back(items_[index]); + } + } + return result; +} + +bool FormData::getItem(const std::string& name, FormItem& item) const { + auto it = name_index_.find(name); + if (it != name_index_.end() && !it->second.empty()) { + item = items_[it->second.front()]; + return true; + } + return false; +} + +bool FormData::getField(const std::string& name, std::string& value) const { + FormItem item; + if (getItem(name, item) && item.type == FormItem::Type::kField) { + value = item.value; + return true; + } + return false; +} + +bool FormData::getFile(const std::string& name, std::string& filename, std::string& content) const { + FormItem item; + if (getItem(name, item) && item.type == FormItem::Type::kFile) { + filename = item.filename; + content = item.value; + return true; + } + return false; +} + +bool FormData::contains(const std::string& name) const { + return name_index_.find(name) != name_index_.end(); +} + +void FormData::clear() { + items_.clear(); + name_index_.clear(); +} + +} +} +} \ No newline at end of file diff --git a/modules/http/server/middlewares/form_data.h b/modules/http/server/middlewares/form_data.h new file mode 100644 index 0000000..182805e --- /dev/null +++ b/modules/http/server/middlewares/form_data.h @@ -0,0 +1,105 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SERVER_FORM_DATA_H_20250419 +#define TBOX_HTTP_SERVER_FORM_DATA_H_20250419 + +#include +#include +#include + +namespace tbox { +namespace http { +namespace server { + +/** + * 表单项类型,可以是普通的文本字段或文件 + */ +struct FormItem { + enum class Type { + kField, // 普通字段 + kFile // 文件 + }; + + Type type = Type::kField; + std::string name; // 字段名称 + std::string value; // 字段值(对于普通字段) + std::string filename; // 文件名(对于文件) + std::string content_type; // 内容类型 + std::map headers; // 其他头部信息 +}; + +/** + * 表单数据类,存储解析后的表单数据 + */ +class FormData { + public: + FormData() = default; + ~FormData() = default; + + /** + * 添加一个表单项 + */ + void addItem(const FormItem& item); + + /** + * 获取所有表单项 + */ + const std::vector& items() const; + + /** + * 获取具有指定名称的所有表单项 + */ + std::vector getItems(const std::string& name) const; + + /** + * 获取指定名称的第一个表单项 + */ + bool getItem(const std::string& name, FormItem& item) const; + + /** + * 获取指定名称的第一个表单项的值(如果是字段) + */ + bool getField(const std::string& name, std::string& value) const; + + /** + * 获取指定名称的第一个文件(如果是文件) + */ + bool getFile(const std::string& name, std::string& filename, std::string& content) const; + + /** + * 检查是否包含指定名称的表单项 + */ + bool contains(const std::string& name) const; + + /** + * 清空表单数据 + */ + void clear(); + + private: + std::vector items_; + std::map> name_index_; // 名称到索引的映射 +}; + +} +} +} + +#endif // TBOX_HTTP_SERVER_FORM_DATA_H_20250419 \ No newline at end of file diff --git a/modules/http/server/middlewares/multipart_form_middleware.cpp b/modules/http/server/middlewares/multipart_form_middleware.cpp new file mode 100644 index 0000000..be7e669 --- /dev/null +++ b/modules/http/server/middlewares/multipart_form_middleware.cpp @@ -0,0 +1,356 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "multipart_form_middleware.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../context.h" +#include "route_key.h" + +namespace tbox { +namespace http { +namespace server { + +// 匿名命名空间,这些函数只在本文件内可见 +namespace { + +/** + * 解析Content-Type头部中的boundary参数 + */ +bool ParseBoundary(const std::string& content_type, std::string &boundary) { + // 查找boundary参数 + size_t pos = content_type.find("boundary="); + if (pos == std::string::npos) + return false; + + pos += 9; // "boundary="的长度 + + // 如果boundary参数值被引号包围,则去掉引号 + if (content_type[pos] == '"') { + size_t end_pos = content_type.find('"', pos + 1); + if (end_pos == std::string::npos) + return false; + boundary = content_type.substr(pos + 1, end_pos - pos - 1); + + // 找到参数的结束位置(分号或字符串结束) + } else { + size_t end_pos = content_type.find(';', pos); + if (end_pos == std::string::npos) + end_pos = content_type.length(); + boundary = content_type.substr(pos, end_pos - pos); + } + return true; +} + +/** + * 解析Content-Disposition头部内容 + */ +bool ParseContentDisposition(const std::string& header_value, FormItem& item) { + // 确保这是一个表单数据字段 + if (header_value.find("form-data") == std::string::npos) { + LogNotice("Not a form-data Content-Disposition"); + return false; + } + + // 解析参数 + size_t pos = 0; + while ((pos = header_value.find(';', pos)) != std::string::npos) { + pos++; // 跳过分号 + + // 跳过空白字符 + while (pos < header_value.length() && std::isspace(header_value[pos])) + pos++; + + // 查找参数名称和值 + size_t eq_pos = header_value.find('=', pos); + if (eq_pos == std::string::npos) + continue; + + std::string param_name = header_value.substr(pos, eq_pos - pos); + + // 处理引号包围的值 + size_t value_start = eq_pos + 1; + if (header_value[value_start] == '"') { + value_start++; + size_t value_end = header_value.find('"', value_start); + if (value_end == std::string::npos) + break; + + std::string param_value = header_value.substr(value_start, value_end - value_start); + + if (param_name == "name") { + item.name = param_value; + item.type = FormItem::Type::kField; + + } else if (param_name == "filename") { + item.filename = param_value; + item.type = FormItem::Type::kFile; + } + + pos = value_end + 1; + } + } + + return true; +} + +/** + * 解析表单项头部 + */ +bool ParseFormItemHeaders(const std::string& headers_str, FormItem& item) { + std::istringstream headers_stream(headers_str); + std::string line; + + // 逐行解析头部 + while (std::getline(headers_stream, line)) { + // 移除行尾的\r + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + // 跳过空行 + if (line.empty()) + continue; + + // 解析头部字段 + size_t colon_pos = line.find(':'); + if (colon_pos == std::string::npos) { + LogNotice("Invalid header format: %s", line.c_str()); + continue; + } + + std::string header_name = util::string::Strip(line.substr(0, colon_pos)); + std::string header_value = util::string::Strip(line.substr(colon_pos + 1)); + + // 存储头部 + item.headers[header_name] = header_value; + + // 处理Content-Disposition头部 + if (util::string::ToLower(header_name) == "content-disposition") + ParseContentDisposition(header_value, item); + + // 处理Content-Type头部 + if (util::string::ToLower(header_name) == "content-type") + item.content_type = header_value; + } + + return !item.name.empty(); +} + +/** + * 解析单个表单部分 + */ +bool ParseFormPart(const std::string& part, FormItem& item) { + // 分离头部和正文 + size_t headers_end = part.find("\r\n\r\n"); + if (headers_end == std::string::npos) { + LogNotice("Invalid form part format"); + return false; + } + + std::string headers_str = part.substr(0, headers_end); + std::string body = part.substr(headers_end + 4); // +4 跳过两个CRLF + + // 解析头部 + if (!ParseFormItemHeaders(headers_str, item)) + return false; + + // 设置内容 + item.value = body; + + return true; +} + +bool ParseAsMultipartFormData(const Request& req, const std::string &content_type, FormData& form_data) { + // 解析boundary参数 + std::string boundary; + if (!ParseBoundary(content_type, boundary)) { + LogNotice("Failed to parse boundary from Content-Type"); + return false; + } + + // 带两个连字符的完整边界 + const std::string delimiter = "--" + boundary; + + // 分割请求体以获取各个部分 + size_t pos = 0; + + // 跳过第一个边界 + pos = req.body.find(delimiter, pos); + if (pos == std::string::npos) { + LogNotice("Initial boundary not found"); + return false; + } + + pos += delimiter.length(); + // 跳过CRLF + if (req.body.substr(pos, 2) == "\r\n") + pos += 2; + + // 查找每个部分 + while (true) { + // 查找下一个边界 + size_t next_pos = req.body.find(delimiter, pos); + + // 没有找到更多边界 + if (next_pos == std::string::npos) + break; + + // 提取该部分的内容(不包括前导CRLF) + const std::string part = req.body.substr(pos, next_pos - pos - 2); // 减去末尾的CRLF + + // 解析表单项 + FormItem item; + if (ParseFormPart(part, item)) + form_data.addItem(item); + + // 移动到下一个部分 + pos = next_pos + delimiter.length(); + + // 检查是否是最后一个边界 + if (req.body.substr(pos, 2) == "--") + break; // 这是最后一个边界 + + // 跳过CRLF + if (req.body.substr(pos, 2) == "\r\n") + pos += 2; + } + + return true; +} + +bool ParseAsFormUrlEncoded(const Request& req, FormData& form_data) { + // 分割请求体以获取各个字段 + const std::string &body = req.body; + size_t pos = 0; + + while (pos < body.length()) { + // 查找下一个字段分隔符(&) + size_t next_pos = body.find('&', pos); + if (next_pos == std::string::npos) + next_pos = body.length(); + + // 提取键值对 + std::string pair = body.substr(pos, next_pos - pos); + size_t eq_pos = pair.find('='); + + if (eq_pos != std::string::npos) { + std::string name = UrlDecode(pair.substr(0, eq_pos)); + std::string value = UrlDecode(pair.substr(eq_pos + 1)); + + FormItem item; + item.type = FormItem::Type::kField; + item.name = name; + item.value = value; + form_data.addItem(item); + } + + // 移动到下一个字段 + pos = next_pos + 1; + } + + return true; +} + + +} // 匿名命名空间结束 + +struct MultipartFormMiddleware::Data { + std::map handlers; +}; + +MultipartFormMiddleware::MultipartFormMiddleware() : d_(new Data) { } + +MultipartFormMiddleware::~MultipartFormMiddleware() { delete d_; } + +void MultipartFormMiddleware::post(const std::string& path, FormHandler&& handler) { + registerHandler(Method::kPost, path, std::move(handler)); +} + +void MultipartFormMiddleware::get(const std::string& path, FormHandler&& handler) { + registerHandler(Method::kGet, path, std::move(handler)); +} + +void MultipartFormMiddleware::registerHandler(Method method, const std::string& path, FormHandler&& handler) { + RouteKey key{method, path}; + d_->handlers[key] = std::move(handler); +} + +void MultipartFormMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { + Request& req = sp_ctx->req(); + + // 检查路由是否匹配 + RouteKey key{req.method, req.url.path}; + auto it = d_->handlers.find(key); + if (it == d_->handlers.end()) { + // 如果没有匹配,继续下一个中间件 + next(); + return; + } + + // 解析表单数据 + FormData form_data; + bool parsed = false; + + // 获取Content-Type + auto content_type_it = req.headers.find("Content-Type"); + if (content_type_it != req.headers.end()) { + const std::string& content_type = content_type_it->second; + // 处理multipart/form-data + if (content_type.find("multipart/form-data") != std::string::npos) { + parsed = ParseAsMultipartFormData(req, content_type, form_data); + + // 处理application/x-www-form-urlencoded + } else if (content_type.find("application/x-www-form-urlencoded") != std::string::npos) { + parsed = ParseAsFormUrlEncoded(req, form_data); + } + } + + // 对于GET请求,解析查询参数 + if (req.method == Method::kGet && !req.url.query.empty()) { + for (const auto& param : req.url.query) { + FormItem item; + item.type = FormItem::Type::kField; + item.name = param.first; + item.value = param.second; + form_data.addItem(item); + } + parsed = true; + } + + // 如果解析成功,则调用处理函数 + if (parsed) { + it->second(sp_ctx, form_data, next); + } else { + // 如果解析失败,继续下一个中间件 + next(); + } +} + +} +} +} \ No newline at end of file diff --git a/modules/http/server/middlewares/multipart_form_middleware.h b/modules/http/server/middlewares/multipart_form_middleware.h new file mode 100644 index 0000000..6c8f0ef --- /dev/null +++ b/modules/http/server/middlewares/multipart_form_middleware.h @@ -0,0 +1,87 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SERVER_MULTIPART_FORM_DATA_H_20250419 +#define TBOX_HTTP_SERVER_MULTIPART_FORM_DATA_H_20250419 + +#include + +#include "../middleware.h" +#include "../context.h" +#include "form_data.h" + +namespace tbox { +namespace http { +namespace server { + +/** + * MultipartFormData中间件,用于处理multipart/form-data表单 + */ +class MultipartFormMiddleware : public Middleware { + public: + explicit MultipartFormMiddleware(); + ~MultipartFormMiddleware(); + + NONCOPYABLE(MultipartFormMiddleware); + + // 处理表单数据的回调函数类型 + using FormHandler = std::function; + + /** + * 注册POST请求处理器 + * + * @param path 请求路径 + * @param handler 处理函数 + */ + void post(const std::string& path, FormHandler&& handler); + + /** + * 注册GET请求处理器 + * + * @param path 请求路径 + * @param handler 处理函数 + */ + void get(const std::string& path, FormHandler&& handler); + + /** + * 注册特定Method的请求处理器 + * + * @param method HTTP方法 + * @param path 请求路径 + * @param handler 处理函数 + */ + void registerHandler(Method method, const std::string& path, FormHandler&& handler); + + protected: + /** + * 中间件处理函数,解析multipart/form-data数据 + */ + virtual void handle(ContextSptr sp_ctx, const NextFunc& next) override; + + private: + + struct Data; + Data *d_; +}; + +} +} +} + +#endif // TBOX_HTTP_SERVER_MULTIPART_FORM_DATA_H_20250419 \ No newline at end of file diff --git a/modules/http/server/middlewares/route_key.h b/modules/http/server/middlewares/route_key.h new file mode 100644 index 0000000..5179570 --- /dev/null +++ b/modules/http/server/middlewares/route_key.h @@ -0,0 +1,26 @@ +#ifndef TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 +#define TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 + +#include "../../common.h" + +namespace tbox { +namespace http { +namespace server { + +// 用于存储处理函数的映射 +struct RouteKey { + Method method; + std::string path; + + bool operator<(const RouteKey& other) const { + if (method != other.method) + return method < other.method; + return path < other.path; + } +}; + +} +} +} + +#endif // TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 \ No newline at end of file diff --git a/modules/http/server/router.cpp b/modules/http/server/middlewares/router_middleware.cpp similarity index 39% rename from modules/http/server/router.cpp rename to modules/http/server/middlewares/router_middleware.cpp index 76f69cc..fdfda63 100644 --- a/modules/http/server/router.cpp +++ b/modules/http/server/middlewares/router_middleware.cpp @@ -17,41 +17,31 @@ * project authors may be found in the CONTRIBUTORS.md file in the root * of the source tree. */ -#include "router.h" +#include "router_middleware.h" #include +#include "route_key.h" namespace tbox { namespace http { namespace server { -struct Router::Data { - std::map cbs_; +struct RouterMiddleware::Data { + std::map cbs_; }; -Router::Router() : - d_(new Data) +RouterMiddleware::RouterMiddleware() : d_(new Data) { } -Router::~Router() +RouterMiddleware::~RouterMiddleware() { delete d_; } -void Router::handle(ContextSptr sp_ctx, const NextFunc &next) +void RouterMiddleware::handle(ContextSptr sp_ctx, const NextFunc &next) { - std::string prefix; - switch (sp_ctx->req().method) { - case Method::kGet: prefix = "get:"; break; - case Method::kPut: prefix = "put:"; break; - case Method::kPost: prefix = "post:"; break; - case Method::kDelete: prefix = "del:"; break; - case Method::kOptions: prefix = "opt:"; break; - case Method::kHead: prefix = "head:"; break; - case Method::kTrace: prefix = "trace:"; break; - default:; - } - - auto iter = d_->cbs_.find(prefix + sp_ctx->req().url.path); + RouteKey key{sp_ctx->req().method, sp_ctx->req().url.path}; + + auto iter = d_->cbs_.find(key); if (iter != d_->cbs_.end()) { if (iter->second) { iter->second(sp_ctx, next); @@ -61,45 +51,45 @@ void Router::handle(ContextSptr sp_ctx, const NextFunc &next) next(); } -Router& Router::get(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::get(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("get:") + path] = cb; + d_->cbs_[RouteKey{Method::kGet, path}] = std::move(cb); return *this; } -Router& Router::post(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::post(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("post:") + path] = cb; + d_->cbs_[RouteKey{Method::kPost, path}] = std::move(cb); return *this; } -Router& Router::put(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::put(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("put:") + path] = cb; + d_->cbs_[RouteKey{Method::kPut, path}] = std::move(cb); return *this; } -Router& Router::del(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::del(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("del:") + path] = cb; + d_->cbs_[RouteKey{Method::kDelete, path}] = std::move(cb); return *this; } -Router& Router::opt(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::opt(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("opt:") + path] = cb; + d_->cbs_[RouteKey{Method::kOptions, path}] = std::move(cb); return *this; } -Router& Router::head(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::head(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("head:") + path] = cb; + d_->cbs_[RouteKey{Method::kHead, path}] = std::move(cb); return *this; } -Router& Router::trace(const std::string &path, const RequestCallback &cb) +RouterMiddleware& RouterMiddleware::trace(const std::string &path, RequestCallback &&cb) { - d_->cbs_[std::string("trace:") + path] = cb; + d_->cbs_[RouteKey{Method::kTrace, path}] = std::move(cb); return *this; } diff --git a/modules/http/server/router.h b/modules/http/server/middlewares/router_middleware.h similarity index 58% rename from modules/http/server/router.h rename to modules/http/server/middlewares/router_middleware.h index 941f19d..0bfae16 100644 --- a/modules/http/server/router.h +++ b/modules/http/server/middlewares/router_middleware.h @@ -20,26 +20,26 @@ #ifndef TBOX_HTTP_SERVER_ROUTER_H_20220508 #define TBOX_HTTP_SERVER_ROUTER_H_20220508 -#include "middleware.h" -#include "context.h" +#include "../middleware.h" +#include "../context.h" namespace tbox { namespace http { namespace server { -class Router : public Middleware { +class RouterMiddleware : public Middleware { public: - Router(); - ~Router(); + RouterMiddleware(); + ~RouterMiddleware(); public: - Router& get (const std::string &path, const RequestCallback &cb); - Router& post (const std::string &path, const RequestCallback &cb); - Router& put (const std::string &path, const RequestCallback &cb); - Router& del (const std::string &path, const RequestCallback &cb); - Router& opt (const std::string &path, const RequestCallback &cb); - Router& head (const std::string &path, const RequestCallback &cb); - Router& trace(const std::string &path, const RequestCallback &cb); + RouterMiddleware& get (const std::string &path, RequestCallback &&cb); + RouterMiddleware& post (const std::string &path, RequestCallback &&cb); + RouterMiddleware& put (const std::string &path, RequestCallback &&cb); + RouterMiddleware& del (const std::string &path, RequestCallback &&cb); + RouterMiddleware& opt (const std::string &path, RequestCallback &&cb); + RouterMiddleware& head (const std::string &path, RequestCallback &&cb); + RouterMiddleware& trace(const std::string &path, RequestCallback &&cb); public: virtual void handle(ContextSptr sp_ctx, const NextFunc &next) override; diff --git a/modules/http/url.cpp b/modules/http/url.cpp index fe2659c..0bca5ae 100644 --- a/modules/http/url.cpp +++ b/modules/http/url.cpp @@ -39,7 +39,7 @@ char HexCharToValue(char hex_char) else if (('a' <= hex_char) && (hex_char <= 'f')) return hex_char - 'a' + 10; else - throw std::out_of_range("should be A-Z a-z 0-9"); + throw std::runtime_error("should be A-Z a-z 0-9"); } } @@ -82,8 +82,13 @@ std::string UrlDecode(const std::string &url_str) if (c == '%') { state = State::kStart; tmp = 0; - } else + } else if (c == '+') { + local_str.push_back(' '); + } else if (c == '#') { + break; //! 结束 + } else { local_str.push_back(c); + } } else { if (state == State::kStart) { tmp = HexCharToValue(c) << 4; @@ -95,6 +100,10 @@ std::string UrlDecode(const std::string &url_str) } } } + + if (state != State::kNone) + throw std::runtime_error("url imcomplete"); + return local_str; } diff --git a/modules/http/url_test.cpp b/modules/http/url_test.cpp index 9add928..580d4a0 100644 --- a/modules/http/url_test.cpp +++ b/modules/http/url_test.cpp @@ -27,7 +27,8 @@ TEST(Url, UrlDecode) { EXPECT_EQ(UrlDecode("hello%20world"), "hello world"); EXPECT_EQ(UrlDecode(R"(%23%25%26%2B%2F%5C%3D%3F%20%2E%3A)"), R"(#%&+/\=? .:)"); - EXPECT_THROW(UrlDecode("hello%2world"), std::out_of_range); + EXPECT_THROW(UrlDecode("hello%2world"), std::runtime_error); + EXPECT_THROW(UrlDecode("hello%"), std::runtime_error); } TEST(Url, UrlEncode) diff --git a/version.mk b/version.mk index d78609b..7d8f2b1 100644 --- a/version.mk +++ b/version.mk @@ -21,4 +21,4 @@ # TBOX版本号 TBOX_VERSION_MAJOR := 1 TBOX_VERSION_MINOR := 12 -TBOX_VERSION_REVISION := 5 +TBOX_VERSION_REVISION := 6 -- Gitee From d2ee5bbf1cd7d0623a852e36dca61cf50ecaf643 Mon Sep 17 00:00:00 2001 From: lichunjun Date: Sat, 19 Apr 2025 18:16:22 +0800 Subject: [PATCH 2/6] =?UTF-8?q?opt(http):=20=E4=BC=98=E5=8C=96Router?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/http/server/router/Makefile | 2 +- examples/http/server/router/router.cpp | 23 +-- examples/http/server/router/webpage.cpp | 233 ++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 examples/http/server/router/webpage.cpp diff --git a/examples/http/server/router/Makefile b/examples/http/server/router/Makefile index 469385c..657dd6e 100644 --- a/examples/http/server/router/Makefile +++ b/examples/http/server/router/Makefile @@ -21,7 +21,7 @@ PROJECT := examples/http/server/router EXE_NAME := ${PROJECT} -CPP_SRC_FILES := router.cpp +CPP_SRC_FILES := router.cpp webpage.cpp CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) LDFLAGS += \ diff --git a/examples/http/server/router/router.cpp b/examples/http/server/router/router.cpp index 3a5d413..3a2b217 100644 --- a/examples/http/server/router/router.cpp +++ b/examples/http/server/router/router.cpp @@ -29,6 +29,12 @@ using namespace tbox::event; using namespace tbox::http; using namespace tbox::http::server; +namespace webpage { +extern const char *kHtmlHome; +extern const char *kHtmlPage1; +extern const char *kHtmlPage2; +} + int main(int argc, char **argv) { std::string bind_addr = "0.0.0.0:12345"; @@ -69,23 +75,18 @@ int main(int argc, char **argv) router .get("/", [](ContextSptr ctx, const NextFunc &next) { ctx->res().status_code = StatusCode::k200_OK; - ctx->res().body = -R"( - - - -

page_1

-

page_2

- -)"; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = webpage::kHtmlHome; }) .get("/1", [](ContextSptr ctx, const NextFunc &next) { ctx->res().status_code = StatusCode::k200_OK; - ctx->res().body = "

page 1

"; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = webpage::kHtmlPage1; }) .get("/2", [](ContextSptr ctx, const NextFunc &next) { ctx->res().status_code = StatusCode::k200_OK; - ctx->res().body = "

page 2

"; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = webpage::kHtmlPage2; }); sp_sig_event->setCallback( diff --git a/examples/http/server/router/webpage.cpp b/examples/http/server/router/webpage.cpp new file mode 100644 index 0000000..1cd9840 --- /dev/null +++ b/examples/http/server/router/webpage.cpp @@ -0,0 +1,233 @@ + + +namespace webpage { + +const char* kHtmlHome = +R"( + + + + + TBOX HTTP 示例 + + + +
+

TBOX HTTP 示例服务器

+

这是一个简单的C++ HTTP服务器演示

+
+ +
+
+

页面一

+

演示第一个示例页面

+ 访问 +
+ +
+

页面二

+

演示第二个示例页面

+ 访问 +
+
+ + + + +)"; + +const char* kHtmlPage1 = + R"( + + + + + 页面一 - TBOX HTTP 示例 + + + +
+

页面一

+
+

这是第一个示例页面,展示了TBOX HTTP服务器的基本路由功能。

+

您可以根据需要自由扩展这个页面的内容和样式。

+
+ 返回首页 +
+ +)"; + +const char* kHtmlPage2 = +R"( + + + + + 页面二 - TBOX HTTP 示例 + + + +
+

页面二

+
+

欢迎来到第二个示例页面!这里展示了更多的HTML内容。

+ +
+

TBOX框架特点:

+
    +
  • 高性能的事件循环
  • +
  • 简洁的HTTP服务器API
  • +
  • 灵活的中间件系统
  • +
  • 易于集成的模块化设计
  • +
+
+
+ 返回首页 +
+ +)"; + +} \ No newline at end of file -- Gitee From 691b376675cc98692a11461b182d32afc9804d77 Mon Sep 17 00:00:00 2001 From: lichunjun Date: Sun, 20 Apr 2025 07:15:45 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(http):=20=E9=87=8D=E5=91=BD=E5=90=8Dmu?= =?UTF-8?q?ltipart=5Ffrom=5Fmiddleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/http/server/file_download/Makefile | 38 ++ .../server/file_download/file_download.cpp | 183 +++++++++ .../http/server/form_data/file_upload.cpp | 12 +- modules/http/CMakeLists.txt | 6 +- modules/http/Makefile | 6 +- .../file_downloader_middleware.cpp | 357 ++++++++++++++++++ .../middlewares/file_downloader_middleware.h | 129 +++++++ ...iddleware.cpp => form_data_middleware.cpp} | 18 +- ...rm_middleware.h => form_data_middleware.h} | 16 +- 9 files changed, 738 insertions(+), 27 deletions(-) create mode 100644 examples/http/server/file_download/Makefile create mode 100644 examples/http/server/file_download/file_download.cpp create mode 100644 modules/http/server/middlewares/file_downloader_middleware.cpp create mode 100644 modules/http/server/middlewares/file_downloader_middleware.h rename modules/http/server/middlewares/{multipart_form_middleware.cpp => form_data_middleware.cpp} (94%) rename modules/http/server/middlewares/{multipart_form_middleware.h => form_data_middleware.h} (81%) diff --git a/examples/http/server/file_download/Makefile b/examples/http/server/file_download/Makefile new file mode 100644 index 0000000..3b44e45 --- /dev/null +++ b/examples/http/server/file_download/Makefile @@ -0,0 +1,38 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/http/server/file_download +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := file_download.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' -std=c++17 $(CXXFLAGS) +LDFLAGS += \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lstdc++fs \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk \ No newline at end of file diff --git a/examples/http/server/file_download/file_download.cpp b/examples/http/server/file_download/file_download.cpp new file mode 100644 index 0000000..561ca4c --- /dev/null +++ b/examples/http/server/file_download/file_download.cpp @@ -0,0 +1,183 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * 文件下载服务器示例 + * + * 这个示例展示了如何使用 FileDownloaderMiddleware 实现文件下载功能 + * 包含目录浏览和文件下载功能 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::http; +using namespace tbox::http::server; + +// 文件下载服务器状态页面 +const char *kStatusHtml = R"( + + + + + 文件下载服务器 + + + +

文件下载服务器

+
+
状态: 运行中
+
版本: 1.0.0
+
目录浏览: 已启用
+
+ + + +)"; + +int main(int argc, char **argv) +{ + std::string serve_dir = "./"; + std::string bind_addr = "0.0.0.0:8080"; + + // 处理命令行参数 + if (argc >= 2) + serve_dir = argv[1]; + + if (argc >= 3) + bind_addr = argv[2]; + + // 启用日志输出 + LogOutput_Enable(); + LogInfo("Starting file download server"); + + // 检查目录是否存在 + if (!tbox::util::fs::IsDirectoryExist(serve_dir)) { + LogErr("Directory '%s' doesn't exist or is not a directory", serve_dir.c_str()); + return 1; + } + + // 创建事件循环和信号处理 + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + // 确保资源被正确释放 + SetScopeExitAction( + [=] { + delete sp_sig_event; + delete sp_loop; + } + ); + + // 初始化信号处理 + sp_sig_event->initialize({SIGINT, SIGTERM}, Event::Mode::kPersist); + sp_sig_event->enable(); + + // 创建HTTP服务器 + Server srv(sp_loop); + if (!srv.initialize(SockAddr::FromString(bind_addr), 5)) { + LogErr("HTTP server initialization failed"); + return 1; + } + + srv.start(); + srv.setContextLogEnable(true); + + // 创建路由器和文件下载中间件 + RouterMiddleware router; + FileDownloaderMiddleware file_downloader; + + // 添加状态页面路由 + router.get("/api/status", [](ContextSptr ctx, const NextFunc& next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "application/json"; + ctx->res().body = R"({"status":"running","version":"1.0.0"})"; + }); + + router.get("/status", [](ContextSptr ctx, const NextFunc& next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = kStatusHtml; + }); + + // 配置文件下载中间件 + if (!file_downloader.addDirectory("/", serve_dir)) { + LogErr("Failed to add directory: %s", serve_dir.c_str()); + return 1; + } + + // 启用目录浏览功能 + file_downloader.setDirectoryListingEnabled(true); + + // 添加常见的 MIME 类型映射(如果需要额外的映射) + // file_downloader.setMimeType("pdf", "application/pdf"); + + // 添加中间件到服务器 + srv.use(&router); + srv.use(&file_downloader); + + // 设置信号处理回调 + sp_sig_event->setCallback( + [&] (int sig) { + LogInfo("Received signal %d, preparing to exit...", sig); + srv.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("File download server started, listening on http://%s/, serving directory: %s", + bind_addr.c_str(), serve_dir.c_str()); + + // 运行事件循环 + sp_loop->runLoop(); + + LogInfo("Server stopped"); + srv.cleanup(); + + LogInfo("Exiting"); + return 0; +} \ No newline at end of file diff --git a/examples/http/server/form_data/file_upload.cpp b/examples/http/server/form_data/file_upload.cpp index 57a722b..8f5cc81 100644 --- a/examples/http/server/form_data/file_upload.cpp +++ b/examples/http/server/form_data/file_upload.cpp @@ -21,7 +21,7 @@ /** * 文件上传服务器示例 * - * 这个示例展示了如何使用 MultipartFormData 中间件处理文件上传 + * 这个示例展示了如何使用 FormDataMiddleware 中间件处理文件上传 */ #include @@ -36,7 +36,7 @@ #include #include #include -#include +#include #include using namespace std; @@ -180,7 +180,7 @@ int main(int argc, char **argv) // 创建路由和表单数据处理器 RouterMiddleware router; - MultipartFormMiddleware form_data_handler; + FormDataMiddleware form_data; // 添加首页路由 router.get("/", [](ContextSptr ctx, const NextFunc& next) { @@ -190,7 +190,7 @@ int main(int argc, char **argv) }); // 添加文件上传处理 - form_data_handler.post("/upload", [&upload_dir](ContextSptr ctx, const FormData& form_data, const NextFunc& next) { + form_data.post("/upload", [&upload_dir](ContextSptr ctx, const FormData& form_data, const NextFunc& next) { // 获取表单字段 std::string name, email, description, filename, file_content; @@ -239,7 +239,7 @@ int main(int argc, char **argv) // 添加中间件到服务器 srv.use(&router); - srv.use(&form_data_handler); + srv.use(&form_data); // 设置信号处理回调 sp_sig_event->setCallback( @@ -260,4 +260,4 @@ int main(int argc, char **argv) LogInfo("Exiting"); return 0; -} \ No newline at end of file +} diff --git a/modules/http/CMakeLists.txt b/modules/http/CMakeLists.txt index e119bc3..440b5fd 100644 --- a/modules/http/CMakeLists.txt +++ b/modules/http/CMakeLists.txt @@ -40,7 +40,8 @@ set(TBOX_HTTP_SOURCES server/context.cpp server/middlewares/router_middleware.cpp server/middlewares/form_data.cpp - server/middlewares/multipart_form_middleware.cpp + server/middlewares/form_data_middleware.cpp + server/middlewares/file_downloader_middleware.cpp client/client.cpp) set(TBOX_HTTP_TEST_SOURCES @@ -98,7 +99,8 @@ install( FILES server/middlewares/router_middleware.h server/middlewares/form_data.h - server/middlewares/multipart_form_middleware.h + server/middlewares/form_data_middleware.h + server/middlewares/file_downloader_middleware.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/http/server/middlewares ) diff --git a/modules/http/Makefile b/modules/http/Makefile index c449c42..6e588d9 100644 --- a/modules/http/Makefile +++ b/modules/http/Makefile @@ -35,7 +35,8 @@ HEAD_FILES = \ server/middleware.h \ server/middlewares/router_middleware.h \ server/middlewares/form_data.h \ - server/middlewares/multipart_form_middleware.h \ + server/middlewares/form_data_middleware.h \ + server/middlewares/file_downloader_middleware.h \ client/client.h \ CPP_SRC_FILES = \ @@ -49,7 +50,8 @@ CPP_SRC_FILES = \ server/context.cpp \ server/middlewares/router_middleware.cpp \ server/middlewares/form_data.cpp \ - server/middlewares/multipart_form_middleware.cpp \ + server/middlewares/form_data_middleware.cpp \ + server/middlewares/file_downloader_middleware.cpp \ client/client.cpp \ CXXFLAGS := -DMODULE_ID='"tbox.http"' $(CXXFLAGS) diff --git a/modules/http/server/middlewares/file_downloader_middleware.cpp b/modules/http/server/middlewares/file_downloader_middleware.cpp new file mode 100644 index 0000000..13cee69 --- /dev/null +++ b/modules/http/server/middlewares/file_downloader_middleware.cpp @@ -0,0 +1,357 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "file_downloader_middleware.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace tbox { +namespace http { +namespace server { + +// 目录配置项 +struct DirectoryConfig { + std::string url_prefix; // URL前缀 + std::string local_path; // 本地路径 + std::string default_file; // 默认文件 +}; + +// 中间件私有数据结构 +struct FileDownloaderMiddleware::Data { + std::vector directories; // 目录配置列表 + std::map path_mappings;// 特定路径映射 + std::map mime_types; // MIME类型映射 + std::string default_mime_type; // 默认MIME类型 + bool directory_listing_enabled; // 是否允许目录列表 + + Data() : default_mime_type("application/octet-stream"), directory_listing_enabled(false) { + // 初始化常见MIME类型 + mime_types["html"] = "text/html"; + mime_types["htm"] = "text/html"; + mime_types["css"] = "text/css"; + mime_types["js"] = "application/javascript"; + mime_types["json"] = "application/json"; + mime_types["xml"] = "application/xml"; + mime_types["txt"] = "text/plain"; + mime_types["png"] = "image/png"; + mime_types["jpg"] = "image/jpeg"; + mime_types["jpeg"] = "image/jpeg"; + mime_types["gif"] = "image/gif"; + mime_types["webp"] = "image/webp"; + mime_types["svg"] = "image/svg+xml"; + mime_types["ico"] = "image/x-icon"; + mime_types["pdf"] = "application/pdf"; + mime_types["zip"] = "application/zip"; + mime_types["tar"] = "application/x-tar"; + mime_types["gz"] = "application/gzip"; + mime_types["mp3"] = "audio/mpeg"; + mime_types["mp4"] = "video/mp4"; + mime_types["woff"] = "font/woff"; + mime_types["woff2"] = "font/woff2"; + mime_types["ttf"] = "font/ttf"; + mime_types["otf"] = "font/otf"; + } +}; + +FileDownloaderMiddleware::FileDownloaderMiddleware() : d_(new Data) {} + +FileDownloaderMiddleware::~FileDownloaderMiddleware() { + delete d_; +} + +bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, + const std::string& local_path, + const std::string& default_file) { + // 验证URL前缀是否以'/'开头 + if (url_prefix.empty() || url_prefix[0] != '/') { + LogErr("Invalid URL prefix: %s. Must start with '/'", url_prefix.c_str()); + return false; + } + + // 验证本地路径是否存在且是目录 + struct stat path_stat; + if (stat(local_path.c_str(), &path_stat) != 0 || !S_ISDIR(path_stat.st_mode)) { + LogErr("Invalid local path: %s. Directory does not exist", local_path.c_str()); + return false; + } + + // 添加到目录列表 + DirectoryConfig config; + config.url_prefix = url_prefix; + config.local_path = local_path; + config.default_file = default_file; + + // 确保本地路径以'/'结尾 + if (!config.local_path.empty() && config.local_path.back() != '/') + config.local_path += '/'; + + d_->directories.push_back(config); + LogInfo("Added directory mapping: %s -> %s", url_prefix.c_str(), local_path.c_str()); + return true; +} + +void FileDownloaderMiddleware::setDirectoryListingEnabled(bool enable) { + d_->directory_listing_enabled = enable; +} + +void FileDownloaderMiddleware::setPathMapping(const std::string& url, const std::string& file) { + d_->path_mappings[url] = file; +} + +void FileDownloaderMiddleware::setDefaultMimeType(const std::string& mime_type) { + d_->default_mime_type = mime_type; +} + +void FileDownloaderMiddleware::setMimeType(const std::string& ext, const std::string& mime_type) { + d_->mime_types[ext] = mime_type; +} + +void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { + const auto& request = sp_ctx->req(); + + // 只处理GET和HEAD请求 + if (request.method != Method::kGet && request.method != Method::kHead) { + next(); + return; + } + + const std::string& request_path = request.url.path; + + // 检查特定路径映射 + auto mapping_it = d_->path_mappings.find(request_path); + if (mapping_it != d_->path_mappings.end()) { + if (respondFile(sp_ctx, mapping_it->second)) + return; + } + + // 查找匹配的目录配置 + for (const auto& dir : d_->directories) { + // 检查URL是否以该目录前缀开头 + if (request_path.find(dir.url_prefix) == 0) { + // 获取相对路径部分 + std::string rel_path = request_path.substr(dir.url_prefix.length()); + + // 如果路径以'/'开头,去掉这个斜杠避免双斜杠 + if (!rel_path.empty() && rel_path[0] == '/') + rel_path = rel_path.substr(1); + + // 构造本地文件路径 + std::string file_path = dir.local_path + rel_path; + + // 检查路径安全性 + if (!isPathSafe(file_path)) { + LogWarn("Unsafe path detected: %s", file_path.c_str()); + sp_ctx->res().status_code = StatusCode::k403_Forbidden; + return; + } + + // 检查路径是否是目录 + struct stat path_stat; + if (stat(file_path.c_str(), &path_stat) == 0) { + if (S_ISDIR(path_stat.st_mode)) { + // 如果是目录且路径不以'/'结尾,进行重定向 + if (!request_path.empty() && request_path.back() != '/') { + sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; + sp_ctx->res().headers["Location"] = request_path + "/"; + return; + } + + // 尝试访问默认文件 + std::string default_file_path = file_path + dir.default_file; + if (stat(default_file_path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode)) { + if (respondFile(sp_ctx, default_file_path)) + return; + } + + // 如果允许目录列表,生成目录内容 + if (d_->directory_listing_enabled) { + if (respondDirectoryListing(sp_ctx, file_path, request_path)) + return; + } + + // 否则返回403 Forbidden + LogInfo("Directory listing disabled for: %s", file_path.c_str()); + sp_ctx->res().status_code = StatusCode::k403_Forbidden; + return; + } else if (S_ISREG(path_stat.st_mode)) { + // 如果是普通文件,直接响应文件内容 + if (respondFile(sp_ctx, file_path)) + return; + } + } + } + } + + // 如果没有找到匹配的文件,传递给下一个中间件 + next(); +} + +bool FileDownloaderMiddleware::isPathSafe(const std::string& path) const { + // 检查是否有".."路径组件,这可能导致目录遍历 + std::istringstream path_stream(path); + std::string component; + + while (std::getline(path_stream, component, '/')) { + if (component == "..") + return false; // 不允许上级目录访问 + } + + return true; +} + +std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) const { + // 查找最后一个点的位置 + size_t dot_pos = filename.find_last_of('.'); + if (dot_pos != std::string::npos) { + std::string ext = filename.substr(dot_pos + 1); + // 转换为小写 + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c){ return std::tolower(c); }); + + // 在MIME类型映射中查找 + auto it = d_->mime_types.find(ext); + if (it != d_->mime_types.end()) { + return it->second; + } + } + + // 未找到匹配的MIME类型,返回默认值 + return d_->default_mime_type; +} + +bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path) { + // 打开文件 + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + LogWarn("Failed to open file: %s", file_path.c_str()); + return false; + } + + // 获取文件大小 + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + // 设置响应头 + auto& res = sp_ctx->res(); + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = getMimeType(file_path); + res.headers["Content-Length"] = std::to_string(file_size); + + // 添加缓存控制头(可选) + res.headers["Cache-Control"] = "max-age=86400"; // 1天缓存 + + // 如果是HEAD请求,不返回内容 + if (sp_ctx->req().method == Method::kHead) { + return true; + } + + // 读取文件内容 + std::vector buffer(file_size); + if (file.read(buffer.data(), file_size)) { + res.body.assign(buffer.data(), file_size); + LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); + return true; + } + + LogErr("Failed to read file: %s", file_path.c_str()); + return false; +} + +bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, + const std::string& dir_path, + const std::string& url_path) { + try { + // 生成HTML目录列表 + std::stringstream html; + html << "\n" + << "\n" + << "\n" + << " Directory listing for " << url_path << "\n" + << " \n" + << "\n" + << "\n" + << "

Directory listing for " << url_path << "

\n" + << "
    \n"; + + // 如果不是根目录,添加返回上级目录的链接 + if (url_path != "/") { + size_t last_slash = url_path.find_last_of('/', url_path.size() - 2); + if (last_slash != std::string::npos) { + std::string parent_url = url_path.substr(0, last_slash + 1); + html << "
  • ..
  • \n"; + } + } + + // 列出目录中的项目 + std::vector entries; + if (!util::fs::ListDirectory(dir_path, entries)) { + LogErr("Failed to list directory: %s", dir_path.c_str()); + return false; + } + + for (const auto& name : entries) { + std::string entry_path = dir_path + "/" + name; + std::string href = url_path + name; + + if (util::fs::IsDirectoryExist(entry_path)) { + href += "/"; + html << "
  • " << name << "/
  • \n"; + } else { + html << "
  • " << name << "
  • \n"; + } + } + + html << "
\n" + << "\n" + << ""; + + // 设置响应 + auto& res = sp_ctx->res(); + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/html; charset=utf-8"; + res.body = html.str(); + + LogInfo("Served directory listing for: %s", dir_path.c_str()); + return true; + } catch (const std::exception& e) { + LogErr("Failed to generate directory listing: %s", e.what()); + return false; + } +} + +} +} +} \ No newline at end of file diff --git a/modules/http/server/middlewares/file_downloader_middleware.h b/modules/http/server/middlewares/file_downloader_middleware.h new file mode 100644 index 0000000..f1ac685 --- /dev/null +++ b/modules/http/server/middlewares/file_downloader_middleware.h @@ -0,0 +1,129 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 +#define TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 + +#include +#include +#include + +#include +#include "../middleware.h" +#include "../context.h" + +namespace tbox { +namespace http { +namespace server { + +/** + * 文件下载中间件 + * + * 用于处理静态文件的下载请求,可以设置多个目录作为文件源 + * 自动防止目录遍历攻击(防止访问指定目录之外的文件) + */ +class FileDownloaderMiddleware : public Middleware { + public: + /** + * 构造函数 + */ + explicit FileDownloaderMiddleware(); + ~FileDownloaderMiddleware(); + + NONCOPYABLE(FileDownloaderMiddleware); + + /** + * 添加文件目录 + * + * @param url_prefix URL前缀,以'/'开头 + * @param local_path 本地文件目录的路径,可以是相对路径或绝对路径 + * @param default_file 默认文件,当请求目录时返回的文件,默认是"index.html" + * @return 是否添加成功 + */ + bool addDirectory(const std::string& url_prefix, + const std::string& local_path, + const std::string& default_file = "index.html"); + + /** + * 设置是否允许列出目录内容 + * + * @param enable 是否启用列目录功能 + */ + void setDirectoryListingEnabled(bool enable); + + /** + * 设置路径映射,允许将特定URL映射到特定文件 + * + * @param url URL路径 + * @param file 文件路径 + */ + void setPathMapping(const std::string& url, const std::string& file); + + /** + * 设置默认的MIME类型 + * + * @param mime_type MIME类型字符串 + */ + void setDefaultMimeType(const std::string& mime_type); + + /** + * 设置文件扩展名到MIME类型的映射 + * + * @param ext 文件扩展名(不包含'.') + * @param mime_type MIME类型字符串 + */ + void setMimeType(const std::string& ext, const std::string& mime_type); + + protected: + /** + * 实现Middleware接口的处理函数 + */ + virtual void handle(ContextSptr sp_ctx, const NextFunc& next) override; + + private: + /** + * 检查路径是否安全(防止目录遍历攻击) + */ + bool isPathSafe(const std::string& path) const; + + /** + * 根据文件扩展名获取MIME类型 + */ + std::string getMimeType(const std::string& filename) const; + + /** + * 响应文件内容 + */ + bool respondFile(ContextSptr sp_ctx, const std::string& file_path); + + /** + * 生成目录列表 + */ + bool respondDirectoryListing(ContextSptr sp_ctx, const std::string& dir_path, const std::string& url_path); + + private: + struct Data; + Data* d_; +}; + +} +} +} + +#endif // TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 \ No newline at end of file diff --git a/modules/http/server/middlewares/multipart_form_middleware.cpp b/modules/http/server/middlewares/form_data_middleware.cpp similarity index 94% rename from modules/http/server/middlewares/multipart_form_middleware.cpp rename to modules/http/server/middlewares/form_data_middleware.cpp index be7e669..b3bf28e 100644 --- a/modules/http/server/middlewares/multipart_form_middleware.cpp +++ b/modules/http/server/middlewares/form_data_middleware.cpp @@ -17,7 +17,7 @@ * project authors may be found in the CONTRIBUTORS.md file in the root * of the source tree. */ -#include "multipart_form_middleware.h" +#include "form_data_middleware.h" #include #include @@ -279,28 +279,28 @@ bool ParseAsFormUrlEncoded(const Request& req, FormData& form_data) { } // 匿名命名空间结束 -struct MultipartFormMiddleware::Data { +struct FormDataMiddleware::Data { std::map handlers; }; -MultipartFormMiddleware::MultipartFormMiddleware() : d_(new Data) { } +FormDataMiddleware::FormDataMiddleware() : d_(new Data) { } -MultipartFormMiddleware::~MultipartFormMiddleware() { delete d_; } +FormDataMiddleware::~FormDataMiddleware() { delete d_; } -void MultipartFormMiddleware::post(const std::string& path, FormHandler&& handler) { +void FormDataMiddleware::post(const std::string& path, FormHandler&& handler) { registerHandler(Method::kPost, path, std::move(handler)); } -void MultipartFormMiddleware::get(const std::string& path, FormHandler&& handler) { +void FormDataMiddleware::get(const std::string& path, FormHandler&& handler) { registerHandler(Method::kGet, path, std::move(handler)); } -void MultipartFormMiddleware::registerHandler(Method method, const std::string& path, FormHandler&& handler) { +void FormDataMiddleware::registerHandler(Method method, const std::string& path, FormHandler&& handler) { RouteKey key{method, path}; d_->handlers[key] = std::move(handler); } -void MultipartFormMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { +void FormDataMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { Request& req = sp_ctx->req(); // 检查路由是否匹配 @@ -353,4 +353,4 @@ void MultipartFormMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { } } -} \ No newline at end of file +} diff --git a/modules/http/server/middlewares/multipart_form_middleware.h b/modules/http/server/middlewares/form_data_middleware.h similarity index 81% rename from modules/http/server/middlewares/multipart_form_middleware.h rename to modules/http/server/middlewares/form_data_middleware.h index 6c8f0ef..14f98d4 100644 --- a/modules/http/server/middlewares/multipart_form_middleware.h +++ b/modules/http/server/middlewares/form_data_middleware.h @@ -17,8 +17,8 @@ * project authors may be found in the CONTRIBUTORS.md file in the root * of the source tree. */ -#ifndef TBOX_HTTP_SERVER_MULTIPART_FORM_DATA_H_20250419 -#define TBOX_HTTP_SERVER_MULTIPART_FORM_DATA_H_20250419 +#ifndef TBOX_HTTP_SERVER_FORM_DATA_MIDDLEWARE_H_20250419 +#define TBOX_HTTP_SERVER_FORM_DATA_MIDDLEWARE_H_20250419 #include @@ -31,14 +31,14 @@ namespace http { namespace server { /** - * MultipartFormData中间件,用于处理multipart/form-data表单 + * FormDataData中间件,用于处理multipart/form-data表单 */ -class MultipartFormMiddleware : public Middleware { +class FormDataMiddleware : public Middleware { public: - explicit MultipartFormMiddleware(); - ~MultipartFormMiddleware(); + explicit FormDataMiddleware(); + ~FormDataMiddleware(); - NONCOPYABLE(MultipartFormMiddleware); + NONCOPYABLE(FormDataMiddleware); // 处理表单数据的回调函数类型 using FormHandler = std::function; @@ -84,4 +84,4 @@ class MultipartFormMiddleware : public Middleware { } } -#endif // TBOX_HTTP_SERVER_MULTIPART_FORM_DATA_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_FORM_DATA_MIDDLEWARE_H_20250419 -- Gitee From 37cf0c68b6bd370c10643e6123fca3f456b1f629 Mon Sep 17 00:00:00 2001 From: Hevake Lee Date: Mon, 21 Apr 2025 00:47:00 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(http,eventx,util):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96FileDownloaderMiddleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.添加了util::fs::GetFileType(),获取文件类型; 2.优化ThreadPool与WorkThread,解决临时对象在子线程中释放的问题 --- .../server/async_respond/async_respond.cpp | 2 +- examples/http/server/file_download/Makefile | 4 +- .../server/file_download/file_download.cpp | 20 +- examples/http/server/form_data/Makefile | 2 +- .../http/server/form_data/file_upload.cpp | 24 +- examples/http/server/router/Makefile | 2 +- examples/http/server/router/router.cpp | 4 +- examples/http/server/router/webpage.cpp | 32 ++- examples/http/server/simple/simple.cpp | 2 +- modules/eventx/thread_pool.cpp | 10 +- modules/eventx/work_thread.cpp | 11 +- modules/http/Makefile | 2 +- modules/http/request.cpp | 12 +- modules/http/respond.cpp | 12 +- .../file_downloader_middleware.cpp | 232 +++++++++--------- .../middlewares/file_downloader_middleware.h | 50 ++-- modules/http/server/middlewares/form_data.cpp | 4 +- modules/http/server/middlewares/form_data.h | 4 +- .../middlewares/form_data_middleware.cpp | 82 +++---- .../server/middlewares/form_data_middleware.h | 12 +- modules/http/server/middlewares/route_key.h | 21 +- .../server/middlewares/router_middleware.cpp | 38 +-- .../server/middlewares/router_middleware.h | 19 +- modules/http/server/request_parser.cpp | 9 + modules/http/server/server.cpp | 4 +- modules/http/server/server.h | 2 +- modules/http/server/server_imp.cpp | 12 +- modules/http/server/server_imp.h | 4 +- modules/http/server/types.h | 2 +- modules/util/fs.cpp | 26 +- modules/util/fs.h | 22 +- 31 files changed, 395 insertions(+), 287 deletions(-) diff --git a/examples/http/server/async_respond/async_respond.cpp b/examples/http/server/async_respond/async_respond.cpp index 8ab88ff..8c0193a 100644 --- a/examples/http/server/async_respond/async_respond.cpp +++ b/examples/http/server/async_respond/async_respond.cpp @@ -71,7 +71,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 //! 添加请求处理 srv.use( diff --git a/examples/http/server/file_download/Makefile b/examples/http/server/file_download/Makefile index 3b44e45..78438b1 100644 --- a/examples/http/server/file_download/Makefile +++ b/examples/http/server/file_download/Makefile @@ -9,7 +9,7 @@ # \\ \ \ / # -============' # -# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# Copyright (c) 2025 Hevake and contributors, all rights reserved. # # This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) # Use of this source code is governed by MIT license that can be found @@ -35,4 +35,4 @@ LDFLAGS += \ -lstdc++fs \ -lpthread -ldl -include $(TOP_DIR)/mk/exe_common.mk \ No newline at end of file +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/http/server/file_download/file_download.cpp b/examples/http/server/file_download/file_download.cpp index 561ca4c..08da42c 100644 --- a/examples/http/server/file_download/file_download.cpp +++ b/examples/http/server/file_download/file_download.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -20,7 +20,7 @@ /** * 文件下载服务器示例 - * + * * 这个示例展示了如何使用 FileDownloaderMiddleware 实现文件下载功能 * 包含目录浏览和文件下载功能 */ @@ -87,14 +87,14 @@ int main(int argc, char **argv) // 处理命令行参数 if (argc >= 2) serve_dir = argv[1]; - + if (argc >= 3) bind_addr = argv[2]; // 启用日志输出 LogOutput_Enable(); LogInfo("Starting file download server"); - + // 检查目录是否存在 if (!tbox::util::fs::IsDirectoryExist(serve_dir)) { LogErr("Directory '%s' doesn't exist or is not a directory", serve_dir.c_str()); @@ -125,11 +125,11 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 // 创建路由器和文件下载中间件 RouterMiddleware router; - FileDownloaderMiddleware file_downloader; + FileDownloaderMiddleware file_downloader(sp_loop); // 添加状态页面路由 router.get("/api/status", [](ContextSptr ctx, const NextFunc& next) { @@ -169,15 +169,15 @@ int main(int argc, char **argv) } ); - LogInfo("File download server started, listening on http://%s/, serving directory: %s", + LogInfo("File download server started, listening on http://%s/, serving directory: %s", bind_addr.c_str(), serve_dir.c_str()); - + // 运行事件循环 sp_loop->runLoop(); - + LogInfo("Server stopped"); srv.cleanup(); LogInfo("Exiting"); return 0; -} \ No newline at end of file +} diff --git a/examples/http/server/form_data/Makefile b/examples/http/server/form_data/Makefile index 348fe46..699a8f3 100644 --- a/examples/http/server/form_data/Makefile +++ b/examples/http/server/form_data/Makefile @@ -9,7 +9,7 @@ # \\ \ \ / # -============' # -# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# Copyright (c) 2025 Hevake and contributors, all rights reserved. # # This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) # Use of this source code is governed by MIT license that can be found diff --git a/examples/http/server/form_data/file_upload.cpp b/examples/http/server/form_data/file_upload.cpp index 8f5cc81..894d046 100644 --- a/examples/http/server/form_data/file_upload.cpp +++ b/examples/http/server/form_data/file_upload.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -20,7 +20,7 @@ /** * 文件上传服务器示例 - * + * * 这个示例展示了如何使用 FormDataMiddleware 中间件处理文件上传 */ @@ -176,7 +176,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 // 创建路由和表单数据处理器 RouterMiddleware router; @@ -193,29 +193,29 @@ int main(int argc, char **argv) form_data.post("/upload", [&upload_dir](ContextSptr ctx, const FormData& form_data, const NextFunc& next) { // 获取表单字段 std::string name, email, description, filename, file_content; - + if (!form_data.getField("name", name)) name = "未提供"; - + if (!form_data.getField("email", email)) email = "未提供"; - + if (!form_data.getField("description", description)) description = "未提供"; - + // 获取上传的文件 if (form_data.getFile("file", filename, file_content)) { // 保存文件 if (!filename.empty()) { std::string file_path = upload_dir + "/" + filename; std::ofstream outfile(file_path, std::ios::binary); - + if (outfile.is_open()) { outfile.write(file_content.c_str(), file_content.size()); outfile.close(); - + LogInfo("File saved: %s", file_path.c_str()); - + // 返回成功页面 ctx->res().status_code = StatusCode::k200_OK; ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; @@ -251,10 +251,10 @@ int main(int argc, char **argv) ); LogInfo("File upload server started, listening on http://%s/", bind_addr.c_str()); - + // 运行事件循环 sp_loop->runLoop(); - + LogInfo("Server stopped"); srv.cleanup(); diff --git a/examples/http/server/router/Makefile b/examples/http/server/router/Makefile index 657dd6e..1d8fbd9 100644 --- a/examples/http/server/router/Makefile +++ b/examples/http/server/router/Makefile @@ -9,7 +9,7 @@ # \\ \ \ / # -============' # -# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# Copyright (c) 2025 Hevake and contributors, all rights reserved. # # This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) # Use of this source code is governed by MIT license that can be found diff --git a/examples/http/server/router/router.cpp b/examples/http/server/router/router.cpp index 3a2b217..f4131b4 100644 --- a/examples/http/server/router/router.cpp +++ b/examples/http/server/router/router.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -67,7 +67,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 RouterMiddleware router; srv.use(&router); diff --git a/examples/http/server/router/webpage.cpp b/examples/http/server/router/webpage.cpp index 1cd9840..0cb6360 100644 --- a/examples/http/server/router/webpage.cpp +++ b/examples/http/server/router/webpage.cpp @@ -1,4 +1,22 @@ - +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ namespace webpage { @@ -84,13 +102,13 @@ R"(

页面一

演示第一个示例页面

- 访问 + 访问
- +

页面二

演示第二个示例页面

- 访问 + 访问
@@ -158,7 +176,7 @@ const char* kHtmlPage1 = )"; -const char* kHtmlPage2 = +const char* kHtmlPage2 = R"( @@ -214,7 +232,7 @@ R"(

页面二

欢迎来到第二个示例页面!这里展示了更多的HTML内容。

- +

TBOX框架特点:

    @@ -230,4 +248,4 @@ R"( )"; -} \ No newline at end of file +} diff --git a/examples/http/server/simple/simple.cpp b/examples/http/server/simple/simple.cpp index 6593dd9..983fec9 100644 --- a/examples/http/server/simple/simple.cpp +++ b/examples/http/server/simple/simple.cpp @@ -60,7 +60,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 //! 添加请求处理 srv.use( diff --git a/modules/eventx/thread_pool.cpp b/modules/eventx/thread_pool.cpp index 5f1c44c..0523313 100644 --- a/modules/eventx/thread_pool.cpp +++ b/modules/eventx/thread_pool.cpp @@ -352,16 +352,18 @@ void ThreadPool::threadProc(ThreadToken thread_token) wait_time_cost.count() / 1000, exec_time_cost.count() / 1000); - if (item->main_cb) { - RECORD_SCOPE(); - d_->wp_loop->runInLoop(item->main_cb, "ThreadPool::threadProc, invoke main_cb"); - } + auto main_cb = std::move(item->main_cb); { std::lock_guard lg(d_->lock); d_->doing_tasks_token.erase(item->token); d_->task_pool.free(item); } + + if (main_cb) { + RECORD_SCOPE(); + d_->wp_loop->runInLoop(std::move(main_cb), "ThreadPool::threadProc, invoke main_cb"); + } } } diff --git a/modules/eventx/work_thread.cpp b/modules/eventx/work_thread.cpp index fe6ed46..ba99743 100644 --- a/modules/eventx/work_thread.cpp +++ b/modules/eventx/work_thread.cpp @@ -239,16 +239,19 @@ void WorkThread::threadProc() wait_time_cost.count() / 1000, exec_time_cost.count() / 1000); - if (item->main_cb && item->main_loop != nullptr) { - RECORD_SCOPE(); - item->main_loop->runInLoop(item->main_cb, "WorkThread::threadProc, invoke main_cb"); - } + auto main_cb = std::move(item->main_cb); + auto main_loop = item->main_loop; { std::lock_guard lg(d_->lock); d_->doing_tasks_token.erase(item->token); d_->task_pool.free(item); } + + if (main_cb && main_loop != nullptr) { + RECORD_SCOPE(); + main_loop->runInLoop(std::move(main_cb), "WorkThread::threadProc, invoke main_cb"); + } } } diff --git a/modules/http/Makefile b/modules/http/Makefile index 6e588d9..6fc3bb0 100644 --- a/modules/http/Makefile +++ b/modules/http/Makefile @@ -64,7 +64,7 @@ TEST_CPP_SRC_FILES = \ url_test.cpp \ server/request_parser_test.cpp \ -TEST_LDFLAGS := $(LDFLAGS) -ltbox_network -ltbox_log -ltbox_event -ltbox_util -ltbox_base -ldl +TEST_LDFLAGS := $(LDFLAGS) -ltbox_network -ltbox_log -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base -ldl ENABLE_SHARED_LIB = no diff --git a/modules/http/request.cpp b/modules/http/request.cpp index c4a1041..2ba6134 100644 --- a/modules/http/request.cpp +++ b/modules/http/request.cpp @@ -33,9 +33,17 @@ std::string Request::toString() const { std::ostringstream oss; oss << MethodToString(method) << " " << UrlPathToString(url) << " " << HttpVerToString(http_ver) << CRLF; - for (auto &head : headers) + + bool has_content_length = false; + for (auto &head : headers) { oss << head.first << ": " << head.second << CRLF; - oss << "Content-Length: " << body.length() << CRLF; + if (head.first == "Content-Length") + has_content_length = true; + } + + if (!has_content_length) + oss << "Content-Length: " << body.length() << CRLF; + oss << CRLF; oss << body; diff --git a/modules/http/respond.cpp b/modules/http/respond.cpp index 3f1df03..f527d08 100644 --- a/modules/http/respond.cpp +++ b/modules/http/respond.cpp @@ -32,9 +32,17 @@ std::string Respond::toString() const { std::ostringstream oss; oss << HttpVerToString(http_ver) << " " << StatusCodeToString(status_code) << CRLF; - for (auto &head : headers) + + bool has_content_length = false; + for (auto &head : headers) { oss << head.first << ": " << head.second << CRLF; - oss << "Content-Length: " << body.length() << CRLF; + if (head.first == "Content-Length") + has_content_length = true; + } + + if (!has_content_length) + oss << "Content-Length: " << body.length() << CRLF; + oss << CRLF; oss << body; diff --git a/modules/http/server/middlewares/file_downloader_middleware.cpp b/modules/http/server/middlewares/file_downloader_middleware.cpp index 13cee69..a6993da 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.cpp +++ b/modules/http/server/middlewares/file_downloader_middleware.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -21,19 +21,33 @@ #include #include -#include #include -#include #include #include #include #include +#include namespace tbox { namespace http { namespace server { +namespace { +bool IsPathSafe(const std::string& path) { + // 检查是否有".."路径组件,这可能导致目录遍历 + std::istringstream path_stream(path); + std::string component; + + while (std::getline(path_stream, component, '/')) { + if (component == "..") + return false; // 不允许上级目录访问 + } + + return true; +} +} + // 目录配置项 struct DirectoryConfig { std::string url_prefix; // URL前缀 @@ -43,13 +57,18 @@ struct DirectoryConfig { // 中间件私有数据结构 struct FileDownloaderMiddleware::Data { + eventx::ThreadPool worker; std::vector directories; // 目录配置列表 std::map path_mappings;// 特定路径映射 std::map mime_types; // MIME类型映射 std::string default_mime_type; // 默认MIME类型 bool directory_listing_enabled; // 是否允许目录列表 - Data() : default_mime_type("application/octet-stream"), directory_listing_enabled(false) { + Data(event::Loop *wp_loop) + : worker(wp_loop) + , default_mime_type("application/octet-stream") + , directory_listing_enabled(false) { + worker.initialize(); // 初始化常见MIME类型 mime_types["html"] = "text/html"; mime_types["htm"] = "text/html"; @@ -78,15 +97,15 @@ struct FileDownloaderMiddleware::Data { } }; -FileDownloaderMiddleware::FileDownloaderMiddleware() : d_(new Data) {} +FileDownloaderMiddleware::FileDownloaderMiddleware(event::Loop *wp_loop) + : d_(new Data(wp_loop)) +{ } -FileDownloaderMiddleware::~FileDownloaderMiddleware() { - delete d_; -} +FileDownloaderMiddleware::~FileDownloaderMiddleware() { delete d_; } -bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, - const std::string& local_path, - const std::string& default_file) { +bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, + const std::string& local_path, + const std::string& default_file) { // 验证URL前缀是否以'/'开头 if (url_prefix.empty() || url_prefix[0] != '/') { LogErr("Invalid URL prefix: %s. Must start with '/'", url_prefix.c_str()); @@ -94,8 +113,7 @@ bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, } // 验证本地路径是否存在且是目录 - struct stat path_stat; - if (stat(local_path.c_str(), &path_stat) != 0 || !S_ISDIR(path_stat.st_mode)) { + if (!util::fs::IsDirectoryExist(local_path)) { LogErr("Invalid local path: %s. Directory does not exist", local_path.c_str()); return false; } @@ -133,7 +151,7 @@ void FileDownloaderMiddleware::setMimeType(const std::string& ext, const std::st void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { const auto& request = sp_ctx->req(); - + // 只处理GET和HEAD请求 if (request.method != Method::kGet && request.method != Method::kHead) { next(); @@ -141,149 +159,139 @@ void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) } const std::string& request_path = request.url.path; - + // 检查特定路径映射 auto mapping_it = d_->path_mappings.find(request_path); if (mapping_it != d_->path_mappings.end()) { if (respondFile(sp_ctx, mapping_it->second)) return; } - + // 查找匹配的目录配置 for (const auto& dir : d_->directories) { // 检查URL是否以该目录前缀开头 if (request_path.find(dir.url_prefix) == 0) { // 获取相对路径部分 std::string rel_path = request_path.substr(dir.url_prefix.length()); - + // 如果路径以'/'开头,去掉这个斜杠避免双斜杠 if (!rel_path.empty() && rel_path[0] == '/') rel_path = rel_path.substr(1); - + // 构造本地文件路径 std::string file_path = dir.local_path + rel_path; - + // 检查路径安全性 - if (!isPathSafe(file_path)) { + if (!IsPathSafe(file_path)) { LogWarn("Unsafe path detected: %s", file_path.c_str()); sp_ctx->res().status_code = StatusCode::k403_Forbidden; return; } - + + auto file_type = util::fs::GetFileType(file_path); // 检查路径是否是目录 - struct stat path_stat; - if (stat(file_path.c_str(), &path_stat) == 0) { - if (S_ISDIR(path_stat.st_mode)) { - // 如果是目录且路径不以'/'结尾,进行重定向 - if (!request_path.empty() && request_path.back() != '/') { - sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; - sp_ctx->res().headers["Location"] = request_path + "/"; - return; - } - - // 尝试访问默认文件 - std::string default_file_path = file_path + dir.default_file; - if (stat(default_file_path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode)) { - if (respondFile(sp_ctx, default_file_path)) - return; - } - - // 如果允许目录列表,生成目录内容 - if (d_->directory_listing_enabled) { - if (respondDirectoryListing(sp_ctx, file_path, request_path)) - return; - } - - // 否则返回403 Forbidden - LogInfo("Directory listing disabled for: %s", file_path.c_str()); - sp_ctx->res().status_code = StatusCode::k403_Forbidden; + if (file_type == util::fs::FileType::kDirectory) { + // 如果是目录且路径不以'/'结尾,进行重定向 + if (!request_path.empty() && request_path.back() != '/') { + sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; + sp_ctx->res().headers["Location"] = request_path + "/"; return; - } else if (S_ISREG(path_stat.st_mode)) { - // 如果是普通文件,直接响应文件内容 - if (respondFile(sp_ctx, file_path)) + } + + // 尝试访问默认文件 + std::string default_file_path = file_path + dir.default_file; + if (util::fs::GetFileType(default_file_path) == util::fs::FileType::kRegular) { + if (respondFile(sp_ctx, default_file_path)) + return; + } + + // 如果允许目录列表,生成目录内容 + if (d_->directory_listing_enabled) { + if (respondDirectoryListing(sp_ctx, file_path, request_path)) return; } + + // 否则返回403 Forbidden + LogInfo("Directory listing disabled for: %s", file_path.c_str()); + sp_ctx->res().status_code = StatusCode::k403_Forbidden; + return; + + } else if (file_type == util::fs::FileType::kRegular) { + // 如果是普通文件,直接响应文件内容 + if (respondFile(sp_ctx, file_path)) + return; } } } - + // 如果没有找到匹配的文件,传递给下一个中间件 next(); } -bool FileDownloaderMiddleware::isPathSafe(const std::string& path) const { - // 检查是否有".."路径组件,这可能导致目录遍历 - std::istringstream path_stream(path); - std::string component; - - while (std::getline(path_stream, component, '/')) { - if (component == "..") - return false; // 不允许上级目录访问 - } - - return true; -} - std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) const { // 查找最后一个点的位置 size_t dot_pos = filename.find_last_of('.'); if (dot_pos != std::string::npos) { - std::string ext = filename.substr(dot_pos + 1); - // 转换为小写 - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c){ return std::tolower(c); }); - + std::string ext = util::string::ToLower(filename.substr(dot_pos + 1)); // 在MIME类型映射中查找 auto it = d_->mime_types.find(ext); - if (it != d_->mime_types.end()) { + if (it != d_->mime_types.end()) return it->second; - } } - + // 未找到匹配的MIME类型,返回默认值 return d_->default_mime_type; } bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path) { - // 打开文件 - std::ifstream file(file_path, std::ios::binary | std::ios::ate); - if (!file.is_open()) { - LogWarn("Failed to open file: %s", file_path.c_str()); + auto& res = sp_ctx->res(); + + if (!util::fs::IsFileExist(file_path)) { + LogNotice("file not exist: %s", file_path.c_str()); + res.status_code = StatusCode::k404_NotFound; return false; } - - // 获取文件大小 - std::streamsize file_size = file.tellg(); - file.seekg(0, std::ios::beg); - - // 设置响应头 - auto& res = sp_ctx->res(); - res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = getMimeType(file_path); - res.headers["Content-Length"] = std::to_string(file_size); - - // 添加缓存控制头(可选) res.headers["Cache-Control"] = "max-age=86400"; // 1天缓存 - - // 如果是HEAD请求,不返回内容 - if (sp_ctx->req().method == Method::kHead) { - return true; - } - - // 读取文件内容 - std::vector buffer(file_size); - if (file.read(buffer.data(), file_size)) { - res.body.assign(buffer.data(), file_size); - LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); - return true; - } - - LogErr("Failed to read file: %s", file_path.c_str()); - return false; + + d_->worker.execute( + [sp_ctx, file_path] { + auto& res = sp_ctx->res(); + + // 打开文件 + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + LogWarn("Failed to open file: %s", file_path.c_str()); + res.status_code = StatusCode::k404_NotFound; + return; + } + + // 获取文件大小 + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + // 设置响应头 + res.status_code = StatusCode::k200_OK; + res.headers["Content-Length"] = std::to_string(file_size); + + // 如果是HEAD请求,不返回内容 + if (sp_ctx->req().method == Method::kHead) + return; + + res.body = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); + }, + [sp_ctx, file_path] { + LogInfo("Served file: %s", file_path.c_str()); + } + ); + + return true; } -bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, - const std::string& dir_path, +bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, + const std::string& dir_path, const std::string& url_path) { try { // 生成HTML目录列表 @@ -305,7 +313,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, << "\n" << "

    Directory listing for " << url_path << "

    \n" << "
      \n"; - + // 如果不是根目录,添加返回上级目录的链接 if (url_path != "/") { size_t last_slash = url_path.find_last_of('/', url_path.size() - 2); @@ -314,7 +322,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, html << "
    • ..
    • \n"; } } - + // 列出目录中的项目 std::vector entries; if (!util::fs::ListDirectory(dir_path, entries)) { @@ -325,7 +333,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, for (const auto& name : entries) { std::string entry_path = dir_path + "/" + name; std::string href = url_path + name; - + if (util::fs::IsDirectoryExist(entry_path)) { href += "/"; html << "
    • " << name << "/
    • \n"; @@ -333,17 +341,17 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, html << "
    • " << name << "
    • \n"; } } - + html << "
    \n" << "\n" << ""; - + // 设置响应 auto& res = sp_ctx->res(); res.status_code = StatusCode::k200_OK; res.headers["Content-Type"] = "text/html; charset=utf-8"; res.body = html.str(); - + LogInfo("Served directory listing for: %s", dir_path.c_str()); return true; } catch (const std::exception& e) { @@ -354,4 +362,4 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, } } -} \ No newline at end of file +} diff --git a/modules/http/server/middlewares/file_downloader_middleware.h b/modules/http/server/middlewares/file_downloader_middleware.h index f1ac685..562f1ef 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.h +++ b/modules/http/server/middlewares/file_downloader_middleware.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -25,6 +25,8 @@ #include #include +#include + #include "../middleware.h" #include "../context.h" @@ -34,7 +36,7 @@ namespace server { /** * 文件下载中间件 - * + * * 用于处理静态文件的下载请求,可以设置多个目录作为文件源 * 自动防止目录遍历攻击(防止访问指定目录之外的文件) */ @@ -43,50 +45,51 @@ class FileDownloaderMiddleware : public Middleware { /** * 构造函数 */ - explicit FileDownloaderMiddleware(); - ~FileDownloaderMiddleware(); + explicit FileDownloaderMiddleware(event::Loop *wp_loop); + virtual ~FileDownloaderMiddleware(); NONCOPYABLE(FileDownloaderMiddleware); /** * 添加文件目录 - * - * @param url_prefix URL前缀,以'/'开头 - * @param local_path 本地文件目录的路径,可以是相对路径或绝对路径 - * @param default_file 默认文件,当请求目录时返回的文件,默认是"index.html" - * @return 是否添加成功 + * + * \param url_prefix URL前缀,以'/'开头 + * \param local_path 本地文件目录的路径,可以是相对路径或绝对路径 + * \param default_file 默认文件,当请求目录时返回的文件,默认是"index.html" + * + * \return 是否添加成功 */ - bool addDirectory(const std::string& url_prefix, + bool addDirectory(const std::string& url_prefix, const std::string& local_path, const std::string& default_file = "index.html"); /** * 设置是否允许列出目录内容 - * - * @param enable 是否启用列目录功能 + * + * \param enable 是否启用列目录功能 */ void setDirectoryListingEnabled(bool enable); /** * 设置路径映射,允许将特定URL映射到特定文件 - * - * @param url URL路径 - * @param file 文件路径 + * + * \param url URL路径 + * \param file 文件路径 */ void setPathMapping(const std::string& url, const std::string& file); /** * 设置默认的MIME类型 - * - * @param mime_type MIME类型字符串 + * + * \param mime_type MIME类型字符串 */ void setDefaultMimeType(const std::string& mime_type); /** * 设置文件扩展名到MIME类型的映射 - * - * @param ext 文件扩展名(不包含'.') - * @param mime_type MIME类型字符串 + * + * \param ext 文件扩展名(不包含'.') + * \param mime_type MIME类型字符串 */ void setMimeType(const std::string& ext, const std::string& mime_type); @@ -97,11 +100,6 @@ class FileDownloaderMiddleware : public Middleware { virtual void handle(ContextSptr sp_ctx, const NextFunc& next) override; private: - /** - * 检查路径是否安全(防止目录遍历攻击) - */ - bool isPathSafe(const std::string& path) const; - /** * 根据文件扩展名获取MIME类型 */ @@ -126,4 +124,4 @@ class FileDownloaderMiddleware : public Middleware { } } -#endif // TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 diff --git a/modules/http/server/middlewares/form_data.cpp b/modules/http/server/middlewares/form_data.cpp index a7ec7b5..ec267f2 100644 --- a/modules/http/server/middlewares/form_data.cpp +++ b/modules/http/server/middlewares/form_data.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -83,4 +83,4 @@ void FormData::clear() { } } -} \ No newline at end of file +} diff --git a/modules/http/server/middlewares/form_data.h b/modules/http/server/middlewares/form_data.h index 182805e..13c9ae1 100644 --- a/modules/http/server/middlewares/form_data.h +++ b/modules/http/server/middlewares/form_data.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -102,4 +102,4 @@ class FormData { } } -#endif // TBOX_HTTP_SERVER_FORM_DATA_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_FORM_DATA_H_20250419 diff --git a/modules/http/server/middlewares/form_data_middleware.cpp b/modules/http/server/middlewares/form_data_middleware.cpp index b3bf28e..4b8f5bc 100644 --- a/modules/http/server/middlewares/form_data_middleware.cpp +++ b/modules/http/server/middlewares/form_data_middleware.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -47,9 +47,9 @@ bool ParseBoundary(const std::string& content_type, std::string &boundary) { size_t pos = content_type.find("boundary="); if (pos == std::string::npos) return false; - + pos += 9; // "boundary="的长度 - + // 如果boundary参数值被引号包围,则去掉引号 if (content_type[pos] == '"') { size_t end_pos = content_type.find('"', pos + 1); @@ -76,23 +76,23 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { LogNotice("Not a form-data Content-Disposition"); return false; } - + // 解析参数 size_t pos = 0; while ((pos = header_value.find(';', pos)) != std::string::npos) { pos++; // 跳过分号 - + // 跳过空白字符 while (pos < header_value.length() && std::isspace(header_value[pos])) pos++; - + // 查找参数名称和值 size_t eq_pos = header_value.find('=', pos); if (eq_pos == std::string::npos) continue; - + std::string param_name = header_value.substr(pos, eq_pos - pos); - + // 处理引号包围的值 size_t value_start = eq_pos + 1; if (header_value[value_start] == '"') { @@ -100,9 +100,9 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { size_t value_end = header_value.find('"', value_start); if (value_end == std::string::npos) break; - + std::string param_value = header_value.substr(value_start, value_end - value_start); - + if (param_name == "name") { item.name = param_value; item.type = FormItem::Type::kField; @@ -111,11 +111,11 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { item.filename = param_value; item.type = FormItem::Type::kFile; } - + pos = value_end + 1; } } - + return true; } @@ -125,39 +125,39 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { bool ParseFormItemHeaders(const std::string& headers_str, FormItem& item) { std::istringstream headers_stream(headers_str); std::string line; - + // 逐行解析头部 while (std::getline(headers_stream, line)) { // 移除行尾的\r if (!line.empty() && line.back() == '\r') line.pop_back(); - + // 跳过空行 if (line.empty()) continue; - + // 解析头部字段 size_t colon_pos = line.find(':'); if (colon_pos == std::string::npos) { LogNotice("Invalid header format: %s", line.c_str()); continue; } - + std::string header_name = util::string::Strip(line.substr(0, colon_pos)); std::string header_value = util::string::Strip(line.substr(colon_pos + 1)); - + // 存储头部 item.headers[header_name] = header_value; - + // 处理Content-Disposition头部 if (util::string::ToLower(header_name) == "content-disposition") ParseContentDisposition(header_value, item); - + // 处理Content-Type头部 if (util::string::ToLower(header_name) == "content-type") item.content_type = header_value; } - + return !item.name.empty(); } @@ -171,17 +171,17 @@ bool ParseFormPart(const std::string& part, FormItem& item) { LogNotice("Invalid form part format"); return false; } - + std::string headers_str = part.substr(0, headers_end); std::string body = part.substr(headers_end + 4); // +4 跳过两个CRLF - + // 解析头部 if (!ParseFormItemHeaders(headers_str, item)) return false; - + // 设置内容 item.value = body; - + return true; } @@ -195,22 +195,22 @@ bool ParseAsMultipartFormData(const Request& req, const std::string &content_typ // 带两个连字符的完整边界 const std::string delimiter = "--" + boundary; - + // 分割请求体以获取各个部分 size_t pos = 0; - + // 跳过第一个边界 pos = req.body.find(delimiter, pos); if (pos == std::string::npos) { LogNotice("Initial boundary not found"); return false; } - + pos += delimiter.length(); // 跳过CRLF if (req.body.substr(pos, 2) == "\r\n") pos += 2; - + // 查找每个部分 while (true) { // 查找下一个边界 @@ -219,27 +219,27 @@ bool ParseAsMultipartFormData(const Request& req, const std::string &content_typ // 没有找到更多边界 if (next_pos == std::string::npos) break; - + // 提取该部分的内容(不包括前导CRLF) const std::string part = req.body.substr(pos, next_pos - pos - 2); // 减去末尾的CRLF - + // 解析表单项 FormItem item; if (ParseFormPart(part, item)) form_data.addItem(item); - + // 移动到下一个部分 pos = next_pos + delimiter.length(); // 检查是否是最后一个边界 if (req.body.substr(pos, 2) == "--") break; // 这是最后一个边界 - + // 跳过CRLF if (req.body.substr(pos, 2) == "\r\n") pos += 2; } - + return true; } @@ -247,32 +247,32 @@ bool ParseAsFormUrlEncoded(const Request& req, FormData& form_data) { // 分割请求体以获取各个字段 const std::string &body = req.body; size_t pos = 0; - + while (pos < body.length()) { // 查找下一个字段分隔符(&) size_t next_pos = body.find('&', pos); if (next_pos == std::string::npos) next_pos = body.length(); - + // 提取键值对 std::string pair = body.substr(pos, next_pos - pos); size_t eq_pos = pair.find('='); - + if (eq_pos != std::string::npos) { std::string name = UrlDecode(pair.substr(0, eq_pos)); std::string value = UrlDecode(pair.substr(eq_pos + 1)); - + FormItem item; item.type = FormItem::Type::kField; item.name = name; item.value = value; form_data.addItem(item); } - + // 移动到下一个字段 pos = next_pos + 1; } - + return true; } @@ -302,7 +302,7 @@ void FormDataMiddleware::registerHandler(Method method, const std::string& path, void FormDataMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { Request& req = sp_ctx->req(); - + // 检查路由是否匹配 RouteKey key{req.method, req.url.path}; auto it = d_->handlers.find(key); @@ -315,7 +315,7 @@ void FormDataMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { // 解析表单数据 FormData form_data; bool parsed = false; - + // 获取Content-Type auto content_type_it = req.headers.find("Content-Type"); if (content_type_it != req.headers.end()) { diff --git a/modules/http/server/middlewares/form_data_middleware.h b/modules/http/server/middlewares/form_data_middleware.h index 14f98d4..23a4b32 100644 --- a/modules/http/server/middlewares/form_data_middleware.h +++ b/modules/http/server/middlewares/form_data_middleware.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -31,7 +31,9 @@ namespace http { namespace server { /** - * FormDataData中间件,用于处理multipart/form-data表单 + * 表单中间件,用于处理: + * - multipart/form-data + * - application/x-www-form-urlencoded */ class FormDataMiddleware : public Middleware { public: @@ -45,7 +47,7 @@ class FormDataMiddleware : public Middleware { /** * 注册POST请求处理器 - * + * * @param path 请求路径 * @param handler 处理函数 */ @@ -53,7 +55,7 @@ class FormDataMiddleware : public Middleware { /** * 注册GET请求处理器 - * + * * @param path 请求路径 * @param handler 处理函数 */ @@ -61,7 +63,7 @@ class FormDataMiddleware : public Middleware { /** * 注册特定Method的请求处理器 - * + * * @param method HTTP方法 * @param path 请求路径 * @param handler 处理函数 diff --git a/modules/http/server/middlewares/route_key.h b/modules/http/server/middlewares/route_key.h index 5179570..5678406 100644 --- a/modules/http/server/middlewares/route_key.h +++ b/modules/http/server/middlewares/route_key.h @@ -1,3 +1,22 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ #ifndef TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 #define TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 @@ -23,4 +42,4 @@ struct RouteKey { } } -#endif // TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 diff --git a/modules/http/server/middlewares/router_middleware.cpp b/modules/http/server/middlewares/router_middleware.cpp index fdfda63..ef74654 100644 --- a/modules/http/server/middlewares/router_middleware.cpp +++ b/modules/http/server/middlewares/router_middleware.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -26,7 +26,7 @@ namespace http { namespace server { struct RouterMiddleware::Data { - std::map cbs_; + std::map route_handles; }; RouterMiddleware::RouterMiddleware() : d_(new Data) @@ -40,9 +40,9 @@ RouterMiddleware::~RouterMiddleware() void RouterMiddleware::handle(ContextSptr sp_ctx, const NextFunc &next) { RouteKey key{sp_ctx->req().method, sp_ctx->req().url.path}; - - auto iter = d_->cbs_.find(key); - if (iter != d_->cbs_.end()) { + + auto iter = d_->route_handles.find(key); + if (iter != d_->route_handles.end()) { if (iter->second) { iter->second(sp_ctx, next); return; @@ -51,45 +51,45 @@ void RouterMiddleware::handle(ContextSptr sp_ctx, const NextFunc &next) next(); } -RouterMiddleware& RouterMiddleware::get(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::get(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kGet, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kGet, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::post(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::post(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kPost, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kPost, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::put(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::put(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kPut, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kPut, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::del(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::del(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kDelete, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kDelete, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::opt(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::opt(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kOptions, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kOptions, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::head(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::head(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kHead, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kHead, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::trace(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::trace(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kTrace, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kTrace, path}] = std::move(handler); return *this; } diff --git a/modules/http/server/middlewares/router_middleware.h b/modules/http/server/middlewares/router_middleware.h index 0bfae16..b0e5e0d 100644 --- a/modules/http/server/middlewares/router_middleware.h +++ b/modules/http/server/middlewares/router_middleware.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -27,19 +27,22 @@ namespace tbox { namespace http { namespace server { +/** + * 路由中间件 + */ class RouterMiddleware : public Middleware { public: RouterMiddleware(); ~RouterMiddleware(); public: - RouterMiddleware& get (const std::string &path, RequestCallback &&cb); - RouterMiddleware& post (const std::string &path, RequestCallback &&cb); - RouterMiddleware& put (const std::string &path, RequestCallback &&cb); - RouterMiddleware& del (const std::string &path, RequestCallback &&cb); - RouterMiddleware& opt (const std::string &path, RequestCallback &&cb); - RouterMiddleware& head (const std::string &path, RequestCallback &&cb); - RouterMiddleware& trace(const std::string &path, RequestCallback &&cb); + RouterMiddleware& get (const std::string &path, RequestHandler &&handler); + RouterMiddleware& post (const std::string &path, RequestHandler &&handler); + RouterMiddleware& put (const std::string &path, RequestHandler &&handler); + RouterMiddleware& del (const std::string &path, RequestHandler &&handler); + RouterMiddleware& opt (const std::string &path, RequestHandler &&handler); + RouterMiddleware& head (const std::string &path, RequestHandler &&handler); + RouterMiddleware& trace(const std::string &path, RequestHandler &&handler); public: virtual void handle(ContextSptr sp_ctx, const NextFunc &next) override; diff --git a/modules/http/server/request_parser.cpp b/modules/http/server/request_parser.cpp index b049d4c..f64547c 100644 --- a/modules/http/server/request_parser.cpp +++ b/modules/http/server/request_parser.cpp @@ -20,6 +20,7 @@ #include "request_parser.h" #include #include +#include #include namespace tbox { @@ -46,6 +47,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto method_str = str.substr(pos, method_str_end); auto method = StringToMethod(method_str); if (method == Method::kUnset) { + LogNotice("method is invalid, method_str:%s", method_str.c_str()); state_ = State::kFail; return pos; } @@ -60,6 +62,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) //! 获取 url auto url_str_begin = str.find_first_not_of(' ', method_str_end); if (url_str_begin == std::string::npos || url_str_begin >= end_pos) { + LogNotice("parse url fail, url not exist."); state_ = State::kFail; return pos; } @@ -67,6 +70,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto url_str_end = str.find_first_of(' ', url_str_begin); auto url_str = str.substr(url_str_begin, url_str_end - url_str_begin); if (!StringToUrlPath(url_str, sp_request_->url)) { + LogNotice("parse url fail, url_str:%s", url_str.c_str()); state_ = State::kFail; return pos; } @@ -74,6 +78,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) //! 获取版本 auto ver_str_begin = str.find_first_not_of(' ', url_str_end); if (ver_str_begin == std::string::npos || ver_str_begin >= end_pos) { + LogNotice("ver not exist"); state_ = State::kFail; return pos; } @@ -81,12 +86,14 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto ver_str_end = end_pos; auto ver_str = str.substr(ver_str_begin, ver_str_end - ver_str_begin); if (ver_str.compare(0, 5, "HTTP/") != 0) { + LogNotice("ver is invalid, ver_str:%s", ver_str.c_str()); state_ = State::kFail; return pos; } auto ver = StringToHttpVer(ver_str); if (ver == HttpVer::kUnset) { + LogNotice("ver is invalid, ver_str:%s", ver_str.c_str()); state_ = State::kFail; return pos; } @@ -117,6 +124,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto colon_pos = str.find_first_of(':', pos); if (colon_pos == std::string::npos || colon_pos >= end_pos) { + LogNotice("can't find ':' in header line"); state_ = State::kFail; return pos; } @@ -124,6 +132,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto head_key = util::string::Strip(str.substr(pos, colon_pos - pos)); auto head_value_start_pos = str.find_first_not_of(' ', colon_pos + 1); //! 要略掉空白 if (head_value_start_pos == std::string::npos || head_value_start_pos >= end_pos) { + LogNotice("can't find header value"); state_ = State::kFail; return pos; } diff --git a/modules/http/server/server.cpp b/modules/http/server/server.cpp index 75944f4..1951253 100644 --- a/modules/http/server/server.cpp +++ b/modules/http/server/server.cpp @@ -63,9 +63,9 @@ void Server::setContextLogEnable(bool enable) return impl_->setContextLogEnable(enable); } -void Server::use(const RequestCallback &cb) +void Server::use(RequestHandler &&handler) { - impl_->use(cb); + impl_->use(std::move(handler)); } void Server::use(Middleware *wp_middleware) diff --git a/modules/http/server/server.h b/modules/http/server/server.h index dce740d..d3bcee9 100644 --- a/modules/http/server/server.h +++ b/modules/http/server/server.h @@ -54,7 +54,7 @@ class Server { void setContextLogEnable(bool enable); public: - void use(const RequestCallback &cb); + void use(RequestHandler &&handler); void use(Middleware *wp_middleware); private: diff --git a/modules/http/server/server_imp.cpp b/modules/http/server/server_imp.cpp index 5f3b019..8db8a8f 100644 --- a/modules/http/server/server_imp.cpp +++ b/modules/http/server/server_imp.cpp @@ -82,7 +82,7 @@ void Server::Impl::cleanup() if (state_ != State::kNone) { stop(); - req_cb_.clear(); + req_handler_.clear(); tcp_server_.cleanup(); for (auto conn : conns_) @@ -93,14 +93,14 @@ void Server::Impl::cleanup() } } -void Server::Impl::use(const RequestCallback &cb) +void Server::Impl::use(RequestHandler &&handler) { - req_cb_.push_back(cb); + req_handler_.push_back(std::move(handler)); } void Server::Impl::use(Middleware *wp_middleware) { - req_cb_.push_back(bind(&Middleware::handle, wp_middleware, _1, _2)); + req_handler_.push_back(bind(&Middleware::handle, wp_middleware, _1, _2)); } void Server::Impl::onTcpConnected(const TcpServer::ConnToken &ct) @@ -271,10 +271,10 @@ void Server::Impl::commitRespond(const TcpServer::ConnToken &ct, int index, Resp void Server::Impl::handle(ContextSptr sp_ctx, size_t cb_index) { RECORD_SCOPE(); - if (cb_index >= req_cb_.size()) + if (cb_index >= req_handler_.size()) return; - auto func = req_cb_.at(cb_index); + auto func = req_handler_.at(cb_index); ++cb_level_; if (func) diff --git a/modules/http/server/server_imp.h b/modules/http/server/server_imp.h index 398aaef..878813a 100644 --- a/modules/http/server/server_imp.h +++ b/modules/http/server/server_imp.h @@ -55,7 +55,7 @@ class Server::Impl { void setContextLogEnable(bool enable) { context_log_enable_ = enable; } public: - void use(const RequestCallback &cb); + void use(RequestHandler &&handler); void use(Middleware *wp_middleware); void commitRespond(const TcpServer::ConnToken &ct, int index, Respond *res); @@ -84,7 +84,7 @@ class Server::Impl { Server *wp_parent_; TcpServer tcp_server_; - vector req_cb_; + vector req_handler_; set conns_; //! 仅用于保存Connection指针,用于释放 State state_ = State::kNone; bool context_log_enable_ = false; diff --git a/modules/http/server/types.h b/modules/http/server/types.h index 3e64cc8..ff0db76 100644 --- a/modules/http/server/types.h +++ b/modules/http/server/types.h @@ -30,7 +30,7 @@ namespace server { class Context; using ContextSptr = std::shared_ptr; using NextFunc = std::function; -using RequestCallback = std::function; +using RequestHandler = std::function; } } diff --git a/modules/util/fs.cpp b/modules/util/fs.cpp index c373300..4f17dfc 100644 --- a/modules/util/fs.cpp +++ b/modules/util/fs.cpp @@ -40,15 +40,27 @@ using std::ifstream; using std::ofstream; using std::exception; +FileType GetFileType(const std::string &file_path) +{ + struct stat st; + + if (::stat(file_path.c_str(), &st) == 0) { + if (S_ISDIR(st.st_mode)) return FileType::kDirectory; + if (S_ISREG(st.st_mode)) return FileType::kRegular; + if (S_ISCHR(st.st_mode)) return FileType::kCharacterDevice; + if (S_ISBLK(st.st_mode)) return FileType::kBlockDevice; + if (S_ISLNK(st.st_mode)) return FileType::kSymbolLink; + if (S_ISSOCK(st.st_mode)) return FileType::kSocket; + if (S_ISFIFO(st.st_mode)) return FileType::kNamedPipe; + } + + return FileType::kNone; +} + bool IsFileExist(const std::string &filename) { -#if 1 int ret = ::access(filename.c_str(), F_OK); return ret == 0; -#else - ifstream ifs(filename); - return ifs.is_open(); -#endif } bool ReadStringFromTextFile(const std::string &filename, std::string &content) @@ -239,9 +251,7 @@ bool MakeLink(const std::string &old_path, const std::string &new_path, bool all bool IsDirectoryExist(const std::string &dir) { - struct stat sb; - //! 如果读到dir的inode信息,且该inode是DIR,则返回true - return ((::stat(dir.c_str(), &sb) == 0) && S_ISDIR(sb.st_mode)); + return GetFileType(dir) == FileType::kDirectory; } bool MakeDirectory(const std::string &origin_dir_path, bool allow_log_print) diff --git a/modules/util/fs.h b/modules/util/fs.h index 166ed90..acdbf2f 100644 --- a/modules/util/fs.h +++ b/modules/util/fs.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -32,6 +32,26 @@ namespace fs { // 文件相关 //////////////////////////////////////////////////////////////////// +//! 文件类型 +enum class FileType { + kNone, //!< 未知 + kDirectory, //!< 目录 + kRegular, //!< 常规文件 + kCharacterDevice, //!< 字符设备 + kBlockDevice, //!< 块设备 + kSymbolLink, //!< 符号链接 + kSocket, //!< 套接字 + kNamedPipe, //!< 有名管道 +}; + +/** + * 获取文件类型 + * + * \param file_path 文件路径 + * \return FileType 文件类型 + */ +FileType GetFileType(const std::string &file_path); + /** * 检查文件是否存在 * -- Gitee From e921aa2c01fbefa697e8bfa31c8192edd839f6e8 Mon Sep 17 00:00:00 2001 From: Hevake Lee Date: Mon, 21 Apr 2025 00:47:00 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(http,eventx,util):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96FileDownloaderMiddleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.添加了util::fs::GetFileType(),获取文件类型; 2.优化ThreadPool与WorkThread,解决临时对象在子线程中释放的问题 --- .../server/async_respond/async_respond.cpp | 2 +- examples/http/server/file_download/Makefile | 4 +- .../server/file_download/file_download.cpp | 20 +- examples/http/server/form_data/Makefile | 2 +- .../http/server/form_data/file_upload.cpp | 24 +- examples/http/server/router/Makefile | 2 +- examples/http/server/router/router.cpp | 4 +- examples/http/server/router/webpage.cpp | 32 ++- examples/http/server/simple/simple.cpp | 2 +- modules/eventx/thread_pool.cpp | 22 +- modules/eventx/work_thread.cpp | 21 +- modules/http/Makefile | 2 +- modules/http/request.cpp | 12 +- modules/http/respond.cpp | 12 +- .../file_downloader_middleware.cpp | 236 +++++++++--------- .../middlewares/file_downloader_middleware.h | 50 ++-- modules/http/server/middlewares/form_data.cpp | 4 +- modules/http/server/middlewares/form_data.h | 4 +- .../middlewares/form_data_middleware.cpp | 82 +++--- .../server/middlewares/form_data_middleware.h | 12 +- modules/http/server/middlewares/route_key.h | 21 +- .../server/middlewares/router_middleware.cpp | 38 +-- .../server/middlewares/router_middleware.h | 19 +- modules/http/server/request_parser.cpp | 9 + modules/http/server/server.cpp | 4 +- modules/http/server/server.h | 2 +- modules/http/server/server_imp.cpp | 12 +- modules/http/server/server_imp.h | 4 +- modules/http/server/types.h | 2 +- modules/util/fs.cpp | 26 +- modules/util/fs.h | 22 +- 31 files changed, 420 insertions(+), 288 deletions(-) diff --git a/examples/http/server/async_respond/async_respond.cpp b/examples/http/server/async_respond/async_respond.cpp index 8ab88ff..8c0193a 100644 --- a/examples/http/server/async_respond/async_respond.cpp +++ b/examples/http/server/async_respond/async_respond.cpp @@ -71,7 +71,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 //! 添加请求处理 srv.use( diff --git a/examples/http/server/file_download/Makefile b/examples/http/server/file_download/Makefile index 3b44e45..78438b1 100644 --- a/examples/http/server/file_download/Makefile +++ b/examples/http/server/file_download/Makefile @@ -9,7 +9,7 @@ # \\ \ \ / # -============' # -# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# Copyright (c) 2025 Hevake and contributors, all rights reserved. # # This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) # Use of this source code is governed by MIT license that can be found @@ -35,4 +35,4 @@ LDFLAGS += \ -lstdc++fs \ -lpthread -ldl -include $(TOP_DIR)/mk/exe_common.mk \ No newline at end of file +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/http/server/file_download/file_download.cpp b/examples/http/server/file_download/file_download.cpp index 561ca4c..08da42c 100644 --- a/examples/http/server/file_download/file_download.cpp +++ b/examples/http/server/file_download/file_download.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -20,7 +20,7 @@ /** * 文件下载服务器示例 - * + * * 这个示例展示了如何使用 FileDownloaderMiddleware 实现文件下载功能 * 包含目录浏览和文件下载功能 */ @@ -87,14 +87,14 @@ int main(int argc, char **argv) // 处理命令行参数 if (argc >= 2) serve_dir = argv[1]; - + if (argc >= 3) bind_addr = argv[2]; // 启用日志输出 LogOutput_Enable(); LogInfo("Starting file download server"); - + // 检查目录是否存在 if (!tbox::util::fs::IsDirectoryExist(serve_dir)) { LogErr("Directory '%s' doesn't exist or is not a directory", serve_dir.c_str()); @@ -125,11 +125,11 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 // 创建路由器和文件下载中间件 RouterMiddleware router; - FileDownloaderMiddleware file_downloader; + FileDownloaderMiddleware file_downloader(sp_loop); // 添加状态页面路由 router.get("/api/status", [](ContextSptr ctx, const NextFunc& next) { @@ -169,15 +169,15 @@ int main(int argc, char **argv) } ); - LogInfo("File download server started, listening on http://%s/, serving directory: %s", + LogInfo("File download server started, listening on http://%s/, serving directory: %s", bind_addr.c_str(), serve_dir.c_str()); - + // 运行事件循环 sp_loop->runLoop(); - + LogInfo("Server stopped"); srv.cleanup(); LogInfo("Exiting"); return 0; -} \ No newline at end of file +} diff --git a/examples/http/server/form_data/Makefile b/examples/http/server/form_data/Makefile index 348fe46..699a8f3 100644 --- a/examples/http/server/form_data/Makefile +++ b/examples/http/server/form_data/Makefile @@ -9,7 +9,7 @@ # \\ \ \ / # -============' # -# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# Copyright (c) 2025 Hevake and contributors, all rights reserved. # # This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) # Use of this source code is governed by MIT license that can be found diff --git a/examples/http/server/form_data/file_upload.cpp b/examples/http/server/form_data/file_upload.cpp index 8f5cc81..894d046 100644 --- a/examples/http/server/form_data/file_upload.cpp +++ b/examples/http/server/form_data/file_upload.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -20,7 +20,7 @@ /** * 文件上传服务器示例 - * + * * 这个示例展示了如何使用 FormDataMiddleware 中间件处理文件上传 */ @@ -176,7 +176,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 // 创建路由和表单数据处理器 RouterMiddleware router; @@ -193,29 +193,29 @@ int main(int argc, char **argv) form_data.post("/upload", [&upload_dir](ContextSptr ctx, const FormData& form_data, const NextFunc& next) { // 获取表单字段 std::string name, email, description, filename, file_content; - + if (!form_data.getField("name", name)) name = "未提供"; - + if (!form_data.getField("email", email)) email = "未提供"; - + if (!form_data.getField("description", description)) description = "未提供"; - + // 获取上传的文件 if (form_data.getFile("file", filename, file_content)) { // 保存文件 if (!filename.empty()) { std::string file_path = upload_dir + "/" + filename; std::ofstream outfile(file_path, std::ios::binary); - + if (outfile.is_open()) { outfile.write(file_content.c_str(), file_content.size()); outfile.close(); - + LogInfo("File saved: %s", file_path.c_str()); - + // 返回成功页面 ctx->res().status_code = StatusCode::k200_OK; ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; @@ -251,10 +251,10 @@ int main(int argc, char **argv) ); LogInfo("File upload server started, listening on http://%s/", bind_addr.c_str()); - + // 运行事件循环 sp_loop->runLoop(); - + LogInfo("Server stopped"); srv.cleanup(); diff --git a/examples/http/server/router/Makefile b/examples/http/server/router/Makefile index 657dd6e..1d8fbd9 100644 --- a/examples/http/server/router/Makefile +++ b/examples/http/server/router/Makefile @@ -9,7 +9,7 @@ # \\ \ \ / # -============' # -# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# Copyright (c) 2025 Hevake and contributors, all rights reserved. # # This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) # Use of this source code is governed by MIT license that can be found diff --git a/examples/http/server/router/router.cpp b/examples/http/server/router/router.cpp index 3a2b217..f4131b4 100644 --- a/examples/http/server/router/router.cpp +++ b/examples/http/server/router/router.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -67,7 +67,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 RouterMiddleware router; srv.use(&router); diff --git a/examples/http/server/router/webpage.cpp b/examples/http/server/router/webpage.cpp index 1cd9840..0cb6360 100644 --- a/examples/http/server/router/webpage.cpp +++ b/examples/http/server/router/webpage.cpp @@ -1,4 +1,22 @@ - +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ namespace webpage { @@ -84,13 +102,13 @@ R"(

    页面一

    演示第一个示例页面

    - 访问 + 访问
    - +

    页面二

    演示第二个示例页面

    - 访问 + 访问
@@ -158,7 +176,7 @@ const char* kHtmlPage1 = )"; -const char* kHtmlPage2 = +const char* kHtmlPage2 = R"( @@ -214,7 +232,7 @@ R"(

页面二

欢迎来到第二个示例页面!这里展示了更多的HTML内容。

- +

TBOX框架特点:

    @@ -230,4 +248,4 @@ R"( )"; -} \ No newline at end of file +} diff --git a/examples/http/server/simple/simple.cpp b/examples/http/server/simple/simple.cpp index 6593dd9..983fec9 100644 --- a/examples/http/server/simple/simple.cpp +++ b/examples/http/server/simple/simple.cpp @@ -60,7 +60,7 @@ int main(int argc, char **argv) } srv.start(); - srv.setContextLogEnable(true); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 //! 添加请求处理 srv.use( diff --git a/modules/eventx/thread_pool.cpp b/modules/eventx/thread_pool.cpp index 5f1c44c..c7dc13a 100644 --- a/modules/eventx/thread_pool.cpp +++ b/modules/eventx/thread_pool.cpp @@ -299,7 +299,8 @@ void ThreadPool::threadProc(ThreadToken thread_token) std::unique_lock lk(d_->lock); /** - * 如果当前空闲的线程数量大于等于未被领取的任务数,且当前的线程个数已超过长驻线程数,说明线程数据已满足现有要求则退出当前线程 + * 如果当前空闲的线程数量大于等于未被领取的任务数,且当前的线程个数已超过长驻线程数, + * 说明线程数据已满足现有要求则退出当前线程 */ if ((d_->idle_thread_num >= d_->undo_tasks_cabinet.size()) && (d_->threads_cabinet.size() > d_->min_thread_num)) { LogDbg("thread %u will exit, no more work.", thread_token.id()); @@ -352,16 +353,27 @@ void ThreadPool::threadProc(ThreadToken thread_token) wait_time_cost.count() / 1000, exec_time_cost.count() / 1000); - if (item->main_cb) { - RECORD_SCOPE(); - d_->wp_loop->runInLoop(item->main_cb, "ThreadPool::threadProc, invoke main_cb"); - } + /** + * 有时在妥托给WorkThread执行动作时,会在lamda中捕获智能指针,它所指向的 + * 对象的析构函数是有动作的,如:http的sp_ctx要在析构中发送HTTP回复,如果 + * 析构函数在子线程中执行,则会出现不希望见到的多线程竞争。为此,我们在main_cb + * 中也让它持有这个智能指针,希望智能指针所指的对象只在主线程中析构。 + * + * 为了保证main_cb中的持有的对象能够在main_loop线程中被析构, + * 所以这里要先task_pool.free(),然后再runInLoop(std::move(main_cpp)) + */ + auto main_cb = std::move(item->main_cb); { std::lock_guard lg(d_->lock); d_->doing_tasks_token.erase(item->token); d_->task_pool.free(item); } + + if (main_cb) { + RECORD_SCOPE(); + d_->wp_loop->runInLoop(std::move(main_cb), "ThreadPool::threadProc, invoke main_cb"); + } } } diff --git a/modules/eventx/work_thread.cpp b/modules/eventx/work_thread.cpp index fe6ed46..2cfb110 100644 --- a/modules/eventx/work_thread.cpp +++ b/modules/eventx/work_thread.cpp @@ -239,16 +239,29 @@ void WorkThread::threadProc() wait_time_cost.count() / 1000, exec_time_cost.count() / 1000); - if (item->main_cb && item->main_loop != nullptr) { - RECORD_SCOPE(); - item->main_loop->runInLoop(item->main_cb, "WorkThread::threadProc, invoke main_cb"); - } + /** + * 有时在妥托给WorkThread执行动作时,会在lamda中捕获智能指针,它所指向的 + * 对象的析构函数是有动作的,如:http的sp_ctx要在析构中发送HTTP回复,如果 + * 析构函数在子线程中执行,则会出现不希望见到的多线程竞争。为此,我们在main_cb + * 中也让它持有这个智能指针,希望智能指针所指的对象只在主线程中析构。 + * + * 为了保证main_cb中的持有的对象能够在main_loop线程中被析构, + * 所以这里要先task_pool.free(),然后再runInLoop(std::move(main_cpp)) + */ + + auto main_cb = std::move(item->main_cb); + auto main_loop = item->main_loop; { std::lock_guard lg(d_->lock); d_->doing_tasks_token.erase(item->token); d_->task_pool.free(item); } + + if (main_cb && main_loop != nullptr) { + RECORD_SCOPE(); + main_loop->runInLoop(std::move(main_cb), "WorkThread::threadProc, invoke main_cb"); + } } } diff --git a/modules/http/Makefile b/modules/http/Makefile index 6e588d9..6fc3bb0 100644 --- a/modules/http/Makefile +++ b/modules/http/Makefile @@ -64,7 +64,7 @@ TEST_CPP_SRC_FILES = \ url_test.cpp \ server/request_parser_test.cpp \ -TEST_LDFLAGS := $(LDFLAGS) -ltbox_network -ltbox_log -ltbox_event -ltbox_util -ltbox_base -ldl +TEST_LDFLAGS := $(LDFLAGS) -ltbox_network -ltbox_log -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base -ldl ENABLE_SHARED_LIB = no diff --git a/modules/http/request.cpp b/modules/http/request.cpp index c4a1041..2ba6134 100644 --- a/modules/http/request.cpp +++ b/modules/http/request.cpp @@ -33,9 +33,17 @@ std::string Request::toString() const { std::ostringstream oss; oss << MethodToString(method) << " " << UrlPathToString(url) << " " << HttpVerToString(http_ver) << CRLF; - for (auto &head : headers) + + bool has_content_length = false; + for (auto &head : headers) { oss << head.first << ": " << head.second << CRLF; - oss << "Content-Length: " << body.length() << CRLF; + if (head.first == "Content-Length") + has_content_length = true; + } + + if (!has_content_length) + oss << "Content-Length: " << body.length() << CRLF; + oss << CRLF; oss << body; diff --git a/modules/http/respond.cpp b/modules/http/respond.cpp index 3f1df03..f527d08 100644 --- a/modules/http/respond.cpp +++ b/modules/http/respond.cpp @@ -32,9 +32,17 @@ std::string Respond::toString() const { std::ostringstream oss; oss << HttpVerToString(http_ver) << " " << StatusCodeToString(status_code) << CRLF; - for (auto &head : headers) + + bool has_content_length = false; + for (auto &head : headers) { oss << head.first << ": " << head.second << CRLF; - oss << "Content-Length: " << body.length() << CRLF; + if (head.first == "Content-Length") + has_content_length = true; + } + + if (!has_content_length) + oss << "Content-Length: " << body.length() << CRLF; + oss << CRLF; oss << body; diff --git a/modules/http/server/middlewares/file_downloader_middleware.cpp b/modules/http/server/middlewares/file_downloader_middleware.cpp index 13cee69..ede2343 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.cpp +++ b/modules/http/server/middlewares/file_downloader_middleware.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -21,19 +21,33 @@ #include #include -#include #include -#include #include #include #include #include +#include namespace tbox { namespace http { namespace server { +namespace { +bool IsPathSafe(const std::string& path) { + // 检查是否有".."路径组件,这可能导致目录遍历 + std::istringstream path_stream(path); + std::string component; + + while (std::getline(path_stream, component, '/')) { + if (component == "..") + return false; // 不允许上级目录访问 + } + + return true; +} +} + // 目录配置项 struct DirectoryConfig { std::string url_prefix; // URL前缀 @@ -43,13 +57,18 @@ struct DirectoryConfig { // 中间件私有数据结构 struct FileDownloaderMiddleware::Data { + eventx::ThreadPool worker; std::vector directories; // 目录配置列表 std::map path_mappings;// 特定路径映射 std::map mime_types; // MIME类型映射 std::string default_mime_type; // 默认MIME类型 bool directory_listing_enabled; // 是否允许目录列表 - Data() : default_mime_type("application/octet-stream"), directory_listing_enabled(false) { + Data(event::Loop *wp_loop) + : worker(wp_loop) + , default_mime_type("application/octet-stream") + , directory_listing_enabled(false) { + worker.initialize(); // 初始化常见MIME类型 mime_types["html"] = "text/html"; mime_types["htm"] = "text/html"; @@ -76,17 +95,21 @@ struct FileDownloaderMiddleware::Data { mime_types["ttf"] = "font/ttf"; mime_types["otf"] = "font/otf"; } + + ~Data() { + worker.cleanup(); + } }; -FileDownloaderMiddleware::FileDownloaderMiddleware() : d_(new Data) {} +FileDownloaderMiddleware::FileDownloaderMiddleware(event::Loop *wp_loop) + : d_(new Data(wp_loop)) +{ } -FileDownloaderMiddleware::~FileDownloaderMiddleware() { - delete d_; -} +FileDownloaderMiddleware::~FileDownloaderMiddleware() { delete d_; } -bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, - const std::string& local_path, - const std::string& default_file) { +bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, + const std::string& local_path, + const std::string& default_file) { // 验证URL前缀是否以'/'开头 if (url_prefix.empty() || url_prefix[0] != '/') { LogErr("Invalid URL prefix: %s. Must start with '/'", url_prefix.c_str()); @@ -94,8 +117,7 @@ bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, } // 验证本地路径是否存在且是目录 - struct stat path_stat; - if (stat(local_path.c_str(), &path_stat) != 0 || !S_ISDIR(path_stat.st_mode)) { + if (!util::fs::IsDirectoryExist(local_path)) { LogErr("Invalid local path: %s. Directory does not exist", local_path.c_str()); return false; } @@ -133,7 +155,7 @@ void FileDownloaderMiddleware::setMimeType(const std::string& ext, const std::st void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { const auto& request = sp_ctx->req(); - + // 只处理GET和HEAD请求 if (request.method != Method::kGet && request.method != Method::kHead) { next(); @@ -141,149 +163,139 @@ void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) } const std::string& request_path = request.url.path; - + // 检查特定路径映射 auto mapping_it = d_->path_mappings.find(request_path); if (mapping_it != d_->path_mappings.end()) { if (respondFile(sp_ctx, mapping_it->second)) return; } - + // 查找匹配的目录配置 for (const auto& dir : d_->directories) { // 检查URL是否以该目录前缀开头 if (request_path.find(dir.url_prefix) == 0) { // 获取相对路径部分 std::string rel_path = request_path.substr(dir.url_prefix.length()); - + // 如果路径以'/'开头,去掉这个斜杠避免双斜杠 if (!rel_path.empty() && rel_path[0] == '/') rel_path = rel_path.substr(1); - + // 构造本地文件路径 std::string file_path = dir.local_path + rel_path; - + // 检查路径安全性 - if (!isPathSafe(file_path)) { + if (!IsPathSafe(file_path)) { LogWarn("Unsafe path detected: %s", file_path.c_str()); sp_ctx->res().status_code = StatusCode::k403_Forbidden; return; } - + + auto file_type = util::fs::GetFileType(file_path); // 检查路径是否是目录 - struct stat path_stat; - if (stat(file_path.c_str(), &path_stat) == 0) { - if (S_ISDIR(path_stat.st_mode)) { - // 如果是目录且路径不以'/'结尾,进行重定向 - if (!request_path.empty() && request_path.back() != '/') { - sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; - sp_ctx->res().headers["Location"] = request_path + "/"; - return; - } - - // 尝试访问默认文件 - std::string default_file_path = file_path + dir.default_file; - if (stat(default_file_path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode)) { - if (respondFile(sp_ctx, default_file_path)) - return; - } - - // 如果允许目录列表,生成目录内容 - if (d_->directory_listing_enabled) { - if (respondDirectoryListing(sp_ctx, file_path, request_path)) - return; - } - - // 否则返回403 Forbidden - LogInfo("Directory listing disabled for: %s", file_path.c_str()); - sp_ctx->res().status_code = StatusCode::k403_Forbidden; + if (file_type == util::fs::FileType::kDirectory) { + // 如果是目录且路径不以'/'结尾,进行重定向 + if (!request_path.empty() && request_path.back() != '/') { + sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; + sp_ctx->res().headers["Location"] = request_path + "/"; return; - } else if (S_ISREG(path_stat.st_mode)) { - // 如果是普通文件,直接响应文件内容 - if (respondFile(sp_ctx, file_path)) + } + + // 尝试访问默认文件 + std::string default_file_path = file_path + dir.default_file; + if (util::fs::GetFileType(default_file_path) == util::fs::FileType::kRegular) { + if (respondFile(sp_ctx, default_file_path)) + return; + } + + // 如果允许目录列表,生成目录内容 + if (d_->directory_listing_enabled) { + if (respondDirectoryListing(sp_ctx, file_path, request_path)) return; } + + // 否则返回403 Forbidden + LogInfo("Directory listing disabled for: %s", file_path.c_str()); + sp_ctx->res().status_code = StatusCode::k403_Forbidden; + return; + + } else if (file_type == util::fs::FileType::kRegular) { + // 如果是普通文件,直接响应文件内容 + if (respondFile(sp_ctx, file_path)) + return; } } } - + // 如果没有找到匹配的文件,传递给下一个中间件 next(); } -bool FileDownloaderMiddleware::isPathSafe(const std::string& path) const { - // 检查是否有".."路径组件,这可能导致目录遍历 - std::istringstream path_stream(path); - std::string component; - - while (std::getline(path_stream, component, '/')) { - if (component == "..") - return false; // 不允许上级目录访问 - } - - return true; -} - std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) const { // 查找最后一个点的位置 size_t dot_pos = filename.find_last_of('.'); if (dot_pos != std::string::npos) { - std::string ext = filename.substr(dot_pos + 1); - // 转换为小写 - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c){ return std::tolower(c); }); - + std::string ext = util::string::ToLower(filename.substr(dot_pos + 1)); // 在MIME类型映射中查找 auto it = d_->mime_types.find(ext); - if (it != d_->mime_types.end()) { + if (it != d_->mime_types.end()) return it->second; - } } - + // 未找到匹配的MIME类型,返回默认值 return d_->default_mime_type; } bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path) { - // 打开文件 - std::ifstream file(file_path, std::ios::binary | std::ios::ate); - if (!file.is_open()) { - LogWarn("Failed to open file: %s", file_path.c_str()); + auto& res = sp_ctx->res(); + + if (!util::fs::IsFileExist(file_path)) { + LogNotice("file not exist: %s", file_path.c_str()); + res.status_code = StatusCode::k404_NotFound; return false; } - - // 获取文件大小 - std::streamsize file_size = file.tellg(); - file.seekg(0, std::ios::beg); - - // 设置响应头 - auto& res = sp_ctx->res(); - res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = getMimeType(file_path); - res.headers["Content-Length"] = std::to_string(file_size); - - // 添加缓存控制头(可选) res.headers["Cache-Control"] = "max-age=86400"; // 1天缓存 - - // 如果是HEAD请求,不返回内容 - if (sp_ctx->req().method == Method::kHead) { - return true; - } - - // 读取文件内容 - std::vector buffer(file_size); - if (file.read(buffer.data(), file_size)) { - res.body.assign(buffer.data(), file_size); - LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); - return true; - } - - LogErr("Failed to read file: %s", file_path.c_str()); - return false; + + d_->worker.execute( + [sp_ctx, file_path] { + auto& res = sp_ctx->res(); + + // 打开文件 + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + LogWarn("Failed to open file: %s", file_path.c_str()); + res.status_code = StatusCode::k404_NotFound; + return; + } + + // 获取文件大小 + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + // 设置响应头 + res.status_code = StatusCode::k200_OK; + res.headers["Content-Length"] = std::to_string(file_size); + + // 如果是HEAD请求,不返回内容 + if (sp_ctx->req().method == Method::kHead) + return; + + res.body = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); + }, + [sp_ctx, file_path] { + LogInfo("Served file: %s", file_path.c_str()); + } + ); + + return true; } -bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, - const std::string& dir_path, +bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, + const std::string& dir_path, const std::string& url_path) { try { // 生成HTML目录列表 @@ -305,7 +317,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, << "\n" << "

    Directory listing for " << url_path << "

    \n" << "
      \n"; - + // 如果不是根目录,添加返回上级目录的链接 if (url_path != "/") { size_t last_slash = url_path.find_last_of('/', url_path.size() - 2); @@ -314,7 +326,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, html << "
    • ..
    • \n"; } } - + // 列出目录中的项目 std::vector entries; if (!util::fs::ListDirectory(dir_path, entries)) { @@ -325,7 +337,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, for (const auto& name : entries) { std::string entry_path = dir_path + "/" + name; std::string href = url_path + name; - + if (util::fs::IsDirectoryExist(entry_path)) { href += "/"; html << "
    • " << name << "/
    • \n"; @@ -333,17 +345,17 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, html << "
    • " << name << "
    • \n"; } } - + html << "
    \n" << "\n" << ""; - + // 设置响应 auto& res = sp_ctx->res(); res.status_code = StatusCode::k200_OK; res.headers["Content-Type"] = "text/html; charset=utf-8"; res.body = html.str(); - + LogInfo("Served directory listing for: %s", dir_path.c_str()); return true; } catch (const std::exception& e) { @@ -354,4 +366,4 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, } } -} \ No newline at end of file +} diff --git a/modules/http/server/middlewares/file_downloader_middleware.h b/modules/http/server/middlewares/file_downloader_middleware.h index f1ac685..562f1ef 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.h +++ b/modules/http/server/middlewares/file_downloader_middleware.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -25,6 +25,8 @@ #include #include +#include + #include "../middleware.h" #include "../context.h" @@ -34,7 +36,7 @@ namespace server { /** * 文件下载中间件 - * + * * 用于处理静态文件的下载请求,可以设置多个目录作为文件源 * 自动防止目录遍历攻击(防止访问指定目录之外的文件) */ @@ -43,50 +45,51 @@ class FileDownloaderMiddleware : public Middleware { /** * 构造函数 */ - explicit FileDownloaderMiddleware(); - ~FileDownloaderMiddleware(); + explicit FileDownloaderMiddleware(event::Loop *wp_loop); + virtual ~FileDownloaderMiddleware(); NONCOPYABLE(FileDownloaderMiddleware); /** * 添加文件目录 - * - * @param url_prefix URL前缀,以'/'开头 - * @param local_path 本地文件目录的路径,可以是相对路径或绝对路径 - * @param default_file 默认文件,当请求目录时返回的文件,默认是"index.html" - * @return 是否添加成功 + * + * \param url_prefix URL前缀,以'/'开头 + * \param local_path 本地文件目录的路径,可以是相对路径或绝对路径 + * \param default_file 默认文件,当请求目录时返回的文件,默认是"index.html" + * + * \return 是否添加成功 */ - bool addDirectory(const std::string& url_prefix, + bool addDirectory(const std::string& url_prefix, const std::string& local_path, const std::string& default_file = "index.html"); /** * 设置是否允许列出目录内容 - * - * @param enable 是否启用列目录功能 + * + * \param enable 是否启用列目录功能 */ void setDirectoryListingEnabled(bool enable); /** * 设置路径映射,允许将特定URL映射到特定文件 - * - * @param url URL路径 - * @param file 文件路径 + * + * \param url URL路径 + * \param file 文件路径 */ void setPathMapping(const std::string& url, const std::string& file); /** * 设置默认的MIME类型 - * - * @param mime_type MIME类型字符串 + * + * \param mime_type MIME类型字符串 */ void setDefaultMimeType(const std::string& mime_type); /** * 设置文件扩展名到MIME类型的映射 - * - * @param ext 文件扩展名(不包含'.') - * @param mime_type MIME类型字符串 + * + * \param ext 文件扩展名(不包含'.') + * \param mime_type MIME类型字符串 */ void setMimeType(const std::string& ext, const std::string& mime_type); @@ -97,11 +100,6 @@ class FileDownloaderMiddleware : public Middleware { virtual void handle(ContextSptr sp_ctx, const NextFunc& next) override; private: - /** - * 检查路径是否安全(防止目录遍历攻击) - */ - bool isPathSafe(const std::string& path) const; - /** * 根据文件扩展名获取MIME类型 */ @@ -126,4 +124,4 @@ class FileDownloaderMiddleware : public Middleware { } } -#endif // TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_FILE_DOWNLOADER_MIDDLEWARE_H_20250419 diff --git a/modules/http/server/middlewares/form_data.cpp b/modules/http/server/middlewares/form_data.cpp index a7ec7b5..ec267f2 100644 --- a/modules/http/server/middlewares/form_data.cpp +++ b/modules/http/server/middlewares/form_data.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -83,4 +83,4 @@ void FormData::clear() { } } -} \ No newline at end of file +} diff --git a/modules/http/server/middlewares/form_data.h b/modules/http/server/middlewares/form_data.h index 182805e..13c9ae1 100644 --- a/modules/http/server/middlewares/form_data.h +++ b/modules/http/server/middlewares/form_data.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -102,4 +102,4 @@ class FormData { } } -#endif // TBOX_HTTP_SERVER_FORM_DATA_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_FORM_DATA_H_20250419 diff --git a/modules/http/server/middlewares/form_data_middleware.cpp b/modules/http/server/middlewares/form_data_middleware.cpp index b3bf28e..4b8f5bc 100644 --- a/modules/http/server/middlewares/form_data_middleware.cpp +++ b/modules/http/server/middlewares/form_data_middleware.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -47,9 +47,9 @@ bool ParseBoundary(const std::string& content_type, std::string &boundary) { size_t pos = content_type.find("boundary="); if (pos == std::string::npos) return false; - + pos += 9; // "boundary="的长度 - + // 如果boundary参数值被引号包围,则去掉引号 if (content_type[pos] == '"') { size_t end_pos = content_type.find('"', pos + 1); @@ -76,23 +76,23 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { LogNotice("Not a form-data Content-Disposition"); return false; } - + // 解析参数 size_t pos = 0; while ((pos = header_value.find(';', pos)) != std::string::npos) { pos++; // 跳过分号 - + // 跳过空白字符 while (pos < header_value.length() && std::isspace(header_value[pos])) pos++; - + // 查找参数名称和值 size_t eq_pos = header_value.find('=', pos); if (eq_pos == std::string::npos) continue; - + std::string param_name = header_value.substr(pos, eq_pos - pos); - + // 处理引号包围的值 size_t value_start = eq_pos + 1; if (header_value[value_start] == '"') { @@ -100,9 +100,9 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { size_t value_end = header_value.find('"', value_start); if (value_end == std::string::npos) break; - + std::string param_value = header_value.substr(value_start, value_end - value_start); - + if (param_name == "name") { item.name = param_value; item.type = FormItem::Type::kField; @@ -111,11 +111,11 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { item.filename = param_value; item.type = FormItem::Type::kFile; } - + pos = value_end + 1; } } - + return true; } @@ -125,39 +125,39 @@ bool ParseContentDisposition(const std::string& header_value, FormItem& item) { bool ParseFormItemHeaders(const std::string& headers_str, FormItem& item) { std::istringstream headers_stream(headers_str); std::string line; - + // 逐行解析头部 while (std::getline(headers_stream, line)) { // 移除行尾的\r if (!line.empty() && line.back() == '\r') line.pop_back(); - + // 跳过空行 if (line.empty()) continue; - + // 解析头部字段 size_t colon_pos = line.find(':'); if (colon_pos == std::string::npos) { LogNotice("Invalid header format: %s", line.c_str()); continue; } - + std::string header_name = util::string::Strip(line.substr(0, colon_pos)); std::string header_value = util::string::Strip(line.substr(colon_pos + 1)); - + // 存储头部 item.headers[header_name] = header_value; - + // 处理Content-Disposition头部 if (util::string::ToLower(header_name) == "content-disposition") ParseContentDisposition(header_value, item); - + // 处理Content-Type头部 if (util::string::ToLower(header_name) == "content-type") item.content_type = header_value; } - + return !item.name.empty(); } @@ -171,17 +171,17 @@ bool ParseFormPart(const std::string& part, FormItem& item) { LogNotice("Invalid form part format"); return false; } - + std::string headers_str = part.substr(0, headers_end); std::string body = part.substr(headers_end + 4); // +4 跳过两个CRLF - + // 解析头部 if (!ParseFormItemHeaders(headers_str, item)) return false; - + // 设置内容 item.value = body; - + return true; } @@ -195,22 +195,22 @@ bool ParseAsMultipartFormData(const Request& req, const std::string &content_typ // 带两个连字符的完整边界 const std::string delimiter = "--" + boundary; - + // 分割请求体以获取各个部分 size_t pos = 0; - + // 跳过第一个边界 pos = req.body.find(delimiter, pos); if (pos == std::string::npos) { LogNotice("Initial boundary not found"); return false; } - + pos += delimiter.length(); // 跳过CRLF if (req.body.substr(pos, 2) == "\r\n") pos += 2; - + // 查找每个部分 while (true) { // 查找下一个边界 @@ -219,27 +219,27 @@ bool ParseAsMultipartFormData(const Request& req, const std::string &content_typ // 没有找到更多边界 if (next_pos == std::string::npos) break; - + // 提取该部分的内容(不包括前导CRLF) const std::string part = req.body.substr(pos, next_pos - pos - 2); // 减去末尾的CRLF - + // 解析表单项 FormItem item; if (ParseFormPart(part, item)) form_data.addItem(item); - + // 移动到下一个部分 pos = next_pos + delimiter.length(); // 检查是否是最后一个边界 if (req.body.substr(pos, 2) == "--") break; // 这是最后一个边界 - + // 跳过CRLF if (req.body.substr(pos, 2) == "\r\n") pos += 2; } - + return true; } @@ -247,32 +247,32 @@ bool ParseAsFormUrlEncoded(const Request& req, FormData& form_data) { // 分割请求体以获取各个字段 const std::string &body = req.body; size_t pos = 0; - + while (pos < body.length()) { // 查找下一个字段分隔符(&) size_t next_pos = body.find('&', pos); if (next_pos == std::string::npos) next_pos = body.length(); - + // 提取键值对 std::string pair = body.substr(pos, next_pos - pos); size_t eq_pos = pair.find('='); - + if (eq_pos != std::string::npos) { std::string name = UrlDecode(pair.substr(0, eq_pos)); std::string value = UrlDecode(pair.substr(eq_pos + 1)); - + FormItem item; item.type = FormItem::Type::kField; item.name = name; item.value = value; form_data.addItem(item); } - + // 移动到下一个字段 pos = next_pos + 1; } - + return true; } @@ -302,7 +302,7 @@ void FormDataMiddleware::registerHandler(Method method, const std::string& path, void FormDataMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { Request& req = sp_ctx->req(); - + // 检查路由是否匹配 RouteKey key{req.method, req.url.path}; auto it = d_->handlers.find(key); @@ -315,7 +315,7 @@ void FormDataMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { // 解析表单数据 FormData form_data; bool parsed = false; - + // 获取Content-Type auto content_type_it = req.headers.find("Content-Type"); if (content_type_it != req.headers.end()) { diff --git a/modules/http/server/middlewares/form_data_middleware.h b/modules/http/server/middlewares/form_data_middleware.h index 14f98d4..23a4b32 100644 --- a/modules/http/server/middlewares/form_data_middleware.h +++ b/modules/http/server/middlewares/form_data_middleware.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -31,7 +31,9 @@ namespace http { namespace server { /** - * FormDataData中间件,用于处理multipart/form-data表单 + * 表单中间件,用于处理: + * - multipart/form-data + * - application/x-www-form-urlencoded */ class FormDataMiddleware : public Middleware { public: @@ -45,7 +47,7 @@ class FormDataMiddleware : public Middleware { /** * 注册POST请求处理器 - * + * * @param path 请求路径 * @param handler 处理函数 */ @@ -53,7 +55,7 @@ class FormDataMiddleware : public Middleware { /** * 注册GET请求处理器 - * + * * @param path 请求路径 * @param handler 处理函数 */ @@ -61,7 +63,7 @@ class FormDataMiddleware : public Middleware { /** * 注册特定Method的请求处理器 - * + * * @param method HTTP方法 * @param path 请求路径 * @param handler 处理函数 diff --git a/modules/http/server/middlewares/route_key.h b/modules/http/server/middlewares/route_key.h index 5179570..5678406 100644 --- a/modules/http/server/middlewares/route_key.h +++ b/modules/http/server/middlewares/route_key.h @@ -1,3 +1,22 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ #ifndef TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 #define TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 @@ -23,4 +42,4 @@ struct RouteKey { } } -#endif // TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 \ No newline at end of file +#endif // TBOX_HTTP_SERVER_ROUTE_KEY_H_20250419 diff --git a/modules/http/server/middlewares/router_middleware.cpp b/modules/http/server/middlewares/router_middleware.cpp index fdfda63..ef74654 100644 --- a/modules/http/server/middlewares/router_middleware.cpp +++ b/modules/http/server/middlewares/router_middleware.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -26,7 +26,7 @@ namespace http { namespace server { struct RouterMiddleware::Data { - std::map cbs_; + std::map route_handles; }; RouterMiddleware::RouterMiddleware() : d_(new Data) @@ -40,9 +40,9 @@ RouterMiddleware::~RouterMiddleware() void RouterMiddleware::handle(ContextSptr sp_ctx, const NextFunc &next) { RouteKey key{sp_ctx->req().method, sp_ctx->req().url.path}; - - auto iter = d_->cbs_.find(key); - if (iter != d_->cbs_.end()) { + + auto iter = d_->route_handles.find(key); + if (iter != d_->route_handles.end()) { if (iter->second) { iter->second(sp_ctx, next); return; @@ -51,45 +51,45 @@ void RouterMiddleware::handle(ContextSptr sp_ctx, const NextFunc &next) next(); } -RouterMiddleware& RouterMiddleware::get(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::get(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kGet, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kGet, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::post(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::post(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kPost, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kPost, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::put(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::put(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kPut, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kPut, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::del(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::del(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kDelete, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kDelete, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::opt(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::opt(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kOptions, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kOptions, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::head(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::head(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kHead, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kHead, path}] = std::move(handler); return *this; } -RouterMiddleware& RouterMiddleware::trace(const std::string &path, RequestCallback &&cb) +RouterMiddleware& RouterMiddleware::trace(const std::string &path, RequestHandler &&handler) { - d_->cbs_[RouteKey{Method::kTrace, path}] = std::move(cb); + d_->route_handles[RouteKey{Method::kTrace, path}] = std::move(handler); return *this; } diff --git a/modules/http/server/middlewares/router_middleware.h b/modules/http/server/middlewares/router_middleware.h index 0bfae16..b0e5e0d 100644 --- a/modules/http/server/middlewares/router_middleware.h +++ b/modules/http/server/middlewares/router_middleware.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -27,19 +27,22 @@ namespace tbox { namespace http { namespace server { +/** + * 路由中间件 + */ class RouterMiddleware : public Middleware { public: RouterMiddleware(); ~RouterMiddleware(); public: - RouterMiddleware& get (const std::string &path, RequestCallback &&cb); - RouterMiddleware& post (const std::string &path, RequestCallback &&cb); - RouterMiddleware& put (const std::string &path, RequestCallback &&cb); - RouterMiddleware& del (const std::string &path, RequestCallback &&cb); - RouterMiddleware& opt (const std::string &path, RequestCallback &&cb); - RouterMiddleware& head (const std::string &path, RequestCallback &&cb); - RouterMiddleware& trace(const std::string &path, RequestCallback &&cb); + RouterMiddleware& get (const std::string &path, RequestHandler &&handler); + RouterMiddleware& post (const std::string &path, RequestHandler &&handler); + RouterMiddleware& put (const std::string &path, RequestHandler &&handler); + RouterMiddleware& del (const std::string &path, RequestHandler &&handler); + RouterMiddleware& opt (const std::string &path, RequestHandler &&handler); + RouterMiddleware& head (const std::string &path, RequestHandler &&handler); + RouterMiddleware& trace(const std::string &path, RequestHandler &&handler); public: virtual void handle(ContextSptr sp_ctx, const NextFunc &next) override; diff --git a/modules/http/server/request_parser.cpp b/modules/http/server/request_parser.cpp index b049d4c..f64547c 100644 --- a/modules/http/server/request_parser.cpp +++ b/modules/http/server/request_parser.cpp @@ -20,6 +20,7 @@ #include "request_parser.h" #include #include +#include #include namespace tbox { @@ -46,6 +47,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto method_str = str.substr(pos, method_str_end); auto method = StringToMethod(method_str); if (method == Method::kUnset) { + LogNotice("method is invalid, method_str:%s", method_str.c_str()); state_ = State::kFail; return pos; } @@ -60,6 +62,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) //! 获取 url auto url_str_begin = str.find_first_not_of(' ', method_str_end); if (url_str_begin == std::string::npos || url_str_begin >= end_pos) { + LogNotice("parse url fail, url not exist."); state_ = State::kFail; return pos; } @@ -67,6 +70,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto url_str_end = str.find_first_of(' ', url_str_begin); auto url_str = str.substr(url_str_begin, url_str_end - url_str_begin); if (!StringToUrlPath(url_str, sp_request_->url)) { + LogNotice("parse url fail, url_str:%s", url_str.c_str()); state_ = State::kFail; return pos; } @@ -74,6 +78,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) //! 获取版本 auto ver_str_begin = str.find_first_not_of(' ', url_str_end); if (ver_str_begin == std::string::npos || ver_str_begin >= end_pos) { + LogNotice("ver not exist"); state_ = State::kFail; return pos; } @@ -81,12 +86,14 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto ver_str_end = end_pos; auto ver_str = str.substr(ver_str_begin, ver_str_end - ver_str_begin); if (ver_str.compare(0, 5, "HTTP/") != 0) { + LogNotice("ver is invalid, ver_str:%s", ver_str.c_str()); state_ = State::kFail; return pos; } auto ver = StringToHttpVer(ver_str); if (ver == HttpVer::kUnset) { + LogNotice("ver is invalid, ver_str:%s", ver_str.c_str()); state_ = State::kFail; return pos; } @@ -117,6 +124,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto colon_pos = str.find_first_of(':', pos); if (colon_pos == std::string::npos || colon_pos >= end_pos) { + LogNotice("can't find ':' in header line"); state_ = State::kFail; return pos; } @@ -124,6 +132,7 @@ size_t RequestParser::parse(const void *data_ptr, size_t data_size) auto head_key = util::string::Strip(str.substr(pos, colon_pos - pos)); auto head_value_start_pos = str.find_first_not_of(' ', colon_pos + 1); //! 要略掉空白 if (head_value_start_pos == std::string::npos || head_value_start_pos >= end_pos) { + LogNotice("can't find header value"); state_ = State::kFail; return pos; } diff --git a/modules/http/server/server.cpp b/modules/http/server/server.cpp index 75944f4..1951253 100644 --- a/modules/http/server/server.cpp +++ b/modules/http/server/server.cpp @@ -63,9 +63,9 @@ void Server::setContextLogEnable(bool enable) return impl_->setContextLogEnable(enable); } -void Server::use(const RequestCallback &cb) +void Server::use(RequestHandler &&handler) { - impl_->use(cb); + impl_->use(std::move(handler)); } void Server::use(Middleware *wp_middleware) diff --git a/modules/http/server/server.h b/modules/http/server/server.h index dce740d..d3bcee9 100644 --- a/modules/http/server/server.h +++ b/modules/http/server/server.h @@ -54,7 +54,7 @@ class Server { void setContextLogEnable(bool enable); public: - void use(const RequestCallback &cb); + void use(RequestHandler &&handler); void use(Middleware *wp_middleware); private: diff --git a/modules/http/server/server_imp.cpp b/modules/http/server/server_imp.cpp index 5f3b019..8db8a8f 100644 --- a/modules/http/server/server_imp.cpp +++ b/modules/http/server/server_imp.cpp @@ -82,7 +82,7 @@ void Server::Impl::cleanup() if (state_ != State::kNone) { stop(); - req_cb_.clear(); + req_handler_.clear(); tcp_server_.cleanup(); for (auto conn : conns_) @@ -93,14 +93,14 @@ void Server::Impl::cleanup() } } -void Server::Impl::use(const RequestCallback &cb) +void Server::Impl::use(RequestHandler &&handler) { - req_cb_.push_back(cb); + req_handler_.push_back(std::move(handler)); } void Server::Impl::use(Middleware *wp_middleware) { - req_cb_.push_back(bind(&Middleware::handle, wp_middleware, _1, _2)); + req_handler_.push_back(bind(&Middleware::handle, wp_middleware, _1, _2)); } void Server::Impl::onTcpConnected(const TcpServer::ConnToken &ct) @@ -271,10 +271,10 @@ void Server::Impl::commitRespond(const TcpServer::ConnToken &ct, int index, Resp void Server::Impl::handle(ContextSptr sp_ctx, size_t cb_index) { RECORD_SCOPE(); - if (cb_index >= req_cb_.size()) + if (cb_index >= req_handler_.size()) return; - auto func = req_cb_.at(cb_index); + auto func = req_handler_.at(cb_index); ++cb_level_; if (func) diff --git a/modules/http/server/server_imp.h b/modules/http/server/server_imp.h index 398aaef..878813a 100644 --- a/modules/http/server/server_imp.h +++ b/modules/http/server/server_imp.h @@ -55,7 +55,7 @@ class Server::Impl { void setContextLogEnable(bool enable) { context_log_enable_ = enable; } public: - void use(const RequestCallback &cb); + void use(RequestHandler &&handler); void use(Middleware *wp_middleware); void commitRespond(const TcpServer::ConnToken &ct, int index, Respond *res); @@ -84,7 +84,7 @@ class Server::Impl { Server *wp_parent_; TcpServer tcp_server_; - vector req_cb_; + vector req_handler_; set conns_; //! 仅用于保存Connection指针,用于释放 State state_ = State::kNone; bool context_log_enable_ = false; diff --git a/modules/http/server/types.h b/modules/http/server/types.h index 3e64cc8..ff0db76 100644 --- a/modules/http/server/types.h +++ b/modules/http/server/types.h @@ -30,7 +30,7 @@ namespace server { class Context; using ContextSptr = std::shared_ptr; using NextFunc = std::function; -using RequestCallback = std::function; +using RequestHandler = std::function; } } diff --git a/modules/util/fs.cpp b/modules/util/fs.cpp index c373300..4f17dfc 100644 --- a/modules/util/fs.cpp +++ b/modules/util/fs.cpp @@ -40,15 +40,27 @@ using std::ifstream; using std::ofstream; using std::exception; +FileType GetFileType(const std::string &file_path) +{ + struct stat st; + + if (::stat(file_path.c_str(), &st) == 0) { + if (S_ISDIR(st.st_mode)) return FileType::kDirectory; + if (S_ISREG(st.st_mode)) return FileType::kRegular; + if (S_ISCHR(st.st_mode)) return FileType::kCharacterDevice; + if (S_ISBLK(st.st_mode)) return FileType::kBlockDevice; + if (S_ISLNK(st.st_mode)) return FileType::kSymbolLink; + if (S_ISSOCK(st.st_mode)) return FileType::kSocket; + if (S_ISFIFO(st.st_mode)) return FileType::kNamedPipe; + } + + return FileType::kNone; +} + bool IsFileExist(const std::string &filename) { -#if 1 int ret = ::access(filename.c_str(), F_OK); return ret == 0; -#else - ifstream ifs(filename); - return ifs.is_open(); -#endif } bool ReadStringFromTextFile(const std::string &filename, std::string &content) @@ -239,9 +251,7 @@ bool MakeLink(const std::string &old_path, const std::string &new_path, bool all bool IsDirectoryExist(const std::string &dir) { - struct stat sb; - //! 如果读到dir的inode信息,且该inode是DIR,则返回true - return ((::stat(dir.c_str(), &sb) == 0) && S_ISDIR(sb.st_mode)); + return GetFileType(dir) == FileType::kDirectory; } bool MakeDirectory(const std::string &origin_dir_path, bool allow_log_print) diff --git a/modules/util/fs.h b/modules/util/fs.h index 166ed90..acdbf2f 100644 --- a/modules/util/fs.h +++ b/modules/util/fs.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2025 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -32,6 +32,26 @@ namespace fs { // 文件相关 //////////////////////////////////////////////////////////////////// +//! 文件类型 +enum class FileType { + kNone, //!< 未知 + kDirectory, //!< 目录 + kRegular, //!< 常规文件 + kCharacterDevice, //!< 字符设备 + kBlockDevice, //!< 块设备 + kSymbolLink, //!< 符号链接 + kSocket, //!< 套接字 + kNamedPipe, //!< 有名管道 +}; + +/** + * 获取文件类型 + * + * \param file_path 文件路径 + * \return FileType 文件类型 + */ +FileType GetFileType(const std::string &file_path); + /** * 检查文件是否存在 * -- Gitee From b3705592b0421c188d48b2ec458b819248559242 Mon Sep 17 00:00:00 2001 From: Hevake Lee Date: Mon, 21 Apr 2025 23:04:00 +0800 Subject: [PATCH 6/6] =?UTF-8?q?opt(http):=20=E5=B0=86ThreadPool=E6=8D=A2?= =?UTF-8?q?=E6=88=90WorkThread?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/http/server/file_download/Makefile | 1 + .../server/file_download/file_download.cpp | 7 ++ .../file_downloader_middleware.cpp | 100 ++++++++++-------- .../middlewares/file_downloader_middleware.h | 2 +- 4 files changed, 62 insertions(+), 48 deletions(-) diff --git a/examples/http/server/file_download/Makefile b/examples/http/server/file_download/Makefile index 78438b1..8ac04f3 100644 --- a/examples/http/server/file_download/Makefile +++ b/examples/http/server/file_download/Makefile @@ -29,6 +29,7 @@ LDFLAGS += \ -ltbox_network \ -ltbox_eventx \ -ltbox_event \ + -ltbox_trace \ -ltbox_log \ -ltbox_util \ -ltbox_base \ diff --git a/examples/http/server/file_download/file_download.cpp b/examples/http/server/file_download/file_download.cpp index 08da42c..8d37a1c 100644 --- a/examples/http/server/file_download/file_download.cpp +++ b/examples/http/server/file_download/file_download.cpp @@ -38,6 +38,7 @@ #include #include #include +#include using namespace std; using namespace tbox; @@ -95,6 +96,12 @@ int main(int argc, char **argv) LogOutput_Enable(); LogInfo("Starting file download server"); +#if 0 //! 要监控性能时,打开 + auto &trace_sink = tbox::trace::Sink::GetInstance(); + trace_sink.setPathPrefix("/tmp/file_download/trace"); + trace_sink.enable(); +#endif + // 检查目录是否存在 if (!tbox::util::fs::IsDirectoryExist(serve_dir)) { LogErr("Directory '%s' doesn't exist or is not a directory", serve_dir.c_str()); diff --git a/modules/http/server/middlewares/file_downloader_middleware.cpp b/modules/http/server/middlewares/file_downloader_middleware.cpp index ede2343..2e7d0f3 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.cpp +++ b/modules/http/server/middlewares/file_downloader_middleware.cpp @@ -27,7 +27,8 @@ #include #include #include -#include +#include +#include namespace tbox { namespace http { @@ -57,18 +58,20 @@ struct DirectoryConfig { // 中间件私有数据结构 struct FileDownloaderMiddleware::Data { - eventx::ThreadPool worker; + eventx::WorkThread worker; std::vector directories; // 目录配置列表 std::map path_mappings;// 特定路径映射 std::map mime_types; // MIME类型映射 std::string default_mime_type; // 默认MIME类型 bool directory_listing_enabled; // 是否允许目录列表 + size_t switch_to_worker_filesize_threshold; Data(event::Loop *wp_loop) : worker(wp_loop) , default_mime_type("application/octet-stream") - , directory_listing_enabled(false) { - worker.initialize(); + , directory_listing_enabled(false) + , switch_to_worker_filesize_threshold(100 << 10) + { // 初始化常见MIME类型 mime_types["html"] = "text/html"; mime_types["htm"] = "text/html"; @@ -95,10 +98,6 @@ struct FileDownloaderMiddleware::Data { mime_types["ttf"] = "font/ttf"; mime_types["otf"] = "font/otf"; } - - ~Data() { - worker.cleanup(); - } }; FileDownloaderMiddleware::FileDownloaderMiddleware(event::Loop *wp_loop) @@ -211,12 +210,12 @@ void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) // 如果允许目录列表,生成目录内容 if (d_->directory_listing_enabled) { - if (respondDirectoryListing(sp_ctx, file_path, request_path)) + if (respondDirectory(sp_ctx, file_path, request_path)) return; } // 否则返回403 Forbidden - LogInfo("Directory listing disabled for: %s", file_path.c_str()); + LogNotice("Directory listing disabled for: %s", file_path.c_str()); sp_ctx->res().status_code = StatusCode::k403_Forbidden; return; @@ -250,53 +249,58 @@ std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) c bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path) { auto& res = sp_ctx->res(); - if (!util::fs::IsFileExist(file_path)) { - LogNotice("file not exist: %s", file_path.c_str()); + // 打开文件 + std::ifstream file(file_path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { res.status_code = StatusCode::k404_NotFound; - return false; + return true; } res.headers["Content-Type"] = getMimeType(file_path); - res.headers["Cache-Control"] = "max-age=86400"; // 1天缓存 - - d_->worker.execute( - [sp_ctx, file_path] { - auto& res = sp_ctx->res(); - - // 打开文件 - std::ifstream file(file_path, std::ios::binary | std::ios::ate); - if (!file.is_open()) { - LogWarn("Failed to open file: %s", file_path.c_str()); - res.status_code = StatusCode::k404_NotFound; - return; - } - - // 获取文件大小 - std::streamsize file_size = file.tellg(); - file.seekg(0, std::ios::beg); - // 设置响应头 - res.status_code = StatusCode::k200_OK; - res.headers["Content-Length"] = std::to_string(file_size); + // 获取文件大小 + size_t file_size = static_cast(file.tellg()); + file.seekg(0, std::ios::beg); - // 如果是HEAD请求,不返回内容 - if (sp_ctx->req().method == Method::kHead) - return; + // 如果是HEAD请求,不返回内容 + if (sp_ctx->req().method == Method::kHead) { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Length"] = std::to_string(file_size); + return true; + } - res.body = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); - }, - [sp_ctx, file_path] { - LogInfo("Served file: %s", file_path.c_str()); - } - ); + //! 文件是否大于100KB + if (file_size < d_->switch_to_worker_filesize_threshold) { + //! 小文件就直接读了 + res.status_code = StatusCode::k200_OK; + res.headers["Content-Length"] = std::to_string(file_size); + //! 将文件内容读到body中去 + res.body = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); + + } else { + //! 文件太大就采用子线程去读 + d_->worker.execute( + [sp_ctx, file_path] { + auto& res = sp_ctx->res(); + if (util::fs::ReadBinaryFromFile(file_path, res.body)) { + res.status_code = StatusCode::k200_OK; + res.headers["Content-Length"] = std::to_string(res.body.size()); + LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), res.body.size()); + } else { + res.status_code = StatusCode::k404_NotFound; + } + }, + [sp_ctx] { } //! 这是为了确保sp_ctx在主线程上析构 + ); + } return true; } -bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, - const std::string& dir_path, - const std::string& url_path) { +bool FileDownloaderMiddleware::respondDirectory(ContextSptr sp_ctx, + const std::string& dir_path, + const std::string& url_path) { try { // 生成HTML目录列表 std::stringstream html; @@ -338,7 +342,8 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, std::string entry_path = dir_path + "/" + name; std::string href = url_path + name; - if (util::fs::IsDirectoryExist(entry_path)) { + auto entry_type = util::fs::GetFileType(entry_path); + if (entry_type == util::fs::FileType::kDirectory) { href += "/"; html << "
  • " << name << "/
  • \n"; } else { @@ -358,6 +363,7 @@ bool FileDownloaderMiddleware::respondDirectoryListing(ContextSptr sp_ctx, LogInfo("Served directory listing for: %s", dir_path.c_str()); return true; + } catch (const std::exception& e) { LogErr("Failed to generate directory listing: %s", e.what()); return false; diff --git a/modules/http/server/middlewares/file_downloader_middleware.h b/modules/http/server/middlewares/file_downloader_middleware.h index 562f1ef..8e0e5b5 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.h +++ b/modules/http/server/middlewares/file_downloader_middleware.h @@ -113,7 +113,7 @@ class FileDownloaderMiddleware : public Middleware { /** * 生成目录列表 */ - bool respondDirectoryListing(ContextSptr sp_ctx, const std::string& dir_path, const std::string& url_path); + bool respondDirectory(ContextSptr sp_ctx, const std::string& dir_path, const std::string& url_path); private: struct Data; -- Gitee