Compare commits

..

6 Commits

Author SHA1 Message Date
yhirose 073b587962 Release v0.41.0 2026-04-03 21:50:24 -04:00
yhirose 96785eea21 Add parse_url 2026-04-03 20:54:17 -04:00
yhirose 3093bdd9ab Fix #2416 2026-04-03 18:27:12 -04:00
crueter 6607a6a592 [cmake] Allow using pre-existing zstd target if it exists (#2390)
adds support for pre-existing `zstd::libzstd` which is useful for
projects that bundle their own zstd in a way that doesn't get caught by
`CONFIG`

Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-30 21:26:20 -04:00
DavidKorczynski 831b64bdeb Add two new fuzzers (#2412)
The goal is to increase code coverage by way of OSS-Fuzz. A recent code
coverage report is available at
https://storage.googleapis.com/oss-fuzz-coverage/cpp-httplib/reports/20260326/linux/report.html

Signed-off-by: David Korczynski <david@adalogics.com>
2026-03-28 15:00:10 -04:00
yhirose 32c82492de Add workflow_dispatch trigger to docs deployment workflow 2026-03-28 11:08:25 -04:00
11 changed files with 569 additions and 208 deletions
+1
View File
@@ -4,6 +4,7 @@ on:
branches: [master]
paths:
- 'docs-src/**'
workflow_dispatch:
permissions:
contents: read
pages: write
+22 -16
View File
@@ -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}
+3 -5
View File
@@ -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')
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.40.0"
version = "0.41.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+3 -3
View File
@@ -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");
});
```
+3 -3
View File
@@ -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");
});
```
+164 -144
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.40.0"
#define CPPHTTPLIB_VERSION_NUM "0x002800"
#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)>;
@@ -1297,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 = "",
@@ -12999,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()) {
@@ -13025,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_) {
@@ -14749,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")) {
@@ -14770,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
@@ -20182,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") {
@@ -20203,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;
+9 -1
View File
@@ -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
+59
View File
@@ -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, &params);
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;
}
+52
View File
@@ -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;
}
+252 -35
View File
@@ -3120,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) {
@@ -3169,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");
});
@@ -3203,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");
});
@@ -12397,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");
@@ -12677,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