Compare commits

..

27 Commits

Author SHA1 Message Date
yhirose b6c55c6030 Release v0.22.0 2025-06-24 08:01:53 -04:00
yhirose 28dcf379e8 Merge commit from fork 2025-06-24 07:56:00 -04:00
yhirose 91e79e9a63 Fix #1777 (#2160)
* Add benchmark unit test

* Update

* Update

* Update

* Change the default value of CPPHTTPLIB_IDLE_INTERVAL_USECOND to 1ms
2025-06-24 07:44:10 -04:00
yhirose 27879c4874 Fix #2157 (#2158)
* Fix #2157

* Fix Windows build error: wrap std::max in parentheses to avoid macro conflict

- On Windows, max/min are often defined as macros by windows.h
- This causes compilation errors with std::max/std::min
- Solution: use (std::max) to prevent macro expansion
- Fixes CI build error: error C2589: '(': illegal token on right side of '::'

Fixes: error in coalesce_ranges function on line 5376
2025-06-23 08:35:56 -04:00
DSiekmeier 08a0452fb2 Update README.md (#2156)
According to the curl manpage, the option is called "--max-time".
2025-06-18 07:05:23 -04:00
yhirose 3a1f379e75 Release v0.21.0 2025-06-09 22:19:30 -04:00
Marek Kwasecki fd8da4d8e4 Feature/multipart headers (#2152)
* Adds headers to multipart form data

Adds a `headers` field to the `MultipartFormData` struct.

Populates this field by parsing headers from the multipart form data.
This allows access to specific headers associated with each form data part.

* Adds multipart header access test

Verifies the correct retrieval of headers from multipart form data file parts.

Ensures that custom and content-related headers are accessible and parsed as expected.

* Enables automatic test discovery with GoogleTest

Uses `gtest_discover_tests` to automatically find and run tests, simplifying test maintenance and improving discoverability.

* Removes explicit GoogleTest include

* Refactors header parsing logic

Improves header parsing by using a dedicated parsing function,
resulting in cleaner and more robust code.

This change also adds error handling during header parsing,
returning an error and marking the request as invalid
if parsing fails.

* clang-format corrected

* Renames variable for better readability.

Renames the `customHeader` variable to `custom_header` for improved code readability and consistency.

* typo
2025-06-09 15:59:25 -04:00
yhirose 365cbe37fa Fix #2101 2025-05-25 21:56:28 -04:00
Piotr 4a7aae5469 Detect if afunix.h exists (#2145)
Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-05-23 23:14:48 -04:00
yhirose fd324e1412 Add KeepAliveTest.MaxCount 2025-05-20 21:41:50 +09:00
QWERTIOX 366eda72dc Specify version in meson.build (#2139)
I prefer to specify version when I'm calling dependency() and lack of makes me unable to do it
2025-05-16 10:02:32 -04:00
yhirose c216dc94d2 Code cleanup 2025-05-09 18:45:31 +09:00
yhirose 61893a00a4 Fix #2135 2025-05-03 22:50:47 +09:00
yhirose 3af7f2c161 Release v0.20.1 2025-05-03 21:24:22 +09:00
yhirose a0de42ebc4 clang-format 2025-05-03 17:40:34 +09:00
Ville Vesilehto 7b752106ac Merge commit from fork
* fix(parser): Limit line length in getline

Prevents potential infinite loop and memory exhaustion in
stream_line_reader::getline by enforcing max line length.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>

* fix: increase default max line length to 32k

LONG_QUERY_VALUE test is set at 25k.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>

* test(client): expect read error with too long query

Adds a test case (`TooLongQueryValue`) to verify client behavior
when the request URI is excessively long, exceeding
`CPPHTTPLIB_MAX_LINE_LENGTH`. In this scenario, the server is
expected to reset the connection.

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>

---------

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
2025-05-03 04:39:01 -04:00
yhirose 9589519d58 Fix #2130 2025-04-17 11:52:22 -04:00
Alexey Sokolov caf7c55785 Fix regression of #2121 (#2126) 2025-04-08 16:08:41 -04:00
yhirose 9e4aed482e Fix style error 2025-04-06 09:02:25 -04:00
yhirose 65d6316d65 Fix #2113 2025-04-05 22:40:08 -04:00
yhirose 3e3a8cc02f Made the max timeout threshold for SSL longer. 2025-04-05 22:38:50 -04:00
KTGH b7e33b08f1 Add missing component comment (#2124)
Fix #2123
2025-03-31 20:34:28 -04:00
Alexey Sokolov 0dbe8ba144 Support zstd also via pkg-config (#2121)
* Support zstd also via pkg-config

It doesn't always provide cmake config

* Find zstd with pkg-config also in non-required case

Code by @sum01, slightly modified
2025-03-29 11:46:22 -04:00
Piotr dbc4af819a Fix compilation error on windows (#2118)
afunix.h uses types declared in winsock2.h, and so has to be included
after it. Including afunix.h first will result in a somewhat unhelpful
compilation error:

error C3646: 'sun_family': unknown override specifier

Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-03-25 08:36:20 -04:00
yhirose 7dbf5471ce Fix the style error and comment 2025-03-24 19:16:48 -04:00
Piotr 72b35befb2 Add AF_UNIX support on windows (#2115)
Signed-off-by: Piotr Stankiewicz <piotr.stankiewicz@docker.com>
2025-03-24 19:14:24 -04:00
Jean-Francois Simoneau 65ce51aed7 Fix start_time shadow variable (#2114) 2025-03-18 19:17:47 -04:00
6 changed files with 593 additions and 50 deletions
+18 -2
View File
@@ -18,7 +18,7 @@
-------------------------------------------------------------------------------
After installation with Cmake, a find_package(httplib COMPONENTS OpenSSL ZLIB Brotli) is available.
After installation with Cmake, a find_package(httplib COMPONENTS OpenSSL ZLIB Brotli zstd) is available.
This creates a httplib::httplib target (if found and if listed components are supported).
It can be linked like so:
@@ -159,10 +159,26 @@ elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
endif()
if(HTTPLIB_REQUIRE_ZSTD)
find_package(zstd REQUIRED)
find_package(zstd)
if(NOT zstd_FOUND)
find_package(PkgConfig REQUIRED)
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
endif()
set(HTTPLIB_IS_USING_ZSTD TRUE)
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
find_package(zstd QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(zstd QUIET IMPORTED_TARGET libzstd)
if(TARGET PkgConfig::zstd)
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
endif()
endif()
endif()
# Both find_package and PkgConf set a XXX_FOUND var
set(HTTPLIB_IS_USING_ZSTD ${zstd_FOUND})
endif()
+17 -1
View File
@@ -285,6 +285,22 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
});
```
### Pre request handler
```cpp
svr.set_pre_request_handler([](const auto& req, auto& res) {
if (req.matched_route == "/user/:user") {
auto user = req.path_params.at("user");
if (user != "john") {
res.status = StatusCode::Forbidden_403;
res.set_content("error", "text/html");
return Server::HandlerResponse::Handled;
}
}
return Server::HandlerResponse::Unhandled;
});
```
### 'multipart/form-data' POST data
```cpp
@@ -655,7 +671,7 @@ cli.set_connection_timeout(0, 300000); // 300 milliseconds
cli.set_read_timeout(5, 0); // 5 seconds
cli.set_write_timeout(5, 0); // 5 seconds
// This method works the same as curl's `--max-timeout` option
// This method works the same as curl's `--max-time` option
svr.set_max_timeout(5000); // 5 seconds
```
+19 -1
View File
@@ -39,7 +39,25 @@ if(@HTTPLIB_IS_USING_BROTLI@)
endif()
if(@HTTPLIB_IS_USING_ZSTD@)
find_dependency(zstd)
set(httplib_fd_zstd_quiet_arg)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_QUIETLY)
set(httplib_fd_zstd_quiet_arg QUIET)
endif()
set(httplib_fd_zstd_required_arg)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
set(httplib_fd_zstd_required_arg REQUIRED)
endif()
find_package(zstd QUIET)
if(NOT zstd_FOUND)
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
if(PKG_CONFIG_FOUND)
pkg_check_modules(zstd ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg} IMPORTED_TARGET libzstd)
if(TARGET PkgConfig::zstd)
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
endif()
endif()
endif()
set(httplib_zstd_FOUND ${zstd_FOUND})
endif()
+174 -34
View File
@@ -8,7 +8,7 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.20.0"
#define CPPHTTPLIB_VERSION "0.22.0"
/*
* Configuration
@@ -76,7 +76,7 @@
#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND
#ifdef _WIN32
#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 10000
#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000
#else
#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0
#endif
@@ -90,6 +90,10 @@
#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192
#endif
#ifndef CPPHTTPLIB_HEADER_MAX_COUNT
#define CPPHTTPLIB_HEADER_MAX_COUNT 100
#endif
#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT
#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20
#endif
@@ -145,6 +149,10 @@
#define CPPHTTPLIB_LISTEN_BACKLOG 5
#endif
#ifndef CPPHTTPLIB_MAX_LINE_LENGTH
#define CPPHTTPLIB_MAX_LINE_LENGTH 32768
#endif
/*
* Headers
*/
@@ -188,6 +196,14 @@ using ssize_t = long;
#include <winsock2.h>
#include <ws2tcpip.h>
#if defined(__has_include)
#if __has_include(<afunix.h>)
// afunix.h uses types declared in winsock2.h, so has to be included after it.
#include <afunix.h>
#define CPPHTTPLIB_HAVE_AFUNIX_H 1
#endif
#endif
#ifndef WSA_FLAG_NO_HANDLE_INHERIT
#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
#endif
@@ -532,6 +548,7 @@ struct MultipartFormData {
std::string content;
std::string filename;
std::string content_type;
Headers headers;
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
using MultipartFormDataMap = std::multimap<std::string, MultipartFormData>;
@@ -624,6 +641,7 @@ using Ranges = std::vector<Range>;
struct Request {
std::string method;
std::string path;
std::string matched_route;
Params params;
Headers headers;
std::string body;
@@ -875,10 +893,16 @@ namespace detail {
class MatcherBase {
public:
MatcherBase(std::string pattern) : pattern_(pattern) {}
virtual ~MatcherBase() = default;
const std::string &pattern() const { return pattern_; }
// Match request path and populate its matches and
virtual bool match(Request &request) const = 0;
private:
std::string pattern_;
};
/**
@@ -930,7 +954,8 @@ private:
*/
class RegexMatcher final : public MatcherBase {
public:
RegexMatcher(const std::string &pattern) : regex_(pattern) {}
RegexMatcher(const std::string &pattern)
: MatcherBase(pattern), regex_(pattern) {}
bool match(Request &request) const override;
@@ -997,9 +1022,12 @@ public:
}
Server &set_exception_handler(ExceptionHandler handler);
Server &set_pre_routing_handler(HandlerWithResponse handler);
Server &set_post_routing_handler(Handler handler);
Server &set_pre_request_handler(HandlerWithResponse handler);
Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
Server &set_logger(Logger logger);
@@ -1080,8 +1108,7 @@ private:
bool listen_internal();
bool routing(Request &req, Response &res, Stream &strm);
bool handle_file_request(const Request &req, Response &res,
bool head = false);
bool handle_file_request(const Request &req, Response &res);
bool dispatch_request(Request &req, Response &res,
const Handlers &handlers) const;
bool dispatch_request_for_content_reader(
@@ -1142,6 +1169,7 @@ private:
ExceptionHandler exception_handler_;
HandlerWithResponse pre_routing_handler_;
Handler post_routing_handler_;
HandlerWithResponse pre_request_handler_;
Expect100ContinueHandler expect_100_continue_handler_;
Logger logger_;
@@ -2059,7 +2087,9 @@ template <size_t N> inline constexpr size_t str_len(const char (&)[N]) {
}
inline bool is_numeric(const std::string &str) {
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
return !str.empty() &&
std::all_of(str.cbegin(), str.cend(),
[](unsigned char c) { return std::isdigit(c); });
}
inline uint64_t get_header_value_u64(const Headers &headers,
@@ -3064,6 +3094,11 @@ inline bool stream_line_reader::getline() {
#endif
for (size_t i = 0;; i++) {
if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
// Treat exceptionally long lines as an error to
// prevent infinite loops/memory exhaustion
return false;
}
char byte;
auto n = strm_.read(&byte, 1);
@@ -3365,7 +3400,7 @@ private:
time_t write_timeout_sec_;
time_t write_timeout_usec_;
time_t max_timeout_msec_;
const std::chrono::time_point<std::chrono::steady_clock> start_time;
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
std::vector<char> read_buff_;
size_t read_buff_off_ = 0;
@@ -3403,7 +3438,7 @@ private:
time_t write_timeout_sec_;
time_t write_timeout_usec_;
time_t max_timeout_msec_;
const std::chrono::time_point<std::chrono::steady_clock> start_time;
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
};
#endif
@@ -3538,7 +3573,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
hints.ai_flags = socket_flags;
}
#ifndef _WIN32
#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H)
if (hints.ai_family == AF_UNIX) {
const auto addrlen = host.length();
if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; }
@@ -3562,11 +3597,19 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
sizeof(addr) - sizeof(addr.sun_path) + addrlen);
#ifndef SOCK_CLOEXEC
#ifndef _WIN32
fcntl(sock, F_SETFD, FD_CLOEXEC);
#endif
#endif
if (socket_options) { socket_options(sock); }
#ifdef _WIN32
// Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so
// remove the option.
detail::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0);
#endif
bool dummy;
if (!bind_or_connect(sock, hints, dummy)) {
close_socket(sock);
@@ -4316,6 +4359,8 @@ inline bool read_headers(Stream &strm, Headers &headers) {
char buf[bufsiz];
stream_line_reader line_reader(strm, buf, bufsiz);
size_t header_count = 0;
for (;;) {
if (!line_reader.getline()) { return false; }
@@ -4336,6 +4381,9 @@ inline bool read_headers(Stream &strm, Headers &headers) {
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
// Check header count limit
if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
// Exclude line terminator
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
@@ -4345,6 +4393,8 @@ inline bool read_headers(Stream &strm, Headers &headers) {
})) {
return false;
}
header_count++;
}
return true;
@@ -4447,9 +4497,13 @@ inline bool read_content_chunked(Stream &strm, T &x,
// chunked transfer coding data without the final CRLF.
if (!line_reader.getline()) { return true; }
size_t trailer_header_count = 0;
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
// Check trailer header count limit
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
// Exclude line terminator
constexpr auto line_terminator_len = 2;
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
@@ -4459,6 +4513,8 @@ inline bool read_content_chunked(Stream &strm, T &x,
x.headers.emplace(key, val);
});
trailer_header_count++;
if (!line_reader.getline()) { return false; }
}
@@ -5007,6 +5063,16 @@ public:
return false;
}
// parse and emplace space trimmed headers into a map
if (!parse_header(
header.data(), header.data() + header.size(),
[&](const std::string &key, const std::string &val) {
file_.headers.emplace(key, val);
})) {
is_valid_ = false;
return false;
}
constexpr const char header_content_type[] = "Content-Type:";
if (start_with_case_ignore(header, header_content_type)) {
@@ -5106,6 +5172,7 @@ private:
file_.name.clear();
file_.filename.clear();
file_.content_type.clear();
file_.headers.clear();
}
bool start_with_case_ignore(const std::string &a, const char *b) const {
@@ -5279,13 +5346,68 @@ serialize_multipart_formdata(const MultipartFormDataItems &items,
return body;
}
inline void coalesce_ranges(Ranges &ranges, size_t content_length) {
if (ranges.size() <= 1) return;
// Sort ranges by start position
std::sort(ranges.begin(), ranges.end(),
[](const Range &a, const Range &b) { return a.first < b.first; });
Ranges coalesced;
coalesced.reserve(ranges.size());
for (auto &r : ranges) {
auto first_pos = r.first;
auto last_pos = r.second;
// Handle special cases like in range_error
if (first_pos == -1 && last_pos == -1) {
first_pos = 0;
last_pos = static_cast<ssize_t>(content_length);
}
if (first_pos == -1) {
first_pos = static_cast<ssize_t>(content_length) - last_pos;
last_pos = static_cast<ssize_t>(content_length) - 1;
}
if (last_pos == -1 || last_pos >= static_cast<ssize_t>(content_length)) {
last_pos = static_cast<ssize_t>(content_length) - 1;
}
// Skip invalid ranges
if (!(0 <= first_pos && first_pos <= last_pos &&
last_pos < static_cast<ssize_t>(content_length))) {
continue;
}
// Coalesce with previous range if overlapping or adjacent (but not
// identical)
if (!coalesced.empty()) {
auto &prev = coalesced.back();
// Check if current range overlaps or is adjacent to previous range
// but don't coalesce identical ranges (allow duplicates)
if (first_pos <= prev.second + 1 &&
!(first_pos == prev.first && last_pos == prev.second)) {
// Extend the previous range
prev.second = (std::max)(prev.second, last_pos);
continue;
}
}
// Add new range
coalesced.emplace_back(first_pos, last_pos);
}
ranges = std::move(coalesced);
}
inline bool range_error(Request &req, Response &res) {
if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {
ssize_t content_len = static_cast<ssize_t>(
res.content_length_ ? res.content_length_ : res.body.size());
ssize_t prev_first_pos = -1;
ssize_t prev_last_pos = -1;
std::vector<std::pair<ssize_t, ssize_t>> processed_ranges;
size_t overwrapping_count = 0;
// NOTE: The following Range check is based on '14.2. Range' in RFC 9110
@@ -5328,18 +5450,21 @@ inline bool range_error(Request &req, Response &res) {
return true;
}
// Ranges must be in ascending order
if (first_pos <= prev_first_pos) { return true; }
// Request must not have more than two overlapping ranges
if (first_pos <= prev_last_pos) {
overwrapping_count++;
if (overwrapping_count > 2) { return true; }
for (const auto &processed_range : processed_ranges) {
if (!(last_pos < processed_range.first ||
first_pos > processed_range.second)) {
overwrapping_count++;
if (overwrapping_count > 2) { return true; }
break; // Only count once per range
}
}
prev_first_pos = (std::max)(prev_first_pos, first_pos);
prev_last_pos = (std::max)(prev_last_pos, last_pos);
processed_ranges.emplace_back(first_pos, last_pos);
}
// After validation, coalesce overlapping ranges as per RFC 9110
coalesce_ranges(req.ranges, static_cast<size_t>(content_len));
}
return false;
@@ -6046,6 +6171,8 @@ inline void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec,
auto actual_timeout_msec =
(std::min)(max_timeout_msec - duration_msec, timeout_msec);
if (actual_timeout_msec < 0) { actual_timeout_msec = 0; }
actual_timeout_sec = actual_timeout_msec / 1000;
actual_timeout_usec = (actual_timeout_msec % 1000) * 1000;
}
@@ -6060,7 +6187,7 @@ inline SocketStream::SocketStream(
read_timeout_usec_(read_timeout_usec),
write_timeout_sec_(write_timeout_sec),
write_timeout_usec_(write_timeout_usec),
max_timeout_msec_(max_timeout_msec), start_time(start_time),
max_timeout_msec_(max_timeout_msec), start_time_(start_time),
read_buff_(read_buff_size_, 0) {}
inline SocketStream::~SocketStream() = default;
@@ -6158,7 +6285,7 @@ inline socket_t SocketStream::socket() const { return sock_; }
inline time_t SocketStream::duration() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time)
std::chrono::steady_clock::now() - start_time_)
.count();
}
@@ -6196,7 +6323,8 @@ inline time_t BufferStream::duration() const { return 0; }
inline const std::string &BufferStream::get_buffer() const { return buffer; }
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern)
: MatcherBase(pattern) {
constexpr const char marker[] = "/:";
// One past the last ending position of a path param substring
@@ -6447,6 +6575,11 @@ inline Server &Server::set_post_routing_handler(Handler handler) {
return *this;
}
inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) {
pre_request_handler_ = std::move(handler);
return *this;
}
inline Server &Server::set_logger(Logger logger) {
logger_ = std::move(logger);
return *this;
@@ -6858,8 +6991,7 @@ Server::read_content_core(Stream &strm, Request &req, Response &res,
return true;
}
inline bool Server::handle_file_request(const Request &req, Response &res,
bool head) {
inline bool Server::handle_file_request(const Request &req, Response &res) {
for (const auto &entry : base_dirs_) {
// Prefix match
if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
@@ -6892,7 +7024,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res,
return true;
});
if (!head && file_request_handler_) {
if (req.method != "HEAD" && file_request_handler_) {
file_request_handler_(req, res);
}
@@ -7026,9 +7158,8 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
}
// File handler
auto is_head_request = req.method == "HEAD";
if ((req.method == "GET" || is_head_request) &&
handle_file_request(req, res, is_head_request)) {
if ((req.method == "GET" || req.method == "HEAD") &&
handle_file_request(req, res)) {
return true;
}
@@ -7103,7 +7234,11 @@ inline bool Server::dispatch_request(Request &req, Response &res,
const auto &handler = x.second;
if (matcher->match(req)) {
handler(req, res);
req.matched_route = matcher->pattern();
if (!pre_request_handler_ ||
pre_request_handler_(req, res) != HandlerResponse::Handled) {
handler(req, res);
}
return true;
}
}
@@ -7230,7 +7365,11 @@ inline bool Server::dispatch_request_for_content_reader(
const auto &handler = x.second;
if (matcher->match(req)) {
handler(req, res, content_reader);
req.matched_route = matcher->pattern();
if (!pre_request_handler_ ||
pre_request_handler_(req, res) != HandlerResponse::Handled) {
handler(req, res, content_reader);
}
return true;
}
}
@@ -7318,8 +7457,9 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
}
// Setup `is_connection_closed` method
req.is_connection_closed = [&]() {
return !detail::is_socket_alive(strm.socket());
auto sock = strm.socket();
req.is_connection_closed = [sock]() {
return !detail::is_socket_alive(sock);
};
// Routing
@@ -9200,7 +9340,7 @@ inline SSLSocketStream::SSLSocketStream(
read_timeout_usec_(read_timeout_usec),
write_timeout_sec_(write_timeout_sec),
write_timeout_usec_(write_timeout_usec),
max_timeout_msec_(max_timeout_msec), start_time(start_time) {
max_timeout_msec_(max_timeout_msec), start_time_(start_time) {
SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY);
}
@@ -9306,7 +9446,7 @@ inline socket_t SSLSocketStream::socket() const { return sock_; }
inline time_t SSLSocketStream::duration() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time)
std::chrono::steady_clock::now() - start_time_)
.count();
}
+2 -2
View File
@@ -87,7 +87,7 @@ if get_option('cpp-httplib_compile')
soversion: version.split('.')[0] + '.' + version.split('.')[1],
install: true
)
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, link_with: lib, sources: httplib_ch[1])
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, link_with: lib, sources: httplib_ch[1], version: version)
import('pkgconfig').generate(
lib,
@@ -98,7 +98,7 @@ if get_option('cpp-httplib_compile')
)
else
install_headers('httplib.h')
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: '.')
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: '.', version: version)
import('pkgconfig').generate(
name: 'cpp-httplib',
+363 -10
View File
@@ -3,7 +3,11 @@
#include <signal.h>
#ifndef _WIN32
#include <arpa/inet.h>
#include <curl/curl.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
#include <gtest/gtest.h>
@@ -43,6 +47,10 @@ const int PORT = 1234;
const string LONG_QUERY_VALUE = string(25000, '@');
const string LONG_QUERY_URL = "/long-query-value?key=" + LONG_QUERY_VALUE;
const string TOO_LONG_QUERY_VALUE = string(35000, '@');
const string TOO_LONG_QUERY_URL =
"/too-long-query-value?key=" + TOO_LONG_QUERY_VALUE;
const std::string JSON_DATA = "{\"hello\":\"world\"}";
const string LARGE_DATA = string(1024 * 1024 * 100, '@'); // 100MB
@@ -70,7 +78,6 @@ static void read_file(const std::string &path, std::string &out) {
fs.read(&out[0], static_cast<std::streamsize>(size));
}
#ifndef _WIN32
class UnixSocketTest : public ::testing::Test {
protected:
void TearDown() override { std::remove(pathname_.c_str()); }
@@ -167,6 +174,7 @@ TEST_F(UnixSocketTest, abstract) {
}
#endif
#ifndef _WIN32
TEST(SocketStream, wait_writable_UNIX) {
int fds[2];
ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
@@ -2259,7 +2267,7 @@ TEST(NoContentTest, ContentLength) {
}
}
TEST(RoutingHandlerTest, PreRoutingHandler) {
TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
@@ -2350,6 +2358,63 @@ TEST(RoutingHandlerTest, PreRoutingHandler) {
}
}
TEST(RequestHandlerTest, PreRequestHandler) {
auto route_path = "/user/:user";
Server svr;
svr.Get("/hi", [](const Request &, Response &res) {
res.set_content("hi", "text/plain");
});
svr.Get(route_path, [](const Request &req, Response &res) {
res.set_content(req.path_params.at("user"), "text/plain");
});
svr.set_pre_request_handler([&](const Request &req, Response &res) {
if (req.matched_route == route_path) {
auto user = req.path_params.at("user");
if (user != "john") {
res.status = StatusCode::Forbidden_403;
res.set_content("error", "text/html");
return Server::HandlerResponse::Handled;
}
}
return Server::HandlerResponse::Unhandled;
});
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
{
auto res = cli.Get("/hi");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("hi", res->body);
}
{
auto res = cli.Get("/user/john");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("john", res->body);
}
{
auto res = cli.Get("/user/invalid-user");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
EXPECT_EQ("error", res->body);
}
}
TEST(InvalidFormatTest, StatusCode) {
Server svr;
@@ -2867,6 +2932,11 @@ protected:
EXPECT_EQ(LONG_QUERY_URL, req.target);
EXPECT_EQ(LONG_QUERY_VALUE, req.get_param_value("key"));
})
.Get("/too-long-query-value",
[&](const Request &req, Response & /*res*/) {
EXPECT_EQ(TOO_LONG_QUERY_URL, req.target);
EXPECT_EQ(TOO_LONG_QUERY_VALUE, req.get_param_value("key"));
})
.Get("/array-param",
[&](const Request &req, Response & /*res*/) {
EXPECT_EQ(3u, req.get_param_value_count("array"));
@@ -3089,6 +3159,47 @@ TEST_F(ServerTest, GetMethod200) {
EXPECT_EQ("Hello World!", res->body);
}
TEST(BenchmarkTest, SimpleGetPerformance) {
Server svr;
svr.Get("/benchmark", [&](const Request & /*req*/, Response &res) {
res.set_content("Benchmark Response", "text/plain");
});
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli("localhost", PORT);
const int NUM_REQUESTS = 50;
const int MAX_AVERAGE_MS = 5;
auto warmup = cli.Get("/benchmark");
ASSERT_TRUE(warmup);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < NUM_REQUESTS; ++i) {
auto res = cli.Get("/benchmark");
ASSERT_TRUE(res) << "Request " << i << " failed";
EXPECT_EQ(StatusCode::OK_200, res->status);
}
auto end = std::chrono::high_resolution_clock::now();
auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
double avg_ms = static_cast<double>(total_ms) / NUM_REQUESTS;
std::cout << "Standalone: " << NUM_REQUESTS << " requests in " << total_ms
<< "ms (avg: " << avg_ms << "ms)" << std::endl;
EXPECT_LE(avg_ms, MAX_AVERAGE_MS) << "Standalone test too slow: " << avg_ms << "ms (Issue #1777)";
}
TEST_F(ServerTest, GetEmptyFile) {
auto res = cli_.Get("/empty_file");
ASSERT_TRUE(res);
@@ -3655,6 +3766,13 @@ TEST_F(ServerTest, LongQueryValue) {
EXPECT_EQ(StatusCode::UriTooLong_414, res->status);
}
TEST_F(ServerTest, TooLongQueryValue) {
auto res = cli_.Get(TOO_LONG_QUERY_URL.c_str());
ASSERT_FALSE(res);
EXPECT_EQ(Error::Read, res.error());
}
TEST_F(ServerTest, TooLongHeader) {
Request req;
req.method = "GET";
@@ -3709,6 +3827,50 @@ TEST_F(ServerTest, TooLongHeader) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST_F(ServerTest, HeaderCountAtLimit) {
// Test with headers just under the 100 limit
httplib::Headers headers;
// Add 95 custom headers (the client will add Host, User-Agent, Accept, etc.)
// This should keep us just under the 100 header limit
for (int i = 0; i < 95; i++) {
std::string name = "X-Test-Header-" + std::to_string(i);
std::string value = "value" + std::to_string(i);
headers.emplace(name, value);
}
// This should work fine as we're under the limit
auto res = cli_.Get("/hi", headers);
EXPECT_TRUE(res);
if (res) {
EXPECT_EQ(StatusCode::OK_200, res->status);
}
}
TEST_F(ServerTest, HeaderCountExceedsLimit) {
// Test with many headers to exceed the 100 limit
httplib::Headers headers;
// Add 150 headers to definitely exceed the 100 limit
for (int i = 0; i < 150; i++) {
std::string name = "X-Test-Header-" + std::to_string(i);
std::string value = "value" + std::to_string(i);
headers.emplace(name, value);
}
// This should fail due to exceeding header count limit
auto res = cli_.Get("/hi", headers);
// The request should either fail or return 400 Bad Request
if (res) {
// If we get a response, it should be 400 Bad Request
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
} else {
// Or the request should fail entirely
EXPECT_FALSE(res);
}
}
TEST_F(ServerTest, PercentEncoding) {
auto res = cli_.Get("/e%6edwith%");
ASSERT_TRUE(res);
@@ -3746,6 +3908,32 @@ TEST_F(ServerTest, PlusSignEncoding) {
EXPECT_EQ("a +b", res->body);
}
TEST_F(ServerTest, HeaderCountSecurityTest) {
// This test simulates a potential DoS attack using many headers
// to verify our security fix prevents memory exhaustion
httplib::Headers attack_headers;
// Attempt to add many headers like an attacker would (200 headers to far exceed limit)
for (int i = 0; i < 200; i++) {
std::string name = "X-Attack-Header-" + std::to_string(i);
std::string value = "attack_payload_" + std::to_string(i);
attack_headers.emplace(name, value);
}
// Try to POST with excessive headers
auto res = cli_.Post("/", attack_headers, "test_data", "text/plain");
// Should either fail or return 400 Bad Request due to security limit
if (res) {
// If we get a response, it should be 400 Bad Request
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
} else {
// Request failed, which is the expected behavior for DoS protection
EXPECT_FALSE(res);
}
}
TEST_F(ServerTest, MultipartFormData) {
MultipartFormDataItems items = {
{"text1", "text default", "", ""},
@@ -3919,6 +4107,16 @@ TEST_F(ServerTest, GetStreamedWithRangeMultipart) {
EXPECT_EQ("267", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(267U, res->body.size());
// Check that both range contents are present
EXPECT_TRUE(res->body.find("bc\r\n") != std::string::npos);
EXPECT_TRUE(res->body.find("ef\r\n") != std::string::npos);
// Check that Content-Range headers are present for both ranges
EXPECT_TRUE(res->body.find("Content-Range: bytes 1-2/7") !=
std::string::npos);
EXPECT_TRUE(res->body.find("Content-Range: bytes 4-5/7") !=
std::string::npos);
}
TEST_F(ServerTest, GetStreamedWithTooManyRanges) {
@@ -3936,14 +4134,59 @@ TEST_F(ServerTest, GetStreamedWithTooManyRanges) {
EXPECT_EQ(0U, res->body.size());
}
TEST_F(ServerTest, GetStreamedWithNonAscendingRanges) {
auto res = cli_.Get("/streamed-with-range?error",
{{make_range_header({{0, -1}, {0, -1}})}});
TEST_F(ServerTest, GetStreamedWithOverwrapping) {
auto res =
cli_.Get("/streamed-with-range", {{make_range_header({{1, 4}, {2, 5}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
EXPECT_EQ("0", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(0U, res->body.size());
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ(5U, res->body.size());
// Check that overlapping ranges are coalesced into a single range
EXPECT_EQ("bcdef", res->body);
EXPECT_EQ("bytes 1-5/7", res->get_header_value("Content-Range"));
// Should be single range, not multipart
EXPECT_TRUE(res->has_header("Content-Range"));
EXPECT_EQ("text/plain", res->get_header_value("Content-Type"));
}
TEST_F(ServerTest, GetStreamedWithNonAscendingRanges) {
auto res =
cli_.Get("/streamed-with-range", {{make_range_header({{4, 5}, {0, 2}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ(268U, res->body.size());
// Check that both range contents are present
EXPECT_TRUE(res->body.find("ef\r\n") != std::string::npos);
EXPECT_TRUE(res->body.find("abc\r\n") != std::string::npos);
// Check that Content-Range headers are present for both ranges
EXPECT_TRUE(res->body.find("Content-Range: bytes 4-5/7") !=
std::string::npos);
EXPECT_TRUE(res->body.find("Content-Range: bytes 0-2/7") !=
std::string::npos);
}
TEST_F(ServerTest, GetStreamedWithDuplicateRanges) {
auto res =
cli_.Get("/streamed-with-range", {{make_range_header({{0, 2}, {0, 2}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ(269U, res->body.size());
// Check that both duplicate range contents are present
size_t first_abc = res->body.find("abc\r\n");
EXPECT_TRUE(first_abc != std::string::npos);
size_t second_abc = res->body.find("abc\r\n", first_abc + 1);
EXPECT_TRUE(second_abc != std::string::npos);
// Check that Content-Range headers are present for both ranges
size_t first_range = res->body.find("Content-Range: bytes 0-2/7");
EXPECT_TRUE(first_range != std::string::npos);
size_t second_range =
res->body.find("Content-Range: bytes 0-2/7", first_range + 1);
EXPECT_TRUE(second_range != std::string::npos);
}
TEST_F(ServerTest, GetStreamedWithRangesMoreThanTwoOverwrapping) {
@@ -4049,6 +4292,19 @@ TEST_F(ServerTest, GetWithRange4) {
EXPECT_EQ(std::string("fg"), res->body);
}
TEST_F(ServerTest, GetWithRange5) {
auto res = cli_.Get("/with-range", {
make_range_header({{0, 5}}),
{"Accept-Encoding", ""},
});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ("6", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
EXPECT_EQ("bytes 0-5/7", res->get_header_value("Content-Range"));
EXPECT_EQ(std::string("abcdef"), res->body);
}
TEST_F(ServerTest, GetWithRangeOffsetGreaterThanContent) {
auto res = cli_.Get("/with-range", {{make_range_header({{10000, 20000}})}});
ASSERT_TRUE(res);
@@ -5716,6 +5972,41 @@ TEST(KeepAliveTest, ReadTimeout) {
EXPECT_EQ("b", resb->body);
}
TEST(KeepAliveTest, MaxCount) {
size_t keep_alive_max_count = 3;
Server svr;
svr.set_keep_alive_max_count(keep_alive_max_count);
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
res.set_content("Hello World!", "text/plain");
});
auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, PORT);
cli.set_keep_alive(true);
for (size_t i = 0; i < 5; i++) {
auto result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(StatusCode::OK_200, result->status);
if (i == keep_alive_max_count - 1) {
EXPECT_EQ("close", result->get_header_value("Connection"));
} else {
EXPECT_FALSE(result->has_header("Connection"));
}
}
}
TEST(KeepAliveTest, Issue1041) {
Server svr;
svr.set_keep_alive_timeout(3);
@@ -7902,6 +8193,68 @@ TEST(MultipartFormDataTest, ContentLength) {
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
TEST(MultipartFormDataTest, AccessPartHeaders) {
auto handled = false;
Server svr;
svr.Post("/test", [&](const Request &req, Response &) {
ASSERT_EQ(2u, req.files.size());
auto it = req.files.begin();
ASSERT_EQ("text1", it->second.name);
ASSERT_EQ("text1", it->second.content);
ASSERT_EQ(1, it->second.headers.count("Content-Length"));
auto content_length = it->second.headers.find("CONTENT-length");
ASSERT_EQ("5", content_length->second);
ASSERT_EQ(3, it->second.headers.size());
++it;
ASSERT_EQ("text2", it->second.name);
ASSERT_EQ("text2", it->second.content);
auto &headers = it->second.headers;
ASSERT_EQ(3, headers.size());
auto custom_header = headers.find("x-whatever");
ASSERT_TRUE(custom_header != headers.end());
ASSERT_NE("customvalue", custom_header->second);
ASSERT_EQ("CustomValue", custom_header->second);
ASSERT_TRUE(headers.find("X-Test") == headers.end()); // text1 header
handled = true;
});
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_TRUE(handled);
});
svr.wait_until_ready();
auto req = "POST /test HTTP/1.1\r\n"
"Content-Type: multipart/form-data;boundary=--------\r\n"
"Content-Length: 232\r\n"
"\r\n----------\r\n"
"Content-Disposition: form-data; name=\"text1\"\r\n"
"Content-Length: 5\r\n"
"X-Test: 1\r\n"
"\r\n"
"text1"
"\r\n----------\r\n"
"Content-Disposition: form-data; name=\"text2\"\r\n"
"Content-Type: text/plain\r\n"
"X-Whatever: CustomValue\r\n"
"\r\n"
"text2"
"\r\n------------\r\n"
"That should be disregarded. Not even read";
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
#endif
TEST(TaskQueueTest, IncreaseAtomicInteger) {
@@ -8623,7 +8976,7 @@ TEST(MaxTimeoutTest, ContentStream) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
TEST(MaxTimeoutTest, ContentStreamSSL) {
time_t timeout = 2000;
time_t threshold = 500; // SSL_shutdown is slow on some operating systems.
time_t threshold = 1200; // SSL_shutdown is slow on some operating systems.
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);