mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 073b587962 | |||
| 96785eea21 | |||
| 3093bdd9ab | |||
| 6607a6a592 | |||
| 831b64bdeb | |||
| 32c82492de | |||
| b7e02de4a7 | |||
| a9359df42e | |||
| 9a97e948f0 | |||
| 6fd97aeca0 | |||
| 05540e4d50 | |||
| ceefc14e7d |
@@ -4,6 +4,7 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs-src/**'
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
|
||||
+22
-16
@@ -242,27 +242,33 @@ endif()
|
||||
# zstd < 1.5.6 does not provide the CMake imported target `zstd::libzstd`.
|
||||
# Older versions must be consumed via their pkg-config file.
|
||||
if(HTTPLIB_REQUIRE_ZSTD)
|
||||
find_package(zstd 1.5.6 CONFIG)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
|
||||
if (NOT TARGET zstd::libzstd)
|
||||
find_package(zstd 1.5.6 CONFIG)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
add_library(zstd::libzstd ALIAS PkgConfig::zstd)
|
||||
endif()
|
||||
endif()
|
||||
set(HTTPLIB_IS_USING_ZSTD TRUE)
|
||||
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
|
||||
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(zstd QUIET IMPORTED_TARGET libzstd)
|
||||
if (TARGET zstd::libzstd)
|
||||
set(HTTPLIB_IS_USING_ZSTD TRUE)
|
||||
else()
|
||||
find_package(zstd 1.5.6 CONFIG 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)
|
||||
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()
|
||||
# Both find_package and PkgConf set a XXX_FOUND var
|
||||
set(HTTPLIB_IS_USING_ZSTD ${zstd_FOUND})
|
||||
endif()
|
||||
|
||||
# Used for default, common dirs that the end-user can change (if needed)
|
||||
@@ -317,13 +323,13 @@ if(HTTPLIB_COMPILE)
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
|
||||
)
|
||||
|
||||
|
||||
# Add C++20 module support if requested
|
||||
# Include from separate file to prevent parse errors on older CMake versions
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28")
|
||||
include(cmake/modules.cmake)
|
||||
endif()
|
||||
|
||||
|
||||
set_target_properties(${PROJECT_NAME}
|
||||
PROPERTIES
|
||||
VERSION ${${PROJECT_NAME}_VERSION}
|
||||
|
||||
@@ -461,7 +461,7 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
||||
|
||||
### Response user data
|
||||
|
||||
`res.user_data` is a `std::map<std::string, httplib::any>` that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||
|
||||
```cpp
|
||||
struct AuthContext {
|
||||
@@ -471,12 +471,12 @@ struct AuthContext {
|
||||
|
||||
svr.set_pre_routing_handler([](const auto& req, auto& res) {
|
||||
auto token = req.get_header_value("Authorization");
|
||||
res.user_data["auth"] = AuthContext{decode_token(token)};
|
||||
res.user_data.set("auth", AuthContext{decode_token(token)});
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto& /*req*/, auto& res) {
|
||||
auto* ctx = httplib::any_cast<AuthContext>(&res.user_data["auth"]);
|
||||
auto* ctx = res.user_data.get<AuthContext>("auth");
|
||||
if (!ctx) {
|
||||
res.status = StatusCode::Unauthorized_401;
|
||||
return;
|
||||
@@ -485,8 +485,6 @@ svr.Get("/me", [](const auto& /*req*/, auto& res) {
|
||||
});
|
||||
```
|
||||
|
||||
`httplib::any` mirrors the C++17 `std::any` API. On C++17 and later it is an alias for `std::any`; on C++11/14 a compatible implementation is provided.
|
||||
|
||||
### Form data handling
|
||||
|
||||
#### URL-encoded form data ('application/x-www-form-urlencoded')
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.39.0"
|
||||
version = "0.41.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -164,13 +164,13 @@ Use `res.user_data` to pass data from middleware to handlers. This is useful for
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto &req, auto &res) {
|
||||
res.user_data["auth_user"] = std::string("alice");
|
||||
res.user_data.set("auth_user", std::string("alice"));
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto &req, auto &res) {
|
||||
auto user = std::any_cast<std::string>(res.user_data.at("auth_user"));
|
||||
res.set_content("Hello, " + user, "text/plain");
|
||||
auto *user = res.user_data.get<std::string>("auth_user");
|
||||
res.set_content("Hello, " + *user, "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -164,13 +164,13 @@ svr.set_post_routing_handler([](const auto &req, auto &res) {
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto &req, auto &res) {
|
||||
res.user_data["auth_user"] = std::string("alice");
|
||||
res.user_data.set("auth_user", std::string("alice"));
|
||||
return httplib::Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const auto &req, auto &res) {
|
||||
auto user = std::any_cast<std::string>(res.user_data.at("auth_user"));
|
||||
res.set_content("Hello, " + user, "text/plain");
|
||||
auto *user = res.user_data.get<std::string>("auth_user");
|
||||
res.set_content("Hello, " + *user, "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.39.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002700"
|
||||
#define CPPHTTPLIB_VERSION "0.41.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002900"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -333,9 +333,6 @@ using socket_t = int;
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#if __cplusplus >= 201703L
|
||||
#include <any>
|
||||
#endif
|
||||
|
||||
// On macOS with a TLS backend, enable Keychain root certificates by default
|
||||
// unless the user explicitly opts out.
|
||||
@@ -701,6 +698,93 @@ inline bool parse_port(const std::string &s, int &port) {
|
||||
return parse_port(s.data(), s.size(), port);
|
||||
}
|
||||
|
||||
struct UrlComponents {
|
||||
std::string scheme;
|
||||
std::string host;
|
||||
std::string port;
|
||||
std::string path;
|
||||
std::string query;
|
||||
};
|
||||
|
||||
inline bool parse_url(const std::string &url, UrlComponents &uc) {
|
||||
uc = {};
|
||||
size_t pos = 0;
|
||||
|
||||
auto sep = url.find("://");
|
||||
if (sep != std::string::npos) {
|
||||
uc.scheme = url.substr(0, sep);
|
||||
|
||||
// Scheme must be [a-z]+ only
|
||||
if (uc.scheme.empty()) { return false; }
|
||||
for (auto c : uc.scheme) {
|
||||
if (c < 'a' || c > 'z') { return false; }
|
||||
}
|
||||
|
||||
pos = sep + 3;
|
||||
} else if (url.compare(0, 2, "//") == 0) {
|
||||
pos = 2;
|
||||
}
|
||||
|
||||
auto has_authority_prefix = pos > 0;
|
||||
auto has_authority = has_authority_prefix || (!url.empty() && url[0] != '/' &&
|
||||
url[0] != '?' && url[0] != '#');
|
||||
if (has_authority) {
|
||||
if (pos < url.size() && url[pos] == '[') {
|
||||
auto close = url.find(']', pos);
|
||||
if (close == std::string::npos) { return false; }
|
||||
uc.host = url.substr(pos + 1, close - pos - 1);
|
||||
|
||||
// IPv6 host must be [a-fA-F0-9:]+ only
|
||||
if (uc.host.empty()) { return false; }
|
||||
for (auto c : uc.host) {
|
||||
if (!((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') ||
|
||||
(c >= '0' && c <= '9') || c == ':')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
pos = close + 1;
|
||||
} else {
|
||||
auto end = url.find_first_of(":/?#", pos);
|
||||
if (end == std::string::npos) { end = url.size(); }
|
||||
uc.host = url.substr(pos, end - pos);
|
||||
pos = end;
|
||||
}
|
||||
|
||||
if (pos < url.size() && url[pos] == ':') {
|
||||
++pos;
|
||||
auto end = url.find_first_of("/?#", pos);
|
||||
if (end == std::string::npos) { end = url.size(); }
|
||||
uc.port = url.substr(pos, end - pos);
|
||||
pos = end;
|
||||
}
|
||||
|
||||
// Without :// or //, the entire input must be consumed as host[:port].
|
||||
// If there is leftover (path, query, etc.), this is not a valid
|
||||
// host[:port] string — clear and reparse as a plain path.
|
||||
if (!has_authority_prefix && pos < url.size()) {
|
||||
uc.host.clear();
|
||||
uc.port.clear();
|
||||
pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos < url.size() && url[pos] != '?' && url[pos] != '#') {
|
||||
auto end = url.find_first_of("?#", pos);
|
||||
if (end == std::string::npos) { end = url.size(); }
|
||||
uc.path = url.substr(pos, end - pos);
|
||||
pos = end;
|
||||
}
|
||||
|
||||
if (pos < url.size() && url[pos] == '?') {
|
||||
auto end = url.find('#', pos);
|
||||
if (end == std::string::npos) { end = url.size(); }
|
||||
uc.query = url.substr(pos, end - pos);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
enum SSLVerifierResponse {
|
||||
@@ -797,42 +881,15 @@ using Match = std::smatch;
|
||||
using DownloadProgress = std::function<bool(size_t current, size_t total)>;
|
||||
using UploadProgress = std::function<bool(size_t current, size_t total)>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// httplib::any — type-erased value container (C++11 compatible)
|
||||
// On C++17+ builds, thin wrappers around std::any are provided.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#if __cplusplus >= 201703L
|
||||
|
||||
using any = std::any;
|
||||
using bad_any_cast = std::bad_any_cast;
|
||||
|
||||
template <typename T> T any_cast(const any &a) { return std::any_cast<T>(a); }
|
||||
template <typename T> T any_cast(any &a) { return std::any_cast<T>(a); }
|
||||
template <typename T> T any_cast(any &&a) {
|
||||
return std::any_cast<T>(std::move(a));
|
||||
}
|
||||
template <typename T> const T *any_cast(const any *a) noexcept {
|
||||
return std::any_cast<T>(a);
|
||||
}
|
||||
template <typename T> T *any_cast(any *a) noexcept {
|
||||
return std::any_cast<T>(a);
|
||||
}
|
||||
|
||||
#else // C++11/14 implementation
|
||||
|
||||
class bad_any_cast : public std::bad_cast {
|
||||
public:
|
||||
const char *what() const noexcept override { return "bad any_cast"; }
|
||||
};
|
||||
|
||||
/*
|
||||
* detail: type-erased storage used by UserData.
|
||||
* ABI-stable regardless of C++ standard — always uses this custom
|
||||
* implementation instead of std::any.
|
||||
*/
|
||||
namespace detail {
|
||||
|
||||
using any_type_id = const void *;
|
||||
|
||||
// Returns a unique per-type ID without RTTI.
|
||||
// The static address is stable across TUs because function templates are
|
||||
// implicitly inline and the ODR merges their statics into one.
|
||||
template <typename T> any_type_id any_typeid() noexcept {
|
||||
static const char id = 0;
|
||||
return &id;
|
||||
@@ -855,89 +912,60 @@ template <typename T> struct any_value final : any_storage {
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class any {
|
||||
std::unique_ptr<detail::any_storage> storage_;
|
||||
|
||||
class UserData {
|
||||
public:
|
||||
any() noexcept = default;
|
||||
any(const any &o) : storage_(o.storage_ ? o.storage_->clone() : nullptr) {}
|
||||
any(any &&) noexcept = default;
|
||||
any &operator=(const any &o) {
|
||||
storage_ = o.storage_ ? o.storage_->clone() : nullptr;
|
||||
return *this;
|
||||
UserData() = default;
|
||||
UserData(UserData &&) noexcept = default;
|
||||
UserData &operator=(UserData &&) noexcept = default;
|
||||
|
||||
UserData(const UserData &o) {
|
||||
for (const auto &e : o.entries_) {
|
||||
if (e.second) { entries_[e.first] = e.second->clone(); }
|
||||
}
|
||||
}
|
||||
any &operator=(any &&) noexcept = default;
|
||||
|
||||
template <
|
||||
typename T, typename D = typename std::decay<T>::type,
|
||||
typename std::enable_if<!std::is_same<D, any>::value, int>::type = 0>
|
||||
any(T &&v) : storage_(new detail::any_value<D>(std::forward<T>(v))) {}
|
||||
|
||||
template <
|
||||
typename T, typename D = typename std::decay<T>::type,
|
||||
typename std::enable_if<!std::is_same<D, any>::value, int>::type = 0>
|
||||
any &operator=(T &&v) {
|
||||
storage_.reset(new detail::any_value<D>(std::forward<T>(v)));
|
||||
UserData &operator=(const UserData &o) {
|
||||
if (this != &o) {
|
||||
entries_.clear();
|
||||
for (const auto &e : o.entries_) {
|
||||
if (e.second) { entries_[e.first] = e.second->clone(); }
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool has_value() const noexcept { return storage_ != nullptr; }
|
||||
void reset() noexcept { storage_.reset(); }
|
||||
template <typename T> void set(const std::string &key, T &&value) {
|
||||
using D = typename std::decay<T>::type;
|
||||
entries_[key].reset(new detail::any_value<D>(std::forward<T>(value)));
|
||||
}
|
||||
|
||||
template <typename T> friend T *any_cast(any *a) noexcept;
|
||||
template <typename T> friend const T *any_cast(const any *a) noexcept;
|
||||
template <typename T> T *get(const std::string &key) noexcept {
|
||||
auto it = entries_.find(key);
|
||||
if (it == entries_.end() || !it->second) { return nullptr; }
|
||||
if (it->second->type_id() != detail::any_typeid<T>()) { return nullptr; }
|
||||
return &static_cast<detail::any_value<T> *>(it->second.get())->value;
|
||||
}
|
||||
|
||||
template <typename T> const T *get(const std::string &key) const noexcept {
|
||||
auto it = entries_.find(key);
|
||||
if (it == entries_.end() || !it->second) { return nullptr; }
|
||||
if (it->second->type_id() != detail::any_typeid<T>()) { return nullptr; }
|
||||
return &static_cast<const detail::any_value<T> *>(it->second.get())->value;
|
||||
}
|
||||
|
||||
bool has(const std::string &key) const noexcept {
|
||||
return entries_.find(key) != entries_.end();
|
||||
}
|
||||
|
||||
void erase(const std::string &key) { entries_.erase(key); }
|
||||
|
||||
void clear() noexcept { entries_.clear(); }
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::unique_ptr<detail::any_storage>>
|
||||
entries_;
|
||||
};
|
||||
|
||||
template <typename T> T *any_cast(any *a) noexcept {
|
||||
if (!a || !a->storage_) { return nullptr; }
|
||||
if (a->storage_->type_id() != detail::any_typeid<T>()) { return nullptr; }
|
||||
return &static_cast<detail::any_value<T> *>(a->storage_.get())->value;
|
||||
}
|
||||
|
||||
template <typename T> const T *any_cast(const any *a) noexcept {
|
||||
if (!a || !a->storage_) { return nullptr; }
|
||||
if (a->storage_->type_id() != detail::any_typeid<T>()) { return nullptr; }
|
||||
return &static_cast<const detail::any_value<T> *>(a->storage_.get())->value;
|
||||
}
|
||||
|
||||
template <typename T> T any_cast(const any &a) {
|
||||
using U =
|
||||
typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
const U *p = any_cast<U>(&a);
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
if (!p) { throw bad_any_cast{}; }
|
||||
#else
|
||||
if (!p) { std::abort(); }
|
||||
#endif
|
||||
return static_cast<T>(*p);
|
||||
}
|
||||
|
||||
template <typename T> T any_cast(any &a) {
|
||||
using U =
|
||||
typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
U *p = any_cast<U>(&a);
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
if (!p) { throw bad_any_cast{}; }
|
||||
#else
|
||||
if (!p) { std::abort(); }
|
||||
#endif
|
||||
return static_cast<T>(*p);
|
||||
}
|
||||
|
||||
template <typename T> T any_cast(any &&a) {
|
||||
using U =
|
||||
typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
U *p = any_cast<U>(&a);
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
if (!p) { throw bad_any_cast{}; }
|
||||
#else
|
||||
if (!p) { std::abort(); }
|
||||
#endif
|
||||
return static_cast<T>(std::move(*p));
|
||||
}
|
||||
|
||||
#endif // __cplusplus >= 201703L
|
||||
|
||||
struct Response;
|
||||
using ResponseHandler = std::function<bool(const Response &response)>;
|
||||
|
||||
@@ -1270,6 +1298,7 @@ struct Request {
|
||||
bool is_multipart_form_data() const;
|
||||
|
||||
// private members...
|
||||
bool body_consumed_ = false;
|
||||
size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;
|
||||
size_t content_length_ = 0;
|
||||
ContentProvider content_provider_;
|
||||
@@ -1296,7 +1325,7 @@ struct Response {
|
||||
|
||||
// User-defined context — set by pre-routing/pre-request handlers and read
|
||||
// by route handlers to pass arbitrary data (e.g. decoded auth tokens).
|
||||
std::map<std::string, any> user_data;
|
||||
UserData user_data;
|
||||
|
||||
bool has_header(const std::string &key) const;
|
||||
std::string get_header_value(const std::string &key, const char *def = "",
|
||||
@@ -1479,6 +1508,8 @@ using SocketOptions = std::function<void(socket_t sock)>;
|
||||
|
||||
void default_socket_options(socket_t sock);
|
||||
|
||||
bool set_socket_opt(socket_t sock, int level, int optname, int optval);
|
||||
|
||||
const char *status_message(int status);
|
||||
|
||||
std::string to_string(Error error);
|
||||
@@ -1568,6 +1599,13 @@ ssize_t write_headers(Stream &strm, const Headers &headers);
|
||||
bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec,
|
||||
time_t usec);
|
||||
|
||||
size_t get_multipart_content_length(const UploadFormDataItems &items,
|
||||
const std::string &boundary);
|
||||
|
||||
ContentProvider
|
||||
make_multipart_content_provider(const UploadFormDataItems &items,
|
||||
const std::string &boundary);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class Server {
|
||||
@@ -4337,10 +4375,6 @@ inline bool set_socket_opt_impl(socket_t sock, int level, int optname,
|
||||
optlen) == 0;
|
||||
}
|
||||
|
||||
inline bool set_socket_opt(socket_t sock, int level, int optname, int optval) {
|
||||
return set_socket_opt_impl(sock, level, optname, &optval, sizeof(optval));
|
||||
}
|
||||
|
||||
inline bool set_socket_opt_time(socket_t sock, int level, int optname,
|
||||
time_t sec, time_t usec) {
|
||||
#ifdef _WIN32
|
||||
@@ -6088,7 +6122,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
|
||||
#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);
|
||||
set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0);
|
||||
#endif
|
||||
|
||||
bool dummy;
|
||||
@@ -8243,6 +8277,7 @@ make_multipart_content_provider(const UploadFormDataItems &items,
|
||||
struct MultipartState {
|
||||
std::vector<std::string> owned;
|
||||
std::vector<MultipartSegment> segs;
|
||||
std::vector<char> buf = std::vector<char>(CPPHTTPLIB_SEND_BUFSIZ);
|
||||
};
|
||||
auto state = std::make_shared<MultipartState>();
|
||||
state->owned = std::move(owned);
|
||||
@@ -8251,19 +8286,49 @@ make_multipart_content_provider(const UploadFormDataItems &items,
|
||||
state->segs = std::move(segs);
|
||||
|
||||
return [state](size_t offset, size_t length, DataSink &sink) -> bool {
|
||||
// Buffer multiple small segments into fewer, larger writes to avoid
|
||||
// excessive TCP packets when there are many form data items (#2410)
|
||||
auto &buf = state->buf;
|
||||
auto buf_size = buf.size();
|
||||
size_t buf_len = 0;
|
||||
size_t remaining = length;
|
||||
|
||||
// Find the first segment containing 'offset'
|
||||
size_t pos = 0;
|
||||
for (const auto &seg : state->segs) {
|
||||
// Loop invariant: pos <= offset (proven by advancing pos only when
|
||||
// offset - pos >= seg.size, i.e., the segment doesn't contain offset)
|
||||
if (seg.size > 0 && offset - pos < seg.size) {
|
||||
size_t seg_offset = offset - pos;
|
||||
size_t available = seg.size - seg_offset;
|
||||
size_t to_write = (std::min)(available, length);
|
||||
return sink.write(seg.data + seg_offset, to_write);
|
||||
}
|
||||
size_t seg_idx = 0;
|
||||
for (; seg_idx < state->segs.size(); seg_idx++) {
|
||||
const auto &seg = state->segs[seg_idx];
|
||||
if (seg.size > 0 && offset - pos < seg.size) { break; }
|
||||
pos += seg.size;
|
||||
}
|
||||
return true; // past end (shouldn't be reached when content_length is exact)
|
||||
|
||||
size_t seg_offset = (seg_idx < state->segs.size()) ? offset - pos : 0;
|
||||
|
||||
for (; seg_idx < state->segs.size() && remaining > 0; seg_idx++) {
|
||||
const auto &seg = state->segs[seg_idx];
|
||||
size_t available = seg.size - seg_offset;
|
||||
size_t to_copy = (std::min)(available, remaining);
|
||||
const char *src = seg.data + seg_offset;
|
||||
seg_offset = 0; // only the first segment has a non-zero offset
|
||||
|
||||
while (to_copy > 0) {
|
||||
size_t space = buf_size - buf_len;
|
||||
size_t chunk = (std::min)(to_copy, space);
|
||||
std::memcpy(buf.data() + buf_len, src, chunk);
|
||||
buf_len += chunk;
|
||||
src += chunk;
|
||||
to_copy -= chunk;
|
||||
remaining -= chunk;
|
||||
|
||||
if (buf_len == buf_size) {
|
||||
if (!sink.write(buf.data(), buf_len)) { return false; }
|
||||
buf_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buf_len > 0) { return sink.write(buf.data(), buf_len); }
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9134,13 +9199,18 @@ inline bool setup_client_tls_session(const std::string &host, tls::ctx_t &ctx,
|
||||
*/
|
||||
|
||||
inline void default_socket_options(socket_t sock) {
|
||||
detail::set_socket_opt(sock, SOL_SOCKET,
|
||||
set_socket_opt(sock, SOL_SOCKET,
|
||||
#ifdef SO_REUSEPORT
|
||||
SO_REUSEPORT,
|
||||
SO_REUSEPORT,
|
||||
#else
|
||||
SO_REUSEADDR,
|
||||
SO_REUSEADDR,
|
||||
#endif
|
||||
1);
|
||||
1);
|
||||
}
|
||||
|
||||
inline bool set_socket_opt(socket_t sock, int level, int optname, int optval) {
|
||||
return detail::set_socket_opt_impl(sock, level, optname, &optval,
|
||||
sizeof(optval));
|
||||
}
|
||||
|
||||
inline std::string get_bearer_token_auth(const Request &req) {
|
||||
@@ -11288,6 +11358,8 @@ inline bool Server::read_content_core(
|
||||
return false;
|
||||
}
|
||||
|
||||
req.body_consumed_ = true;
|
||||
|
||||
if (req.is_multipart_form_data()) {
|
||||
if (!multipart_form_data_parser.is_valid()) {
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
@@ -11558,9 +11630,7 @@ inline bool Server::listen_internal() {
|
||||
detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO,
|
||||
write_timeout_sec_, write_timeout_usec_);
|
||||
|
||||
if (tcp_nodelay_) {
|
||||
detail::set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
|
||||
}
|
||||
if (tcp_nodelay_) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); }
|
||||
|
||||
if (!task_queue->enqueue(
|
||||
[this, sock]() { process_and_close_socket(sock); })) {
|
||||
@@ -11906,8 +11976,19 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
|
||||
// any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
|
||||
// tolerated for compatibility with existing clients.
|
||||
if (req.get_header_value_u64("Content-Length") > 0 &&
|
||||
req.has_header("Transfer-Encoding")) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// Check if the request URI doesn't exceed the limit
|
||||
if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::UriTooLong_414;
|
||||
output_error_log(Error::ExceedUriMaxLength, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
@@ -11936,6 +12017,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
if (req.has_header("Accept")) {
|
||||
const auto &accept_header = req.get_header_value("Accept");
|
||||
if (!detail::parse_accept_header(accept_header, req.accept_content_types)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
output_error_log(Error::HTTPParsing, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
@@ -11945,6 +12027,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
if (req.has_header("Range")) {
|
||||
const auto &range_header_value = req.get_header_value("Range");
|
||||
if (!detail::parse_range_header(range_header_value, req.ranges)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::RangeNotSatisfiable_416;
|
||||
output_error_log(Error::InvalidRangeHeader, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
@@ -12072,6 +12155,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
}
|
||||
}
|
||||
#endif
|
||||
auto ret = false;
|
||||
if (routed) {
|
||||
if (res.status == -1) {
|
||||
res.status = req.ranges.empty() ? StatusCode::OK_200
|
||||
@@ -12079,6 +12163,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
}
|
||||
|
||||
// Serve file content by using a content provider
|
||||
auto file_open_error = false;
|
||||
if (!res.file_content_path_.empty()) {
|
||||
const auto &path = res.file_content_path_;
|
||||
auto mm = std::make_shared<detail::mmap>(path.c_str());
|
||||
@@ -12088,37 +12173,53 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
res.content_provider_ = nullptr;
|
||||
res.status = StatusCode::NotFound_404;
|
||||
output_error_log(Error::OpenFile, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
file_open_error = true;
|
||||
} else {
|
||||
auto content_type = res.file_content_content_type_;
|
||||
if (content_type.empty()) {
|
||||
content_type = detail::find_content_type(
|
||||
path, file_extension_and_mimetype_map_, default_file_mimetype_);
|
||||
}
|
||||
|
||||
auto content_type = res.file_content_content_type_;
|
||||
if (content_type.empty()) {
|
||||
content_type = detail::find_content_type(
|
||||
path, file_extension_and_mimetype_map_, default_file_mimetype_);
|
||||
res.set_content_provider(
|
||||
mm->size(), content_type,
|
||||
[mm](size_t offset, size_t length, DataSink &sink) -> bool {
|
||||
sink.write(mm->data() + offset, length);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
res.set_content_provider(
|
||||
mm->size(), content_type,
|
||||
[mm](size_t offset, size_t length, DataSink &sink) -> bool {
|
||||
sink.write(mm->data() + offset, length);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (detail::range_error(req, res)) {
|
||||
if (file_open_error) {
|
||||
ret = write_response(strm, close_connection, req, res);
|
||||
} else if (detail::range_error(req, res)) {
|
||||
res.body.clear();
|
||||
res.content_length_ = 0;
|
||||
res.content_provider_ = nullptr;
|
||||
res.status = StatusCode::RangeNotSatisfiable_416;
|
||||
return write_response(strm, close_connection, req, res);
|
||||
ret = write_response(strm, close_connection, req, res);
|
||||
} else {
|
||||
ret = write_response_with_content(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
return write_response_with_content(strm, close_connection, req, res);
|
||||
} else {
|
||||
if (res.status == -1) { res.status = StatusCode::NotFound_404; }
|
||||
|
||||
return write_response(strm, close_connection, req, res);
|
||||
ret = write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// Drain any unconsumed request body to prevent request smuggling on
|
||||
// keep-alive connections.
|
||||
if (!req.body_consumed_ && detail::expect_content(req)) {
|
||||
int drain_status = 200; // required by read_content signature
|
||||
if (!detail::read_content(
|
||||
strm, req, payload_max_length_, drain_status, nullptr,
|
||||
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
||||
// Body exceeds payload limit or read error — close the connection
|
||||
// to prevent leftover bytes from being misinterpreted.
|
||||
connection_closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline bool Server::is_valid() const { return true; }
|
||||
@@ -12926,20 +13027,21 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
auto location = res.get_header_value("location");
|
||||
if (location.empty()) { return false; }
|
||||
|
||||
thread_local const std::regex re(
|
||||
R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)");
|
||||
detail::UrlComponents uc;
|
||||
if (!detail::parse_url(location, uc)) { return false; }
|
||||
|
||||
std::smatch m;
|
||||
if (!std::regex_match(location, m, re)) { return false; }
|
||||
// Only follow http/https redirects
|
||||
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto scheme = is_ssl() ? "https" : "http";
|
||||
|
||||
auto next_scheme = m[1].str();
|
||||
auto next_host = m[2].str();
|
||||
if (next_host.empty()) { next_host = m[3].str(); }
|
||||
auto port_str = m[4].str();
|
||||
auto next_path = m[5].str();
|
||||
auto next_query = m[6].str();
|
||||
auto next_scheme = std::move(uc.scheme);
|
||||
auto next_host = std::move(uc.host);
|
||||
auto port_str = std::move(uc.port);
|
||||
auto next_path = std::move(uc.path);
|
||||
auto next_query = std::move(uc.query);
|
||||
|
||||
auto next_port = port_;
|
||||
if (!port_str.empty()) {
|
||||
@@ -12952,7 +13054,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
if (next_host.empty()) { next_host = host_; }
|
||||
if (next_path.empty()) { next_path = "/"; }
|
||||
|
||||
auto path = decode_query_component(next_path, true) + next_query;
|
||||
auto path = decode_path_component(next_path) + next_query;
|
||||
|
||||
// Same host redirect - use current client
|
||||
if (next_scheme == scheme && next_host == host_ && next_port == port_) {
|
||||
@@ -14676,12 +14778,9 @@ inline Client::Client(const std::string &scheme_host_port)
|
||||
inline Client::Client(const std::string &scheme_host_port,
|
||||
const std::string &client_cert_path,
|
||||
const std::string &client_key_path) {
|
||||
const static std::regex re(
|
||||
R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)");
|
||||
|
||||
std::smatch m;
|
||||
if (std::regex_match(scheme_host_port, m, re)) {
|
||||
auto scheme = m[1].str();
|
||||
detail::UrlComponents uc;
|
||||
if (detail::parse_url(scheme_host_port, uc) && !uc.host.empty()) {
|
||||
auto &scheme = uc.scheme;
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (!scheme.empty() && (scheme != "http" && scheme != "https")) {
|
||||
@@ -14697,12 +14796,10 @@ inline Client::Client(const std::string &scheme_host_port,
|
||||
|
||||
auto is_ssl = scheme == "https";
|
||||
|
||||
auto host = m[2].str();
|
||||
if (host.empty()) { host = m[3].str(); }
|
||||
auto host = std::move(uc.host);
|
||||
|
||||
auto port_str = m[4].str();
|
||||
auto port = is_ssl ? 443 : 80;
|
||||
if (!port_str.empty() && !detail::parse_port(port_str, port)) { return; }
|
||||
if (!uc.port.empty() && !detail::parse_port(uc.port, port)) { return; }
|
||||
|
||||
if (is_ssl) {
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
@@ -20109,12 +20206,10 @@ inline bool WebSocket::is_open() const { return !closed_; }
|
||||
inline WebSocketClient::WebSocketClient(
|
||||
const std::string &scheme_host_port_path, const Headers &headers)
|
||||
: headers_(headers) {
|
||||
const static std::regex re(
|
||||
R"(([a-z]+):\/\/(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?(\/.*))");
|
||||
|
||||
std::smatch m;
|
||||
if (std::regex_match(scheme_host_port_path, m, re)) {
|
||||
auto scheme = m[1].str();
|
||||
detail::UrlComponents uc;
|
||||
if (detail::parse_url(scheme_host_port_path, uc) && !uc.scheme.empty() &&
|
||||
!uc.host.empty() && !uc.path.empty()) {
|
||||
auto &scheme = uc.scheme;
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (scheme != "ws" && scheme != "wss") {
|
||||
@@ -20130,14 +20225,12 @@ inline WebSocketClient::WebSocketClient(
|
||||
|
||||
auto is_ssl = scheme == "wss";
|
||||
|
||||
host_ = m[2].str();
|
||||
if (host_.empty()) { host_ = m[3].str(); }
|
||||
host_ = std::move(uc.host);
|
||||
|
||||
auto port_str = m[4].str();
|
||||
port_ = is_ssl ? 443 : 80;
|
||||
if (!port_str.empty() && !detail::parse_port(port_str, port_)) { return; }
|
||||
if (!uc.port.empty() && !detail::parse_port(uc.port, port_)) { return; }
|
||||
|
||||
path_ = m[5].str();
|
||||
path_ = std::move(uc.path);
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
is_ssl_ = is_ssl;
|
||||
|
||||
@@ -13,8 +13,10 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = /usr/local/opt/brotli
|
||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer
|
||||
|
||||
# Runs all the tests and also fuzz tests against seed corpus.
|
||||
all : server_fuzzer
|
||||
all : $(FUZZERS)
|
||||
./server_fuzzer corpus/*
|
||||
|
||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||
@@ -23,5 +25,11 @@ server_fuzzer : server_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
||||
|
||||
header_parser_fuzzer : header_parser_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
url_parser_fuzzer : url_parser_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
clean:
|
||||
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size < 2) return 0;
|
||||
|
||||
uint8_t selector = data[0];
|
||||
const char *payload = reinterpret_cast<const char *>(data + 1);
|
||||
size_t payload_size = size - 1;
|
||||
std::string input(payload, payload_size);
|
||||
|
||||
switch (selector % 7) {
|
||||
case 0: {
|
||||
// parse_range_header
|
||||
httplib::Ranges ranges;
|
||||
httplib::detail::parse_range_header(input, ranges);
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// parse_accept_header
|
||||
std::vector<std::string> content_types;
|
||||
httplib::detail::parse_accept_header(input, content_types);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// extract_media_type with params
|
||||
std::map<std::string, std::string> params;
|
||||
httplib::detail::extract_media_type(input, ¶ms);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// parse_multipart_boundary
|
||||
std::string boundary;
|
||||
httplib::detail::parse_multipart_boundary(input, boundary);
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// parse_disposition_params
|
||||
httplib::Params params;
|
||||
httplib::detail::parse_disposition_params(input, params);
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
// parse_http_date
|
||||
httplib::detail::parse_http_date(input);
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
// can_compress_content_type
|
||||
httplib::detail::can_compress_content_type(input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size < 2) return 0;
|
||||
|
||||
// Use first byte to select which parsing function to exercise
|
||||
uint8_t selector = data[0];
|
||||
const char *payload = reinterpret_cast<const char *>(data + 1);
|
||||
size_t payload_size = size - 1;
|
||||
std::string input(payload, payload_size);
|
||||
|
||||
switch (selector % 6) {
|
||||
case 0: {
|
||||
// parse_query_text
|
||||
httplib::Params params;
|
||||
httplib::detail::parse_query_text(payload, payload_size, params);
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// decode_query_component
|
||||
httplib::decode_query_component(input, true);
|
||||
httplib::decode_query_component(input, false);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// decode_path_component
|
||||
httplib::decode_path_component(input);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// encode_query_component
|
||||
httplib::encode_query_component(input);
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// normalize_query_string
|
||||
httplib::detail::normalize_query_string(input);
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
// is_valid_path
|
||||
httplib::detail::is_valid_path(input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
+550
-127
@@ -274,7 +274,7 @@ TEST(SocketStream, wait_writable_INET) {
|
||||
sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 1);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 1));
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
int disconnected_svr_sock = -1;
|
||||
@@ -316,6 +316,19 @@ TEST(SocketStream, wait_writable_INET) {
|
||||
}
|
||||
#endif // #ifndef _WIN32
|
||||
|
||||
TEST(SetSocketOptTest, TcpNoDelay) {
|
||||
auto sock = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
ASSERT_NE(sock, INVALID_SOCKET);
|
||||
EXPECT_TRUE(set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1));
|
||||
|
||||
int val = 0;
|
||||
socklen_t len = sizeof(val);
|
||||
ASSERT_EQ(0, ::getsockopt(sock, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<char *>(&val), &len));
|
||||
EXPECT_NE(val, 0);
|
||||
detail::close_socket(sock);
|
||||
}
|
||||
|
||||
TEST(ClientTest, MoveConstructible) {
|
||||
EXPECT_FALSE(std::is_copy_constructible<Client>::value);
|
||||
EXPECT_TRUE(std::is_nothrow_move_constructible<Client>::value);
|
||||
@@ -1449,13 +1462,8 @@ TEST_F(ChunkedEncodingTest, WithResponseHandlerAndContentReceiver) {
|
||||
}
|
||||
|
||||
TEST(RangeTest, FromHTTPBin_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/range/32"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/range/32"};
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto port = 443;
|
||||
@@ -1489,12 +1497,16 @@ TEST(RangeTest, FromHTTPBin_Online) {
|
||||
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
|
||||
}
|
||||
|
||||
// go-httpbin (httpbingo.org) returns 206 even when the range covers the
|
||||
// entire resource, while the original httpbin returned 200. Both are
|
||||
// acceptable per RFC 9110 §15.3.7, so we accept either status code.
|
||||
{
|
||||
Headers headers = {make_range_header({{0, 31}})};
|
||||
auto res = cli.Get(path, headers);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ("abcdefghijklmnopqrstuvwxyzabcdef", res->body);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_TRUE(res->status == StatusCode::OK_200 ||
|
||||
res->status == StatusCode::PartialContent_206);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1502,14 +1514,17 @@ TEST(RangeTest, FromHTTPBin_Online) {
|
||||
auto res = cli.Get(path, headers);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ("abcdefghijklmnopqrstuvwxyzabcdef", res->body);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_TRUE(res->status == StatusCode::OK_200 ||
|
||||
res->status == StatusCode::PartialContent_206);
|
||||
}
|
||||
|
||||
// go-httpbin returns 206 with clamped range for over-range requests,
|
||||
// while the original httpbin returned 416. Both behaviors are observed
|
||||
// in real servers, so we only verify the request succeeds.
|
||||
{
|
||||
Headers headers = {make_range_header({{0, 32}})};
|
||||
auto res = cli.Get(path, headers);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,13 +1638,8 @@ TEST(ConnectionErrorTest, Timeout_Online) {
|
||||
}
|
||||
|
||||
TEST(CancelTest, NoCancel_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/range/32"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/range/32"};
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto port = 443;
|
||||
@@ -1647,13 +1657,11 @@ TEST(CancelTest, NoCancel_Online) {
|
||||
}
|
||||
|
||||
TEST(CancelTest, WithCancelSmallPayload_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto path = std::string{"/range/32"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/range/32"};
|
||||
#endif
|
||||
// Use /bytes with a large payload so that the DownloadProgress callback
|
||||
// (which only fires for Content-Length responses) is invoked before the
|
||||
// entire body is received, giving cancellation a chance to fire.
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/bytes/524288"};
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto port = 443;
|
||||
@@ -1670,13 +1678,8 @@ TEST(CancelTest, WithCancelSmallPayload_Online) {
|
||||
}
|
||||
|
||||
TEST(CancelTest, WithCancelLargePayload_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto path = std::string{"/range/65536"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/range/65536"};
|
||||
#endif
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/bytes/524288"};
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto port = 443;
|
||||
@@ -2027,13 +2030,8 @@ static std::string remove_whitespace(const std::string &input) {
|
||||
}
|
||||
|
||||
TEST(BaseAuthTest, FromHTTPWatch_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/basic-auth/hello/world"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/basic-auth/hello/world"};
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
auto port = 443;
|
||||
@@ -2053,8 +2051,9 @@ TEST(BaseAuthTest, FromHTTPWatch_Online) {
|
||||
auto res =
|
||||
cli.Get(path, {make_basic_authentication_header("hello", "world")});
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ("{\"authenticated\":true,\"user\":\"hello\"}",
|
||||
remove_whitespace(res->body));
|
||||
auto body = remove_whitespace(res->body);
|
||||
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
||||
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -2062,8 +2061,9 @@ TEST(BaseAuthTest, FromHTTPWatch_Online) {
|
||||
cli.set_basic_auth("hello", "world");
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ("{\"authenticated\":true,\"user\":\"hello\"}",
|
||||
remove_whitespace(res->body));
|
||||
auto body = remove_whitespace(res->body);
|
||||
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
||||
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -2084,24 +2084,12 @@ TEST(BaseAuthTest, FromHTTPWatch_Online) {
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(DigestAuthTest, FromHTTPWatch_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto unauth_path = std::string{"/digest-auth/auth/hello/world"};
|
||||
auto paths = std::vector<std::string>{
|
||||
"/digest-auth/auth/hello/world/MD5",
|
||||
"/digest-auth/auth/hello/world/SHA-256",
|
||||
"/digest-auth/auth/hello/world/SHA-512",
|
||||
};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto unauth_path = std::string{"/httpbin/digest-auth/auth/hello/world"};
|
||||
auto paths = std::vector<std::string>{
|
||||
"/httpbin/digest-auth/auth/hello/world/MD5",
|
||||
"/httpbin/digest-auth/auth/hello/world/SHA-256",
|
||||
"/httpbin/digest-auth/auth/hello/world/SHA-512",
|
||||
"/httpbin/digest-auth/auth-int/hello/world/MD5",
|
||||
};
|
||||
#endif
|
||||
|
||||
auto port = 443;
|
||||
SSLClient cli(host, port);
|
||||
@@ -2118,27 +2106,18 @@ TEST(DigestAuthTest, FromHTTPWatch_Online) {
|
||||
for (const auto &path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
ASSERT_TRUE(res);
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
std::string algo(path.substr(path.rfind('/') + 1));
|
||||
EXPECT_EQ(
|
||||
remove_whitespace("{\"algorithm\":\"" + algo +
|
||||
"\",\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
remove_whitespace(res->body));
|
||||
#else
|
||||
EXPECT_EQ("{\"authenticated\":true,\"user\":\"hello\"}",
|
||||
remove_whitespace(res->body));
|
||||
#endif
|
||||
auto body = remove_whitespace(res->body);
|
||||
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
||||
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
cli.set_digest_auth("hello", "bad");
|
||||
for (const auto &path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2178,7 +2157,8 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
|
||||
}
|
||||
|
||||
TEST(AbsoluteRedirectTest, Redirect_Online) {
|
||||
auto host = "nghttp2.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/absolute-redirect/3"};
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
SSLClient cli(host);
|
||||
@@ -2187,13 +2167,14 @@ TEST(AbsoluteRedirectTest, Redirect_Online) {
|
||||
#endif
|
||||
|
||||
cli.set_follow_location(true);
|
||||
auto res = cli.Get("/httpbin/absolute-redirect/3");
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, Redirect_Online) {
|
||||
auto host = "nghttp2.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/redirect/3"};
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
SSLClient cli(host);
|
||||
@@ -2202,13 +2183,14 @@ TEST(RedirectTest, Redirect_Online) {
|
||||
#endif
|
||||
|
||||
cli.set_follow_location(true);
|
||||
auto res = cli.Get("/httpbin/redirect/3");
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(RelativeRedirectTest, Redirect_Online) {
|
||||
auto host = "nghttp2.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/relative-redirect/3"};
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
SSLClient cli(host);
|
||||
@@ -2217,13 +2199,14 @@ TEST(RelativeRedirectTest, Redirect_Online) {
|
||||
#endif
|
||||
|
||||
cli.set_follow_location(true);
|
||||
auto res = cli.Get("/httpbin/relative-redirect/3");
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(TooManyRedirectTest, Redirect_Online) {
|
||||
auto host = "nghttp2.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/redirect/21"};
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
SSLClient cli(host);
|
||||
@@ -2232,7 +2215,7 @@ TEST(TooManyRedirectTest, Redirect_Online) {
|
||||
#endif
|
||||
|
||||
cli.set_follow_location(true);
|
||||
auto res = cli.Get("/httpbin/redirect/21");
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(!res);
|
||||
EXPECT_EQ(Error::ExceedRedirectCount, res.error());
|
||||
}
|
||||
@@ -3137,45 +3120,40 @@ TEST(RequestHandlerTest, PreRequestHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AnyTest, BasicOperations) {
|
||||
// Default construction
|
||||
httplib::any a;
|
||||
EXPECT_FALSE(a.has_value());
|
||||
TEST(UserDataTest, BasicOperations) {
|
||||
httplib::UserData ud;
|
||||
|
||||
// Value construction and any_cast (pointer form, noexcept)
|
||||
httplib::any b(42);
|
||||
EXPECT_TRUE(b.has_value());
|
||||
auto *p = httplib::any_cast<int>(&b);
|
||||
// Initially empty
|
||||
EXPECT_FALSE(ud.has("key"));
|
||||
EXPECT_EQ(nullptr, ud.get<int>("key"));
|
||||
|
||||
// set and get
|
||||
ud.set("key", 42);
|
||||
EXPECT_TRUE(ud.has("key"));
|
||||
auto *p = ud.get<int>("key");
|
||||
ASSERT_NE(nullptr, p);
|
||||
EXPECT_EQ(42, *p);
|
||||
|
||||
// Type mismatch → nullptr
|
||||
auto *q = httplib::any_cast<std::string>(&b);
|
||||
EXPECT_EQ(nullptr, q);
|
||||
EXPECT_EQ(nullptr, ud.get<std::string>("key"));
|
||||
|
||||
// any_cast (value form) succeeds
|
||||
EXPECT_EQ(42, httplib::any_cast<int>(b));
|
||||
// Overwrite with different type
|
||||
ud.set("key", std::string("hello"));
|
||||
EXPECT_EQ(nullptr, ud.get<int>("key"));
|
||||
auto *s = ud.get<std::string>("key");
|
||||
ASSERT_NE(nullptr, s);
|
||||
EXPECT_EQ("hello", *s);
|
||||
|
||||
// any_cast (value form) throws on type mismatch
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
EXPECT_THROW(httplib::any_cast<std::string>(b), httplib::bad_any_cast);
|
||||
#endif
|
||||
// erase
|
||||
ud.erase("key");
|
||||
EXPECT_FALSE(ud.has("key"));
|
||||
|
||||
// Copy
|
||||
httplib::any c = b;
|
||||
EXPECT_EQ(42, httplib::any_cast<int>(c));
|
||||
|
||||
// Move
|
||||
httplib::any d = std::move(c);
|
||||
EXPECT_EQ(42, httplib::any_cast<int>(d));
|
||||
|
||||
// Assignment with different type
|
||||
b = std::string("hello");
|
||||
EXPECT_EQ("hello", httplib::any_cast<std::string>(b));
|
||||
|
||||
// Reset
|
||||
b.reset();
|
||||
EXPECT_FALSE(b.has_value());
|
||||
// clear
|
||||
ud.set("a", 1);
|
||||
ud.set("b", 2);
|
||||
ud.clear();
|
||||
EXPECT_FALSE(ud.has("a"));
|
||||
EXPECT_FALSE(ud.has("b"));
|
||||
}
|
||||
|
||||
TEST(RequestHandlerTest, ResponseUserDataInPreRouting) {
|
||||
@@ -3186,12 +3164,12 @@ TEST(RequestHandlerTest, ResponseUserDataInPreRouting) {
|
||||
Server svr;
|
||||
|
||||
svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
|
||||
res.user_data["auth"] = AuthCtx{"alice"};
|
||||
res.user_data.set("auth", AuthCtx{"alice"});
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/me", [](const Request & /*req*/, Response &res) {
|
||||
auto *ctx = httplib::any_cast<AuthCtx>(&res.user_data["auth"]);
|
||||
auto *ctx = res.user_data.get<AuthCtx>("auth");
|
||||
ASSERT_NE(nullptr, ctx);
|
||||
res.set_content("Hello " + ctx->user_id, "text/plain");
|
||||
});
|
||||
@@ -3220,12 +3198,12 @@ TEST(RequestHandlerTest, ResponseUserDataInPreRequest) {
|
||||
Server svr;
|
||||
|
||||
svr.set_pre_request_handler([](const Request & /*req*/, Response &res) {
|
||||
res.user_data["role"] = RoleCtx{"admin"};
|
||||
res.user_data.set("role", RoleCtx{"admin"});
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.Get("/role", [](const Request & /*req*/, Response &res) {
|
||||
auto *ctx = httplib::any_cast<RoleCtx>(&res.user_data["role"]);
|
||||
auto *ctx = res.user_data.get<RoleCtx>("role");
|
||||
ASSERT_NE(nullptr, ctx);
|
||||
res.set_content(ctx->role, "text/plain");
|
||||
});
|
||||
@@ -8372,13 +8350,8 @@ TEST(GetWithParametersTest, GetWithParameters2) {
|
||||
}
|
||||
|
||||
TEST(ClientDefaultHeadersTest, DefaultHeaders_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/range/32"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/range/32"};
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
SSLClient cli(host);
|
||||
@@ -8939,7 +8912,7 @@ TEST(ClientVulnerabilityTest, UnboundedReadWithoutContentLength) {
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
@@ -9045,7 +9018,7 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
@@ -9156,7 +9129,7 @@ TEST(ClientVulnerabilityTest, ContentReceiverBypassesDefaultPayloadMaxLength) {
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
@@ -9265,7 +9238,7 @@ TEST(ClientVulnerabilityTest,
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
@@ -9373,7 +9346,7 @@ TEST(ClientVulnerabilityTest,
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
@@ -9509,7 +9482,7 @@ TEST(ClientVulnerabilityTest, ZipBombWithoutContentLength) {
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 3);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 3));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
@@ -9669,13 +9642,8 @@ TEST(SSLClientTest, UpdateCAStoreWithPem_Online) {
|
||||
}
|
||||
|
||||
TEST(SSLClientTest, ServerNameIndication_Online) {
|
||||
#ifdef CPPHTTPLIB_DEFAULT_HTTPBIN
|
||||
auto host = "httpcan.org";
|
||||
auto host = "httpbingo.org";
|
||||
auto path = std::string{"/get"};
|
||||
#else
|
||||
auto host = "nghttp2.org";
|
||||
auto path = std::string{"/httpbin/get"};
|
||||
#endif
|
||||
|
||||
SSLClient cli(host, 443);
|
||||
auto res = cli.Get(path);
|
||||
@@ -12062,6 +12030,97 @@ TEST(MultipartFormDataTest, UploadItemsHasContentLength) {
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, ContentProviderCoalescesWrites) {
|
||||
// Verify that make_multipart_content_provider coalesces many small segments
|
||||
// into fewer sink.write() calls to avoid TCP packet fragmentation (#2410).
|
||||
constexpr size_t kItemCount = 1000;
|
||||
|
||||
UploadFormDataItems items;
|
||||
items.reserve(kItemCount);
|
||||
for (size_t i = 0; i < kItemCount; i++) {
|
||||
items.push_back(
|
||||
{"field" + std::to_string(i), "value" + std::to_string(i), "", ""});
|
||||
}
|
||||
|
||||
const std::string boundary = "----test-boundary";
|
||||
auto content_length = detail::get_multipart_content_length(items, boundary);
|
||||
auto provider = detail::make_multipart_content_provider(items, boundary);
|
||||
|
||||
// Drive the provider the same way write_content_with_progress does
|
||||
size_t write_count = 0;
|
||||
size_t total_bytes = 0;
|
||||
|
||||
DataSink sink;
|
||||
size_t offset = 0;
|
||||
sink.write = [&](const char *d, size_t l) -> bool {
|
||||
(void)d;
|
||||
write_count++;
|
||||
total_bytes += l;
|
||||
offset += l;
|
||||
return true;
|
||||
};
|
||||
sink.is_writable = []() -> bool { return true; };
|
||||
|
||||
while (offset < content_length) {
|
||||
ASSERT_TRUE(provider(offset, content_length - offset, sink));
|
||||
}
|
||||
|
||||
EXPECT_EQ(content_length, total_bytes);
|
||||
|
||||
// The total number of segments is 3 * kItemCount + 1 = 3001.
|
||||
// With buffering into 64KB blocks, write_count should be much smaller.
|
||||
auto segment_count = 3 * kItemCount + 1;
|
||||
EXPECT_LT(write_count, segment_count / 10);
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, ManyItemsEndToEnd) {
|
||||
// Integration test: send many UploadFormDataItems and verify the server
|
||||
// receives all of them correctly (#2410).
|
||||
constexpr size_t kItemCount = 500;
|
||||
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/upload", [&](const Request &req, Response &res) {
|
||||
EXPECT_EQ(kItemCount, req.form.fields.size());
|
||||
for (size_t i = 0; i < kItemCount; i++) {
|
||||
auto key = "field" + std::to_string(i);
|
||||
auto val = "value" + std::to_string(i);
|
||||
auto it = req.form.fields.find(key);
|
||||
if (it != req.form.fields.end()) {
|
||||
EXPECT_EQ(val, it->second.content);
|
||||
} else {
|
||||
ADD_FAILURE() << "Missing field: " << key;
|
||||
}
|
||||
}
|
||||
res.set_content("ok", "text/plain");
|
||||
handled = true;
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port(HOST);
|
||||
auto t = thread([&] { svr.listen_after_bind(); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
ASSERT_TRUE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
UploadFormDataItems items;
|
||||
items.reserve(kItemCount);
|
||||
for (size_t i = 0; i < kItemCount; i++) {
|
||||
items.push_back(
|
||||
{"field" + std::to_string(i), "value" + std::to_string(i), "", ""});
|
||||
}
|
||||
|
||||
Client cli(HOST, port);
|
||||
auto res = cli.Post("/upload", items);
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, MakeFileProvider) {
|
||||
// Verify make_file_provider sends a file's contents correctly.
|
||||
const std::string file_content(4096, 'Z');
|
||||
@@ -12333,6 +12392,38 @@ TEST(RedirectTest, RedirectToUrlWithPlusInQueryParameters) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RedirectTest, RedirectWithPlusInPath) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [](const Request & /*req*/, Response &res) {
|
||||
res.set_redirect("/a+b");
|
||||
});
|
||||
|
||||
// Route pattern uses regex; escape + as \\+
|
||||
svr.Get(R"(/a\+b)", [](const Request &req, Response &res) {
|
||||
res.set_content(req.path, "text/plain");
|
||||
});
|
||||
|
||||
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);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("/a+b", res->body);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, Issue2185_Online) {
|
||||
SSLClient client("github.com");
|
||||
@@ -12400,7 +12491,7 @@ TEST(VulnerabilityTest, CRLFInjectionInHeaders) {
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 1);
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 1));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(srv, 1);
|
||||
@@ -12613,6 +12704,196 @@ TEST(PathParamsTest, SemicolonInTheMiddleIsNotAParam) {
|
||||
EXPECT_EQ(request.path_params, expected_params);
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, VariousPatterns) {
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("http://example.com:8080/path?q=1#frag", uc));
|
||||
EXPECT_EQ("http", uc.scheme);
|
||||
EXPECT_EQ("example.com", uc.host);
|
||||
EXPECT_EQ("8080", uc.port);
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
EXPECT_EQ("?q=1", uc.query);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("https://example.com/path", uc));
|
||||
EXPECT_EQ("https", uc.scheme);
|
||||
EXPECT_EQ("example.com", uc.host);
|
||||
EXPECT_TRUE(uc.port.empty());
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("http://[::1]:8080/path", uc));
|
||||
EXPECT_EQ("::1", uc.host);
|
||||
EXPECT_EQ("8080", uc.port);
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("http://[::1/path", uc));
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("//example.com/path?q=1", uc));
|
||||
EXPECT_TRUE(uc.scheme.empty());
|
||||
EXPECT_EQ("example.com", uc.host);
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
EXPECT_EQ("?q=1", uc.query);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("/path?q=1", uc));
|
||||
EXPECT_TRUE(uc.host.empty());
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
EXPECT_EQ("?q=1", uc.query);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("example.com:8080", uc));
|
||||
EXPECT_EQ("example.com", uc.host);
|
||||
EXPECT_EQ("8080", uc.port);
|
||||
}
|
||||
{
|
||||
// Unix socket path — must not be parsed as host
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("./httplib-server.sock", uc));
|
||||
EXPECT_TRUE(uc.host.empty());
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("", uc));
|
||||
EXPECT_TRUE(uc.host.empty());
|
||||
EXPECT_TRUE(uc.path.empty());
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("HTTP://example.com/path", uc));
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("h2://example.com/path", uc));
|
||||
}
|
||||
{
|
||||
// Accepted by parse_url; callers restrict to http/https
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("ftp://example.com/", uc));
|
||||
EXPECT_EQ("ftp", uc.scheme);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("http://[::1<script>]/path", uc));
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("http://[]/path", uc));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, FragmentHandling) {
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("http://example.com/path#frag", uc));
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
EXPECT_TRUE(uc.query.empty());
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("#frag", uc));
|
||||
EXPECT_TRUE(uc.path.empty());
|
||||
EXPECT_TRUE(uc.query.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, UserinfoHandling) {
|
||||
// Userinfo with @ but no colon — host includes @
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("http://user@host.com/path", uc));
|
||||
EXPECT_EQ("user@host.com", uc.host);
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, IPv6EdgeCases) {
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("[::1]:8080", uc));
|
||||
EXPECT_TRUE(uc.scheme.empty());
|
||||
EXPECT_EQ("::1", uc.host);
|
||||
EXPECT_EQ("8080", uc.port);
|
||||
}
|
||||
{
|
||||
// Zone ID '%25' is not in [a-fA-F0-9:]
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("http://[fe80::1%25eth0]:443/path", uc));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, SchemeEdgeCases) {
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("://evil.com/path", uc));
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("ht-tp://evil.com/path", uc));
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_FALSE(detail::parse_url("h.t://evil.com/path", uc));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, PortEdgeCases) {
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("http://example.com:/path", uc));
|
||||
EXPECT_TRUE(uc.port.empty());
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
}
|
||||
{
|
||||
// parse_url accepts any port string; validation is done by parse_port
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("http://example.com:abc/path", uc));
|
||||
EXPECT_EQ("abc", uc.port);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, WebSocketPatterns) {
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("ws://echo.example.com:8080/ws", uc));
|
||||
EXPECT_EQ("ws", uc.scheme);
|
||||
EXPECT_EQ("echo.example.com", uc.host);
|
||||
EXPECT_EQ("8080", uc.port);
|
||||
EXPECT_EQ("/ws", uc.path);
|
||||
}
|
||||
{
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("wss://echo.example.com/ws", uc));
|
||||
EXPECT_EQ("wss", uc.scheme);
|
||||
EXPECT_EQ("echo.example.com", uc.host);
|
||||
EXPECT_TRUE(uc.port.empty());
|
||||
EXPECT_EQ("/ws", uc.path);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, QueryOnly) {
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("?q=1&r=2", uc));
|
||||
EXPECT_TRUE(uc.host.empty());
|
||||
EXPECT_TRUE(uc.path.empty());
|
||||
EXPECT_EQ("?q=1&r=2", uc.query);
|
||||
}
|
||||
|
||||
TEST(ParseUrlTest, SchemeRelativeWithPort) {
|
||||
detail::UrlComponents uc;
|
||||
ASSERT_TRUE(detail::parse_url("//example.com:443/path", uc));
|
||||
EXPECT_TRUE(uc.scheme.empty());
|
||||
EXPECT_EQ("example.com", uc.host);
|
||||
EXPECT_EQ("443", uc.port);
|
||||
EXPECT_EQ("/path", uc.path);
|
||||
}
|
||||
|
||||
TEST(UniversalClientImplTest, Ipv6LiteralAddress) {
|
||||
// If ipv6 regex working, regex match codepath is taken.
|
||||
// else port will default to 80 in Client impl
|
||||
@@ -17306,3 +17587,145 @@ TEST(SymlinkTest, SymlinkEscapeFromBaseDirectory) {
|
||||
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(RequestSmugglingTest, UnconsumedGETBodyOnFileHandler) {
|
||||
// A GET request with Content-Length to a static file handler must have its
|
||||
// body drained before the keep-alive connection is reused. Otherwise the
|
||||
// unread body bytes are interpreted as the next HTTP request.
|
||||
//
|
||||
// The body is sent AFTER receiving the first response (as in the original
|
||||
// PoC) so that the stream_line_reader cannot buffer it together with the
|
||||
// headers of the first request.
|
||||
Server svr;
|
||||
svr.set_mount_point("/", "./www");
|
||||
|
||||
std::atomic<int> smuggled_count(0);
|
||||
svr.Get("/smuggled", [&](const Request &, Response &res) {
|
||||
smuggled_count++;
|
||||
res.set_content("oops", "text/plain");
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port("localhost");
|
||||
thread t = thread([&] { svr.listen_after_bind(); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
auto error = Error::Success;
|
||||
auto sock = detail::create_client_socket(
|
||||
"localhost", "", port, AF_UNSPEC, false, false, nullptr,
|
||||
/*connection_timeout_sec=*/2, 0,
|
||||
/*read_timeout_sec=*/2, 0,
|
||||
/*write_timeout_sec=*/2, 0, std::string(), error);
|
||||
ASSERT_NE(INVALID_SOCKET, sock);
|
||||
auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); });
|
||||
|
||||
// The "smuggled" request will be sent as the body of the outer GET
|
||||
std::string smuggled = "GET /smuggled HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
// Step 1: Send only the outer request headers (no body yet)
|
||||
std::string outer_headers = "GET /file HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Content-Length: " +
|
||||
std::to_string(smuggled.size()) +
|
||||
"\r\n"
|
||||
"\r\n";
|
||||
|
||||
auto sent = send(sock, outer_headers.data(), outer_headers.size(), 0);
|
||||
ASSERT_EQ(static_cast<ssize_t>(outer_headers.size()), sent);
|
||||
|
||||
// Step 2: Read the first response (server serves file without reading body)
|
||||
std::string first_response;
|
||||
char buf[4096];
|
||||
for (;;) {
|
||||
auto n = recv(sock, buf, sizeof(buf), 0);
|
||||
if (n <= 0) break;
|
||||
first_response.append(buf, static_cast<size_t>(n));
|
||||
// Stop once we have a complete response (headers + body)
|
||||
auto hdr_end = first_response.find("\r\n\r\n");
|
||||
if (hdr_end != std::string::npos) {
|
||||
// Check for Content-Length to know when the body is complete
|
||||
auto cl_pos = first_response.find("Content-Length:");
|
||||
if (cl_pos != std::string::npos) {
|
||||
auto cl_val_start = cl_pos + 15; // length of "Content-Length:"
|
||||
auto cl_val_end = first_response.find("\r\n", cl_val_start);
|
||||
auto cl = std::stoul(
|
||||
first_response.substr(cl_val_start, cl_val_end - cl_val_start));
|
||||
if (first_response.size() >= hdr_end + 4 + cl) { break; }
|
||||
} else {
|
||||
break; // No Content-Length, assume headers-only response
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(first_response.find("HTTP/1.1 200") != std::string::npos);
|
||||
|
||||
// Step 3: Now send the body, which looks like a new HTTP request.
|
||||
// On a vulnerable server the keep-alive loop reads this as a second request.
|
||||
sent = send(sock, smuggled.data(), smuggled.size(), 0);
|
||||
ASSERT_EQ(static_cast<ssize_t>(smuggled.size()), sent);
|
||||
|
||||
// Step 4: Try to read a second response (should NOT exist after fix)
|
||||
std::string second_response;
|
||||
for (;;) {
|
||||
auto n = recv(sock, buf, sizeof(buf), 0);
|
||||
if (n <= 0) break;
|
||||
second_response.append(buf, static_cast<size_t>(n));
|
||||
}
|
||||
|
||||
// The smuggled request must NOT have been processed
|
||||
EXPECT_EQ(0, smuggled_count.load());
|
||||
}
|
||||
|
||||
TEST(RequestSmugglingTest, ContentLengthAndTransferEncodingRejected) {
|
||||
// RFC 9112 §6.3: A request with both Content-Length and Transfer-Encoding
|
||||
// must be rejected with 400 Bad Request.
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &, Response &res) {
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
// Exact "chunked"
|
||||
{
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"Transfer-Encoding: chunked\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request",
|
||||
response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
// Multi-valued Transfer-Encoding (e.g., "gzip, chunked")
|
||||
{
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"Transfer-Encoding: gzip, chunked\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request",
|
||||
response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
}
|
||||
|
||||
+27
-28
@@ -16,29 +16,29 @@ std::string normalizeJson(const std::string &json) {
|
||||
|
||||
template <typename T> void ProxyTest(T &cli, bool basic) {
|
||||
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
||||
auto res = cli.Get("/httpbin/get");
|
||||
auto res = cli.Get("/get");
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ(StatusCode::ProxyAuthenticationRequired_407, res->status);
|
||||
}
|
||||
|
||||
TEST(ProxyTest, NoSSLBasic) {
|
||||
Client cli("nghttp2.org");
|
||||
Client cli("httpbingo.org");
|
||||
ProxyTest(cli, true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(ProxyTest, SSLBasic) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
SSLClient cli("httpbingo.org");
|
||||
ProxyTest(cli, true);
|
||||
}
|
||||
|
||||
TEST(ProxyTest, NoSSLDigest) {
|
||||
Client cli("nghttp2.org");
|
||||
Client cli("httpbingo.org");
|
||||
ProxyTest(cli, false);
|
||||
}
|
||||
|
||||
TEST(ProxyTest, SSLDigest) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
SSLClient cli("httpbingo.org");
|
||||
ProxyTest(cli, false);
|
||||
}
|
||||
#endif
|
||||
@@ -63,24 +63,24 @@ void RedirectProxyText(T &cli, const char *path, bool basic) {
|
||||
}
|
||||
|
||||
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
||||
Client cli("nghttp2.org");
|
||||
RedirectProxyText(cli, "/httpbin/redirect/2", true);
|
||||
Client cli("httpbingo.org");
|
||||
RedirectProxyText(cli, "/redirect/2", true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
||||
Client cli("nghttp2.org");
|
||||
RedirectProxyText(cli, "/httpbin/redirect/2", false);
|
||||
Client cli("httpbingo.org");
|
||||
RedirectProxyText(cli, "/redirect/2", false);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, HTTPBinSSLBasic) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
RedirectProxyText(cli, "/httpbin/redirect/2", true);
|
||||
SSLClient cli("httpbingo.org");
|
||||
RedirectProxyText(cli, "/redirect/2", true);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, HTTPBinSSLDigest) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
RedirectProxyText(cli, "/httpbin/redirect/2", false);
|
||||
SSLClient cli("httpbingo.org");
|
||||
RedirectProxyText(cli, "/redirect/2", false);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -290,26 +290,25 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
#endif
|
||||
|
||||
{
|
||||
auto res = cli.Get("/httpbin/get");
|
||||
auto res = cli.Get("/get");
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
{
|
||||
auto res = cli.Get("/httpbin/redirect/2");
|
||||
auto res = cli.Get("/redirect/2");
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<std::string> paths = {
|
||||
"/httpbin/digest-auth/auth/hello/world/MD5",
|
||||
"/httpbin/digest-auth/auth/hello/world/SHA-256",
|
||||
"/httpbin/digest-auth/auth/hello/world/SHA-512",
|
||||
"/httpbin/digest-auth/auth-int/hello/world/MD5",
|
||||
"/digest-auth/auth/hello/world/MD5",
|
||||
"/digest-auth/auth/hello/world/SHA-256",
|
||||
};
|
||||
|
||||
for (auto path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
normalizeJson(res->body));
|
||||
auto body = normalizeJson(res->body);
|
||||
EXPECT_TRUE(body.find("\"authenticated\":true") != std::string::npos);
|
||||
EXPECT_TRUE(body.find("\"user\":\"hello\"") != std::string::npos);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
}
|
||||
@@ -317,7 +316,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
{
|
||||
int count = 10;
|
||||
while (count--) {
|
||||
auto res = cli.Get("/httpbin/get");
|
||||
auto res = cli.Get("/get");
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
}
|
||||
@@ -325,22 +324,22 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(KeepAliveTest, NoSSLWithBasic) {
|
||||
Client cli("nghttp2.org");
|
||||
Client cli("httpbingo.org");
|
||||
KeepAliveTest(cli, true);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, SSLWithBasic) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
SSLClient cli("httpbingo.org");
|
||||
KeepAliveTest(cli, true);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, NoSSLWithDigest) {
|
||||
Client cli("nghttp2.org");
|
||||
Client cli("httpbingo.org");
|
||||
KeepAliveTest(cli, false);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, SSLWithDigest) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
SSLClient cli("httpbingo.org");
|
||||
KeepAliveTest(cli, false);
|
||||
}
|
||||
#endif
|
||||
@@ -349,11 +348,11 @@ TEST(KeepAliveTest, SSLWithDigest) {
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(ProxyTest, SSLOpenStream) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
SSLClient cli("httpbingo.org");
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
|
||||
auto handle = cli.open_stream("GET", "/httpbin/get");
|
||||
auto handle = cli.open_stream("GET", "/get");
|
||||
ASSERT_TRUE(handle.response);
|
||||
EXPECT_EQ(StatusCode::OK_200, handle.response->status);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user