Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test/httplib.cc
test/httplib.h
test/test
test/server_fuzzer
test/test_no_exit_time_dtors
test/test_proxy
test/test_split
test/test.xcodeproj/xcuser*
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,16 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
> [!NOTE]
> Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.

### Exit-time destructors

By default, the library relies on exit-time destructors for the cleanup of its static objects when the program terminates. To disable these exit-time destructors, define the preprocessor macro `CPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS` before including `httplib.h`.

> [!NOTE]
> When exit-time destructors are disabled, all static variables are allocated on the heap and are not deleted, to prevent their destructors from being called at exit time. This results in purposeful memory leaks, but since the program is exiting, it typically does not affect the application's behavior.

> [!NOTE]
> If you use `std::atexit()` to register a function that accesses client or server objects from this library, it is recommended to disable exit-time destructors. This ensures that the objects remain valid when your registered function runs, avoiding potential issues with destructors being called before your function executes.

License
-------

Expand Down
73 changes: 45 additions & 28 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@
#define CPPHTTPLIB_LISTEN_BACKLOG 5
#endif

#ifndef CPPHTTPLIB_DEFINE_STATIC
#ifdef CPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS
#define CPPHTTPLIB_DEFINE_STATIC(var_type, var, init) \
static var_type &var = *new typename std::remove_cv<var_type>::type init
#else
#define CPPHTTPLIB_DEFINE_STATIC(var_type, var, init) \
static var_type var = typename std::remove_cv<var_type>::type init
#endif
#endif

