From 01fe5b3c5a87150a884d367e0a8cc38164604b72 Mon Sep 17 00:00:00 2001 From: qicosmos Date: Tue, 10 Dec 2024 17:36:27 +0800 Subject: [PATCH] set max http body length (#668) --- include/cinatra/coro_http_client.hpp | 15 +++++++++- include/cinatra/coro_http_connection.hpp | 18 +++++++++++- include/cinatra/coro_http_server.hpp | 6 ++++ include/cinatra/define.h | 2 ++ include/cinatra/http_parser.hpp | 37 +++++++++++++----------- tests/test_cinatra.cpp | 33 +++++++++++++++++++++ 6 files changed, 92 insertions(+), 19 deletions(-) diff --git a/include/cinatra/coro_http_client.hpp b/include/cinatra/coro_http_client.hpp index d9ae32b5..2b3436e0 100644 --- a/include/cinatra/coro_http_client.hpp +++ b/include/cinatra/coro_http_client.hpp @@ -365,6 +365,10 @@ class coro_http_client : public std::enable_shared_from_this { void set_ws_sec_key(std::string sec_key) { ws_sec_key_ = std::move(sec_key); } + void set_max_http_body_size(int64_t max_size) { + max_http_body_len_ = max_size; + } + async_simple::coro::Lazy read_websocket() { co_return co_await async_read_ws(); } @@ -1629,9 +1633,17 @@ class coro_http_client : public std::enable_shared_from_this { parse_ret = -1; } #endif - if (parse_ret < 0) { + if (parse_ret < 0) [[unlikely]] { return std::make_error_code(std::errc::protocol_error); } + + if (parser_.body_len() > max_http_body_len_ || parser_.body_len() < 0) + [[unlikely]] { + CINATRA_LOG_ERROR << "invalid http content length: " + << parser_.body_len(); + return std::make_error_code(std::errc::invalid_argument); + } + head_buf_.consume(header_size); // header size data.resp_headers = parser.get_headers(); data.status = parser.status(); @@ -2408,6 +2420,7 @@ class coro_http_client : public std::enable_shared_from_this { std::string inflate_str_; #endif content_encoding encoding_type_ = content_encoding::none; + int64_t max_http_body_len_ = MAX_HTTP_BODY_SIZE; #if defined(CINATRA_ENABLE_BROTLI) || defined(CINATRA_ENABLE_GZIP) std::string uncompressed_str_; diff --git a/include/cinatra/coro_http_connection.hpp b/include/cinatra/coro_http_connection.hpp index 0ea4fd7f..153c291d 100644 --- a/include/cinatra/coro_http_connection.hpp +++ b/include/cinatra/coro_http_connection.hpp @@ -131,13 +131,24 @@ class coro_http_connection break; } + if (parser_.body_len() > max_http_body_len_ || parser_.body_len() < 0) + [[unlikely]] { + CINATRA_LOG_ERROR << "invalid http content length: " + << parser_.body_len(); + response_.set_status_and_content(status_type::bad_request, + "invalid http content length"); + co_await reply(); + close(); + break; + } + head_buf_.consume(size); keep_alive_ = check_keep_alive(); auto type = request_.get_content_type(); if (type != content_type::chunked && type != content_type::multipart) { - size_t body_len = parser_.body_len(); + size_t body_len = (size_t)parser_.body_len(); if (body_len == 0) { if (parser_.method() == "GET"sv) { if (request_.is_upgrade()) { @@ -443,6 +454,10 @@ class coro_http_connection default_handler_ = handler; } + void set_max_http_body_size(int64_t max_size) { + max_http_body_len_ = max_size; + } + #ifdef INJECT_FOR_HTTP_SEVER_TEST void set_write_failed_forever(bool r) { write_failed_forever_ = r; } @@ -975,6 +990,7 @@ class coro_http_connection default_handler_ = nullptr; std::string chunk_size_str_; std::string remote_addr_; + int64_t max_http_body_len_ = 0; #ifdef INJECT_FOR_HTTP_SEVER_TEST bool write_failed_forever_ = false; bool read_failed_forever_ = false; diff --git a/include/cinatra/coro_http_server.hpp b/include/cinatra/coro_http_server.hpp index b6ad4e74..ee64ef2b 100644 --- a/include/cinatra/coro_http_server.hpp +++ b/include/cinatra/coro_http_server.hpp @@ -58,6 +58,10 @@ class coro_http_server { void set_no_delay(bool r) { no_delay_ = r; } + void set_max_http_body_size(int64_t max_size) { + max_http_body_len_ = max_size; + } + #ifdef CINATRA_ENABLE_SSL void init_ssl(const std::string &cert_file, const std::string &key_file, const std::string &passwd) { @@ -680,6 +684,7 @@ class coro_http_server { if (no_delay_) { conn->tcp_socket().set_option(asio::ip::tcp::no_delay(true)); } + conn->set_max_http_body_size(max_http_body_len_); if (need_shrink_every_time_) { conn->set_shrink_to_fit(true); } @@ -1001,6 +1006,7 @@ class coro_http_server { std::function(coro_http_request &, coro_http_response &)> default_handler_ = nullptr; + int64_t max_http_body_len_ = MAX_HTTP_BODY_SIZE; #ifdef INJECT_FOR_HTTP_SEVER_TEST bool write_failed_forever_ = false; bool read_failed_forever_ = false; diff --git a/include/cinatra/define.h b/include/cinatra/define.h index 20a489d6..67baa230 100644 --- a/include/cinatra/define.h +++ b/include/cinatra/define.h @@ -279,6 +279,8 @@ inline std::unordered_map g_content_type_map = { struct NonSSL {}; struct SSL {}; +inline constexpr int64_t MAX_HTTP_BODY_SIZE = 4294967296; // 4GB + enum class time_format { http_format, utc_format, diff --git a/include/cinatra/http_parser.hpp b/include/cinatra/http_parser.hpp index e71d280f..5ab8fb59 100644 --- a/include/cinatra/http_parser.hpp +++ b/include/cinatra/http_parser.hpp @@ -28,6 +28,21 @@ inline bool iequal0(std::string_view a, std::string_view b) { class http_parser { public: + void parse_body_len() { + auto header_value = this->get_header_value("content-length"sv); + if (header_value.empty()) { + body_len_ = 0; + } + else { + auto [ptr, ec] = std::from_chars( + header_value.data(), header_value.data() + header_value.size(), + body_len_, 10); + if (ec != std::errc{}) { + body_len_ = -1; + } + } + } + int parse_response(const char *data, size_t size, int last_len) { int minor_version; @@ -38,13 +53,7 @@ class http_parser { data, size, &minor_version, &status_, &msg, &msg_len, headers_.data(), &num_headers_, last_len); msg_ = {msg, msg_len}; - auto header_value = this->get_header_value("content-length"sv); - if (header_value.empty()) { - body_len_ = 0; - } - else { - body_len_ = atoi(header_value.data()); - } + parse_body_len(); if (header_len_ < 0) [[unlikely]] { CINATRA_LOG_WARNING << "parse http head failed"; if (num_headers_ == CINATRA_MAX_HTTP_HEADER_FIELD_SIZE) { @@ -86,13 +95,7 @@ class http_parser { body_len_ = 0; } else { - auto content_len = this->get_header_value("content-length"sv); - if (content_len.empty()) { - body_len_ = 0; - } - else { - body_len_ = atoi(content_len.data()); - } + parse_body_len(); } full_url_ = url_; @@ -193,9 +196,9 @@ class http_parser { int header_len() const { return header_len_; } - int body_len() const { return body_len_; } + int64_t body_len() const { return body_len_; } - int total_len() const { return header_len_ + body_len_; } + int64_t total_len() const { return header_len_ + body_len_; } bool is_location() { auto location = this->get_header_value("Location"sv); @@ -250,7 +253,7 @@ class http_parser { std::string_view msg_; size_t num_headers_ = 0; int header_len_ = 0; - int body_len_ = 0; + int64_t body_len_ = 0; bool has_connection_{}; bool has_close_{}; bool has_upgrade_{}; diff --git a/tests/test_cinatra.cpp b/tests/test_cinatra.cpp index 632e82ad..34663fa6 100644 --- a/tests/test_cinatra.cpp +++ b/tests/test_cinatra.cpp @@ -377,6 +377,39 @@ TEST_CASE("test ssl client") { } #endif +TEST_CASE("test invalid http body size") { + coro_http_server server(1, 9001); + server.set_max_http_body_size(10); + server.set_http_handler( + "/get", [](coro_http_request &req, coro_http_response &resp) { + resp.set_status_and_content(status_type::ok, + "ok, it is a long test string!"); + }); + + server.async_start(); + + std::string uri = "http://127.0.0.1:9001/get"; + { + coro_http_client client{}; + auto result = + client.post(uri, "it is a long test string!", req_content_type::text); + CHECK(result.status != 200); + } + + { + coro_http_client client{}; + client.set_max_http_body_size(10); + auto result = client.post(uri, "test", req_content_type::text); + CHECK(result.status != 200); + CHECK(result.net_err == std::errc::invalid_argument); + } + { + coro_http_client client{}; + auto result = client.post(uri, "test", req_content_type::text); + CHECK(result.status == 200); + } +} + bool create_file(std::string_view filename, size_t file_size = 1024) { std::ofstream out(filename.data(), std::ios::binary); if (!out.is_open()) {