mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6c55c6030 | |||
| 28dcf379e8 | |||
| 91e79e9a63 | |||
| 27879c4874 | |||
| 08a0452fb2 | |||
| 3a1f379e75 | |||
| fd8da4d8e4 | |||
| 365cbe37fa | |||
| 4a7aae5469 | |||
| fd324e1412 | |||
| 366eda72dc | |||
| c216dc94d2 | |||
| 61893a00a4 | |||
| 3af7f2c161 | |||
| a0de42ebc4 | |||
| 7b752106ac | |||
| 9589519d58 | |||
| caf7c55785 | |||
| 9e4aed482e | |||
| 65d6316d65 | |||
| 3e3a8cc02f | |||
| b7e33b08f1 | |||
| 0dbe8ba144 | |||
| dbc4af819a | |||
| 7dbf5471ce | |||
| 72b35befb2 | |||
| 65ce51aed7 |
+18
-2
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user