mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-30 13:00:42 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c419c904da | ||
|
|
878ba41099 | ||
|
|
941745c33d | ||
|
|
11daa99c86 | ||
|
|
cbaf751da1 | ||
|
|
33d84a12b6 |
No files matched your search
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.60.1"
|
||||
version = "0.61.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.60.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003c01"
|
||||
#define CPPHTTPLIB_VERSION "0.61.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003d00"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -1141,8 +1141,7 @@ private:
|
||||
pointer operator->() const { return data_ + idx_; }
|
||||
|
||||
iterator_t &operator++() {
|
||||
// Saturating, so that advancing past the last entry of a key (which
|
||||
// get_multimap_value() does when asked for an out-of-range id) stays at
|
||||
// Saturating, so that advancing past the last entry of a key stays at
|
||||
// end() instead of running off the container.
|
||||
if (idx_ >= size_) { return *this; }
|
||||
++idx_;
|
||||
@@ -3477,13 +3476,20 @@ inline bool is_numeric(const std::string &str) {
|
||||
return !str.empty() && std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
|
||||
}
|
||||
|
||||
// std::ranges::next(first, n, last) for C++11
|
||||
template <typename It> inline It bounded_next(It first, size_t n, It last) {
|
||||
for (; n > 0 && first != last; --n) {
|
||||
++first;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
inline size_t get_header_value_u64(const Headers &headers,
|
||||
const std::string &key, size_t def,
|
||||
size_t id, bool &is_invalid_value) {
|
||||
is_invalid_value = false;
|
||||
auto rng = headers.equal_range(key);
|
||||
auto it = rng.first;
|
||||
std::advance(it, static_cast<ssize_t>(id));
|
||||
auto it = bounded_next(rng.first, id, rng.second);
|
||||
if (it != rng.second) {
|
||||
if (is_numeric(it->second)) {
|
||||
// Parse at size_t width so an out-of-range Content-Length is reported
|
||||
@@ -8146,8 +8152,7 @@ inline const char *get_header_value(const Headers &headers,
|
||||
}
|
||||
|
||||
auto rng = headers.equal_range(key);
|
||||
auto it = rng.first;
|
||||
std::advance(it, static_cast<ssize_t>(id));
|
||||
auto it = bounded_next(rng.first, id, rng.second);
|
||||
if (it != rng.second) { return it->second.c_str(); }
|
||||
return def;
|
||||
}
|
||||
@@ -8200,8 +8205,7 @@ template <typename Map>
|
||||
inline typename Map::mapped_type
|
||||
get_multimap_value(const Map &m, const std::string &key, size_t id) {
|
||||
auto rng = m.equal_range(key);
|
||||
auto it = rng.first;
|
||||
std::advance(it, static_cast<ssize_t>(id));
|
||||
auto it = bounded_next(rng.first, id, rng.second);
|
||||
if (it != rng.second) { return it->second; }
|
||||
return typename Map::mapped_type();
|
||||
}
|
||||
@@ -9189,7 +9193,7 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
|
||||
return std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
|
||||
};
|
||||
|
||||
if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
|
||||
if (s.size() > 7 && case_ignore::equal(s.substr(0, 6), "bytes=")) {
|
||||
const auto pos = static_cast<size_t>(6);
|
||||
const auto len = static_cast<size_t>(s.size() - 6);
|
||||
auto all_valid_ranges = true;
|
||||
@@ -9247,8 +9251,30 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
|
||||
} catch (...) { return false; }
|
||||
#endif
|
||||
|
||||
// Removes each quoted-string, honoring a quoted-pair (RFC 9110 Section 5.6.4)
|
||||
inline std::string remove_quoted_strings(const std::string &s) {
|
||||
std::string out;
|
||||
auto in_quotes = false;
|
||||
for (size_t i = 0; i < s.size(); i++) {
|
||||
if (s[i] == '"') {
|
||||
in_quotes = !in_quotes;
|
||||
} else if (!in_quotes) {
|
||||
out += s[i];
|
||||
} else if (s[i] == '\\') {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool parse_accept_header(const std::string &s,
|
||||
std::vector<std::string> &content_types) {
|
||||
// Parameter values are not used here, so a quoted-string is dropped rather
|
||||
// than parsed: a ',' or ';' inside one must not be read as a delimiter.
|
||||
if (s.find('"') != std::string::npos) {
|
||||
return parse_accept_header(remove_quoted_strings(s), content_types);
|
||||
}
|
||||
|
||||
content_types.clear();
|
||||
|
||||
// Empty string is considered valid (no preference)
|
||||
@@ -11706,8 +11732,7 @@ inline bool Request::is_multipart_form_data() const {
|
||||
inline std::string MultipartFormData::get_field(const std::string &key,
|
||||
size_t id) const {
|
||||
auto rng = fields.equal_range(key);
|
||||
auto it = rng.first;
|
||||
std::advance(it, static_cast<ssize_t>(id));
|
||||
auto it = detail::bounded_next(rng.first, id, rng.second);
|
||||
if (it != rng.second) { return it->second.content; }
|
||||
return std::string();
|
||||
}
|
||||
@@ -15919,7 +15944,8 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
client.set_proxy(proxy_host_, proxy_port_);
|
||||
|
||||
if (!proxy_basic_auth_username_.empty()) {
|
||||
if (!proxy_basic_auth_username_.empty() ||
|
||||
!proxy_basic_auth_password_.empty()) {
|
||||
client.set_proxy_basic_auth(proxy_basic_auth_username_,
|
||||
proxy_basic_auth_password_);
|
||||
}
|
||||
@@ -16022,8 +16048,8 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
||||
// would leak proxy credentials to the destination server.
|
||||
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
||||
if (!proxy_basic_auth_username_.empty() &&
|
||||
!proxy_basic_auth_password_.empty() &&
|
||||
if ((!proxy_basic_auth_username_.empty() ||
|
||||
!proxy_basic_auth_password_.empty()) &&
|
||||
!req.has_header("Proxy-Authorization")) {
|
||||
req.headers.insert(make_basic_authentication_header(
|
||||
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
|
||||
|
||||
+124
-16
@@ -1119,6 +1119,39 @@ TEST(ParseAcceptHeaderTest, EmptyListElementsAreIgnored) {
|
||||
EXPECT_FALSE(detail::parse_accept_header(",invalidtype,", result));
|
||||
}
|
||||
|
||||
// RFC 9110 Section 5.6.6: a parameter value may be a quoted-string, and a ','
|
||||
// or ';' inside one does not end the list element or the parameter. Section
|
||||
// 5.6.4: nor does a '"' that a backslash escapes end the quoted-string.
|
||||
TEST(ParseAcceptHeaderTest, DelimitersInQuotedParameterValues) {
|
||||
struct {
|
||||
const char *value;
|
||||
std::vector<std::string> expected;
|
||||
} cases[] = {
|
||||
{"text/html;profile=\"a,b\",application/json",
|
||||
{"text/html", "application/json"}},
|
||||
// A "q=" inside a quoted value is not the weight
|
||||
{"text/html;profile=\"a;q=0.1\",application/json;q=0.5",
|
||||
{"text/html", "application/json"}},
|
||||
// The weight after a quoted value still applies
|
||||
{"text/html;profile=\"a,b;q=0.1\";q=0.8,application/json;q=0.9",
|
||||
{"application/json", "text/html"}},
|
||||
{"text/html;profile=\"a\\\",b\";q=0.8,application/json;q=0.9",
|
||||
{"application/json", "text/html"}},
|
||||
{"text/html;profile=\"a\\\"b\",application/json",
|
||||
{"text/html", "application/json"}},
|
||||
// An escaped backslash does not escape the '"' after it
|
||||
{"text/html;profile=\"a\\\\\",application/json",
|
||||
{"text/html", "application/json"}},
|
||||
};
|
||||
|
||||
for (const auto &c : cases) {
|
||||
std::vector<std::string> result;
|
||||
EXPECT_TRUE(detail::parse_accept_header(c.value, result))
|
||||
<< "value: " << c.value;
|
||||
EXPECT_EQ(c.expected, result) << "value: " << c.value;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
|
||||
Server svr;
|
||||
|
||||
@@ -1890,6 +1923,28 @@ TEST(HeadersOrderTest, IdBeyondTheLastDuplicateYieldsDefault) {
|
||||
EXPECT_EQ("def", req.get_header_value("X-Missing", "def", 3));
|
||||
}
|
||||
|
||||
TEST(HeadersOrderTest, HugeIdYieldsDefault) {
|
||||
Request req;
|
||||
req.set_header("X-First", "1");
|
||||
req.set_header("X-Test", "2");
|
||||
req.params.emplace("first", "1");
|
||||
req.params.emplace("test", "2");
|
||||
req.form.fields.emplace("first", FormField{"first", "1", {}});
|
||||
req.form.fields.emplace("test", FormField{"test", "2", {}});
|
||||
req.form.files.emplace("first", FormData{"first", "1", "", "", {}});
|
||||
req.form.files.emplace("test", FormData{"test", "2", "", "", {}});
|
||||
|
||||
// The second one is negative when read as a signed value
|
||||
const auto max_id = (std::numeric_limits<size_t>::max)();
|
||||
for (auto id : {max_id / 2, max_id}) {
|
||||
EXPECT_EQ("def", req.get_header_value("X-Test", "def", id));
|
||||
EXPECT_EQ(7U, req.get_header_value_u64("X-Test", 7, id));
|
||||
EXPECT_EQ("", req.get_param_value("test", id));
|
||||
EXPECT_EQ("", req.form.get_field("test", id));
|
||||
EXPECT_EQ("", req.form.get_file("test", id).content);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HeadersOrderTest, TraversalFollowsInsertionOrder) {
|
||||
Headers headers;
|
||||
headers.emplace("Host", "example.com");
|
||||
@@ -2033,6 +2088,13 @@ TEST(ParseHeaderValueTest, Range) {
|
||||
EXPECT_EQ(0, ranges[0].first);
|
||||
EXPECT_EQ(-1, ranges[0].second);
|
||||
}
|
||||
|
||||
// RFC 9110 14.1: range unit names are case-insensitive.
|
||||
for (const auto &s : {"Bytes=1-10", "BYTES=1-10", "bYtEs=1-10"}) {
|
||||
Ranges ranges;
|
||||
EXPECT_TRUE(detail::parse_range_header(s, ranges)) << s;
|
||||
EXPECT_EQ((Ranges{{1, 10}}), ranges) << s;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseAcceptEncoding1, AcceptEncoding) {
|
||||
@@ -23425,20 +23487,25 @@ TEST_F(SSEIntegrationTest, NonDigitRetryFieldIgnored) {
|
||||
|
||||
// Test: A retry field of all ASCII digits sets the reconnection time
|
||||
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
||||
std::atomic<int> connection_count{0};
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::vector<std::chrono::steady_clock::time_point> connected_at;
|
||||
|
||||
server_->Get("/zero-retry",
|
||||
[&connection_count](const Request &, Response &res) {
|
||||
connection_count.fetch_add(1);
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||
if (offset == 0) {
|
||||
std::string event = "retry: 0\ndata: hello\n\n";
|
||||
sink.write(event.data(), event.size());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
server_->Get("/zero-retry", [&](const Request &, Response &res) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
connected_at.push_back(std::chrono::steady_clock::now());
|
||||
cv.notify_all();
|
||||
}
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||
if (offset == 0) {
|
||||
std::string event = "retry: 0\ndata: hello\n\n";
|
||||
sink.write(event.data(), event.size());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
Client client("localhost", get_port());
|
||||
sse::SSEClient sse(client, "/zero-retry");
|
||||
@@ -23446,13 +23513,21 @@ TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
||||
sse.set_reconnect_interval(10000);
|
||||
sse.start_async();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
cv.wait_for(lock, std::chrono::seconds(5),
|
||||
[&] { return connected_at.size() >= 2; });
|
||||
}
|
||||
sse.stop();
|
||||
|
||||
// The server-supplied interval is applied, but never below 100ms, so
|
||||
// "retry: 0" does not cause a busy reconnect loop
|
||||
EXPECT_GE(connection_count.load(), 2);
|
||||
EXPECT_LE(connection_count.load(), 10);
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
ASSERT_GE(connected_at.size(), 2u);
|
||||
auto interval_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
connected_at[1] - connected_at[0])
|
||||
.count();
|
||||
EXPECT_GE(interval_ms, 100);
|
||||
}
|
||||
|
||||
// Test: Last-Event-ID sent on reconnect
|
||||
@@ -26900,6 +26975,39 @@ TEST(NoProxyTest, ProxyAuthorizationSentWhenNotBypassed) {
|
||||
EXPECT_TRUE(s.last_had_proxy_authz());
|
||||
}
|
||||
|
||||
TEST(NoProxyTest, ProxyAuthorizationSentWithOneEmptyCredential) {
|
||||
std::atomic<int> hits{0};
|
||||
std::atomic<int> hits_with_authz{0};
|
||||
|
||||
no_proxy_test::ScopedServer proxy_mock;
|
||||
proxy_mock.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||
hits++;
|
||||
if (req.has_header("Proxy-Authorization")) { hits_with_authz++; }
|
||||
if (req.target.find("/redir") != std::string::npos) {
|
||||
res.set_redirect("http://other.example/landed");
|
||||
} else {
|
||||
res.set_content("via-proxy", "text/plain");
|
||||
}
|
||||
});
|
||||
proxy_mock.listen();
|
||||
|
||||
for (const auto &c : {std::make_pair("u", ""), std::make_pair("", "p")}) {
|
||||
hits = 0;
|
||||
hits_with_authz = 0;
|
||||
|
||||
Client cli("public.example", 80);
|
||||
cli.set_proxy("127.0.0.1", proxy_mock.port());
|
||||
cli.set_proxy_basic_auth(c.first, c.second);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
// The redirect to another host goes through the proxy as well
|
||||
auto res = cli.Get("/redir");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(2, hits.load());
|
||||
EXPECT_EQ(2, hits_with_authz.load());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NoProxyTest, EmptyNoProxyKeepsProxyOn) {
|
||||
ProxyAndTargetServers s;
|
||||
auto cli = make_client("anything.test", s);
|
||||
|
||||
Reference in new issue
Block a user