Compare commits

...

9 Commits

Author SHA1 Message Date
yhirose a66a013ed7 Release v0.12.2 2023-03-25 21:47:14 -04:00
yhirose f4b02dfdc1 Fix #1533 2023-03-25 21:13:07 -04:00
yhirose 4cf218643e Code format 2023-03-25 21:12:40 -04:00
yhirose 8f96b69a25 Update test.yaml to use microsoft/setup-msbuild@v1.1 2023-03-23 18:53:27 -04:00
Johannes Flügel d262033ded Prevent overflow in hash function str2tag_core() (#1529)
* str2tag_core(): prevent overflow

* Update httplib.h

works for all sizes of unsigned int and if there exists a #define for max
2023-03-22 14:16:32 -04:00
yhirose 5745eabe69 Updated test.yaml to use ctions/checkout@v3 2023-03-21 18:54:54 -04:00
yhirose 5f18642271 Close #1531 2023-03-20 11:47:06 -04:00
yhirose 88a9278872 Fix #1486 2023-03-11 17:04:08 -05:00
yhirose 9bb3ca8169 Fix #1459 (#1523) 2023-03-10 22:21:42 -05:00
4 changed files with 211 additions and 65 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
git config --global core.autocrlf false
git config --global core.eol lf
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: install brotli library on ubuntu
if: matrix.os == 'ubuntu-latest'
run: sudo apt update && sudo apt-get install -y libbrotli-dev
@@ -32,7 +32,7 @@ jobs:
run: cd test && make fuzz_test
- name: setup msbuild on windows
if: matrix.os == 'windows-latest'
uses: microsoft/setup-msbuild@v1.0.2
uses: microsoft/setup-msbuild@v1.1
- name: make-windows
if: matrix.os == 'windows-latest'
run: |
+22 -1
View File
@@ -347,6 +347,27 @@ svr.Get("/chunked", [&](const Request& req, Response& res) {
});
```
With trailer:
```cpp
svr.Get("/chunked", [&](const Request& req, Response& res) {
res.set_header("Trailer", "Dummy1, Dummy2");
res.set_chunked_content_provider(
"text/plain",
[](size_t offset, DataSink &sink) {
sink.write("123", 3);
sink.write("345", 3);
sink.write("789", 3);
sink.done_with_trailer({
{"Dummy1", "DummyVal1"},
{"Dummy2", "DummyVal2"}
});
return true;
}
);
});
```
### 'Expect: 100-continue' handler
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.
@@ -820,7 +841,7 @@ Note: Windows 8 or lower, Visual Studio 2013 or lower, and Cygwin on Windows are
License
-------
MIT license (© 2022 Yuji Hirose)
MIT license (© 2023 Yuji Hirose)
Special Thanks To
-----------------
+87 -27
View File
@@ -1,14 +1,14 @@
//
// httplib.h
//
// Copyright (c) 2022 Yuji Hirose. All rights reserved.
// Copyright (c) 2023 Yuji Hirose. All rights reserved.
// MIT License
//
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.12.1"
#define CPPHTTPLIB_VERSION "0.12.2"
/*
* Configuration
@@ -371,6 +371,7 @@ public:
std::function<bool(const char *data, size_t data_len)> write;
std::function<void()> done;
std::function<void(const Headers &trailer)> done_with_trailer;
std::ostream os;
private:
@@ -1823,7 +1824,8 @@ std::string params_to_query_str(const Params &params);
void parse_query_text(const std::string &s, Params &params);
bool parse_multipart_boundary(const std::string &content_type, std::string &boundary);
bool parse_multipart_boundary(const std::string &content_type,
std::string &boundary);
bool parse_range_header(const std::string &s, Ranges &ranges);
@@ -2976,9 +2978,14 @@ inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
inline constexpr unsigned int str2tag_core(const char *s, size_t l,
unsigned int h) {
return (l == 0) ? h
: str2tag_core(s + 1, l - 1,
(h * 33) ^ static_cast<unsigned char>(*s));
return (l == 0)
? h
: str2tag_core(
s + 1, l - 1,
// Unsets the 6 high bits of h, therefore no overflow happens
(((std::numeric_limits<unsigned int>::max)() >> 6) &
h * 33) ^
static_cast<unsigned char>(*s));
}
inline unsigned int str2tag(const std::string &s) {
@@ -3391,6 +3398,14 @@ 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 (::tolower(a[i]) != ::tolower(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.
@@ -3414,7 +3429,11 @@ inline bool parse_header(const char *beg, const char *end, T fn) {
}
if (p < end) {
fn(std::string(beg, key_end), decode_url(std::string(p, end), false));
auto key = std::string(beg, key_end);
auto val = compare_case_ignore(key, "Location")
? std::string(p, end)
: decode_url(std::string(p, end), false);
fn(std::move(key), std::move(val));
return true;
}
@@ -3512,7 +3531,8 @@ inline bool read_content_without_length(Stream &strm,
return true;
}
inline bool read_content_chunked(Stream &strm,
template <typename T>
inline bool read_content_chunked(Stream &strm, T &x,
ContentReceiverWithProgress out) {
const auto bufsiz = 16;
char buf[bufsiz];
@@ -3538,15 +3558,29 @@ inline bool read_content_chunked(Stream &strm,
if (!line_reader.getline()) { return false; }
if (strcmp(line_reader.ptr(), "\r\n")) { break; }
if (strcmp(line_reader.ptr(), "\r\n")) { return false; }
if (!line_reader.getline()) { return false; }
}
if (chunk_len == 0) {
// Reader terminator after chunks
if (!line_reader.getline() || strcmp(line_reader.ptr(), "\r\n"))
return false;
assert(chunk_len == 0);
// Trailer
if (!line_reader.getline()) { return false; }
while (strcmp(line_reader.ptr(), "\r\n")) {
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
// Exclude line terminator
constexpr auto line_terminator_len = 2;
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
parse_header(line_reader.ptr(), end,
[&](std::string &&key, std::string &&val) {
x.headers.emplace(std::move(key), std::move(val));
});
if (!line_reader.getline()) { return false; }
}
return true;
@@ -3616,7 +3650,7 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
auto exceed_payload_max_length = false;
if (is_chunked_transfer_encoding(x.headers)) {
ret = read_content_chunked(strm, out);
ret = read_content_chunked(strm, x, out);
} else if (!has_header(x.headers, "Content-Length")) {
ret = read_content_without_length(strm, out);
} else {
@@ -3772,7 +3806,7 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
return ok;
};
data_sink.done = [&](void) {
auto done_with_trailer = [&](const Headers *trailer) {
if (!ok) { return; }
data_available = false;
@@ -3790,16 +3824,36 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
if (!payload.empty()) {
// Emit chunked response header and footer for each chunk
auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
if (!write_data(strm, chunk.data(), chunk.size())) {
if (!strm.is_writable() ||
!write_data(strm, chunk.data(), chunk.size())) {
ok = false;
return;
}
}
static const std::string done_marker("0\r\n\r\n");
static const std::string done_marker("0\r\n");
if (!write_data(strm, done_marker.data(), done_marker.size())) {
ok = false;
}
// Trailer
if (trailer) {
for (const auto &kv : *trailer) {
std::string field_line = kv.first + ": " + kv.second + "\r\n";
if (!write_data(strm, field_line.data(), field_line.size())) {
ok = false;
}
}
}
static const std::string crlf("\r\n");
if (!write_data(strm, crlf.data(), crlf.size())) { ok = false; }
};
data_sink.done = [&](void) { done_with_trailer(nullptr); };
data_sink.done_with_trailer = [&](const Headers &trailer) {
done_with_trailer(&trailer);
};
while (data_available && !is_shutting_down()) {
@@ -6463,11 +6517,11 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
return false;
}
auto location = detail::decode_url(res.get_header_value("location"), true);
auto location = res.get_header_value("location");
if (location.empty()) { return false; }
const static std::regex re(
R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*(?:\?[^#]*)?)(?:#.*)?)");
R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)");
std::smatch m;
if (!std::regex_match(location, m, re)) { return false; }
@@ -6479,6 +6533,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
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_port = port_;
if (!port_str.empty()) {
@@ -6491,22 +6546,24 @@ 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 = detail::decode_url(next_path, true) + next_query;
if (next_scheme == scheme && next_host == host_ && next_port == port_) {
return detail::redirect(*this, req, res, next_path, location, error);
return detail::redirect(*this, req, res, path, location, error);
} else {
if (next_scheme == "https") {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
SSLClient cli(next_host.c_str(), next_port);
cli.copy_settings(*this);
if (ca_cert_store_) { cli.set_ca_cert_store(ca_cert_store_); }
return detail::redirect(cli, req, res, next_path, location, error);
return detail::redirect(cli, req, res, path, location, error);
#else
return false;
#endif
} else {
ClientImpl cli(next_host.c_str(), next_port);
cli.copy_settings(*this);
return detail::redirect(cli, req, res, next_path, location, error);
return detail::redirect(cli, req, res, path, location, error);
}
}
}
@@ -6769,11 +6826,14 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (is_ssl()) {
char buf[1];
if (SSL_peek(socket_.ssl, buf, 1) == 0 &&
SSL_get_error(socket_.ssl, 0) == SSL_ERROR_ZERO_RETURN) {
error = Error::SSLPeerCouldBeClosed_;
return false;
auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1;
if (!is_proxy_enabled) {
char buf[1];
if (SSL_peek(socket_.ssl, buf, 1) == 0 &&
SSL_get_error(socket_.ssl, 0) == SSL_ERROR_ZERO_RETURN) {
error = Error::SSLPeerCouldBeClosed_;
return false;
}
}
}
#endif
+100 -35
View File
@@ -186,7 +186,8 @@ TEST(ParseMultipartBoundaryTest, ValueWithQuote) {
}
TEST(ParseMultipartBoundaryTest, ValueWithCharset) {
string content_type = "multipart/mixed; boundary=THIS_STRING_SEPARATES;charset=UTF-8";
string content_type =
"multipart/mixed; boundary=THIS_STRING_SEPARATES;charset=UTF-8";
string boundary;
auto ret = detail::parse_multipart_boundary(content_type, boundary);
EXPECT_TRUE(ret);
@@ -1710,6 +1711,30 @@ protected:
delete i;
});
})
.Get("/streamed-chunked-with-trailer",
[&](const Request & /*req*/, Response &res) {
auto i = new int(0);
res.set_header("Trailer", "Dummy1, Dummy2");
res.set_chunked_content_provider(
"text/plain",
[i](size_t /*offset*/, DataSink &sink) {
switch (*i) {
case 0: sink.os << "123"; break;
case 1: sink.os << "456"; break;
case 2: sink.os << "789"; break;
case 3: {
sink.done_with_trailer(
{{"Dummy1", "DummyVal1"}, {"Dummy2", "DummyVal2"}});
} break;
}
(*i)++;
return true;
},
[i](bool success) {
EXPECT_TRUE(success);
delete i;
});
})
.Get("/streamed",
[&](const Request & /*req*/, Response &res) {
res.set_content_provider(
@@ -1801,39 +1826,39 @@ protected:
}
})
.Post("/multipart/multi_file_values",
[&](const Request &req, Response & /*res*/) {
EXPECT_EQ(5u, req.files.size());
ASSERT_TRUE(!req.has_file("???"));
ASSERT_TRUE(req.body.empty());
[&](const Request &req, Response & /*res*/) {
EXPECT_EQ(5u, req.files.size());
ASSERT_TRUE(!req.has_file("???"));
ASSERT_TRUE(req.body.empty());
{
{
const auto &text_value = req.get_file_values("text");
EXPECT_EQ(text_value.size(), 1);
auto &text = text_value[0];
EXPECT_TRUE(text.filename.empty());
EXPECT_EQ("default text", text.content);
}
{
const auto &text1_values = req.get_file_values("multi_text1");
EXPECT_EQ(text1_values.size(), 2);
EXPECT_EQ("aaaaa", text1_values[0].content);
EXPECT_EQ("bbbbb", text1_values[1].content);
}
}
{
const auto &text1_values = req.get_file_values("multi_text1");
EXPECT_EQ(text1_values.size(), 2);
EXPECT_EQ("aaaaa", text1_values[0].content);
EXPECT_EQ("bbbbb", text1_values[1].content);
}
{
const auto &file1_values = req.get_file_values("multi_file1");
EXPECT_EQ(file1_values.size(), 2);
auto file1 = file1_values[0];
EXPECT_EQ(file1.filename, "hello.txt");
EXPECT_EQ(file1.content_type, "text/plain");
EXPECT_EQ("h\ne\n\nl\nl\no\n", file1.content);
{
const auto &file1_values = req.get_file_values("multi_file1");
EXPECT_EQ(file1_values.size(), 2);
auto file1 = file1_values[0];
EXPECT_EQ(file1.filename, "hello.txt");
EXPECT_EQ(file1.content_type, "text/plain");
EXPECT_EQ("h\ne\n\nl\nl\no\n", file1.content);
auto file2 = file1_values[1];
EXPECT_EQ(file2.filename, "world.json");
EXPECT_EQ(file2.content_type, "application/json");
EXPECT_EQ("{\n \"world\", true\n}\n", file2.content);
}
})
auto file2 = file1_values[1];
EXPECT_EQ(file2.filename, "world.json");
EXPECT_EQ(file2.content_type, "application/json");
EXPECT_EQ("{\n \"world\", true\n}\n", file2.content);
}
})
.Post("/empty",
[&](const Request &req, Response &res) {
EXPECT_EQ(req.body, "");
@@ -2680,13 +2705,14 @@ TEST_F(ServerTest, MultipartFormData) {
TEST_F(ServerTest, MultipartFormDataMultiFileValues) {
MultipartFormDataItems items = {
{"text", "default text", "", ""},
{"text", "default text", "", ""},
{"multi_text1", "aaaaa", "", ""},
{"multi_text1", "bbbbb", "", ""},
{"multi_text1", "aaaaa", "", ""},
{"multi_text1", "bbbbb", "", ""},
{"multi_file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain"},
{"multi_file1", "{\n \"world\", true\n}\n", "world.json", "application/json"},
{"multi_file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain"},
{"multi_file1", "{\n \"world\", true\n}\n", "world.json",
"application/json"},
};
auto res = cli_.Post("/multipart/multi_file_values", items);
@@ -2920,6 +2946,15 @@ TEST_F(ServerTest, GetStreamedChunked2) {
EXPECT_EQ(std::string("123456789"), res->body);
}
TEST_F(ServerTest, GetStreamedChunkedWithTrailer) {
auto res = cli_.Get("/streamed-chunked-with-trailer");
ASSERT_TRUE(res);
EXPECT_EQ(200, res->status);
EXPECT_EQ(std::string("123456789"), res->body);
EXPECT_EQ(std::string("DummyVal1"), res->get_header_value("Dummy1"));
EXPECT_EQ(std::string("DummyVal2"), res->get_header_value("Dummy2"));
}
TEST_F(ServerTest, LargeChunkedPost) {
Request req;
req.method = "POST";
@@ -3906,9 +3941,8 @@ TEST(ServerStopTest, StopServerWithChunkedTransmission) {
TEST(ServerStopTest, ClientAccessAfterServerDown) {
httplib::Server svr;
svr.Post("/hi", [&](const httplib::Request & /*req*/, httplib::Response &res) {
res.status = 200;
});
svr.Post("/hi", [&](const httplib::Request & /*req*/,
httplib::Response &res) { res.status = 200; });
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
@@ -4582,7 +4616,7 @@ TEST(SSLClientTest, ServerNameIndication_Online) {
}
TEST(SSLClientTest, ServerCertificateVerification1_Online) {
SSLClient cli("google.com");
Client cli("https://google.com");
auto res = cli.Get("/");
ASSERT_TRUE(res);
ASSERT_EQ(301, res->status);
@@ -5978,3 +6012,34 @@ TEST(TaskQueueTest, IncreaseAtomicInteger) {
EXPECT_NO_THROW(task_queue->shutdown());
EXPECT_EQ(number_of_task, count.load());
}
TEST(RedirectTest, RedirectToUrlWithQueryParameters) {
Server svr;
svr.Get("/", [](const Request & /*req*/, Response &res) {
res.set_redirect(R"(/hello?key=val%26key2%3Dval2)");
});
svr.Get("/hello", [](const Request &req, Response &res) {
res.set_content(req.get_param_value("key"), "text/plain");
});
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
std::this_thread::sleep_for(std::chrono::seconds(1));
{
Client cli(HOST, PORT);
cli.set_follow_location(true);
auto res = cli.Get("/");
ASSERT_TRUE(res);
EXPECT_EQ(200, res->status);
EXPECT_EQ("val&key2=val2", res->body);
}
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
}