@@ -2571,10 +2571,46 @@ find_content_type(const std::string &path,
25712571 }
25722572}
25732573
2574+ std::string
2575+ extract_media_type(const std::string &content_type,
2576+ std::map<std::string, std::string> *params = nullptr) {
2577+ // Extract type/subtype from Content-Type value (RFC 2045)
2578+ // e.g. "application/json; charset=utf-8" -> "application/json"
2579+ auto media_type = content_type;
2580+ auto semicolon_pos = media_type.find(';');
2581+ if (semicolon_pos != std::string::npos) {
2582+ auto param_str = media_type.substr(semicolon_pos + 1);
2583+ media_type = media_type.substr(0, semicolon_pos);
2584+
2585+ if (params) {
2586+ // Parse parameters: key=value pairs separated by ';'
2587+ split(param_str.data(), param_str.data() + param_str.size(), ';',
2588+ [&](const char *b, const char *e) {
2589+ std::string key;
2590+ std::string val;
2591+ split(b, e, '=', [&](const char *b2, const char *e2) {
2592+ if (key.empty()) {
2593+ key.assign(b2, e2);
2594+ } else {
2595+ val.assign(b2, e2);
2596+ }
2597+ });
2598+ if (!key.empty()) {
2599+ params->emplace(trim_copy(key), trim_double_quotes_copy(val));
2600+ }
2601+ });
2602+ }
2603+ }
2604+
2605+ // Trim whitespace from media type
2606+ return trim_copy(media_type);
2607+ }
2608+
25742609bool can_compress_content_type(const std::string &content_type) {
25752610 using udl::operator""_t;
25762611
2577- auto tag = str2tag(content_type);
2612+ auto mime_type = extract_media_type(content_type);
2613+ auto tag = str2tag(mime_type);
25782614
25792615 switch (tag) {
25802616 case "image/svg+xml"_t:
@@ -2586,7 +2622,7 @@ bool can_compress_content_type(const std::string &content_type) {
25862622
25872623 case "text/event-stream"_t: return false;
25882624
2589- default: return !content_type .rfind("text/", 0);
2625+ default: return !mime_type .rfind("text/", 0);
25902626 }
25912627}
25922628
@@ -3141,7 +3177,8 @@ bool is_chunked_transfer_encoding(const Headers &headers) {
31413177template <typename T, typename U>
31423178bool prepare_content_receiver(T &x, int &status,
31433179 ContentReceiverWithProgress receiver,
3144- bool decompress, U callback) {
3180+ bool decompress, size_t payload_max_length,
3181+ bool &exceed_payload_max_length, U callback) {
31453182 if (decompress) {
31463183 std::string encoding = x.get_header_value("Content-Encoding");
31473184 std::unique_ptr<decompressor> decompressor;
@@ -3157,12 +3194,22 @@ bool prepare_content_receiver(T &x, int &status,
31573194
31583195 if (decompressor) {
31593196 if (decompressor->is_valid()) {
3197+ size_t decompressed_size = 0;
31603198 ContentReceiverWithProgress out = [&](const char *buf, size_t n,
31613199 size_t off, size_t len) {
3162- return decompressor->decompress(buf, n,
3163- [&](const char *buf2, size_t n2) {
3164- return receiver(buf2, n2, off, len);
3165- });
3200+ return decompressor->decompress(
3201+ buf, n, [&](const char *buf2, size_t n2) {
3202+ // Guard against zip-bomb: check
3203+ // decompressed size against limit.
3204+ if (payload_max_length > 0 &&
3205+ (decompressed_size >= payload_max_length ||
3206+ n2 > payload_max_length - decompressed_size)) {
3207+ exceed_payload_max_length = true;
3208+ return false;
3209+ }
3210+ decompressed_size += n2;
3211+ return receiver(buf2, n2, off, len);
3212+ });
31663213 };
31673214 return callback(std::move(out));
31683215 } else {
@@ -3183,11 +3230,14 @@ template <typename T>
31833230bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
31843231 DownloadProgress progress,
31853232 ContentReceiverWithProgress receiver, bool decompress) {
3233+ bool exceed_payload_max_length = false;
31863234 return prepare_content_receiver(
3187- x, status, std::move(receiver), decompress,
3188- [&](const ContentReceiverWithProgress &out) {
3235+ x, status, std::move(receiver), decompress, payload_max_length,
3236+ exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) {
31893237 auto ret = true;
3190- auto exceed_payload_max_length = false;
3238+ // Note: exceed_payload_max_length may also be set by the decompressor
3239+ // wrapper in prepare_content_receiver when the decompressed payload
3240+ // size exceeds the limit.
31913241
31923242 if (is_chunked_transfer_encoding(x.headers)) {
31933243 auto result = read_content_chunked(strm, x, payload_max_length, out);
@@ -3603,12 +3653,11 @@ std::string normalize_query_string(const std::string &query) {
36033653
36043654bool parse_multipart_boundary(const std::string &content_type,
36053655 std::string &boundary) {
3606- auto boundary_keyword = "boundary=";
3607- auto pos = content_type.find(boundary_keyword);
3608- if (pos == std::string::npos) { return false; }
3609- auto end = content_type.find(';', pos);
3610- auto beg = pos + strlen(boundary_keyword);
3611- boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg));
3656+ std::map<std::string, std::string> params;
3657+ extract_media_type(content_type, ¶ms);
3658+ auto it = params.find("boundary");
3659+ if (it == params.end()) { return false; }
3660+ boundary = it->second;
36123661 return !boundary.empty();
36133662}
36143663
@@ -3776,11 +3825,7 @@ bool parse_accept_header(const std::string &s,
37763825 }
37773826
37783827 // Remove additional parameters from media type
3779- auto param_pos = accept_entry.media_type.find(';');
3780- if (param_pos != std::string::npos) {
3781- accept_entry.media_type =
3782- trim_copy(accept_entry.media_type.substr(0, param_pos));
3783- }
3828+ accept_entry.media_type = extract_media_type(accept_entry.media_type);
37843829
37853830 // Basic validation of media type format
37863831 if (accept_entry.media_type.empty()) {
@@ -5610,7 +5655,7 @@ size_t Request::get_param_value_count(const std::string &key) const {
56105655
56115656bool Request::is_multipart_form_data() const {
56125657 const auto &content_type = get_header_value("Content-Type");
5613- return !content_type.rfind( "multipart/form-data", 0) ;
5658+ return detail::extract_media_type(content_type) == "multipart/form-data";
56145659}
56155660
56165661// Multipart FormData implementation
@@ -7092,7 +7137,8 @@ bool Server::read_content(Stream &strm, Request &req, Response &res) {
70927137 return true;
70937138 })) {
70947139 const auto &content_type = req.get_header_value("Content-Type");
7095- if (!content_type.find("application/x-www-form-urlencoded")) {
7140+ if (detail::extract_media_type(content_type) ==
7141+ "application/x-www-form-urlencoded") {
70967142 if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) {
70977143 res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414?
70987144 output_error_log(Error::ExceedMaxPayloadSize, &req);
@@ -7479,45 +7525,63 @@ bool Server::routing(Request &req, Response &res, Stream &strm) {
74797525 if (detail::expect_content(req)) {
74807526 // Content reader handler
74817527 {
7528+ // Track whether the ContentReader was aborted due to the decompressed
7529+ // payload exceeding `payload_max_length_`.
7530+ // The user handler runs after the lambda returns, so we must restore the
7531+ // 413 status if the handler overwrites it.
7532+ bool content_reader_payload_too_large = false;
7533+
74827534 ContentReader reader(
74837535 [&](ContentReceiver receiver) {
74847536 auto result = read_content_with_content_receiver(
74857537 strm, req, res, std::move(receiver), nullptr, nullptr);
7486- if (!result) { output_error_log(Error::Read, &req); }
7538+ if (!result) {
7539+ output_error_log(Error::Read, &req);
7540+ if (res.status == StatusCode::PayloadTooLarge_413) {
7541+ content_reader_payload_too_large = true;
7542+ }
7543+ }
74877544 return result;
74887545 },
74897546 [&](FormDataHeader header, ContentReceiver receiver) {
74907547 auto result = read_content_with_content_receiver(
74917548 strm, req, res, nullptr, std::move(header),
74927549 std::move(receiver));
7493- if (!result) { output_error_log(Error::Read, &req); }
7550+ if (!result) {
7551+ output_error_log(Error::Read, &req);
7552+ if (res.status == StatusCode::PayloadTooLarge_413) {
7553+ content_reader_payload_too_large = true;
7554+ }
7555+ }
74947556 return result;
74957557 });
74967558
7559+ bool dispatched = false;
74977560 if (req.method == "POST") {
7498- if (dispatch_request_for_content_reader(
7499- req, res, std::move(reader),
7500- post_handlers_for_content_reader_)) {
7501- return true;
7502- }
7561+ dispatched = dispatch_request_for_content_reader(
7562+ req, res, std::move(reader), post_handlers_for_content_reader_);
75037563 } else if (req.method == "PUT") {
7504- if (dispatch_request_for_content_reader(
7505- req, res, std::move(reader),
7506- put_handlers_for_content_reader_)) {
7507- return true;
7508- }
7564+ dispatched = dispatch_request_for_content_reader(
7565+ req, res, std::move(reader), put_handlers_for_content_reader_);
75097566 } else if (req.method == "PATCH") {
7510- if (dispatch_request_for_content_reader(
7511- req, res, std::move(reader),
7512- patch_handlers_for_content_reader_)) {
7513- return true;
7514- }
7567+ dispatched = dispatch_request_for_content_reader(
7568+ req, res, std::move(reader), patch_handlers_for_content_reader_);
75157569 } else if (req.method == "DELETE") {
7516- if (dispatch_request_for_content_reader(
7517- req, res, std::move(reader),
7518- delete_handlers_for_content_reader_)) {
7519- return true;
7570+ dispatched = dispatch_request_for_content_reader(
7571+ req, res, std::move(reader), delete_handlers_for_content_reader_);
7572+ }
7573+
7574+ if (dispatched) {
7575+ if (content_reader_payload_too_large) {
7576+ // Enforce the limit: override any status the handler may have set
7577+ // and return false so the error path sends a plain 413 response.
7578+ res.status = StatusCode::PayloadTooLarge_413;
7579+ res.body.clear();
7580+ res.content_length_ = 0;
7581+ res.content_provider_ = nullptr;
7582+ return false;
75207583 }
7584+ return true;
75217585 }
75227586 }
75237587
@@ -7930,16 +7994,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
79307994 routed = true;
79317995 } else {
79327996 res.status = StatusCode::InternalServerError_500;
7933- std::string val;
7934- auto s = e.what();
7935- for (size_t i = 0; s[i]; i++) {
7936- switch (s[i]) {
7937- case '\r': val += "\\r"; break;
7938- case '\n': val += "\\n"; break;
7939- default: val += s[i]; break;
7940- }
7941- }
7942- res.set_header("EXCEPTION_WHAT", val);
79437997 }
79447998 } catch (...) {
79457999 if (exception_handler_) {
@@ -7948,7 +8002,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
79488002 routed = true;
79498003 } else {
79508004 res.status = StatusCode::InternalServerError_500;
7951- res.set_header("EXCEPTION_WHAT", "UNKNOWN");
79528005 }
79538006 }
79548007#endif
@@ -11629,8 +11682,7 @@ void SSLClient::set_session_verifier(
1162911682 session_verifier_ = std::move(verifier);
1163011683}
1163111684
11632- #if defined(_WIN32) && \
11633- !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
11685+ #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1163411686void SSLClient::enable_windows_certificate_verification(bool enabled) {
1163511687 enable_windows_cert_verification_ = enabled;
1163611688}
@@ -11788,8 +11840,7 @@ bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1178811840 }
1178911841 }
1179011842
11791- #if defined(_WIN32) && \
11792- !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
11843+ #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1179311844 // Additional Windows Schannel verification.
1179411845 // This provides real-time certificate validation with Windows Update
1179511846 // integration, working with both OpenSSL and MbedTLS backends.
@@ -11835,8 +11886,7 @@ void Client::enable_server_hostname_verification(bool enabled) {
1183511886 cli_->enable_server_hostname_verification(enabled);
1183611887}
1183711888
11838- #if defined(_WIN32) && \
11839- !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
11889+ #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1184011890void Client::enable_windows_certificate_verification(bool enabled) {
1184111891 if (is_ssl_) {
1184211892 static_cast<SSLClient &>(*cli_).enable_windows_certificate_verification(
@@ -11959,7 +12009,7 @@ bool enumerate_windows_system_certs(Callback cb) {
1195912009}
1196012010#endif
1196112011
11962- #if defined(__APPLE__) && defined( CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
12012+ #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
1196312013// Enumerate macOS Keychain certificates and call callback with DER data
1196412014template <typename Callback>
1196512015bool enumerate_macos_keychain_certs(Callback cb) {
0 commit comments