From a2afe2bc93de656f14054d9a3405b257afef2c8f Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 13:59:11 -0800 Subject: [PATCH 01/19] Optimize per-request critical path performance - Replace istringstream-based string_split with manual loop (string_utilities.cpp) - Fix http_unescape: parse hex chars directly without substr allocations (http_utils.cpp) - Flatten unique_ptr members in modded_request to plain std::string, eliminating 2 heap allocations per request (modded_request.hpp, webserver.cpp) - Fix strcmp/strcasecmp inconsistency: all HTTP method dispatch now uses case-sensitive strcmp per RFC 7230 (webserver.cpp) - Optimize set_method: skip unnecessary copy+uppercase since MHD provides method strings in uppercase per spec (http_request.cpp) - Cache get_path_pieces() result on first access to avoid re-tokenization on every call (http_request.hpp) - Separate exact vs parameterized routes at registration time; only scan parameterized routes on regex fallback (webserver.hpp, webserver.cpp) - Add shared LRU cache (256 entries) for URL-to-endpoint route matches to avoid repeated regex matching (webserver.hpp, webserver.cpp) - Optimize standardize_url to modify in-place instead of creating extra copies (http_utils.cpp) --- src/http_request.cpp | 2 +- src/http_utils.cpp | 38 ++++--- src/httpserver/details/modded_request.hpp | 4 +- src/httpserver/http_request.hpp | 17 ++- src/httpserver/webserver.hpp | 15 +++ src/string_utilities.cpp | 25 ++++- src/webserver.cpp | 127 +++++++++++++++------- 7 files changed, 161 insertions(+), 67 deletions(-) diff --git a/src/http_request.cpp b/src/http_request.cpp index be532637..6d4b0658 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -39,7 +39,7 @@ struct arguments_accumulator { }; void http_request::set_method(const std::string& method) { - this->method = string_utilities::to_upper_copy(method); + this->method = method; } #ifdef HAVE_DAUTH diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 138c44ef..53e7623f 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -218,19 +218,13 @@ std::vector http_utils::tokenize_url(const std::string& str, const } std::string http_utils::standardize_url(const std::string& url) { - std::string n_url = url; + std::string result = url; - std::string::iterator new_end = std::unique(n_url.begin(), n_url.end(), [](char a, char b) { return (a == b) && (a == '/'); }); - n_url.erase(new_end, n_url.end()); + auto new_end = std::unique(result.begin(), result.end(), [](char a, char b) { return (a == b) && (a == '/'); }); + result.erase(new_end, result.end()); - std::string::size_type n_url_length = n_url.length(); - - std::string result; - - if (n_url_length > 1 && n_url[n_url_length - 1] == '/') { - result = n_url.substr(0, n_url_length - 1); - } else { - result = n_url; + if (result.length() > 1 && result.back() == '/') { + result.pop_back(); } return result; @@ -302,13 +296,19 @@ uint16_t get_port(const struct sockaddr* sa) { } } +static inline int hex_digit_value(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + size_t http_unescape(std::string* val) { if (val->empty()) return 0; unsigned int rpos = 0; unsigned int wpos = 0; - unsigned int num; unsigned int size = val->size(); while (rpos < size && (*val)[rpos] != '\0') { @@ -319,11 +319,15 @@ size_t http_unescape(std::string* val) { rpos++; break; case '%': - if (size > rpos + 2 && ((1 == sscanf(val->substr(rpos + 1, 2).c_str(), "%2x", &num)) || (1 == sscanf(val->substr(rpos + 1, 2).c_str(), "%2X", &num)))) { - (*val)[wpos] = (unsigned char) num; - wpos++; - rpos += 3; - break; + if (size > rpos + 2) { + int hi = hex_digit_value((*val)[rpos + 1]); + int lo = hex_digit_value((*val)[rpos + 2]); + if (hi >= 0 && lo >= 0) { + (*val)[wpos] = static_cast((hi << 4) | lo); + wpos++; + rpos += 3; + break; + } } // intentional fall through! default: diff --git a/src/httpserver/details/modded_request.hpp b/src/httpserver/details/modded_request.hpp index 0ab79ada..49aae1d3 100644 --- a/src/httpserver/details/modded_request.hpp +++ b/src/httpserver/details/modded_request.hpp @@ -37,8 +37,8 @@ namespace details { struct modded_request { struct MHD_PostProcessor *pp = nullptr; - std::unique_ptr complete_uri; - std::unique_ptr standardized_url; + std::string complete_uri; + std::string standardized_url; webserver* ws = nullptr; std::shared_ptr (httpserver::http_resource::*callback)(const httpserver::http_request&); diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 4c1c3323..fb2677c0 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -93,7 +93,11 @@ class http_request { * @return a vector of strings containing all pieces **/ const std::vector get_path_pieces() const { - return http::http_utils::tokenize_url(path); + if (!cache->path_pieces_cached) { + cache->path_pieces = http::http_utils::tokenize_url(path); + cache->path_pieces_cached = true; + } + return cache->path_pieces; } /** @@ -102,9 +106,12 @@ class http_request { * @return the selected piece in form of string **/ const std::string get_path_piece(int index) const { - std::vector post_path = get_path_pieces(); - if (static_cast(post_path.size()) > index) { - return post_path[index]; + if (!cache->path_pieces_cached) { + cache->path_pieces = http::http_utils::tokenize_url(path); + cache->path_pieces_cached = true; + } + if (static_cast(cache->path_pieces.size()) > index) { + return cache->path_pieces[index]; } return EMPTY; } @@ -440,8 +447,10 @@ class http_request { std::string digested_user; #endif // HAVE_DAUTH std::map, http::arg_comparator> unescaped_args; + std::vector path_pieces; bool args_populated = false; + bool path_pieces_cached = false; }; std::unique_ptr cache = std::make_unique(); // Populate the data cache unescaped_args diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index e4d5e313..e7dbba7b 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -40,11 +40,14 @@ #include #endif +#include #include #include +#include #include #include #include +#include #include #ifdef HAVE_GNUTLS @@ -188,6 +191,16 @@ class webserver { std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; + std::map registered_resources_regex; + + struct route_cache_entry { + details::http_endpoint matched_endpoint; + http_resource* resource; + }; + static constexpr size_t ROUTE_CACHE_MAX_SIZE = 256; + std::mutex route_cache_mutex; + std::list> route_cache_list; + std::unordered_map>::iterator> route_cache_map; std::shared_mutex bans_mutex; std::set bans; @@ -226,6 +239,8 @@ class webserver { MHD_Result complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method); + void invalidate_route_cache(); + #ifdef HAVE_GNUTLS // MHD_PskServerCredentialsCallback signature static int psk_cred_handler_func(void* cls, diff --git a/src/string_utilities.cpp b/src/string_utilities.cpp index 7170ccf6..5f2b3884 100644 --- a/src/string_utilities.cpp +++ b/src/string_utilities.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -45,13 +44,29 @@ const std::string to_lower_copy(const std::string& str) { const std::vector string_split(const std::string& s, char sep, bool collapse) { std::vector result; + if (s.empty()) return result; - std::istringstream buf(s); - for (std::string token; getline(buf, token, sep); ) { - if ((collapse && token != "") || !collapse) { - result.push_back(token); + std::string::size_type start = 0; + std::string::size_type end; + + while ((end = s.find(sep, start)) != std::string::npos) { + std::string token = s.substr(start, end - start); + if (!collapse || !token.empty()) { + result.push_back(std::move(token)); } + start = end + 1; } + + // Handle the last token (after the final separator) + // Only add if there's content or if not collapsing + // Note: match istringstream behavior which does not emit trailing empty token + if (start < s.size()) { + std::string token = s.substr(start); + if (!collapse || !token.empty()) { + result.push_back(std::move(token)); + } + } + return result; } diff --git a/src/webserver.cpp b/src/webserver.cpp index 547eda60..0a0946e9 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -213,8 +213,14 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr std::unique_lock registered_resources_lock(registered_resources_mutex); pair::iterator, bool> result = registered_resources.insert(map::value_type(idx, hrm)); - if (!family && result.second && idx.get_url_pars().empty()) { - registered_resources_str.insert(pair(idx.get_url_complete(), result.first->second)); + if (result.second) { + bool is_exact = !family && idx.get_url_pars().empty(); + if (is_exact) { + registered_resources_str.insert(pair(idx.get_url_complete(), result.first->second)); + } + if (idx.is_regex_compiled()) { + registered_resources_regex.insert(map::value_type(idx, hrm)); + } } return result.second; @@ -386,6 +392,12 @@ bool webserver::stop() { return true; } +void webserver::invalidate_route_cache() { + std::lock_guard lock(route_cache_mutex); + route_cache_list.clear(); + route_cache_map.clear(); +} + void webserver::unregister_resource(const string& resource) { // family does not matter - it just checks the url_normalized anyhow details::http_endpoint he(resource, false, true, regex_checking); @@ -393,6 +405,9 @@ void webserver::unregister_resource(const string& resource) { registered_resources.erase(he); registered_resources.erase(he.get_url_complete()); registered_resources_str.erase(he.get_url_complete()); + registered_resources_regex.erase(he); + registered_resources_lock.unlock(); + invalidate_route_cache(); } void webserver::ban_ip(const string& ip) { @@ -509,7 +524,7 @@ void* uri_log(void* cls, const char* uri, struct MHD_Connection *con) { std::ignore = con; auto mr = std::make_unique(); - mr->complete_uri = std::make_unique(uri); + mr->complete_uri = uri; return reinterpret_cast(mr.release()); } @@ -736,41 +751,77 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details { std::shared_lock registered_resources_lock(registered_resources_mutex); if (!single_resource) { - const char* st_url = mr->standardized_url->c_str(); + const char* st_url = mr->standardized_url.c_str(); fe = registered_resources_str.find(st_url); if (fe == registered_resources_str.end()) { if (regex_checking) { - map::iterator found_endpoint; - details::http_endpoint endpoint(st_url, false, false, false); - map::iterator it; - - size_t len = 0; - size_t tot_len = 0; - for (it = registered_resources.begin(); it != registered_resources.end(); ++it) { - size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); - size_t endpoint_tot_len = (*it).first.get_url_complete().size(); - if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { - if ((*it).first.match(endpoint)) { - found = true; - len = endpoint_pieces_len; - tot_len = endpoint_tot_len; - found_endpoint = it; + // Check the LRU route cache first + { + std::lock_guard cache_lock(route_cache_mutex); + auto cache_it = route_cache_map.find(mr->standardized_url); + if (cache_it != route_cache_map.end()) { + // Cache hit — move to front of LRU list + route_cache_list.splice(route_cache_list.begin(), route_cache_list, cache_it->second); + const route_cache_entry& cached = cache_it->second->second; + + vector url_pars = cached.matched_endpoint.get_url_pars(); + vector url_pieces = endpoint.get_url_pieces(); + vector chunks = cached.matched_endpoint.get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); } + + hrm = cached.resource; + found = true; } } - if (found) { - vector url_pars = found_endpoint->first.get_url_pars(); - - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = found_endpoint->first.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + if (!found) { + // Cache miss — perform regex scan + map::iterator found_endpoint; + + map::iterator it; + + size_t len = 0; + size_t tot_len = 0; + for (it = registered_resources_regex.begin(); it != registered_resources_regex.end(); ++it) { + size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); + size_t endpoint_tot_len = (*it).first.get_url_complete().size(); + if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { + if ((*it).first.match(endpoint)) { + found = true; + len = endpoint_pieces_len; + tot_len = endpoint_tot_len; + found_endpoint = it; + } + } } - hrm = found_endpoint->second; + if (found) { + vector url_pars = found_endpoint->first.get_url_pars(); + + vector url_pieces = endpoint.get_url_pieces(); + vector chunks = found_endpoint->first.get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + } + + hrm = found_endpoint->second; + + // Store in LRU cache + { + std::lock_guard cache_lock(route_cache_mutex); + route_cache_list.emplace_front(mr->standardized_url, route_cache_entry{found_endpoint->first, hrm}); + route_cache_map[mr->standardized_url] = route_cache_list.begin(); + + if (route_cache_map.size() > ROUTE_CACHE_MAX_SIZE) { + route_cache_map.erase(route_cache_list.back().first); + route_cache_list.pop_back(); + } + } + } } } } else { @@ -857,7 +908,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details MHD_Result webserver::complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method) { mr->ws = this; - mr->dhr->set_path(mr->standardized_url->c_str()); + mr->dhr->set_path(mr->standardized_url.c_str()); mr->dhr->set_method(method); mr->dhr->set_version(version); @@ -882,33 +933,33 @@ MHD_Result webserver::answer_to_connection(void* cls, MHD_Connection* connection std::string t_url = url; base_unescaper(&t_url, static_cast(cls)->unescaper); - mr->standardized_url = std::make_unique(http_utils::standardize_url(t_url)); + mr->standardized_url = http_utils::standardize_url(t_url); mr->has_body = false; - access_log(static_cast(cls), *(mr->complete_uri) + " METHOD: " + method); + access_log(static_cast(cls), mr->complete_uri + " METHOD: " + method); - if (0 == strcasecmp(method, http_utils::http_method_get)) { + if (0 == strcmp(method, http_utils::http_method_get)) { mr->callback = &http_resource::render_GET; } else if (0 == strcmp(method, http_utils::http_method_post)) { mr->callback = &http_resource::render_POST; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_put)) { + } else if (0 == strcmp(method, http_utils::http_method_put)) { mr->callback = &http_resource::render_PUT; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_delete)) { + } else if (0 == strcmp(method, http_utils::http_method_delete)) { mr->callback = &http_resource::render_DELETE; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_patch)) { + } else if (0 == strcmp(method, http_utils::http_method_patch)) { mr->callback = &http_resource::render_PATCH; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_head)) { + } else if (0 == strcmp(method, http_utils::http_method_head)) { mr->callback = &http_resource::render_HEAD; - } else if (0 ==strcasecmp(method, http_utils::http_method_connect)) { + } else if (0 == strcmp(method, http_utils::http_method_connect)) { mr->callback = &http_resource::render_CONNECT; - } else if (0 == strcasecmp(method, http_utils::http_method_trace)) { + } else if (0 == strcmp(method, http_utils::http_method_trace)) { mr->callback = &http_resource::render_TRACE; - } else if (0 ==strcasecmp(method, http_utils::http_method_options)) { + } else if (0 == strcmp(method, http_utils::http_method_options)) { mr->callback = &http_resource::render_OPTIONS; } From 96ac18f40cec155ef8a9ba865a7dc2e559e96684 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 14:07:07 -0800 Subject: [PATCH 02/19] Fix validation findings: bounds check, DRY, const refs, empty guard - Add bounds checking for url_pieces[chunks[i]] to prevent potential buffer overflow when cached endpoint chunks don't match request pieces - Extract URL parameter extraction from cache hit/miss paths into single block after match resolution (DRY fix) - Use const references for vectors instead of by-value copies to avoid heap allocations under cache lock - Add empty-string guard to standardize_url before calling back() - Extract ensure_path_pieces_cached() helper to deduplicate caching logic in get_path_pieces() and get_path_piece() - Add RFC 7230 comment documenting case-sensitive strcmp intent --- src/http_utils.cpp | 2 ++ src/httpserver/http_request.hpp | 17 ++++++------- src/webserver.cpp | 42 ++++++++++++++++----------------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 53e7623f..43b522bb 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -218,6 +218,8 @@ std::vector http_utils::tokenize_url(const std::string& str, const } std::string http_utils::standardize_url(const std::string& url) { + if (url.empty()) return url; + std::string result = url; auto new_end = std::unique(result.begin(), result.end(), [](char a, char b) { return (a == b) && (a == '/'); }); diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index fb2677c0..6047c4da 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -93,10 +93,7 @@ class http_request { * @return a vector of strings containing all pieces **/ const std::vector get_path_pieces() const { - if (!cache->path_pieces_cached) { - cache->path_pieces = http::http_utils::tokenize_url(path); - cache->path_pieces_cached = true; - } + ensure_path_pieces_cached(); return cache->path_pieces; } @@ -106,10 +103,7 @@ class http_request { * @return the selected piece in form of string **/ const std::string get_path_piece(int index) const { - if (!cache->path_pieces_cached) { - cache->path_pieces = http::http_utils::tokenize_url(path); - cache->path_pieces_cached = true; - } + ensure_path_pieces_cached(); if (static_cast(cache->path_pieces.size()) > index) { return cache->path_pieces[index]; } @@ -453,6 +447,13 @@ class http_request { bool path_pieces_cached = false; }; std::unique_ptr cache = std::make_unique(); + void ensure_path_pieces_cached() const { + if (!cache->path_pieces_cached) { + cache->path_pieces = http::http_utils::tokenize_url(path); + cache->path_pieces_cached = true; + } + } + // Populate the data cache unescaped_args void populate_args() const; diff --git a/src/webserver.cpp b/src/webserver.cpp index 0a0946e9..efa5862e 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -758,6 +758,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details details::http_endpoint endpoint(st_url, false, false, false); // Check the LRU route cache first + const details::http_endpoint* matched_ep = nullptr; { std::lock_guard cache_lock(route_cache_mutex); auto cache_it = route_cache_map.find(mr->standardized_url); @@ -765,14 +766,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details // Cache hit — move to front of LRU list route_cache_list.splice(route_cache_list.begin(), route_cache_list, cache_it->second); const route_cache_entry& cached = cache_it->second->second; - - vector url_pars = cached.matched_endpoint.get_url_pars(); - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = cached.matched_endpoint.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); - } - + matched_ep = &cached.matched_endpoint; hrm = cached.resource; found = true; } @@ -782,15 +776,13 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details // Cache miss — perform regex scan map::iterator found_endpoint; - map::iterator it; - size_t len = 0; size_t tot_len = 0; - for (it = registered_resources_regex.begin(); it != registered_resources_regex.end(); ++it) { - size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); - size_t endpoint_tot_len = (*it).first.get_url_complete().size(); + for (auto it = registered_resources_regex.begin(); it != registered_resources_regex.end(); ++it) { + size_t endpoint_pieces_len = it->first.get_url_pieces().size(); + size_t endpoint_tot_len = it->first.get_url_complete().size(); if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { - if ((*it).first.match(endpoint)) { + if (it->first.match(endpoint)) { found = true; len = endpoint_pieces_len; tot_len = endpoint_tot_len; @@ -800,14 +792,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } if (found) { - vector url_pars = found_endpoint->first.get_url_pars(); - - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = found_endpoint->first.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); - } - + matched_ep = &found_endpoint->first; hrm = found_endpoint->second; // Store in LRU cache @@ -823,6 +808,18 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } } } + + // Extract URL parameters from matched endpoint + if (found && matched_ep != nullptr) { + const auto& url_pars = matched_ep->get_url_pars(); + const auto& url_pieces = endpoint.get_url_pieces(); + const auto& chunks = matched_ep->get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + if (chunks[i] >= 0 && static_cast(chunks[i]) < url_pieces.size()) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + } + } + } } } else { hrm = fe->second; @@ -939,6 +936,7 @@ MHD_Result webserver::answer_to_connection(void* cls, MHD_Connection* connection access_log(static_cast(cls), mr->complete_uri + " METHOD: " + method); + // Case-sensitive per RFC 7230 §3.1.1: HTTP method is case-sensitive. if (0 == strcmp(method, http_utils::http_method_get)) { mr->callback = &http_resource::render_GET; } else if (0 == strcmp(method, http_utils::http_method_post)) { From 7e683da36d6a2e9ddf8b7e44b293813875f010fa Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 14:10:51 -0800 Subject: [PATCH 03/19] Fix use-after-free: copy cache data while holding lock The matched_ep pointer referenced data inside route_cache_list that could be invalidated by another thread (via invalidate_route_cache or LRU eviction) after the cache mutex was released. Fix by copying url_pars and chunk_positions vectors while holding the cache lock, so parameter extraction afterwards uses owned data. For cache miss path, the registered_resources_mutex shared lock is still held so direct reference is safe, but copy for consistency. --- src/webserver.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/webserver.cpp b/src/webserver.cpp index efa5862e..29827888 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -757,8 +757,13 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details if (regex_checking) { details::http_endpoint endpoint(st_url, false, false, false); + // Data needed for parameter extraction after match. + // On cache hit, we copy these while holding the cache lock + // to avoid use-after-free if another thread invalidates cache. + vector matched_url_pars; + vector matched_chunks; + // Check the LRU route cache first - const details::http_endpoint* matched_ep = nullptr; { std::lock_guard cache_lock(route_cache_mutex); auto cache_it = route_cache_map.find(mr->standardized_url); @@ -766,7 +771,8 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details // Cache hit — move to front of LRU list route_cache_list.splice(route_cache_list.begin(), route_cache_list, cache_it->second); const route_cache_entry& cached = cache_it->second->second; - matched_ep = &cached.matched_endpoint; + matched_url_pars = cached.matched_endpoint.get_url_pars(); + matched_chunks = cached.matched_endpoint.get_chunk_positions(); hrm = cached.resource; found = true; } @@ -792,7 +798,9 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } if (found) { - matched_ep = &found_endpoint->first; + // Safe to reference: registered_resources_mutex (shared) is still held + matched_url_pars = found_endpoint->first.get_url_pars(); + matched_chunks = found_endpoint->first.get_chunk_positions(); hrm = found_endpoint->second; // Store in LRU cache @@ -810,13 +818,11 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } // Extract URL parameters from matched endpoint - if (found && matched_ep != nullptr) { - const auto& url_pars = matched_ep->get_url_pars(); + if (found) { const auto& url_pieces = endpoint.get_url_pieces(); - const auto& chunks = matched_ep->get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - if (chunks[i] >= 0 && static_cast(chunks[i]) < url_pieces.size()) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + for (unsigned int i = 0; i < matched_url_pars.size(); i++) { + if (matched_chunks[i] >= 0 && static_cast(matched_chunks[i]) < url_pieces.size()) { + mr->dhr->set_arg(matched_url_pars[i], url_pieces[matched_chunks[i]]); } } } From 305979e8c5947d95749dcf9737a6af1f4ca5b289 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 14:17:56 -0800 Subject: [PATCH 04/19] Fix race condition in unregister_resource cache invalidation Move route cache invalidation inside registered_resources_mutex lock in unregister_resource() to prevent use-after-free when threads retrieve cached resource pointers during unregistration. Also invalidate route cache on register_resource() for correctness when resources are dynamically registered at runtime. Add threading model documentation to http_request lazy caching. --- src/httpserver/http_request.hpp | 6 +++++- src/webserver.cpp | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 6047c4da..2ff2136c 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -427,7 +427,11 @@ class http_request { std::string_view get_connection_value(std::string_view key, enum MHD_ValueKind kind) const; const http::header_view_map get_headerlike_values(enum MHD_ValueKind kind) const; - // Cache certain data items on demand so we can consistently return views + // http_request objects are owned by a single connection and are not + // shared across threads. Lazy caching (path_pieces, args, etc.) is + // safe without synchronization under this invariant. + + // Cache certain data items on demand so we can consistently return views // over the data. Some things we transform before returning to the user for // simplicity (e.g. query_str, requestor), others out of necessity (arg unescaping). // Others (username, password, digested_user) MHD returns as char* that we need diff --git a/src/webserver.cpp b/src/webserver.cpp index 29827888..7bde4fab 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -221,9 +221,12 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr if (idx.is_regex_compiled()) { registered_resources_regex.insert(map::value_type(idx, hrm)); } + registered_resources_lock.unlock(); + invalidate_route_cache(); + return true; } - return result.second; + return false; } bool webserver::start(bool blocking) { @@ -402,12 +405,20 @@ void webserver::unregister_resource(const string& resource) { // family does not matter - it just checks the url_normalized anyhow details::http_endpoint he(resource, false, true, regex_checking); std::unique_lock registered_resources_lock(registered_resources_mutex); + + // Invalidate cache while holding registered_resources_mutex to prevent + // any thread from retrieving dangling resource pointers from the cache + // after we erase from the resource maps. + { + std::lock_guard cache_lock(route_cache_mutex); + route_cache_list.clear(); + route_cache_map.clear(); + } + registered_resources.erase(he); registered_resources.erase(he.get_url_complete()); registered_resources_str.erase(he.get_url_complete()); registered_resources_regex.erase(he); - registered_resources_lock.unlock(); - invalidate_route_cache(); } void webserver::ban_ip(const string& ip) { From 03cf1e7b2e7214745276b55befc9eac81f8747fc Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 18:27:29 -0800 Subject: [PATCH 05/19] Add missing #include for std::move Fixes cpplint build/include_what_you_use warning. --- src/string_utilities.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/string_utilities.cpp b/src/string_utilities.cpp index 5f2b3884..697fbf08 100644 --- a/src/string_utilities.cpp +++ b/src/string_utilities.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace httpserver { From db2225ba7d3a86c27c248d28023823dad268db6f Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 11 Feb 2026 01:45:18 -0800 Subject: [PATCH 06/19] Remove redundant c_str() in set_path call set_path takes const std::string&, so passing .c_str() forces an unnecessary temporary string construction. --- src/webserver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webserver.cpp b/src/webserver.cpp index 7bde4fab..1d72445b 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -922,7 +922,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details MHD_Result webserver::complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method) { mr->ws = this; - mr->dhr->set_path(mr->standardized_url.c_str()); + mr->dhr->set_path(mr->standardized_url); mr->dhr->set_method(method); mr->dhr->set_version(version); From 13e0af8f4fb98f5ecda0eeca28a4d5f824ef9cf6 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 00:45:40 -0800 Subject: [PATCH 07/19] Fix macOS CI: prevent curl from detecting partial OpenSSL curl 7.75.0's NTLM code fails to compile on newer macOS runners because configure detects a partial OpenSSL installation (missing des.h). Adding --without-ssl prevents OpenSSL detection while keeping Secure Transport via --with-darwinssl. Cache key bumped to force a fresh build. --- .github/workflows/verify-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index d5993d24..553088ea 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -467,7 +467,7 @@ jobs: uses: actions/cache@v4 with: path: curl-7.75.0 - key: ${{ matrix.os }}-CURL-pre-built + key: ${{ matrix.os }}-CURL-pre-built-v2 if: ${{ matrix.os == 'macos-latest' }} - name: Build CURL (for testing) @@ -475,7 +475,7 @@ jobs: curl https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-7.75.0.tar.gz -o curl-7.75.0.tar.gz ; tar -xzf curl-7.75.0.tar.gz ; cd curl-7.75.0 ; - ./configure --with-darwinssl ; + ./configure --with-darwinssl --without-ssl ; make ; if: ${{ matrix.os == 'macos-latest' && steps.cache-CURL.outputs.cache-hit != 'true' }} From 27f1be32c4a32066fec395befac06c13c5e14c9d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 01:04:09 -0800 Subject: [PATCH 08/19] Bust stale libmicrohttpd cache on macOS CI The cached libmicrohttpd build has hardcoded GnuTLS include paths from a previous runner version. When make install triggers recompilation on the current runner, gnutls/gnutls.h is not found at the old path. Bumping the cache key forces a fresh build. --- .github/workflows/verify-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 553088ea..4df3e15c 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -512,7 +512,7 @@ jobs: uses: actions/cache@v4 with: path: libmicrohttpd-0.9.77 - key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built-v2 if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' }} - name: Build libmicrohttpd dependency (if not cached) From 8db55fe6f189f3daaaad5d521b2db7aded87b4f4 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 01:14:21 -0800 Subject: [PATCH 09/19] Work around gcov negative hit count bug in coverage report gcov can produce negative branch hit values (gcc bug #68080). Newer gcovr versions treat this as a fatal error. Adding --gcov-ignore-parse-errors=negative_hits.warn_once_per_file lets the coverage report complete while still logging the issue. --- .github/workflows/verify-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 4df3e15c..17ae2955 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -742,7 +742,7 @@ jobs: - name: Generate coverage report run: | cd build - gcovr --root .. --filter '../src/' --xml coverage.xml --xml-pretty --print-summary + gcovr --root .. --filter '../src/' --xml coverage.xml --xml-pretty --print-summary --gcov-ignore-parse-errors=negative_hits.warn_once_per_file if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} - name: Upload coverage to Codecov From d59dc500148438699f09aac744373166a59901cf Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 09:31:33 -0800 Subject: [PATCH 10/19] Fix custom_socket test failure under Valgrind on Ubuntu 24.04 Set SO_REUSEADDR before bind() so the port can be reused when still in TIME_WAIT from prior tests, and connect to 127.0.0.1 instead of localhost to avoid IPv6 resolution mismatch with the IPv4-only socket. --- test/integ/ws_start_stop.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 34ab2fdb..d664301a 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -337,6 +337,9 @@ LT_END_AUTO_TEST(enable_options) LT_BEGIN_AUTO_TEST(ws_start_stop_suite, custom_socket) int fd = socket(AF_INET, SOCK_STREAM, 0); + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr("127.0.0.1"); @@ -353,7 +356,7 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, custom_socket) std::string s; CURL *curl = curl_easy_init(); CURLcode res; - curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_URL, "127.0.0.1:" PORT_STRING "/base"); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); From 755ecc1a156704d9648d773a11cbd581b4f31c5d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 15:37:57 -0800 Subject: [PATCH 11/19] Fixes to release/dist mechanism --- .github/workflows/changelog-check.yml | 84 ++++++ .github/workflows/release.yml | 326 +++++++++++++++++++++++ CONTRIBUTING.md | 16 ++ ChangeLog | 354 ++++++++----------------- Makefile.am | 2 +- scripts/extract-release-notes.sh | 44 +++ scripts/validate-version.sh | 68 +++++ src/create_test_request.cpp | 69 +++++ src/httpserver/create_test_request.hpp | 145 ++++++++++ test/unit/create_test_request_test.cpp | 283 ++++++++++++++++++++ 10 files changed, 1146 insertions(+), 245 deletions(-) create mode 100644 .github/workflows/changelog-check.yml create mode 100644 .github/workflows/release.yml create mode 100755 scripts/extract-release-notes.sh create mode 100755 scripts/validate-version.sh create mode 100644 src/create_test_request.cpp create mode 100644 src/httpserver/create_test_request.hpp create mode 100644 test/unit/create_test_request_test.cpp diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml new file mode 100644 index 00000000..632cedfb --- /dev/null +++ b/.github/workflows/changelog-check.yml @@ -0,0 +1,84 @@ +name: "ChangeLog Check" + +on: + pull_request: + branches: [master] + +jobs: + changelog: + name: Verify ChangeLog updated + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for ChangeLog update + run: | + # Get list of changed files + changed_files=$(git diff --name-only origin/master...HEAD) + + if [ -z "$changed_files" ]; then + echo "No files changed." + exit 0 + fi + + # Check if only exempt files were changed + # Exempt: .github/*, CLAUDE.md, README*, CONTRIBUTING*, CODE_OF_CONDUCT*, + # .gitignore, CPPLINT.cfg, *.md in root + has_non_exempt=false + changelog_modified=false + + while IFS= read -r file; do + # Check if ChangeLog itself was modified + if [ "$file" = "ChangeLog" ]; then + changelog_modified=true + continue + fi + + # Check exempt patterns + case "$file" in + .github/*) continue ;; + CLAUDE.md) continue ;; + README*) continue ;; + CONTRIBUTING*) continue ;; + CODE_OF_CONDUCT*) continue ;; + .gitignore) continue ;; + CPPLINT.cfg) continue ;; + esac + + # Check for *.md files in repo root (no slashes in path) + if echo "$file" | grep -qE '^[^/]+\.md$'; then + continue + fi + + has_non_exempt=true + done <<< "$changed_files" + + if [ "$has_non_exempt" = "false" ]; then + echo "Only exempt files changed — ChangeLog update not required." + exit 0 + fi + + if [ "$changelog_modified" = "false" ]; then + echo "::error::ChangeLog was not updated. All pull requests with code changes must include a ChangeLog entry." + echo "" + echo "Please add a tab-indented entry under the first 'Version X.Y.Z' header in ChangeLog." + echo "See CONTRIBUTING.md for format details." + echo "" + echo "If this PR only changes documentation or CI files, add the [skip changelog] label or ensure" + echo "only exempt paths are modified (.github/*, *.md in root, .gitignore, CPPLINT.cfg)." + exit 1 + fi + + echo "ChangeLog was modified — checking format." + + # Validate first line matches Version header format + first_line=$(head -n 1 ChangeLog) + if ! echo "$first_line" | grep -qE '^Version [0-9]+\.[0-9]+\.[0-9]+'; then + echo "::error::First line of ChangeLog must match 'Version X.Y.Z' format (got: '$first_line')." + exit 1 + fi + + echo "ChangeLog format looks good." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..81db8332 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,326 @@ +name: "Release" + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., 0.20.0 or 0.20.0-rc1)' + required: true + +permissions: + contents: write + +jobs: + validate: + name: Validate release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + is_prerelease: ${{ steps.version.outputs.is_prerelease }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Determine version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.tag }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + fi + VERSION="${VERSION#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + if echo "$VERSION" | grep -qE '-(rc|alpha|beta)'; then + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + + echo "Version: $VERSION" + + - name: Validate version consistency + run: | + chmod +x scripts/validate-version.sh + scripts/validate-version.sh "${{ steps.version.outputs.version }}" + + - name: Extract release notes + run: | + chmod +x scripts/extract-release-notes.sh + scripts/extract-release-notes.sh "${{ steps.version.outputs.version }}" > release-notes.md + echo "--- Release notes ---" + cat release-notes.md + + - name: Upload release notes + uses: actions/upload-artifact@v4 + with: + name: release-notes + path: release-notes.md + + build-dist: + name: Build distribution tarball + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool libgnutls28-dev libcurl4-openssl-dev + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77 + key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + + - name: Build libmicrohttpd (if not cached) + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-0.9.77 + sudo make install + sudo ldconfig + + - name: Build libhttpserver + run: | + ./bootstrap + mkdir build + cd build + ../configure + make + make check + + - name: Create distribution tarball + run: | + cd build + make dist + + - name: Upload tarball + uses: actions/upload-artifact@v4 + with: + name: dist-tarball + path: build/libhttpserver-*.tar.gz + + verify-dist-linux: + name: Verify tarball (Linux) + runs-on: ubuntu-latest + needs: build-dist + steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgnutls28-dev libcurl4-openssl-dev + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77 + key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + + - name: Build libmicrohttpd (if not cached) + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-0.9.77 + sudo make install + sudo ldconfig + + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Build and test from tarball + run: | + tar -xzf libhttpserver-*.tar.gz + cd libhttpserver-*/ + mkdir build + cd build + ../configure + make + make check + + - name: Print test results on failure + if: failure() + run: | + cd libhttpserver-*/build + cat test/test-suite.log || true + + verify-dist-macos: + name: Verify tarball (macOS) + runs-on: macos-latest + needs: build-dist + steps: + - name: Install build tools + run: brew install autoconf automake libtool + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77 + key: macos-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + + - name: Build libmicrohttpd (if not cached) + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-0.9.77 + sudo make install + + - name: Fetch curl from cache + id: cache-curl + uses: actions/cache@v4 + with: + path: curl-7.75.0 + key: macos-latest-CURL-pre-built-v2 + + - name: Build curl (if not cached) + if: steps.cache-curl.outputs.cache-hit != 'true' + run: | + curl https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-7.75.0.tar.gz -o curl-7.75.0.tar.gz + tar -xzf curl-7.75.0.tar.gz + cd curl-7.75.0 + ./configure --with-darwinssl --without-ssl + make + + - name: Install curl + run: | + cd curl-7.75.0 + sudo make install + + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Build and test from tarball + run: | + tar -xzf libhttpserver-*.tar.gz + cd libhttpserver-*/ + mkdir build + cd build + CFLAGS='-mtune=generic' ../configure --disable-fastopen + make + make check + + - name: Print test results on failure + if: failure() + run: | + cd libhttpserver-*/build + cat test/test-suite.log || true + + verify-dist-windows: + name: Verify tarball (Windows) + runs-on: windows-latest + needs: build-dist + defaults: + run: + shell: msys2 {0} + steps: + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + autotools + base-devel + + - name: Install MinGW64 packages + run: | + pacman --noconfirm -S --needed mingw-w64-x86_64-{toolchain,libtool,make,pkg-config,libsystre,doxygen,gnutls,graphviz,curl} + + - name: Build and install libmicrohttpd + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples --enable-poll=no + make + make install + + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Build and test from tarball + run: | + tar -xzf libhttpserver-*.tar.gz + cd libhttpserver-*/ + mkdir build + cd build + ../configure --disable-fastopen + make + make check + + - name: Print test results on failure + if: failure() + run: | + cd libhttpserver-*/build + cat test/test-suite.log || true + + release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [validate, verify-dist-linux, verify-dist-macos, verify-dist-windows] + steps: + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Download release notes + uses: actions/download-artifact@v4 + with: + name: release-notes + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + VERSION="${{ needs.validate.outputs.version }}" + IS_PRERELEASE="${{ needs.validate.outputs.is_prerelease }}" + + PRERELEASE_FLAG="" + if [ "$IS_PRERELEASE" = "true" ]; then + PRERELEASE_FLAG="--prerelease" + fi + + gh release create "$VERSION" \ + --title "libhttpserver $VERSION" \ + --notes-file release-notes.md \ + $PRERELEASE_FLAG \ + libhttpserver-*.tar.gz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee018af3..d923f747 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,6 +127,22 @@ Please follow these steps to have your contribution considered by the maintainer While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. +### ChangeLog + +All pull requests that include user-facing changes must update the `ChangeLog` file. CI enforces this requirement (with exemptions for documentation-only and CI-only changes). + +* Add your entry under the **first** `Version X.Y.Z` header at the top of `ChangeLog`. +* Use a tab-indented, one-line summary of each change: + ``` + Added support for new feature X. + ``` +* If your change spans multiple lines, indent continuation lines with two tabs: + ``` + Fixed a bug where long descriptions would cause the parser to + fail on multi-line input. + ``` +* Changes that only touch files in `.github/`, `*.md` in the repository root, `.gitignore`, or `CPPLINT.cfg` are exempt from the ChangeLog requirement. + ## Styleguides ### Git Commit Messages diff --git a/ChangeLog b/ChangeLog index be175e07..faeccb65 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,5 @@ -Thu Jun 15 8:55:04 2023 -0800 +Version 0.19.0 - 2023-06-15 + Considering family_url as part of the priority when selecting a URL to match. More explicit selection of C++ version. Ability to handle multiple parameters with the same name on the URL. @@ -11,17 +12,11 @@ Thu Jun 15 8:55:04 2023 -0800 Code cleanups. Better use of RAII. Improved test coverage. - -Sun Mar 07 20:02:10 2021 -0800 Cleaned code to support cpplint and extra warnings. Use pointers in place of non-const references. - -Thu Feb 25 20:27:12 2021 -0800 - Simplified dependency management for libmicrohttpd - -Sat Nov 21 07:20:00 2020 -0800 + Simplified dependency management for libmicrohttpd. Added support on build for CodeQL security checks. - Moved builds to travis.com + Moved builds to travis.com. Added IWYU checks as part of build and cleaned-up accordingly. Introduced dual-stack support. Added OS specific tips, and cleaned up some compiler warnings. @@ -31,37 +26,34 @@ Sat Nov 21 07:20:00 2020 -0800 Moved windows builds to AppVeyor. Made the library compatible with libmicrohttpd v0.9.71 and above. -Sat Jun 6 10:21:05 2020 -0800 +Version 0.18.1 - 2020-06-06 + Prevent use of regex in http_endpoint outside of registration which could allow DOS attacks. -Sat May 16 07:20:00 2020 -0800 +Version 0.18.0 - 2020-05-16 + General performance improvements (reduced use of regex, lazy-building of - post-processor) - General code cleanup - General fixes to the documentation - Fixed support on FreeBSD (added missing headers) - Fixed support for Cygwin - Removed depedency on C regex - now using C++11 regex - -Sat Aug 10 18:34:07 2019 -0800 - Added support for TCP-NODELAY - Changed set_path on http_request to have lazy behavior - -Tue Aug 06 22:22:14 2019 -0800 + post-processor). + General code cleanup. + General fixes to the documentation. + Fixed support on FreeBSD (added missing headers). + Fixed support for Cygwin. + Removed depedency on C regex - now using C++11 regex. + Added support for TCP-NODELAY. + Changed set_path on http_request to have lazy behavior. Added support for body parsing in DELETE requests. - Added support for PATCH method + Added support for PATCH method. -Sat Jan 27 21:59:11 2019 -0800 - libhttpserver now includes set of examples to demonstrate the main capabilities of the library +Version 0.17.5 - 2019-01-28 + + libhttpserver now includes set of examples to demonstrate the main capabilities of the library. "examples" are now optionally disabled. - Adds valgrind memcheck to the build system on travis - Travis now tests performance with apache benchmark + Adds valgrind memcheck to the build system on travis. + Travis now tests performance with apache benchmark. Reduced the CPU time spent in normalizing URLs (thus saving ~15% on average per request). - All classes now implement move constructor and move assignment operator + All classes now implement move constructor and move assignment operator. The library now avoids collecting connection properties (headers, arguments, footers, cookies, etc...) unless explicitly asked by the client code. - -Sat Jan 12 00:51:00 2019 -0800 Removed the support for integrated COMET logic. Removed the support for caching logic. Added integ tests. @@ -69,255 +61,129 @@ Sat Jan 12 00:51:00 2019 -0800 Improved interface of the http_response object. Deprecated http_response_builder object. -Thu Dec 26 10:00:30 2018 -0800 - Fixed IPV6 parsing logic. - Added tests to support IP parsing, URL parsing and utilities - -Thu Nov 22 20:58:00 2018 -0800 - Solved problem with the server not being able to start on mac os +Version 0.16.0 - 2018-12-26 -Sun Nov 04 19:28:00 2018 -0800 - Moved http_endpoint as a sub-class of webserver. This avoids usage of friends. + Fixed IPV6 parsing logic. + Added tests to support IP parsing, URL parsing and utilities. -Wed Feb 26 21:31:00 2017 +0000 - Fixed problem with segfault when copying http_response object +Version 0.15.0 - 2018-11-23 -Wed Feb 12 13:14:01 2017 +0000 - Updated to libmicrohttpd 0.9.52 + Solved problem with the server not being able to start on mac os. -Wed Jul 13 02:23:11 2016 +0100 - Fixed problems with large payloads - Fixed memory leak in http_response_ptr +Version 0.14.0 - 2018-11-05 -Tue Dec 29 18:56:31 2015 +0100 - Removed support for event supplier (badly defined, complicated and almost useless) - Eliminated custom selection logic (simplified overall code in webserver.cpp) - Changed comet to use a lock-free implementation + Moved http_endpoint as a sub-class of webserver. This avoids usage of friends. -Sun Dec 27 19:39:01 2015 +0100 - Removed POLL start configuration (THREAD now defaults to POLL or EPOLL on Linux) - Use TCP_FASTOPEN on linux >= 3.6 +Version 0.13.0 - 2017-02-26 -Sat Dec 26 15:08:22 2015 +0100 - Changed http_resource to use classic C++ polymorphism using virtual instead of CRTP + Fixed problem with segfault when copying http_response object. -Fri Jul 17 21:38:54 2015 +0000 - Removed build dependency on pkg-config +Version 0.12.0 - 2017-02-12 -Wed Apr 15 01:40:11 2015 +0000 - Support build on MacOsX - Improved support for CI on travis - Solved bug on event_supplier registering - Solved bug on standardize_url to avoid removing root - Change cycle_callback_ptr so that buffer can be modified - Moved to version 0.9.0 + Updated to libmicrohttpd 0.9.52. -Sun Jul 23 02:46:20 2014 +0100 - Support for building on MinGW/Cygwin systems - min libmicrohttpd version moved to 0.9.37 - Moved to version 0.8.0 +Version 0.11.1 - 2016-07-13 -Sat Mar 23 15:22:40 2014 +0100 - Continue the cleanup reducing webserver.cpp responsibilities - Deep work on documentation - Moved to version 0.7.2 + Fixed problems with large payloads. + Fixed memory leak in http_response_ptr. -Sat Jan 25 16:31:03 2014 +0100 - Cleaned-up webserver.cpp code to extract secondary classes - Enforced immutability of webserver class - Enabled library to compile on g++ 4.1.2 +Version 0.11.0 - 2015-12-26 -Wed Oct 31 17:59:40 2012 +0100 - Added parameter in http_response to specify if it needs to be deleted by - WS - Sebastiano Merlino + Removed support for event supplier (badly defined, complicated and almost useless). + Eliminated custom selection logic (simplified overall code in webserver.cpp). + Changed comet to use a lock-free implementation. + Removed POLL start configuration (THREAD now defaults to POLL or EPOLL on Linux). + Use TCP_FASTOPEN on linux >= 3.6. + Changed http_resource to use classic C++ polymorphism using virtual instead of CRTP. + Removed build dependency on pkg-config. -Wed Oct 31 14:23:57 2012 +0100 - Changed dependency download method - Sebastiano Merlino +Version 0.9.0 - 2015-04-15 -Wed Oct 31 14:13:49 2012 +0100 - Added dependency to travis - Sebastiano Merlino + Support build on MacOsX. + Improved support for CI on travis. + Solved bug on event_supplier registering. + Solved bug on standardize_url to avoid removing root. + Change cycle_callback_ptr so that buffer can be modified. -Wed Oct 31 14:07:30 2012 +0100 - Changed travis build path - Sebastiano Merlino +Version 0.8.0 - 2014-07-23 -Wed Oct 31 14:02:59 2012 +0100 - Added travis conf to repo - Sebastiano Merlino + Support for building on MinGW/Cygwin systems. + min libmicrohttpd version moved to 0.9.37. -Tue Oct 30 16:13:10 2012 +0100 - Changed the buggy debian changelog - Sebastiano Merlino +Version 0.7.2 - 2014-03-23 -Tue Oct 30 16:06:26 2012 +0100 - Changed version to v0.5.4 - Sebastiano Merlino + Continue the cleanup reducing webserver.cpp responsibilities. + Deep work on documentation. -Tue Oct 30 15:59:45 2012 +0100 - Adjusted debian build rules - Sebastiano Merlino +Version 0.7.0 - 2014-01-25 -Tue Oct 30 12:52:04 2012 +0100 - Changed version to 0.5.3 - Added grow method to http_request - Sebastiano Merlino + Cleaned-up webserver.cpp code to extract secondary classes. + Enforced immutability of webserver class. + Enabled library to compile on g++ 4.1.2. -Tue Oct 23 12:46:48 2012 +0200 - Changed version from 0.5.1 to 0.5.2 - Sebastiano Merlino +Version 0.5.4 - 2012-10-30 -Tue Oct 23 12:46:07 2012 +0200 - Changed default log behaviour to print nothing - Added getters and setters for dynamic components of WS - Sebastiano Merlino + Added parameter in http_response to specify if it needs to be deleted by WS. + Changed dependency download method. + Added travis CI configuration. + Changed the buggy debian changelog. -Mon Oct 22 12:13:11 2012 +0200 - Modified version number and changelog in order to prepare tag - Sebastiano Merlino +Version 0.5.3 - 2012-10-30 -Fri Oct 19 17:11:21 2012 +0200 - Added response constructor with byte - Sebastiano Merlino + Added grow method to http_request. -Mon Oct 15 11:16:22 2012 +0200 - Removed unuseful dependency from libuuid - Sebastiano Merlino +Version 0.5.2 - 2012-10-23 -Fri Oct 12 15:42:21 2012 +0200 - Solved a bug that made impossible to parse post data - Sebastiano Merlino + Changed default log behaviour to print nothing. + Added getters and setters for dynamic components of WS. -Wed Oct 10 17:19:25 2012 +0200 - Moved to version 0.5.1 - Sebastiano Merlino +Version 0.5.1 - 2012-10-10 -Wed Oct 10 17:16:26 2012 +0200 - Added querystring to request attributes - Sebastiano Merlino + Added querystring to request attributes. + Added response constructor with byte. + Removed unuseful dependency from libuuid. + Solved a bug that made impossible to parse post data. -Fri Oct 5 18:00:38 2012 +0200 - Merge branch 'master' of https://github.com/etr/libhttpserver - Conflicts: - src/webserver.cpp - Sebastiano Merlino +Version 0.5.0 - 2012-10-05 -Fri Oct 5 17:55:42 2012 +0200 Added -D_REENTRANT to configuration. Aligned debian changelog. - Added comet capabilities to the server. - Sebastiano Merlino - -Tue Sep 25 00:50:45 2012 +0200 - Solved a bug with print in debug mode - Sebastiano Merlino - -Mon Sep 24 15:29:28 2012 +0200 - Modified webserver in order to accept comet calls - Added ignored patters in gitignore - Sebastiano Merlino - -Sun Sep 23 19:10:28 2012 +0200 - Partially solved undefined symbol in wrappers - Sebastiano Merlino - -Sun Sep 23 19:09:54 2012 +0200 - Avoided the usage of the sole option MHD_USE_POLL - Sebastiano Merlino - -Thu Sep 20 08:47:24 2012 +0200 - Added forgotten modded_request.hpp file - Sebastiano Merlino + Added comet capabilities to the server. + Solved a bug with print in debug mode. + Modified webserver in order to accept comet calls. + Partially solved undefined symbol in wrappers. + Avoided the usage of the sole option MHD_USE_POLL. + Added forgotten modded_request.hpp file. + Added .gitignore file. + Moved http_endpoint to details namespace. -Thu Sep 20 08:46:33 2012 +0200 - Added .gitignore file - Sebastiano Merlino +Version 0.4.0 - 2012-08-26 -Sat Sep 15 13:02:52 2012 +0200 - Moved http_endpoint to details namespace - Sebastiano Merlino - -Sat Sep 15 02:39:47 2012 -0700 - Merge pull request #35 from etr/cflags_for_swig_in_pcfile - add -I${includedir}/httpserver to CFLAGS - Sebastiano Merlino - -Tue Aug 28 16:33:45 2012 +0200 - add -I${includedir}/httpserver to CFLAGS - This make swig file generation easier because HTTPSERVER_CFLAGS can be - directly used in swig file generation. - This fix affect only clients that use swing on their code. - Dario Mazza - -Sun Aug 26 19:03:44 2012 +0200 - Changed version. - Aligned version and dependencies in pc and debian files - Updated debian changelog. - Sebastiano Merlino - -Sun Aug 26 18:55:05 2012 +0200 Changed visibility of http_endpoint methods to avoid them to be called - by external applications. + by external applications. Avoided explicit usage of MHD constants in classes interface. Changed http_resource interface in order to avoid copy-constructor calls - and improve performances. + and improve performances. Changed answer_to_connection method in order to avoid multiple checking - on methods and thus improve performances. - Added a way to register personalized error pages. - Sebastiano Merlino - -Wed Aug 8 17:33:39 2012 +0200 - Removed code repetition in handle_request method - Sebastiano Merlino - -Wed Aug 8 12:31:44 2012 +0200 - Added capability to compile with gcov - Changed infinite loop in ws to use wait conditions - Removed a bug from GET-like method handling - Sebastiano Merlino - -Sun Aug 5 18:26:25 2012 +0200 - Modified in order to parse qs in POST/PUT cases - Sebastiano Merlino - -Fri Aug 3 23:36:14 2012 +0200 - Avoid inclusion of internal headers - Sebastiano Merlino - -Thu Aug 2 00:43:02 2012 +0200 - Changed in order to find libmicrohttpd in system - Sebastiano Merlino - -Thu Jul 26 14:08:47 2012 +0200 - Solved some performance and style issues - Sebastiano Merlino - -Wed Jul 25 18:42:48 2012 +0200 - Merge branch 'master' of github.com:etr/libhttpserver - Sebastiano Merlino - -Wed Jul 25 18:41:45 2012 +0200 - Added some comments to http_endpoint and http_request - Sebastiano Merlino - -Wed Jul 25 08:58:04 2012 -0700 - Merge pull request #29 from etr/libtool_version_number - using m4 to define major,minor and revision number in configure.ac - Sebastiano Merlino - -Wed Jul 25 17:50:05 2012 +0200 - using m4 to define major,minor and revision number in configure.ac and send version number to libtool and AC_INIT - Dario Mazza - -Wed Jul 25 17:10:49 2012 +0200 - Changed in order to solve some problems with deb package and rpm package - Sebastiano Merlino - -Tue Jul 24 16:55:51 2012 -0700 - Merge pull request #28 from etr/debpkg_patch_deps - added parameter used to ignore dependecies during debpkg creation - Sebastiano Merlino - -Wed Jul 25 01:51:52 2012 +0200 - added parameter used to ignore dependecies during debpkg creation - Dario Mazza - -Wed Jul 25 00:42:25 2012 +0200 - Adjusted errors in debian rules - Sebastiano Merlino - -Tue Jul 24 16:37:07 2012 +0200 - Modified rpm build in order to compile it - Lowered required version of libmicrohttpd to 0.9.7 - Sebastiano Merlino - -Tue Jul 24 13:28:38 2012 +0200 - Changed also build default directory for debs - Sebastiano Merlino - -Tue Jul 24 13:22:59 2012 +0200 - Changed rules.in in order to avoid relative paths in deb compile - Sebastiano Merlino - -Mon Jul 23 15:42:33 2012 +0200 - Solved a logical error in http_resource route - Added some debug prints - Sebastiano Merlino - -Sun Jul 22 00:24:04 2012 +0200 - Changed in order to add optional optimizations on ws - Sebastiano Merlino - -Sat Jul 21 17:46:03 2012 +0200 - Changed in order to enhance deb packages generation - Added rpm packages generation - Sebastiano Merlino - -Sat Jul 21 00:43:39 2012 +0200 - adjusted error in changelog - Sebastiano Merlino - -Sat Jul 21 00:41:43 2012 +0200 - Changed in order to include debian package creation to makefile - Sebastiano Merlino - -Fri Jul 20 12:11:30 2012 -0700 - Merge pull request #26 from etr/debpackage - project debianized - Sebastiano Merlino - -Fri Jul 20 21:03:43 2012 +0200 - Merge branch 'master' of github.com:etr/libhttpserver - Sebastiano Merlino - -Fri Jul 20 21:03:24 2012 +0200 - Changed version - Sebastiano Merlino - + on methods and thus improve performances. + Added a way to register personalized error pages. + Removed code repetition in handle_request method. + Added capability to compile with gcov. + Changed infinite loop in ws to use wait conditions. + Removed a bug from GET-like method handling. + Modified in order to parse qs in POST/PUT cases. + Avoid inclusion of internal headers. + Changed in order to find libmicrohttpd in system. + Solved some performance and style issues. + Added some comments to http_endpoint and http_request. + using m4 to define major, minor and revision number in configure.ac. + Changed in order to solve some problems with deb package and rpm package. + Added parameter used to ignore dependecies during debpkg creation. + Adjusted errors in debian rules. + Modified rpm build in order to compile it. + Lowered required version of libmicrohttpd to 0.9.7. + Solved a logical error in http_resource route. + Changed in order to add optional optimizations on ws. + Changed in order to enhance deb packages generation. + Added rpm packages generation. + Changed in order to include debian package creation to makefile. diff --git a/Makefile.am b/Makefile.am index a46882fd..02121fde 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,7 +38,7 @@ endif endif -EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) +EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) scripts/extract-release-notes.sh scripts/validate-version.sh MOSTLYCLEANFILES = $(DX_CLEANFILES) *.gcda *.gcno *.gcov DISTCLEANFILES = DIST_REVISION diff --git a/scripts/extract-release-notes.sh b/scripts/extract-release-notes.sh new file mode 100755 index 00000000..ddc129d5 --- /dev/null +++ b/scripts/extract-release-notes.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Extract release notes for a given version from the ChangeLog. +# Usage: extract-release-notes.sh [VERSION] +# If VERSION is omitted, extracts the first (most recent) section. + +set -euo pipefail + +VERSION="${1:-}" + +# Strip leading 'v' if present +VERSION="${VERSION#v}" + +CHANGELOG="${CHANGELOG:-ChangeLog}" + +if [ ! -f "$CHANGELOG" ]; then + echo "Error: $CHANGELOG not found" >&2 + exit 1 +fi + +if [ -z "$VERSION" ]; then + # Extract the first version section (everything between the first and second headers) + awk ' + /^Version [0-9]+\.[0-9]+\.[0-9]+/ { + if (found) exit + found = 1 + next + } + found && /^$/ && !started { next } + found { started = 1; print } + ' "$CHANGELOG" | sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' +else + # Extract notes for the specific version + awk -v ver="$VERSION" ' + /^Version [0-9]+\.[0-9]+\.[0-9]+/ { + if (found) exit + if (index($0, "Version " ver " ") == 1 || $0 == "Version " ver) { + found = 1 + next + } + } + found && /^$/ && !started { next } + found { started = 1; print } + ' "$CHANGELOG" | sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' +fi diff --git a/scripts/validate-version.sh b/scripts/validate-version.sh new file mode 100755 index 00000000..583fdb36 --- /dev/null +++ b/scripts/validate-version.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Validate version consistency between a tag, configure.ac, and ChangeLog. +# Usage: validate-version.sh VERSION +# VERSION should NOT have a 'v' prefix (e.g., "0.19.0"). + +set -euo pipefail + +VERSION="${1:-}" + +if [ -z "$VERSION" ]; then + echo "Usage: validate-version.sh VERSION" >&2 + exit 1 +fi + +# Strip leading 'v' if present +VERSION="${VERSION#v}" + +CONFIGURE_AC="${CONFIGURE_AC:-configure.ac}" +CHANGELOG="${CHANGELOG:-ChangeLog}" + +errors=0 + +# Parse expected major.minor.revision +IFS='.' read -r expected_major expected_minor expected_revision <<< "${VERSION%%-*}" + +if [ -z "$expected_major" ] || [ -z "$expected_minor" ] || [ -z "$expected_revision" ]; then + echo "Error: VERSION must be in X.Y.Z format (got: $VERSION)" >&2 + exit 1 +fi + +# Check configure.ac +if [ ! -f "$CONFIGURE_AC" ]; then + echo "Error: $CONFIGURE_AC not found" >&2 + exit 1 +fi + +actual_major=$(grep 'm4_define(\[libhttpserver_MAJOR_VERSION\]' "$CONFIGURE_AC" | sed 's/.*\[\([0-9]*\)\].*/\1/') +actual_minor=$(grep 'm4_define(\[libhttpserver_MINOR_VERSION\]' "$CONFIGURE_AC" | sed 's/.*\[\([0-9]*\)\].*/\1/') +actual_revision=$(grep 'm4_define(\[libhttpserver_REVISION\]' "$CONFIGURE_AC" | sed 's/.*\[\([0-9]*\)\].*/\1/') + +if [ "$actual_major" != "$expected_major" ] || [ "$actual_minor" != "$expected_minor" ] || [ "$actual_revision" != "$expected_revision" ]; then + echo "Error: configure.ac version ($actual_major.$actual_minor.$actual_revision) does not match tag ($VERSION)" >&2 + errors=$((errors + 1)) +else + echo "OK: configure.ac version matches ($actual_major.$actual_minor.$actual_revision)" +fi + +# Check ChangeLog has a Version header for this version +if [ ! -f "$CHANGELOG" ]; then + echo "Error: $CHANGELOG not found" >&2 + exit 1 +fi + +# Match "Version X.Y.Z" at start of line (allowing trailing date or text) +base_version="${VERSION%%-*}" +if grep -q "^Version ${base_version}" "$CHANGELOG"; then + echo "OK: ChangeLog contains Version ${base_version} header" +else + echo "Error: ChangeLog missing 'Version ${base_version}' header" >&2 + errors=$((errors + 1)) +fi + +if [ "$errors" -gt 0 ]; then + echo "Validation failed with $errors error(s)" >&2 + exit 1 +fi + +echo "Version validation passed for $VERSION" diff --git a/src/create_test_request.cpp b/src/create_test_request.cpp new file mode 100644 index 00000000..31c7d3df --- /dev/null +++ b/src/create_test_request.cpp @@ -0,0 +1,69 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +*/ + +#include "httpserver/create_test_request.hpp" + +#include +#include + +namespace httpserver { + +http_request create_test_request::build() { + http_request req; + + req.set_method(_method); + req.set_path(_path); + req.set_version(_version); + req.set_content(_content); + + req.headers_local = std::move(_headers); + req.footers_local = std::move(_footers); + req.cookies_local = std::move(_cookies); + + for (auto& [key, values] : _args) { + for (auto& value : values) { + req.cache->unescaped_args[key].push_back(std::move(value)); + } + } + req.cache->args_populated = true; + + if (!_querystring.empty()) { + req.cache->querystring = std::move(_querystring); + } + + req.cache->username = std::move(_user); + req.cache->password = std::move(_pass); + +#ifdef HAVE_DAUTH + req.cache->digested_user = std::move(_digested_user); +#endif // HAVE_DAUTH + + req.cache->requestor_ip = std::move(_requestor); + req.requestor_port_local = _requestor_port; + +#ifdef HAVE_GNUTLS + req.tls_enabled_local = _tls_enabled; +#endif // HAVE_GNUTLS + + return req; +} + +} // namespace httpserver diff --git a/src/httpserver/create_test_request.hpp b/src/httpserver/create_test_request.hpp new file mode 100644 index 00000000..177257c2 --- /dev/null +++ b/src/httpserver/create_test_request.hpp @@ -0,0 +1,145 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#if !defined (_HTTPSERVER_HPP_INSIDE_) && !defined (HTTPSERVER_COMPILATION) +#error "Only or can be included directly." +#endif + +#ifndef SRC_HTTPSERVER_CREATE_TEST_REQUEST_HPP_ +#define SRC_HTTPSERVER_CREATE_TEST_REQUEST_HPP_ + +#include +#include +#include + +#include "httpserver/http_request.hpp" +#include "httpserver/http_utils.hpp" + +namespace httpserver { + +class create_test_request { + public: + create_test_request() = default; + + create_test_request& method(const std::string& method) { + _method = method; + return *this; + } + + create_test_request& path(const std::string& path) { + _path = path; + return *this; + } + + create_test_request& version(const std::string& version) { + _version = version; + return *this; + } + + create_test_request& content(const std::string& content) { + _content = content; + return *this; + } + + create_test_request& header(const std::string& key, const std::string& value) { + _headers[key] = value; + return *this; + } + + create_test_request& footer(const std::string& key, const std::string& value) { + _footers[key] = value; + return *this; + } + + create_test_request& cookie(const std::string& key, const std::string& value) { + _cookies[key] = value; + return *this; + } + + create_test_request& arg(const std::string& key, const std::string& value) { + _args[key].push_back(value); + return *this; + } + + create_test_request& querystring(const std::string& querystring) { + _querystring = querystring; + return *this; + } + + create_test_request& user(const std::string& user) { + _user = user; + return *this; + } + + create_test_request& pass(const std::string& pass) { + _pass = pass; + return *this; + } + +#ifdef HAVE_DAUTH + create_test_request& digested_user(const std::string& digested_user) { + _digested_user = digested_user; + return *this; + } +#endif // HAVE_DAUTH + + create_test_request& requestor(const std::string& requestor) { + _requestor = requestor; + return *this; + } + + create_test_request& requestor_port(uint16_t port) { + _requestor_port = port; + return *this; + } + +#ifdef HAVE_GNUTLS + create_test_request& tls_enabled(bool enabled = true) { + _tls_enabled = enabled; + return *this; + } +#endif // HAVE_GNUTLS + + http_request build(); + + private: + std::string _method = "GET"; + std::string _path = "/"; + std::string _version = "HTTP/1.1"; + std::string _content; + http::header_map _headers; + http::header_map _footers; + http::header_map _cookies; + std::map, http::arg_comparator> _args; + std::string _querystring; + std::string _user; + std::string _pass; +#ifdef HAVE_DAUTH + std::string _digested_user; +#endif // HAVE_DAUTH + std::string _requestor = "127.0.0.1"; + uint16_t _requestor_port = 0; +#ifdef HAVE_GNUTLS + bool _tls_enabled = false; +#endif // HAVE_GNUTLS +}; + +} // namespace httpserver +#endif // SRC_HTTPSERVER_CREATE_TEST_REQUEST_HPP_ diff --git a/test/unit/create_test_request_test.cpp b/test/unit/create_test_request_test.cpp new file mode 100644 index 00000000..8b3843a8 --- /dev/null +++ b/test/unit/create_test_request_test.cpp @@ -0,0 +1,283 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include + +#include "./httpserver.hpp" +#include "./littletest.hpp" + +using httpserver::create_test_request; +using httpserver::http_request; +using httpserver::http_resource; +using httpserver::http_response; +using httpserver::string_response; +using httpserver::file_response; + +LT_BEGIN_SUITE(create_test_request_suite) + void set_up() { + } + + void tear_down() { + } +LT_END_SUITE(create_test_request_suite) + +// Test default values +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_default) + auto req = create_test_request().build(); + LT_CHECK_EQ(std::string(req.get_method()), std::string("GET")); + LT_CHECK_EQ(std::string(req.get_path()), std::string("/")); + LT_CHECK_EQ(std::string(req.get_version()), std::string("HTTP/1.1")); + LT_CHECK_EQ(std::string(req.get_content()), std::string("")); +LT_END_AUTO_TEST(build_default) + +// Test custom method and path +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_method_path) + auto req = create_test_request() + .method("POST") + .path("/api/users") + .build(); + LT_CHECK_EQ(std::string(req.get_method()), std::string("POST")); + LT_CHECK_EQ(std::string(req.get_path()), std::string("/api/users")); +LT_END_AUTO_TEST(build_method_path) + +// Test headers +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_headers) + auto req = create_test_request() + .header("Content-Type", "application/json") + .header("Accept", "text/plain") + .build(); + LT_CHECK_EQ(std::string(req.get_header("Content-Type")), std::string("application/json")); + LT_CHECK_EQ(std::string(req.get_header("Accept")), std::string("text/plain")); + LT_CHECK_EQ(std::string(req.get_header("NonExistent")), std::string("")); + + auto headers = req.get_headers(); + LT_CHECK_EQ(headers.size(), static_cast(2)); +LT_END_AUTO_TEST(build_headers) + +// Test footers and cookies +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_footers_cookies) + auto req = create_test_request() + .footer("X-Checksum", "abc123") + .cookie("session_id", "xyz789") + .build(); + LT_CHECK_EQ(std::string(req.get_footer("X-Checksum")), std::string("abc123")); + LT_CHECK_EQ(std::string(req.get_cookie("session_id")), std::string("xyz789")); + + auto footers = req.get_footers(); + LT_CHECK_EQ(footers.size(), static_cast(1)); + + auto cookies = req.get_cookies(); + LT_CHECK_EQ(cookies.size(), static_cast(1)); +LT_END_AUTO_TEST(build_footers_cookies) + +// Test args +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_args) + auto req = create_test_request() + .arg("name", "World") + .arg("lang", "en") + .build(); + LT_CHECK_EQ(std::string(req.get_arg_flat("name")), std::string("World")); + LT_CHECK_EQ(std::string(req.get_arg_flat("lang")), std::string("en")); +LT_END_AUTO_TEST(build_args) + +// Test multiple values per arg key +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_multi_args) + auto req = create_test_request() + .arg("color", "red") + .arg("color", "blue") + .build(); + auto arg = req.get_arg("color"); + LT_CHECK_EQ(arg.values.size(), static_cast(2)); + LT_CHECK_EQ(std::string(arg.values[0]), std::string("red")); + LT_CHECK_EQ(std::string(arg.values[1]), std::string("blue")); +LT_END_AUTO_TEST(build_multi_args) + +// Test querystring +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_querystring) + auto req = create_test_request() + .querystring("?foo=bar&baz=qux") + .build(); + LT_CHECK_EQ(std::string(req.get_querystring()), std::string("?foo=bar&baz=qux")); +LT_END_AUTO_TEST(build_querystring) + +// Test content +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_content) + auto req = create_test_request() + .content("{\"key\":\"value\"}") + .build(); + LT_CHECK_EQ(std::string(req.get_content()), std::string("{\"key\":\"value\"}")); +LT_END_AUTO_TEST(build_content) + +// Test basic auth +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_basic_auth) + auto req = create_test_request() + .user("admin") + .pass("secret") + .build(); + LT_CHECK_EQ(std::string(req.get_user()), std::string("admin")); + LT_CHECK_EQ(std::string(req.get_pass()), std::string("secret")); +LT_END_AUTO_TEST(build_basic_auth) + +// Test requestor +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_requestor) + auto req = create_test_request() + .requestor("192.168.1.1") + .requestor_port(8080) + .build(); + LT_CHECK_EQ(std::string(req.get_requestor()), std::string("192.168.1.1")); + LT_CHECK_EQ(req.get_requestor_port(), static_cast(8080)); +LT_END_AUTO_TEST(build_requestor) + +// Test default requestor +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_default_requestor) + auto req = create_test_request().build(); + LT_CHECK_EQ(std::string(req.get_requestor()), std::string("127.0.0.1")); + LT_CHECK_EQ(req.get_requestor_port(), static_cast(0)); +LT_END_AUTO_TEST(build_default_requestor) + +#ifdef HAVE_GNUTLS +// Test TLS enabled flag +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_tls_enabled) + auto req = create_test_request() + .tls_enabled() + .build(); + LT_CHECK_EQ(req.has_tls_session(), true); +LT_END_AUTO_TEST(build_tls_enabled) + +// Test TLS not enabled by default +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_no_tls) + auto req = create_test_request().build(); + LT_CHECK_EQ(req.has_tls_session(), false); +LT_END_AUTO_TEST(build_no_tls) +#endif // HAVE_GNUTLS + +// Test that all getters on a minimal request return empty without crashing +LT_BEGIN_AUTO_TEST(create_test_request_suite, empty_getters_no_crash) + auto req = create_test_request().build(); + // These should all return empty/default without crashing + LT_CHECK_EQ(std::string(req.get_header("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_footer("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_cookie("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_arg_flat("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_querystring()), std::string("")); + LT_CHECK_EQ(std::string(req.get_content()), std::string("")); + LT_CHECK_EQ(req.get_headers().size(), static_cast(0)); + LT_CHECK_EQ(req.get_footers().size(), static_cast(0)); + LT_CHECK_EQ(req.get_cookies().size(), static_cast(0)); + LT_CHECK_EQ(req.get_args().size(), static_cast(0)); + LT_CHECK_EQ(req.get_args_flat().size(), static_cast(0)); + LT_CHECK_EQ(req.get_path_pieces().size(), static_cast(0)); +LT_END_AUTO_TEST(empty_getters_no_crash) + +// End-to-end: build request, call render, inspect response +class greeting_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request& req) override { + std::string name(req.get_arg_flat("name")); + if (name.empty()) name = "World"; + return std::make_shared("Hello, " + name); + } +}; + +LT_BEGIN_AUTO_TEST(create_test_request_suite, render_with_test_request) + greeting_resource resource; + auto req = create_test_request() + .path("/greet") + .arg("name", "Alice") + .build(); + auto resp = resource.render_GET(req); + auto* sr = dynamic_cast(resp.get()); + LT_ASSERT(sr != nullptr); + LT_CHECK_EQ(std::string(sr->get_content()), std::string("Hello, Alice")); +LT_END_AUTO_TEST(render_with_test_request) + +// Test string_response get_content +LT_BEGIN_AUTO_TEST(create_test_request_suite, string_response_get_content) + string_response resp("test body", 200); + LT_CHECK_EQ(std::string(resp.get_content()), std::string("test body")); +LT_END_AUTO_TEST(string_response_get_content) + +// Test file_response get_filename +LT_BEGIN_AUTO_TEST(create_test_request_suite, file_response_get_filename) + file_response resp("/tmp/test.txt", 200); + LT_CHECK_EQ(std::string(resp.get_filename()), std::string("/tmp/test.txt")); +LT_END_AUTO_TEST(file_response_get_filename) + +// Test full chain of all builder methods +LT_BEGIN_AUTO_TEST(create_test_request_suite, full_chain) + auto req = create_test_request() + .method("PUT") + .path("/api/resource/42") + .version("HTTP/1.0") + .content("request body") + .header("Content-Type", "text/plain") + .header("Authorization", "Bearer token123") + .footer("Trailer", "checksum") + .cookie("session", "abc") + .arg("key1", "val1") + .arg("key2", "val2") + .querystring("?key1=val1&key2=val2") + .user("testuser") + .pass("testpass") + .requestor("10.0.0.1") + .requestor_port(9090) + .build(); + + LT_CHECK_EQ(std::string(req.get_method()), std::string("PUT")); + LT_CHECK_EQ(std::string(req.get_path()), std::string("/api/resource/42")); + LT_CHECK_EQ(std::string(req.get_version()), std::string("HTTP/1.0")); + LT_CHECK_EQ(std::string(req.get_content()), std::string("request body")); + LT_CHECK_EQ(std::string(req.get_header("Content-Type")), std::string("text/plain")); + LT_CHECK_EQ(std::string(req.get_header("Authorization")), std::string("Bearer token123")); + LT_CHECK_EQ(std::string(req.get_footer("Trailer")), std::string("checksum")); + LT_CHECK_EQ(std::string(req.get_cookie("session")), std::string("abc")); + LT_CHECK_EQ(std::string(req.get_arg_flat("key1")), std::string("val1")); + LT_CHECK_EQ(std::string(req.get_arg_flat("key2")), std::string("val2")); + LT_CHECK_EQ(std::string(req.get_querystring()), std::string("?key1=val1&key2=val2")); + LT_CHECK_EQ(std::string(req.get_user()), std::string("testuser")); + LT_CHECK_EQ(std::string(req.get_pass()), std::string("testpass")); + LT_CHECK_EQ(std::string(req.get_requestor()), std::string("10.0.0.1")); + LT_CHECK_EQ(req.get_requestor_port(), static_cast(9090)); +LT_END_AUTO_TEST(full_chain) + +// Test path pieces work with test request +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_path_pieces) + auto req = create_test_request() + .path("/api/users/42") + .build(); + auto pieces = req.get_path_pieces(); + LT_CHECK_EQ(pieces.size(), static_cast(3)); + LT_CHECK_EQ(pieces[0], std::string("api")); + LT_CHECK_EQ(pieces[1], std::string("users")); + LT_CHECK_EQ(pieces[2], std::string("42")); +LT_END_AUTO_TEST(build_path_pieces) + +// Test method is uppercased +LT_BEGIN_AUTO_TEST(create_test_request_suite, method_uppercase) + auto req = create_test_request() + .method("post") + .build(); + LT_CHECK_EQ(std::string(req.get_method()), std::string("POST")); +LT_END_AUTO_TEST(method_uppercase) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV() From ca473210fda9407228eb495d7f11718135d200b6 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 16:01:36 -0800 Subject: [PATCH 12/19] Fix 9 security vulnerabilities found in audit - Fix path traversal in file uploads: sanitize filenames to basename, reject ".." and "." when generate_random_filename_on_upload is off - Fix TOCTOU race in file_response: replace stat-then-open with open-then-fstat; add O_NOFOLLOW on non-Windows - Fix FD leaks in file_response: close fd on lseek failure and zero-size file path - Fix NULL deref in answer_to_connection: check conninfo before use - Fix uninitialized _file_size in file_info (default to 0) - Fix auth skip path bypass: normalize path (resolve ".." segments) before comparing against skip paths - Fix free() vs MHD_free() for digest auth username - Fix unchecked write error during file upload --- src/file_response.cpp | 23 +++++---- src/http_request.cpp | 2 +- src/http_utils.cpp | 15 ++++++ src/httpserver/file_info.hpp | 2 +- src/httpserver/http_utils.hpp | 2 + src/webserver.cpp | 42 ++++++++++++++-- test/integ/authentication.cpp | 33 +++++++++++++ test/integ/file_upload.cpp | 90 +++++++++++++++++++++++++++++++++++ 8 files changed, 194 insertions(+), 15 deletions(-) diff --git a/src/file_response.cpp b/src/file_response.cpp index 669f5e8b..3e915413 100644 --- a/src/file_response.cpp +++ b/src/file_response.cpp @@ -32,24 +32,29 @@ struct MHD_Response; namespace httpserver { MHD_Response* file_response::get_raw_response() { - struct stat sb; +#ifndef _WIN32 + int fd = open(filename.c_str(), O_RDONLY | O_NOFOLLOW); +#else + int fd = open(filename.c_str(), O_RDONLY); +#endif + if (fd == -1) return nullptr; - // Deny everything but regular files - if (stat(filename.c_str(), &sb) == 0) { - if (!S_ISREG(sb.st_mode)) return nullptr; - } else { + struct stat sb; + if (fstat(fd, &sb) != 0 || !S_ISREG(sb.st_mode)) { + close(fd); return nullptr; } - int fd = open(filename.c_str(), O_RDONLY); - if (fd == -1) return nullptr; - off_t size = lseek(fd, 0, SEEK_END); - if (size == (off_t) -1) return nullptr; + if (size == (off_t) -1) { + close(fd); + return nullptr; + } if (size) { return MHD_create_response_from_fd(size, fd); } else { + close(fd); return MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_PERSISTENT); } } diff --git a/src/http_request.cpp b/src/http_request.cpp index d58842b3..1de47519 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -351,7 +351,7 @@ std::string_view http_request::get_digested_user() const { cache->digested_user = EMPTY; if (digested_user_c != nullptr) { cache->digested_user = digested_user_c; - free(digested_user_c); + MHD_free(digested_user_c); } return cache->digested_user; diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 43b522bb..695292a3 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -271,6 +271,21 @@ const std::string http_utils::generate_random_upload_filename(const std::string& return ret_filename; } +std::string http_utils::sanitize_upload_filename(const std::string& filename) { + if (filename.empty()) return ""; + + // Find the basename: take everything after the last '/' or '\' + std::string::size_type pos = filename.find_last_of("/\\"); + std::string basename = (pos != std::string::npos) ? filename.substr(pos + 1) : filename; + + // Reject empty basename, ".", and ".." + if (basename.empty() || basename == "." || basename == "..") { + return ""; + } + + return basename; +} + std::string get_ip_str(const struct sockaddr *sa) { if (!sa) throw std::invalid_argument("socket pointer is null"); diff --git a/src/httpserver/file_info.hpp b/src/httpserver/file_info.hpp index f78c55fa..8fd4f9e9 100644 --- a/src/httpserver/file_info.hpp +++ b/src/httpserver/file_info.hpp @@ -42,7 +42,7 @@ class file_info { file_info() = default; private: - size_t _file_size; + size_t _file_size = 0; std::string _file_system_file_name; std::string _content_type; std::string _transfer_encoding; diff --git a/src/httpserver/http_utils.hpp b/src/httpserver/http_utils.hpp index 35a5a45a..8e44c15b 100644 --- a/src/httpserver/http_utils.hpp +++ b/src/httpserver/http_utils.hpp @@ -272,6 +272,8 @@ class http_utils { static std::string standardize_url(const std::string&); static const std::string generate_random_upload_filename(const std::string& directory); + + static std::string sanitize_upload_filename(const std::string& filename); }; #define COMPARATOR(x, y, op) { \ diff --git a/src/webserver.cpp b/src/webserver.cpp index 20690cab..92d754a2 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -699,7 +699,11 @@ MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, if (mr->ws->generate_random_filename_on_upload) { file.set_file_system_file_name(http_utils::generate_random_upload_filename(mr->ws->file_upload_dir)); } else { - file.set_file_system_file_name(mr->ws->file_upload_dir + "/" + std::string(filename)); + std::string safe_name = http_utils::sanitize_upload_filename(filename); + if (safe_name.empty()) { + return MHD_NO; + } + file.set_file_system_file_name(mr->ws->file_upload_dir + "/" + safe_name); } // to not append to an already existing file, delete an already existing file unlink(file.get_file_system_file_name().c_str()); @@ -731,6 +735,9 @@ MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, if (size > 0) { mr->upload_ostrm->write(data, size); + if (!mr->upload_ostrm->good()) { + return MHD_NO; + } } // update the file size in the map @@ -774,13 +781,40 @@ std::shared_ptr webserver::internal_error_page(details::modded_re } bool webserver::should_skip_auth(const std::string& path) const { + // Normalize path: resolve ".." and "." segments to prevent bypass + std::string normalized; + { + std::vector segments; + std::string::size_type start = 0; + // Skip leading slash + if (!path.empty() && path[0] == '/') { + start = 1; + } + while (start < path.size()) { + auto end = path.find('/', start); + if (end == std::string::npos) end = path.size(); + std::string seg = path.substr(start, end - start); + if (seg == "..") { + if (!segments.empty()) segments.pop_back(); + } else if (!seg.empty() && seg != ".") { + segments.push_back(seg); + } + start = end + 1; + } + normalized = "/"; + for (size_t i = 0; i < segments.size(); i++) { + if (i > 0) normalized += "/"; + normalized += segments[i]; + } + } + for (const auto& skip_path : auth_skip_paths) { - if (skip_path == path) return true; + if (skip_path == normalized) return true; // Support wildcard suffix (e.g., "/public/*") if (skip_path.size() > 2 && skip_path.back() == '*' && skip_path[skip_path.size() - 2] == '/') { std::string prefix = skip_path.substr(0, skip_path.size() - 1); - if (path.compare(0, prefix.size(), prefix) == 0) return true; + if (normalized.compare(0, prefix.size(), prefix) == 0) return true; } } return false; @@ -1028,7 +1062,7 @@ MHD_Result webserver::answer_to_connection(void* cls, MHD_Connection* connection const MHD_ConnectionInfo * conninfo = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CONNECTION_FD); - if (static_cast(cls)->tcp_nodelay) { + if (conninfo != nullptr && static_cast(cls)->tcp_nodelay) { int yes = 1; setsockopt(conninfo->connect_fd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&yes), sizeof(int)); } diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index d280cbd6..57440b2e 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -1090,6 +1090,39 @@ LT_BEGIN_AUTO_TEST(authentication_suite, auth_empty_skip_paths) ws.stop(); LT_END_AUTO_TEST(auth_empty_skip_paths) +// Test that path traversal cannot bypass auth skip paths +// Requesting /public/../protected should NOT skip auth +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_path_traversal_bypass) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("public/info", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // /public/../protected should normalize to /protected, which requires auth + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/public/../protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); // Should require auth, not be skipped + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_path_traversal_bypass) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/integ/file_upload.cpp b/test/integ/file_upload.cpp index 97cae716..5361c1d0 100644 --- a/test/integ/file_upload.cpp +++ b/test/integ/file_upload.cpp @@ -964,6 +964,96 @@ LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_with_content_type) ws->stop(); LT_END_AUTO_TEST(file_upload_with_content_type) +// Send file with a crafted filename for path traversal testing +static std::pair send_file_with_traversal_name(int port, const char* crafted_filename) { + curl_global_init(CURL_GLOBAL_ALL); + + CURL *curl = curl_easy_init(); + + curl_mime *form = curl_mime_init(curl); + curl_mimepart *field = curl_mime_addpart(form); + curl_mime_name(field, TEST_KEY); + // Use the real file for data, but override the filename + curl_mime_filedata(field, TEST_CONTENT_FILEPATH); + curl_mime_filename(field, crafted_filename); + + CURLcode res; + std::string url = "localhost:" + std::to_string(port) + "/upload"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, form); + + res = curl_easy_perform(curl); + long http_code = 0; // NOLINT [runtime/int] + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + curl_mime_free(form); + return {res, http_code}; +} + +// Test that path traversal filenames are rejected +LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_path_traversal_rejected) + string upload_directory = "upload_test_dir"; + MKDIR(upload_directory.c_str()); + + int port = PORT + 2; + auto ws = std::make_unique(create_webserver(port) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory)); + // NOT using generate_random_filename_on_upload - this is the vulnerable path + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + // Attempt path traversal with "../escape" + send_file_with_traversal_name(port, "../escape"); + // The server should reject the upload (MHD_NO causes connection close) + // The key check is that no file was created outside the upload dir + LT_CHECK_EQ(file_exists("escape"), false); + LT_CHECK_EQ(file_exists("./escape"), false); + + ws->stop(); + + // Clean up + rmdir(upload_directory.c_str()); +LT_END_AUTO_TEST(file_upload_path_traversal_rejected) + +// Test that sanitize keeps the basename for normal filenames +LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_sanitize_keeps_basename) + string upload_directory = "upload_test_dir"; + MKDIR(upload_directory.c_str()); + + int port = PORT + 3; + auto ws = std::make_unique(create_webserver(port) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory)); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + // Upload with a path-like filename — should strip to just "myfile.txt" + auto res = send_file_with_traversal_name(port, "some/path/myfile.txt"); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + // The file should be created with only the basename + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + auto file = files.begin()->second.begin(); + string expected_path = upload_directory + "/myfile.txt"; + LT_CHECK_EQ(file->second.get_file_system_file_name(), expected_path); + + ws->stop(); + + // Clean up + unlink(expected_path.c_str()); + rmdir(upload_directory.c_str()); +LT_END_AUTO_TEST(file_upload_sanitize_keeps_basename) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From 28fa48f68778717abc48da09c1fe259180025139 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 17:26:37 -0800 Subject: [PATCH 13/19] Add ChangeLog entries for security fixes --- ChangeLog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ChangeLog b/ChangeLog index faeccb65..6760a4ff 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,17 @@ Version 0.19.0 - 2023-06-15 + Fixed path traversal vulnerability in file uploads when + generate_random_filename_on_upload is disabled. + Fixed TOCTOU race in file_response by replacing stat-then-open with + open-then-fstat; added O_NOFOLLOW on non-Windows. + Fixed file descriptor leaks in file_response on lseek failure and + zero-size file paths. + Fixed NULL pointer dereference when MHD_get_connection_info returns + nullptr for TCP_NODELAY. + Fixed uninitialized _file_size in file_info. + Fixed auth skip path bypass via path traversal (e.g. /public/../protected). + Fixed use of free() instead of MHD_free() for digest auth username. + Fixed unchecked write error during file upload. Considering family_url as part of the priority when selecting a URL to match. More explicit selection of C++ version. Ability to handle multiple parameters with the same name on the URL. From 0908e485f007eb12cae3fe8c9b7a361dd4547131 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 11:21:49 -0800 Subject: [PATCH 14/19] FEATURE-bauth-conditional-compile: Implementation complete Add HAVE_BAUTH conditional compilation for basic authentication, mirroring the existing HAVE_DAUTH pattern. This allows libhttpserver to build against libmicrohttpd installations that lack basic auth support. Changes: - configure.ac: Auto-detect MHD_queue_basic_auth_fail_response, define HAVE_BAUTH flag and AM_CONDITIONAL, add to summary output - src/httpserver/basic_auth_fail_response.hpp: Guard with #ifdef HAVE_BAUTH - src/basic_auth_fail_response.cpp: Guard with #ifdef HAVE_BAUTH - src/httpserver.hpp: Conditionally include basic_auth_fail_response.hpp - src/httpserver/http_request.hpp: Guard get_user(), get_pass(), fetch_user_pass() declarations and username/password cache fields - src/http_request.cpp: Guard fetch_user_pass(), get_user(), get_pass() implementations and basic auth output in operator<< - src/httpserver/create_webserver.hpp: Guard basic_auth()/no_basic_auth() methods and _basic_auth_enabled member - src/httpserver/webserver.hpp: Guard basic_auth_enabled member - src/webserver.cpp: Guard basic_auth_enabled initialization - src/Makefile.am: Make basic_auth_fail_response conditional on HAVE_BAUTH - examples/Makefile.am: Guard basic_authentication and centralized_authentication examples behind HAVE_BAUTH - test/integ/authentication.cpp: Guard basic auth tests with HAVE_BAUTH - test/unit/create_webserver_test.cpp: Guard basic_auth builder test --- configure.ac | 13 +++++++++++++ examples/Makefile.am | 9 +++++++-- src/Makefile.am | 9 +++++++-- src/basic_auth_fail_response.cpp | 4 ++++ src/http_request.cpp | 9 +++++++-- src/httpserver.hpp | 2 ++ src/httpserver/basic_auth_fail_response.hpp | 5 +++++ src/httpserver/create_webserver.hpp | 4 ++++ src/httpserver/http_request.hpp | 8 ++++++++ src/httpserver/webserver.hpp | 2 ++ src/webserver.cpp | 2 ++ test/integ/authentication.cpp | 8 ++++++++ test/unit/create_webserver_test.cpp | 2 ++ 13 files changed, 71 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 5754ae7c..958d045e 100644 --- a/configure.ac +++ b/configure.ac @@ -149,6 +149,11 @@ fi AM_CONDITIONAL([COND_CROSS_COMPILE],[test x"$cond_cross_compile" = x"yes"]) AC_SUBST(COND_CROSS_COMPILE) +# Check for basic auth support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_queue_basic_auth_fail_response], + [have_bauth="yes"], + [have_bauth="no"; AC_MSG_WARN("libmicrohttpd basic auth support not found. Basic auth will be disabled")]) + # Check for digest auth support in libmicrohttpd AC_CHECK_LIB([microhttpd], [MHD_queue_auth_fail_response], [have_dauth="yes"], @@ -264,6 +269,13 @@ fi AM_CONDITIONAL([HAVE_GNUTLS],[test x"$have_gnutls" = x"yes"]) +if test x"$have_bauth" = x"yes"; then + AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_BAUTH" + AM_CFLAGS="$AM_CXXFLAGS -DHAVE_BAUTH" +fi + +AM_CONDITIONAL([HAVE_BAUTH],[test x"$have_bauth" = x"yes"]) + if test x"$have_dauth" = x"yes"; then AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_DAUTH" AM_CFLAGS="$AM_CXXFLAGS -DHAVE_DAUTH" @@ -327,6 +339,7 @@ AC_MSG_NOTICE([Configuration Summary: License : LGPL only Debug : ${debugit} TLS Enabled : ${have_gnutls} + Basic Auth : ${have_bauth} Digest Auth : ${have_dauth} TCP_FASTOPEN : ${is_fastopen_supported} Static : ${static} diff --git a/examples/Makefile.am b/examples/Makefile.am index 37930db0..c04e0acf 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log basic_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -31,7 +31,6 @@ hello_with_get_arg_SOURCES = hello_with_get_arg.cpp args_processing_SOURCES = args_processing.cpp setting_headers_SOURCES = setting_headers.cpp custom_access_log_SOURCES = custom_access_log.cpp -basic_authentication_SOURCES = basic_authentication.cpp minimal_https_SOURCES = minimal_https.cpp minimal_file_response_SOURCES = minimal_file_response.cpp minimal_deferred_SOURCES = minimal_deferred.cpp @@ -44,6 +43,12 @@ benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp file_upload_with_callback_SOURCES = file_upload_with_callback.cpp +if HAVE_BAUTH +noinst_PROGRAMS += basic_authentication centralized_authentication +basic_authentication_SOURCES = basic_authentication.cpp +centralized_authentication_SOURCES = centralized_authentication.cpp +endif + if HAVE_GNUTLS LDADD += -lgnutls noinst_PROGRAMS += minimal_https_psk diff --git a/src/Makefile.am b/src/Makefile.am index 63a16a50..ed8dc8f4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,9 +19,14 @@ AM_CPPFLAGS = -I../ -I$(srcdir)/httpserver/ METASOURCES = AUTO lib_LTLIBRARIES = libhttpserver.la -libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp basic_auth_fail_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp +libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp gettext.h -nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/basic_auth_fail_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/http_arg_value.hpp +nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/http_arg_value.hpp + +if HAVE_BAUTH +libhttpserver_la_SOURCES += basic_auth_fail_response.cpp +nobase_include_HEADERS += httpserver/basic_auth_fail_response.hpp +endif AM_CXXFLAGS += -fPIC -Wall diff --git a/src/basic_auth_fail_response.cpp b/src/basic_auth_fail_response.cpp index 0e00cdc1..1e6aa0e5 100644 --- a/src/basic_auth_fail_response.cpp +++ b/src/basic_auth_fail_response.cpp @@ -18,6 +18,8 @@ USA */ +#ifdef HAVE_BAUTH + #include "httpserver/basic_auth_fail_response.hpp" #include #include @@ -32,3 +34,5 @@ int basic_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_R } } // namespace httpserver + +#endif // HAVE_BAUTH diff --git a/src/http_request.cpp b/src/http_request.cpp index 1de47519..4d67bf39 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -310,6 +310,7 @@ MHD_Result http_request::build_request_querystring(void *cls, enum MHD_ValueKind return MHD_YES; } +#ifdef HAVE_BAUTH void http_request::fetch_user_pass() const { char* password = nullptr; auto* username = MHD_basic_auth_get_username_password(underlying_connection, &password); @@ -339,6 +340,7 @@ std::string_view http_request::get_pass() const { fetch_user_pass(); return cache->password; } +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH std::string_view http_request::get_digested_user() const { @@ -557,8 +559,11 @@ uint16_t http_request::get_requestor_port() const { } std::ostream &operator<< (std::ostream &os, const http_request &r) { - os << r.get_method() << " Request [user:\"" << r.get_user() << "\" pass:\"" << r.get_pass() << "\"] path:\"" - << r.get_path() << "\"" << std::endl; + os << r.get_method() << " Request ["; +#ifdef HAVE_BAUTH + os << "user:\"" << r.get_user() << "\" pass:\"" << r.get_pass() << "\""; +#endif // HAVE_BAUTH + os << "] path:\"" << r.get_path() << "\"" << std::endl; http::dump_header_map(os, "Headers", r.get_headers()); http::dump_header_map(os, "Footers", r.get_footers()); diff --git a/src/httpserver.hpp b/src/httpserver.hpp index 4f19de48..b2bba186 100644 --- a/src/httpserver.hpp +++ b/src/httpserver.hpp @@ -27,7 +27,9 @@ #define _HTTPSERVER_HPP_INSIDE_ +#ifdef HAVE_BAUTH #include "httpserver/basic_auth_fail_response.hpp" +#endif // HAVE_BAUTH #include "httpserver/deferred_response.hpp" #ifdef HAVE_DAUTH #include "httpserver/digest_auth_fail_response.hpp" diff --git a/src/httpserver/basic_auth_fail_response.hpp b/src/httpserver/basic_auth_fail_response.hpp index 87a124f5..d88bbbff 100644 --- a/src/httpserver/basic_auth_fail_response.hpp +++ b/src/httpserver/basic_auth_fail_response.hpp @@ -25,6 +25,8 @@ #ifndef SRC_HTTPSERVER_BASIC_AUTH_FAIL_RESPONSE_HPP_ #define SRC_HTTPSERVER_BASIC_AUTH_FAIL_RESPONSE_HPP_ +#ifdef HAVE_BAUTH + #include #include "httpserver/http_utils.hpp" #include "httpserver/string_response.hpp" @@ -60,4 +62,7 @@ class basic_auth_fail_response : public string_response { }; } // namespace httpserver + +#endif // HAVE_BAUTH + #endif // SRC_HTTPSERVER_BASIC_AUTH_FAIL_RESPONSE_HPP_ diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index 7ade5e17..991b8501 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -261,6 +261,7 @@ class create_webserver { return *this; } +#ifdef HAVE_BAUTH create_webserver& basic_auth() { _basic_auth_enabled = true; return *this; @@ -270,6 +271,7 @@ class create_webserver { _basic_auth_enabled = false; return *this; } +#endif // HAVE_BAUTH create_webserver& digest_auth() { _digest_auth_enabled = true; @@ -438,7 +440,9 @@ class create_webserver { std::string _digest_auth_random = ""; int _nonce_nc_size = 0; http::http_utils::policy_T _default_policy = http::http_utils::ACCEPT; +#ifdef HAVE_BAUTH bool _basic_auth_enabled = true; +#endif // HAVE_BAUTH bool _digest_auth_enabled = true; bool _regex_checking = true; bool _ban_system_enabled = true; diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index b6a015c8..2b621b11 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -60,11 +60,13 @@ class http_request { public: static const char EMPTY[]; +#ifdef HAVE_BAUTH /** * Method used to get the username eventually passed through basic authentication. * @return string representation of the username. **/ std::string_view get_user() const; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH /** @@ -74,11 +76,13 @@ class http_request { std::string_view get_digested_user() const; #endif // HAVE_DAUTH +#ifdef HAVE_BAUTH /** * Method used to get the password eventually passed through basic authentication. * @return string representation of the password. **/ std::string_view get_pass() const; +#endif // HAVE_BAUTH /** * Method used to get the path requested @@ -380,7 +384,9 @@ class http_request { static MHD_Result build_request_querystring(void *cls, enum MHD_ValueKind kind, const char *key, const char *value); +#ifdef HAVE_BAUTH void fetch_user_pass() const; +#endif // HAVE_BAUTH /** * Method used to set an argument value by key. @@ -485,8 +491,10 @@ class http_request { // Others (username, password, digested_user) MHD returns as char* that we need // to make a copy of and free anyway. struct http_request_data_cache { +#ifdef HAVE_BAUTH std::string username; std::string password; +#endif // HAVE_BAUTH std::string querystring; std::string requestor_ip; #ifdef HAVE_DAUTH diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index ea4a404d..66d81ddd 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -168,7 +168,9 @@ class webserver { const int nonce_nc_size; bool running; const http::http_utils::policy_T default_policy; +#ifdef HAVE_BAUTH const bool basic_auth_enabled; +#endif // HAVE_BAUTH const bool digest_auth_enabled; const bool regex_checking; const bool ban_system_enabled; diff --git a/src/webserver.cpp b/src/webserver.cpp index 92d754a2..971d3d5a 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -158,7 +158,9 @@ webserver::webserver(const create_webserver& params): nonce_nc_size(params._nonce_nc_size), running(false), default_policy(params._default_policy), +#ifdef HAVE_BAUTH basic_auth_enabled(params._basic_auth_enabled), +#endif // HAVE_BAUTH digest_auth_enabled(params._digest_auth_enabled), regex_checking(params._regex_checking), ban_system_enabled(params._ban_system_enabled), diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index 57440b2e..b043f566 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -43,7 +43,9 @@ using std::shared_ptr; using httpserver::webserver; using httpserver::create_webserver; using httpserver::http_response; +#ifdef HAVE_BAUTH using httpserver::basic_auth_fail_response; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH using httpserver::digest_auth_fail_response; #endif // HAVE_DAUTH @@ -66,6 +68,7 @@ size_t writefunc(void *ptr, size_t size, size_t nmemb, std::string *s) { return size*nmemb; } +#ifdef HAVE_BAUTH class user_pass_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { @@ -75,6 +78,7 @@ class user_pass_resource : public http_resource { return std::make_shared(std::string(req.get_user()) + " " + std::string(req.get_pass()), 200, "text/plain"); } }; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH class digest_resource : public http_resource { @@ -101,6 +105,7 @@ LT_BEGIN_SUITE(authentication_suite) } LT_END_SUITE(authentication_suite) +#ifdef HAVE_BAUTH LT_BEGIN_AUTO_TEST(authentication_suite, base_auth) webserver ws = create_webserver(PORT); @@ -150,6 +155,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, base_auth_fail) ws.stop(); LT_END_AUTO_TEST(base_auth_fail) +#endif // HAVE_BAUTH // do not run the digest auth tests on windows as curl // appears to have problems with it. @@ -555,6 +561,7 @@ LT_END_AUTO_TEST(digest_user_cache_with_auth) #endif +#ifdef HAVE_BAUTH // Simple resource for centralized auth tests class simple_resource : public http_resource { public: @@ -1122,6 +1129,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_path_traversal_bypass) ws.stop(); LT_END_AUTO_TEST(auth_skip_path_traversal_bypass) +#endif // HAVE_BAUTH LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() diff --git a/test/unit/create_webserver_test.cpp b/test/unit/create_webserver_test.cpp index efae3812..49bc10ff 100644 --- a/test/unit/create_webserver_test.cpp +++ b/test/unit/create_webserver_test.cpp @@ -120,12 +120,14 @@ LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_pedantic_toggle) LT_CHECK_EQ(true, true); LT_END_AUTO_TEST(builder_pedantic_toggle) +#ifdef HAVE_BAUTH // Test basic_auth / no_basic_auth toggle LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_basic_auth_toggle) create_webserver cw1 = create_webserver(8080).basic_auth(); create_webserver cw2 = create_webserver(8080).no_basic_auth(); LT_CHECK_EQ(true, true); LT_END_AUTO_TEST(builder_basic_auth_toggle) +#endif // HAVE_BAUTH // Test digest_auth / no_digest_auth toggle LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_digest_auth_toggle) From 507d29a92171666e8f372d7e94adfd84598a30f1 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 12:15:17 -0800 Subject: [PATCH 15/19] Add HAVE_BAUTH guards to remaining files missed in initial commit Guard basic auth references in ws_start_stop.cpp, basic.cpp, create_test_request_test.cpp, create_test_request.hpp, and create_test_request.cpp that would fail to compile when libmicrohttpd lacks basic auth support. --- src/create_test_request.cpp | 2 ++ src/httpserver/create_test_request.hpp | 4 ++++ test/integ/basic.cpp | 4 ++++ test/integ/ws_start_stop.cpp | 2 ++ test/unit/create_test_request_test.cpp | 2 ++ 5 files changed, 14 insertions(+) diff --git a/src/create_test_request.cpp b/src/create_test_request.cpp index 31c7d3df..985acd39 100644 --- a/src/create_test_request.cpp +++ b/src/create_test_request.cpp @@ -49,8 +49,10 @@ http_request create_test_request::build() { req.cache->querystring = std::move(_querystring); } +#ifdef HAVE_BAUTH req.cache->username = std::move(_user); req.cache->password = std::move(_pass); +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH req.cache->digested_user = std::move(_digested_user); diff --git a/src/httpserver/create_test_request.hpp b/src/httpserver/create_test_request.hpp index 177257c2..a1f193d0 100644 --- a/src/httpserver/create_test_request.hpp +++ b/src/httpserver/create_test_request.hpp @@ -83,6 +83,7 @@ class create_test_request { return *this; } +#ifdef HAVE_BAUTH create_test_request& user(const std::string& user) { _user = user; return *this; @@ -92,6 +93,7 @@ class create_test_request { _pass = pass; return *this; } +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH create_test_request& digested_user(const std::string& digested_user) { @@ -129,8 +131,10 @@ class create_test_request { http::header_map _cookies; std::map, http::arg_comparator> _args; std::string _querystring; +#ifdef HAVE_BAUTH std::string _user; std::string _pass; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH std::string _digested_user; #endif // HAVE_DAUTH diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index d36f2f48..14d4eea0 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -2640,6 +2640,7 @@ class null_value_query_resource : public http_resource { } }; +#ifdef HAVE_BAUTH // Resource that tests auth caching (get_user/get_pass called multiple times) class auth_cache_resource : public http_resource { public: @@ -2654,7 +2655,9 @@ class auth_cache_resource : public http_resource { return std::make_shared(result, 200, "text/plain"); } }; +#endif // HAVE_BAUTH +#ifdef HAVE_BAUTH LT_BEGIN_AUTO_TEST(basic_suite, auth_caching) auth_cache_resource resource; LT_ASSERT_EQ(true, ws->register_resource("auth_cache", &resource)); @@ -2672,6 +2675,7 @@ LT_BEGIN_AUTO_TEST(basic_suite, auth_caching) LT_CHECK_EQ(s, "NO_AUTH"); curl_easy_cleanup(curl); LT_END_AUTO_TEST(auth_caching) +#endif // HAVE_BAUTH // Test query parameters with null/empty values (e.g., ?keyonly&normal=value) // This covers http_request.cpp lines 234 and 248 (arg_value == nullptr branches) diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index d664301a..a754cad3 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -279,7 +279,9 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, disable_options) .no_ipv6() .no_debug() .no_pedantic() +#ifdef HAVE_BAUTH .no_basic_auth() +#endif // HAVE_BAUTH .no_digest_auth() .no_deferred() .no_regex_checking() diff --git a/test/unit/create_test_request_test.cpp b/test/unit/create_test_request_test.cpp index 8b3843a8..db94d176 100644 --- a/test/unit/create_test_request_test.cpp +++ b/test/unit/create_test_request_test.cpp @@ -126,6 +126,7 @@ LT_BEGIN_AUTO_TEST(create_test_request_suite, build_content) LT_CHECK_EQ(std::string(req.get_content()), std::string("{\"key\":\"value\"}")); LT_END_AUTO_TEST(build_content) +#ifdef HAVE_BAUTH // Test basic auth LT_BEGIN_AUTO_TEST(create_test_request_suite, build_basic_auth) auto req = create_test_request() @@ -135,6 +136,7 @@ LT_BEGIN_AUTO_TEST(create_test_request_suite, build_basic_auth) LT_CHECK_EQ(std::string(req.get_user()), std::string("admin")); LT_CHECK_EQ(std::string(req.get_pass()), std::string("secret")); LT_END_AUTO_TEST(build_basic_auth) +#endif // HAVE_BAUTH // Test requestor LT_BEGIN_AUTO_TEST(create_test_request_suite, build_requestor) From ae59cc751f8dceba51c3e4c00b8a8d5f11de66c7 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 15:19:20 -0800 Subject: [PATCH 16/19] Bump version to 0.20.0 and update ChangeLog Add Version 0.20.0 header with bauth conditional compilation and security fix entries. Bump version in configure.ac to match. --- ChangeLog | 8 +++++++- configure.ac | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6760a4ff..7bb0aefb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,8 @@ -Version 0.19.0 - 2023-06-15 +Version 0.20.0 + Added conditional compilation for basic auth (HAVE_BAUTH), mirroring + existing HAVE_DAUTH pattern for digest auth. Basic auth support + is auto-detected via AC_CHECK_LIB and can be disabled at build time. Fixed path traversal vulnerability in file uploads when generate_random_filename_on_upload is disabled. Fixed TOCTOU race in file_response by replacing stat-then-open with @@ -12,6 +15,9 @@ Version 0.19.0 - 2023-06-15 Fixed auth skip path bypass via path traversal (e.g. /public/../protected). Fixed use of free() instead of MHD_free() for digest auth username. Fixed unchecked write error during file upload. + +Version 0.19.0 - 2023-06-15 + Considering family_url as part of the priority when selecting a URL to match. More explicit selection of C++ version. Ability to handle multiple parameters with the same name on the URL. diff --git a/configure.ac b/configure.ac index 958d045e..003170c6 100644 --- a/configure.ac +++ b/configure.ac @@ -21,7 +21,7 @@ AC_PREREQ(2.57) m4_define([libhttpserver_MAJOR_VERSION],[0])dnl -m4_define([libhttpserver_MINOR_VERSION],[19])dnl +m4_define([libhttpserver_MINOR_VERSION],[20])dnl m4_define([libhttpserver_REVISION],[0])dnl m4_define([libhttpserver_PKG_VERSION],[libhttpserver_MAJOR_VERSION.libhttpserver_MINOR_VERSION.libhttpserver_REVISION])dnl m4_define([libhttpserver_LDF_VERSION],[libhttpserver_MAJOR_VERSION:libhttpserver_MINOR_VERSION:libhttpserver_REVISION])dnl From 68bff7807aa4849f7e483182b48caab11e138fbd Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 15:51:25 -0800 Subject: [PATCH 17/19] Update ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 72c98a64..addf8862 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ debian/rules redhat/libhttpserver.SPEC libhttpserver.pc libtool +.worktrees +.claude +CLAUDE.md From 97bd4b196b5b5ef908c21582a5cfc51bd0611e35 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 19:41:39 +0000 Subject: [PATCH 18/19] Add documentation and example for serving binary data from memory The existing string_response already supports binary content (std::string can hold arbitrary bytes), but this was not documented or demonstrated anywhere. This gap caused users to believe a new response type was needed (see PR #368). - Add a note to the README's string_response description clarifying binary data support - Add a new "Serving binary data from memory" section with inline example - Add examples/binary_buffer_response.cpp as a complete, buildable example that serves a PNG image from an in-memory buffer - Register the new example in examples/Makefile.am https://claude.ai/code/session_01S3BvBrSoNvUhpYTyhPYCjJ --- README.md | 37 ++++++++++++- examples/Makefile.am | 3 +- examples/binary_buffer_response.cpp | 81 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 examples/binary_buffer_response.cpp diff --git a/README.md b/README.md index a70ff849..b9d96f37 100644 --- a/README.md +++ b/README.md @@ -807,7 +807,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver As seen in the documentation of [http_resource](#the-resource-object), every extensible method returns in output a `http_response` object. The webserver takes the responsibility to convert the `http_response` object you create into a response on the network. There are 5 types of response that you can create - we will describe them here through their constructors: -* _string_response(**const std::string&** content, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ The most basic type of response. It uses the `content` string passed in construction as body of the HTTP response. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. +* _string_response(**const std::string&** content, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ The most basic type of response. It uses the `content` string passed in construction as body of the HTTP response. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. Note that `std::string` can hold arbitrary binary data (including null bytes), so `string_response` is also the right choice for serving binary content such as images directly from memory — simply set an appropriate `content_type` (e.g., `"image/png"`). * _file_response(**const std::string&** filename, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ Uses the `filename` passed in construction as pointer to a file on disk. The body of the HTTP response will be set using the content of the file. The file must be a regular file and exist on disk. Otherwise libhttpserver will return an error 500 (Internal Server Error). The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. * _basic_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during basic authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. * _digest_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **const std::string&** opaque = `""`, **bool** reload_nonce = `false`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during digest authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The `opaque` represents a value that gets passed to the client and expected to be passed again to the server as-is. This value can be a hexadecimal or base64 string. The `reload_nonce` parameter tells the server to reload the nonce (you should use the value returned by the `check_digest_auth` method on the `http_request`. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. @@ -856,6 +856,41 @@ You will receive the message custom header in reply. You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/setting_headers.cpp). +### Serving binary data from memory +`string_response` is not limited to text — it can serve arbitrary binary content directly from memory. This is useful when you have data in a buffer at runtime (e.g., from a camera, an image processing library, or a database) and want to serve it without writing to disk. + +```cpp + #include + + using namespace httpserver; + + class image_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + // binary_data could come from a camera capture, image library, etc. + std::string binary_data = get_image_bytes_from_camera(); + + return std::make_shared( + std::move(binary_data), 200, "image/jpeg"); + } + }; + + int main() { + webserver ws = create_webserver(8080); + + image_resource ir; + ws.register_resource("/image", &ir); + ws.start(true); + + return 0; + } +``` +To test the above example, you could run the following command from a terminal: + + curl -o image.jpg http://localhost:8080/image + +You can also check the complete example on [github](https://github.com/etr/libhttpserver/blob/master/examples/binary_buffer_response.cpp). + [Back to TOC](#table-of-contents) ## IP Blacklisting and Whitelisting diff --git a/examples/Makefile.am b/examples/Makefile.am index c04e0acf..148fa944 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback binary_buffer_response hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -42,6 +42,7 @@ benchmark_threads_SOURCES = benchmark_threads.cpp benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp file_upload_with_callback_SOURCES = file_upload_with_callback.cpp +binary_buffer_response_SOURCES = binary_buffer_response.cpp if HAVE_BAUTH noinst_PROGRAMS += basic_authentication centralized_authentication diff --git a/examples/binary_buffer_response.cpp b/examples/binary_buffer_response.cpp new file mode 100644 index 00000000..17060137 --- /dev/null +++ b/examples/binary_buffer_response.cpp @@ -0,0 +1,81 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +// This example demonstrates how to serve binary data (e.g., images) directly +// from an in-memory buffer using string_response. Despite its name, +// string_response works with arbitrary binary content because std::string can +// hold any bytes, including null characters. +// +// This is useful when you generate or receive binary data at runtime (e.g., +// from a camera, an image library, or a database) and want to serve it over +// HTTP without writing it to disk first. +// +// To test: +// curl -o output.png http://localhost:8080/image + +#include +#include + +#include + +// Generate a minimal valid 1x1 red PNG image in memory. +// In a real application, this could come from a camera capture, image +// processing library, database blob, etc. +static std::string generate_png_data() { + // Minimal 1x1 red pixel PNG (68 bytes) + static const unsigned char png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, // 8-bit RGB + 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT chunk + 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, // compressed data + 0x00, 0x00, 0x03, 0x00, 0x01, 0x36, 0x28, 0x19, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND chunk + 0x44, 0xae, 0x42, 0x60, 0x82 + }; + + return std::string(reinterpret_cast(png), sizeof(png)); +} + +class image_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + // Build binary content as a std::string. The string can contain any + // bytes — it is not limited to printable characters or null-terminated + // C strings. The size is tracked internally by std::string::size(). + std::string image_data = generate_png_data(); + + // Use string_response with the appropriate content type. The response + // will send the exact bytes contained in the string. + return std::make_shared( + std::move(image_data), 200, "image/png"); + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + image_resource ir; + ws.register_resource("/image", &ir); + ws.start(true); + + return 0; +} From 6fa84e836d490a2ee83f0b8b78553fc3e1032ac4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 22:13:38 +0000 Subject: [PATCH 19/19] Fix CI: add ChangeLog entry and missing include Add ChangeLog entry for the binary buffer example to satisfy the ChangeLog Check workflow. Add missing #include for std::move to fix cpplint warning. https://claude.ai/code/session_01S3BvBrSoNvUhpYTyhPYCjJ --- ChangeLog | 2 ++ examples/binary_buffer_response.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 7bb0aefb..6e9532e3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,7 @@ Version 0.20.0 + Added example and documentation for serving binary data from memory + using string_response (addresses PR #368). Added conditional compilation for basic auth (HAVE_BAUTH), mirroring existing HAVE_DAUTH pattern for digest auth. Basic auth support is auto-detected via AC_CHECK_LIB and can be disabled at build time. diff --git a/examples/binary_buffer_response.cpp b/examples/binary_buffer_response.cpp index 17060137..19559cfc 100644 --- a/examples/binary_buffer_response.cpp +++ b/examples/binary_buffer_response.cpp @@ -32,6 +32,7 @@ #include #include +#include #include