/*
* Headers
*/
Expand Down Expand Up @@ -2919,7 +2929,7 @@ inline std::string decode_url(const std::string &s,

inline std::string file_extension(const std::string &path) {
std::smatch m;
static auto re = std::regex("\\.([a-zA-Z0-9]+)$");
CPPHTTPLIB_DEFINE_STATIC(const std::regex, re, ("\\.([a-zA-Z0-9]+)$"));
if (std::regex_search(path, m, re)) { return m[1].str(); }
return std::string();
}
Expand Down Expand Up @@ -4912,9 +4922,10 @@ class MultipartFormDataParser {
file_.content_type =
trim_copy(header.substr(str_len(header_content_type)));
} else {
static const std::regex re_content_disposition(
R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~",
std::regex_constants::icase);
CPPHTTPLIB_DEFINE_STATIC(
const std::regex, re_content_disposition,
(R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~",
std::regex_constants::icase));

std::smatch m;
if (std::regex_match(header, m, re_content_disposition)) {
Expand All @@ -4935,8 +4946,9 @@ class MultipartFormDataParser {
it = params.find("filename*");
if (it != params.end()) {
// Only allow UTF-8 encoding...
static const std::regex re_rfc5987_encoding(
R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase);
CPPHTTPLIB_DEFINE_STATIC(
const std::regex, re_rfc5987_encoding,
(R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase));

std::smatch m2;
if (std::regex_match(it->second, m2, re_rfc5987_encoding)) {
Expand Down Expand Up @@ -5100,16 +5112,15 @@ inline std::string random_string(size_t length) {
constexpr const char data[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

// std::random_device might actually be deterministic on some
// platforms, but due to lack of support in the c++ standard library,
// doing better requires either some ugly hacks or breaking portability.
static std::random_device seed_gen;

// Request 128 bits of entropy for initialization
static std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(),
seed_gen()};

static std::mt19937 engine(seed_sequence);
static thread_local std::mt19937 engine([]() {
// std::random_device might actually be deterministic on some
// platforms, but due to lack of support in the c++ standard library,
// doing better requires either some ugly hacks or breaking portability.
std::random_device seed_gen;
// Request 128 bits of entropy for initialization
std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()};
return std::mt19937(seed_sequence);
}());

std::string result;
for (size_t i = 0; i < length; i++) {
Expand Down Expand Up @@ -5615,15 +5626,16 @@ class WSInit {
bool is_valid_ = false;
};

static WSInit wsinit_;
CPPHTTPLIB_DEFINE_STATIC(WSInit, wsinit_, ());
#endif

inline bool parse_www_authenticate(const Response &res,
std::map<std::string, std::string> &auth,
bool is_proxy) {
auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
if (res.has_header(auth_key)) {
static auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~");
CPPHTTPLIB_DEFINE_STATIC(const std::regex, re,
(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"));
auto s = res.get_header_value(auth_key);
auto pos = s.find(' ');
if (pos != std::string::npos) {
Expand Down Expand Up @@ -5707,7 +5719,7 @@ inline void hosted_at(const std::string &hostname,
inline std::string append_query_params(const std::string &path,
const Params &params) {
std::string path_with_query = path;
const static std::regex re("[^?]+\\?.*");
CPPHTTPLIB_DEFINE_STATIC(const std::regex, re, ("[^?]+\\?.*"));
auto delm = std::regex_match(path, re) ? '&' : '?';
path_with_query += delm + detail::params_to_query_str(params);
return path_with_query;
Expand Down Expand Up @@ -6481,9 +6493,9 @@ inline bool Server::parse_request_line(const char *s, Request &req) const {
if (count != 3) { return false; }
}

static const std::set<std::string> methods{
"GET", "HEAD", "POST", "PUT", "DELETE",
"CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"};
CPPHTTPLIB_DEFINE_STATIC(std::set<std::string>, methods,
({"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT",
"OPTIONS", "TRACE", "PATCH", "PRI"}));

if (methods.find(req.method) == methods.end()) { return false; }

Expand Down Expand Up @@ -7470,9 +7482,11 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
if (!line_reader.getline()) { return false; }

#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n");
CPPTHTTPLIB_DEFINE_STATIC(std::regex, re,
("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"));
#else
const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
CPPHTTPLIB_DEFINE_STATIC(const std::regex, re,
("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"));
#endif

std::cmatch m;
Expand Down Expand Up @@ -7704,8 +7718,9 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
auto location = res.get_header_value("location");
if (location.empty()) { return false; }

const static std::regex re(
R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)");
CPPHTTPLIB_DEFINE_STATIC(
const std::regex, re,
(R"((?:(https?):)?(?://(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"));

std::smatch m;
if (!std::regex_match(location, m, re)) { return false; }
Expand Down Expand Up @@ -9788,8 +9803,10 @@ 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+))?)");

CPPHTTPLIB_DEFINE_STATIC(
const std::regex, re,
(R"((?:([a-z]+):\/\/)?(?:\[([a-fA-F\d:]+)\]|([^:/?#]+))(?::(\d+))?)"));

std::smatch m;
if (std::regex_match(scheme_host_port, m, re)) {
Expand Down
23 changes: 17 additions & 6 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
BROTLI_DIR = $(PREFIX)/opt/brotli
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec

TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) -pthread -lcurl
TEST_ARGS = gtest_main.o gtest-all.o -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) -pthread -lcurl

# By default, use standalone_fuzz_target_runner.
# This runner does no fuzzing, but simply executes the inputs
Expand All @@ -33,19 +33,30 @@ REALPATH = $(shell which grealpath 2>/dev/null || which realpath 2>/dev/null)
STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \
$(wildcard example/*.h example/*.cc fuzzing/*.h fuzzing/*.cc *.h *.cc ../httplib.h))

all : test test_split
all : test test_no_exit_time_dtors test_split
./test
GTEST_FILTER="ExitTimeDtorsTest.*" ./test_no_exit_time_dtors

proxy : test_proxy
./test_proxy

test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
gtest-all.o : gtest/src/gtest-all.cc
$(CXX) -c -o $@ $(CXXFLAGS) -Igtest -Igtest/include $<

gtest_main.o : gtest/src/gtest_main.cc
$(CXX) -c -o $@ $(CXXFLAGS) -Igtest -Igtest/include $<

test : gtest_main.o gtest-all.o test.cc include_httplib.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS)
@file $@

test_no_exit_time_dtors : gtest_main.o gtest-all.o test.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) -DCPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS \
$(if $(findstring clang,$(CXX)),-Wexit-time-destructors -Werror=exit-time-destructors) test.cc $(TEST_ARGS)

# Note: The intention of test_split is to verify that it works to compile and
# link the split httplib.h, so there is normally no need to execute it.
test_split : test.cc ../httplib.h httplib.cc Makefile cert.pem
test_split : gtest_main.o gtest-all.o test.cc ../httplib.h httplib.cc Makefile cert.pem
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS)

check_abi:
Expand Down Expand Up @@ -73,7 +84,7 @@ style_check: $(STYLE_CHECK_FILES)
echo "All files are properly formatted."; \
fi

test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem
test_proxy : gtest_main.o gtest-all.o test_proxy.cc ../httplib.h Makefile cert.pem
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS)

# Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE).
Expand All @@ -98,5 +109,5 @@ cert.pem:
./gen-certs.sh

clean:
rm -rf test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM
rm -rf test test_no_exit_time_dtors test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM

82 changes: 78 additions & 4 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ using namespace httplib;
const char *HOST = "localhost";
const int PORT = 1234;

const string LONG_QUERY_VALUE = string(25000, '@');
const string LONG_QUERY_URL = "/long-query-value?key=" + LONG_QUERY_VALUE;
CPPHTTPLIB_DEFINE_STATIC(const string, LONG_QUERY_VALUE, (25000, '@'));
CPPHTTPLIB_DEFINE_STATIC(const string, LONG_QUERY_URL,
("/long-query-value?key=" + LONG_QUERY_VALUE));

const std::string JSON_DATA = "{\"hello\":\"world\"}";
CPPHTTPLIB_DEFINE_STATIC(const string, JSON_DATA, ("{\"hello\":\"world\"}"));

const string LARGE_DATA = string(1024 * 1024 * 100, '@'); // 100MB
CPPHTTPLIB_DEFINE_STATIC(const string, LARGE_DATA,
(1024 * 1024 * 100, '@')); // 100MB

MultipartFormData &get_file_value(MultipartFormDataItems &files,
const char *key) {
Expand Down Expand Up @@ -8468,3 +8470,75 @@ TEST(ClientInThreadTest, Issue2068) {
t.join();
}
}

#if defined(__SANITIZE_ADDRESS__)
#define ASAN_ENABLED 1
#else
#if defined(__has_feature)
#if __has_feature(address_sanitizer)
#define ASAN_ENABLED 1
#else
#define ASAN_ENABLED 0
#endif
#else
#define ASAN_ENABLED 0
#endif
#endif

// No death tests on Windows
#ifndef _WIN32
bool KilledByAbortOrSegfault(int exit_status) {
return
#if ASAN_ENABLED
// For ASan in some environments
(WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 1) ||
#endif
(WIFSIGNALED(exit_status) &&
(WTERMSIG(exit_status) == SIGABRT || WTERMSIG(exit_status) == SIGSEGV));
}

Server *issue2097_svr = nullptr;
std::thread *issue2097_svr_thread = nullptr;

TEST(ExitTimeDtorsTest, Issue2097) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
ASSERT_EXIT(
{
issue2097_svr = new Server();
std::atexit([]() {
// Wait a bit before stopping server to simulate delayed exit
std::this_thread::sleep_for(std::chrono::milliseconds(200));
issue2097_svr->stop();
issue2097_svr_thread->join();
});

issue2097_svr_thread = new std::thread([]() {
issue2097_svr->Get(
"/hi", [](const Request & /*req*/, httplib::Response &res) {
res.set_content("Quack", "text/plain");
});

issue2097_svr->listen(HOST, PORT);
});

std::thread cli_thread([]() {
Client cli(HOST, PORT);
while (true) {
auto res = cli.Get("/hi");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});

std::thread([]() {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::exit(42);
}).join();
},
#ifdef CPPHTTPLIB_NO_EXIT_TIME_DESTRUCTORS
::testing::ExitedWithCode(42),
#else
KilledByAbortOrSegfault,
#endif
"");
}
#endif
Loading