Compare commits

..

16 Commits

Author SHA1 Message Date
yhirose 61c418048d Release v0.17.3 2024-09-06 19:58:02 -04:00
yhirose 9720ef8c34 Code cleanup 2024-09-06 19:48:25 -04:00
yhirose 978a4f6345 Fix KeepAliveTest.SSLClientReconnectionPost problem (#1921) 2024-09-06 13:58:24 -04:00
bgs99 80fb03628b Only match path params that span full path segment (#1919)
* Only match path params that span full path segment

* Fix C++11 build
2024-09-06 08:48:51 -04:00
laowai9189 2480c0342c ‘constexpr’ error (#1918)
httplib.h: In member function ‘constexpr size_t httplib::detail::case_ignore_hash::operator()(const string&) const’:
httplib.h:359:30: error: call to non-‘constexpr’ function ‘const _CharT* std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::data() const [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’
  359 |     return hash_core(key.data(), key.size(), 0);
2024-09-06 07:23:29 -04:00
bgs99 eb6f610a45 Fix find_package for curl (#1920) 2024-09-06 07:22:03 -04:00
yhirose cb74e4191b Performance imporovement for Keep-Alive 2024-09-06 00:03:43 -04:00
yhirose dfa641ca41 Misc 2024-09-05 22:54:48 -04:00
yhirose 969a9f99d5 Adjust sleep 2024-09-05 22:54:28 -04:00
yhirose c099b42ba3 Removed write_format 2024-09-05 22:17:56 -04:00
yhirose b8315278cb Add a missing file 2024-09-05 19:35:43 -04:00
yhirose 485f8f2411 Added one more case to MountTest.Redicect unit test. 2024-09-05 17:49:12 -04:00
yhirose 953e4f3841 Adjust sleep duration 2024-09-05 17:45:09 -04:00
yhirose adf65cfe61 Target C++11 for benchmark 2024-09-05 17:44:51 -04:00
yhirose 12c829f6d3 Fix #1389 and #1907 2024-09-05 17:44:32 -04:00
yhirose 913314f1b1 Fix warning 2024-09-05 17:43:51 -04:00
6 changed files with 162 additions and 120 deletions
+1
View File
@@ -21,6 +21,7 @@ test/test.xcodeproj/*/xcuser*
test/*.o
test/*.pem
test/*.srl
work/
benchmark/server
benchmark/server-crow
+18 -10
View File
@@ -1,28 +1,36 @@
CXXFLAGS = -std=c++14 -O2 -I..
CXXFLAGS = -std=c++11 -O2 -I..
THEAD_POOL_COUNT = 16
BENCH_FLAGS = -c 8 -d 5s
BENCH_CMD = bombardier -c 8 -d 5s localhost:8080
# BENCH_CMD = wrk -d 5s http://localhost:8080
# cpp-httplib
bench: server
@./server & export PID=$$!; bombardier $(BENCH_FLAGS) localhost:8080; kill $${PID}
server : cpp-httplib/main.cpp ../httplib.h
g++ -o $@ $(CXXFLAGS) -DCPPHTTPLIB_THREAD_POOL_COUNT=$(THEAD_POOL_COUNT) cpp-httplib/main.cpp
@./server & export PID=$$!; $(BENCH_CMD); kill $${PID}
run : server
@./server
server : cpp-httplib/main.cpp ../httplib.h
g++ -o $@ $(CXXFLAGS) -DCPPHTTPLIB_THREAD_POOL_COUNT=$(THEAD_POOL_COUNT) cpp-httplib/main.cpp
# crow
bench-crow: server-crow
@./server-crow & export PID=$$!; $(BENCH_CMD); kill $${PID}
run-crow : server-crow
@./server-crow
server-crow : crow/main.cpp
g++ -o $@ $(CXXFLAGS) crow/main.cpp
bench-crow: server-crow
@./server-crow & export PID=$$!; bombardier $(BENCH_FLAGS) localhost:8080; kill $${PID}
# flask
bench-flask:
@FLASK_APP=flask/main.py flask run --port=8080 & export PID=$$!; bombardier $(BENCH_FLAGS) localhost:8080; kill $${PID}
@FLASK_APP=flask/main.py flask run --port=8080 & export PID=$$!; $(BENCH_CMD); kill $${PID}
run-flask:
@FLASK_APP=flask/main.py flask run --port=8080
# misc
bench-all: bench bench-crow bench-flask
+91 -108
View File
@@ -8,7 +8,7 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.17.2"
#define CPPHTTPLIB_VERSION "0.17.3"
/*
* Configuration
@@ -321,6 +321,8 @@ make_unique(std::size_t n) {
return std::unique_ptr<T>(new RT[n]);
}
namespace case_ignore {
inline unsigned char to_lower(int c) {
const static unsigned char table[256] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
@@ -345,28 +347,36 @@ inline unsigned char to_lower(int c) {
return table[(unsigned char)(char)c];
}
struct case_ignore_equal {
bool operator()(const std::string &s1, const std::string &s2) const {
return s1.size() == s2.size() &&
std::equal(s1.begin(), s1.end(), s2.begin(), [](char a, char b) {
return to_lower(a) == to_lower(b);
});
inline bool equal(const std::string &a, const std::string &b) {
return a.size() == b.size() &&
std::equal(a.begin(), a.end(), b.begin(),
[](char a, char b) { return to_lower(a) == to_lower(b); });
}
struct equal_to {
bool operator()(const std::string &a, const std::string &b) const {
return equal(a, b);
}
};
struct case_ignore_hash {
constexpr size_t operator()(const std::string &key) const {
struct hash {
size_t operator()(const std::string &key) const {
return hash_core(key.data(), key.size(), 0);
}
constexpr size_t hash_core(const char *s, size_t l, size_t h) const {
return (l == 0)
? h
: hash_core(s + 1, l - 1,
(h * 33) ^ static_cast<unsigned char>(to_lower(*s)));
size_t hash_core(const char *s, size_t l, size_t h) const {
return (l == 0) ? h
: hash_core(s + 1, l - 1,
// Unsets the 6 high bits of h, therefore no
// overflow happens
(((std::numeric_limits<size_t>::max)() >> 6) &
h * 33) ^
static_cast<unsigned char>(to_lower(*s)));
}
};
}; // namespace case_ignore
// This is based on
// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189".
@@ -473,8 +483,8 @@ enum StatusCode {
};
using Headers =
std::unordered_multimap<std::string, std::string, detail::case_ignore_hash,
detail::case_ignore_equal>;
std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
detail::case_ignore::equal_to>;
using Params = std::multimap<std::string, std::string>;
using Match = std::smatch;
@@ -697,8 +707,6 @@ public:
virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0;
virtual socket_t socket() const = 0;
template <typename... Args>
ssize_t write_format(const char *fmt, const Args &...args);
ssize_t write(const char *ptr);
ssize_t write(const std::string &s);
};
@@ -842,7 +850,6 @@ public:
bool match(Request &request) const override;
private:
static constexpr char marker = ':';
// Treat segment separators as the end of path parameter capture
// Does not need to handle query parameters as they are parsed before path
// matching
@@ -1989,30 +1996,6 @@ inline uint64_t Response::get_header_value_u64(const std::string &key,
return detail::get_header_value_u64(headers, key, def, id);
}
template <typename... Args>
inline ssize_t Stream::write_format(const char *fmt, const Args &...args) {
const auto bufsiz = 2048;
std::array<char, bufsiz> buf{};
auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...);
if (sn <= 0) { return sn; }
auto n = static_cast<size_t>(sn);
if (n >= buf.size() - 1) {
std::vector<char> glowable_buf(buf.size());
while (n >= glowable_buf.size() - 1) {
glowable_buf.resize(glowable_buf.size() * 2);
n = static_cast<size_t>(
snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...));
}
return write(&glowable_buf[0], n);
} else {
return write(buf.data(), n);
}
}
inline void default_socket_options(socket_t sock) {
int opt = 1;
#ifdef _WIN32
@@ -3005,7 +2988,7 @@ template <typename T> inline ssize_t handle_EINTR(T fn) {
while (true) {
res = fn();
if (res < 0 && errno == EINTR) {
std::this_thread::sleep_for(std::chrono::milliseconds{1});
std::this_thread::sleep_for(std::chrono::microseconds{1});
continue;
}
break;
@@ -3216,25 +3199,6 @@ private:
};
#endif
inline bool keep_alive(socket_t sock, time_t keep_alive_timeout_sec) {
using namespace std::chrono;
auto start = steady_clock::now();
while (true) {
auto val = select_read(sock, 0, 10000);
if (val < 0) {
return false;
} else if (val == 0) {
auto current = steady_clock::now();
auto duration = duration_cast<milliseconds>(current - start);
auto timeout = keep_alive_timeout_sec * 1000;
if (duration.count() > timeout) { return false; }
std::this_thread::sleep_for(std::chrono::milliseconds(1));
} else {
return true;
}
}
}
template <typename T>
inline bool
process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
@@ -3244,7 +3208,7 @@ process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
auto ret = false;
auto count = keep_alive_max_count;
while (svr_sock != INVALID_SOCKET && count > 0 &&
keep_alive(sock, keep_alive_timeout_sec)) {
select_read(sock, keep_alive_timeout_sec, 0) > 0) {
auto close_connection = count == 1;
auto connection_closed = false;
ret = callback(close_connection, connection_closed);
@@ -4042,14 +4006,6 @@ inline const char *get_header_value(const Headers &headers,
return def;
}
inline bool compare_case_ignore(const std::string &a, const std::string &b) {
if (a.size() != b.size()) { return false; }
for (size_t i = 0; i < b.size(); i++) {
if (to_lower(a[i]) != to_lower(b[i])) { return false; }
}
return true;
}
template <typename T>
inline bool parse_header(const char *beg, const char *end, T fn) {
// Skip trailing spaces and tabs.
@@ -4077,7 +4033,7 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
if (!key_len) { return false; }
auto key = std::string(beg, key_end);
auto val = compare_case_ignore(key, "Location")
auto val = case_ignore::equal(key, "Location")
? std::string(p, end)
: decode_url(std::string(p, end), false);
@@ -4112,17 +4068,15 @@ inline bool read_headers(Stream &strm, Headers &headers) {
if (line_reader.end_with_crlf()) {
// Blank line indicates end of headers.
if (line_reader.size() == 2) { break; }
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
} else {
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
// Blank line indicates end of headers.
if (line_reader.size() == 1) { break; }
line_terminator_len = 1;
}
#else
} else {
continue; // Skip invalid line.
}
#endif
}
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
@@ -4244,7 +4198,7 @@ inline bool read_content_chunked(Stream &strm, T &x,
}
inline bool is_chunked_transfer_encoding(const Headers &headers) {
return compare_case_ignore(
return case_ignore::equal(
get_header_value(headers, "Transfer-Encoding", "", 0), "chunked");
}
@@ -4327,13 +4281,36 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
}
return ret;
});
} // namespace detail
}
inline ssize_t write_request_line(Stream &strm, const std::string &method,
const std::string &path) {
std::string s = method;
s += " ";
s += path;
s += " HTTP/1.1\r\n";
return strm.write(s.data(), s.size());
}
inline ssize_t write_response_line(Stream &strm, int status) {
std::string s = "HTTP/1.1 ";
s += std::to_string(status);
s += " ";
s += httplib::status_message(status);
s += "\r\n";
return strm.write(s.data(), s.size());
}
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
ssize_t write_len = 0;
for (const auto &x : headers) {
auto len =
strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str());
std::string s;
s = x.first;
s += ": ";
s += x.second;
s += "\r\n";
auto len = strm.write(s.data(), s.size());
if (len < 0) { return len; }
write_len += len;
}
@@ -4860,7 +4837,9 @@ private:
const std::string &b) const {
if (a.size() < b.size()) { return false; }
for (size_t i = 0; i < b.size(); i++) {
if (to_lower(a[i]) != to_lower(b[i])) { return false; }
if (case_ignore::to_lower(a[i]) != case_ignore::to_lower(b[i])) {
return false;
}
}
return true;
}
@@ -5873,6 +5852,8 @@ inline socket_t BufferStream::socket() const { return 0; }
inline const std::string &BufferStream::get_buffer() const { return buffer; }
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
static constexpr char marker[] = "/:";
// One past the last ending position of a path param substring
std::size_t last_param_end = 0;
@@ -5885,13 +5866,14 @@ inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
#endif
while (true) {
const auto marker_pos = pattern.find(marker, last_param_end);
const auto marker_pos = pattern.find(
marker, last_param_end == 0 ? last_param_end : last_param_end - 1);
if (marker_pos == std::string::npos) { break; }
static_fragments_.push_back(
pattern.substr(last_param_end, marker_pos - last_param_end));
pattern.substr(last_param_end, marker_pos - last_param_end + 1));
const auto param_name_start = marker_pos + 1;
const auto param_name_start = marker_pos + 2;
auto sep_pos = pattern.find(separator, param_name_start);
if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }
@@ -5953,7 +5935,7 @@ inline bool PathParamsMatcher::match(Request &request) const {
request.path_params.emplace(
param_name, request.path.substr(starting_pos, sep_pos - starting_pos));
// Mark everythin up to '/' as matched
// Mark everything up to '/' as matched
starting_pos = sep_pos + 1;
}
// Returns false if the path is longer than the pattern
@@ -6316,23 +6298,24 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
if (close_connection || req.get_header_value("Connection") == "close") {
res.set_header("Connection", "close");
} else {
std::stringstream ss;
ss << "timeout=" << keep_alive_timeout_sec_
<< ", max=" << keep_alive_max_count_;
res.set_header("Keep-Alive", ss.str());
std::string s = "timeout=";
s += std::to_string(keep_alive_timeout_sec_);
s += ", max=";
s += std::to_string(keep_alive_max_count_);
res.set_header("Keep-Alive", s);
}
if (!res.has_header("Content-Type") &&
(!res.body.empty() || res.content_length_ > 0 || res.content_provider_)) {
if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) &&
!res.has_header("Content-Type")) {
res.set_header("Content-Type", "text/plain");
}
if (!res.has_header("Content-Length") && res.body.empty() &&
!res.content_length_ && !res.content_provider_) {
if (res.body.empty() && !res.content_length_ && !res.content_provider_ &&
!res.has_header("Content-Length")) {
res.set_header("Content-Length", "0");
}
if (!res.has_header("Accept-Ranges") && req.method == "HEAD") {
if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) {
res.set_header("Accept-Ranges", "bytes");
}
@@ -6341,12 +6324,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
// Response line and headers
{
detail::BufferStream bstrm;
if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status,
status_message(res.status))) {
return false;
}
if (!detail::write_response_line(bstrm, res.status)) { return false; }
if (!header_writer_(bstrm, res.headers)) { return false; }
// Flush buffer
@@ -6540,6 +6518,11 @@ inline bool Server::handle_file_request(const Request &req, Response &res,
auto path = entry.base_dir + sub_path;
if (path.back() == '/') { path += "index.html"; }
if (detail::is_dir(path)) {
res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301);
return true;
}
if (detail::is_file(path)) {
for (const auto &kv : entry.headers) {
res.set_header(kv.first, kv.second);
@@ -6651,7 +6634,7 @@ inline bool Server::listen_internal() {
if (errno == EMFILE) {
// The per-process limit of open file descriptors has been reached.
// Try to accept new connections after a short sleep.
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::this_thread::sleep_for(std::chrono::microseconds{1});
continue;
} else if (errno == EINTR || errno == EAGAIN) {
continue;
@@ -7001,8 +6984,8 @@ Server::process_request(Stream &strm, bool close_connection,
switch (status) {
case StatusCode::Continue_100:
case StatusCode::ExpectationFailed_417:
strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status,
status_message(status));
detail::write_response_line(strm, status);
strm.write("\r\n");
break;
default:
connection_closed = true;
@@ -7612,7 +7595,7 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
detail::BufferStream bstrm;
const auto &path = url_encode_ ? detail::encode_url(req.path) : req.path;
bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str());
detail::write_request_line(bstrm, req.method, path);
header_writer_(bstrm, req.headers);
@@ -8713,7 +8696,7 @@ inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock,
auto ret = SSL_shutdown(ssl);
while (ret == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds{100});
ret = SSL_shutdown(ssl);
}
#endif
@@ -8820,7 +8803,7 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
if (SSL_pending(ssl_) > 0) {
return SSL_read(ssl_, ptr, static_cast<int>(size));
} else if (is_readable()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::this_thread::sleep_for(std::chrono::microseconds{10});
ret = SSL_read(ssl_, ptr, static_cast<int>(size));
if (ret >= 0) { return ret; }
err = SSL_get_error(ssl_, ret);
@@ -8851,7 +8834,7 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) {
#endif
if (is_writable()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::this_thread::sleep_for(std::chrono::microseconds{10});
ret = SSL_write(ssl_, ptr, static_cast<int>(handle_size));
if (ret >= 0) { return ret; }
err = SSL_get_error(ssl_, ret);
+1 -1
View File
@@ -24,7 +24,7 @@ else()
FetchContent_MakeAvailable(gtest)
endif()
find_package(curl REQUIRED)
find_package(CURL REQUIRED)
add_executable(httplib-test test.cc)
target_compile_options(httplib-test PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8;/bigobj>")
+50 -1
View File
@@ -5068,6 +5068,43 @@ TEST(MountTest, Unmount) {
EXPECT_EQ(StatusCode::NotFound_404, res->status);
}
TEST(MountTest, Redicect) {
Server svr;
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.set_mount_point("/", "./www");
svr.wait_until_ready();
Client cli("localhost", PORT);
auto res = cli.Get("/dir/");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
res = cli.Get("/dir");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::MovedPermanently_301, res->status);
res = cli.Get("/file");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
res = cli.Get("/file/");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::NotFound_404, res->status);
cli.set_follow_location(true);
res = cli.Get("/dir");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
TEST(ExceptionTest, ThrowExceptionInHandler) {
Server svr;
@@ -7562,6 +7599,18 @@ TEST(PathParamsTest, SequenceOfParams) {
EXPECT_EQ(request.path_params, expected_params);
}
TEST(PathParamsTest, SemicolonInTheMiddleIsNotAParam) {
const auto pattern = "/prefix:suffix";
detail::PathParamsMatcher matcher(pattern);
Request request;
request.path = "/prefix:suffix";
ASSERT_TRUE(matcher.match(request));
const std::unordered_map<std::string, std::string> expected_params = {};
EXPECT_EQ(request.path_params, expected_params);
}
TEST(UniversalClientImplTest, Ipv6LiteralAddress) {
// If ipv6 regex working, regex match codepath is taken.
// else port will default to 80 in Client impl
@@ -7698,7 +7747,7 @@ TEST(Expect100ContinueTest, ServerClosesConnection) {
auto dl = curl_off_t{};
const auto res = curl_easy_getinfo(curl.get(), CURLINFO_SIZE_DOWNLOAD_T, &dl);
ASSERT_EQ(res, CURLE_OK);
ASSERT_EQ(dl, sizeof reject - 1);
ASSERT_EQ(dl, (curl_off_t)sizeof reject - 1);
}
{
+1
View File
@@ -0,0 +1 @@
file