Compare commits

...

32 Commits

Author SHA1 Message Date
yhirose 5814e121df Release v0.18.7 2025-02-08 15:53:35 -05:00
Florian Albrechtskirchinger 7adbccbaf7 Refine when content is expected (#2044)
Consider Content-Length and Transfer-Encoding headers when determining
whether to expect content. Don't handle the HTTP/2 connection preface
pseudo-method PRI.

Fixes #2028.
2025-02-08 15:51:52 -05:00
yhirose eb10c22db1 Add unit test for #609 2025-02-08 10:17:09 -05:00
yhirose 708f860e3a Fix #2042 2025-02-06 05:56:31 -05:00
yhirose eb30f15363 Release v0.18.6 2025-02-05 19:14:20 -05:00
yhirose 4941d5b56b Fix #2033 (#2039) 2025-02-05 12:46:33 -05:00
Florian Albrechtskirchinger 9bbb4741b4 Run clang-format (#2037) 2025-02-02 22:32:33 -05:00
yhirose 282f2feb77 Add a unit test 2025-02-01 22:11:15 -05:00
alex-cornford 60a1f00618 Support building httplib.h on OpenVMS x86 systems (#2031)
Modify for OpenVMS x86 C++. Make tests on OpenVMS currently not supported due to no cmake support.
Changes tested on OpenVMS clang C++ and Fedora & GCC
2025-01-28 18:44:22 -05:00
yhirose 9104054ca5 Fix README example 2025-01-27 13:37:16 -05:00
Baiyies d69f144a99 Update httplib.h (#2030)
fix 'max'
2025-01-26 08:50:10 -05:00
yhirose 929dfbd348 Update copyright year 2025-01-20 00:32:10 -05:00
yhirose 3047183fd9 Update README 2025-01-20 00:02:02 -05:00
yhirose ef5e4044f1 Update README 2025-01-19 23:46:12 -05:00
yhirose 3779800322 Release v0.18.5 2025-01-17 17:38:03 -05:00
yhirose 986a20fb7d Resolve #2017 (#2022)
* Resolve #2017

* Fix warning

* Update README
2025-01-17 17:37:07 -05:00
yhirose 8311e1105f Fix Windows build problem 2025-01-16 23:26:04 -05:00
yhirose ba6845925d Fix #2014 2025-01-16 23:10:58 -05:00
yhirose 343a0fc073 Fix #2011 2025-01-16 21:38:45 -05:00
yhirose 54f8a4d0f3 Release v0.18.4 2025-01-16 01:00:25 -05:00
yhirose 9c36aae4b7 Fix HTTP Response Splitting Vulnerability 2025-01-16 00:04:33 -05:00
yhirose b766025a83 clangformat 2025-01-16 00:03:10 -05:00
yhirose 9b5f76f833 Fix #2012 2024-12-27 17:19:23 -05:00
sinnren d647f484a4 fix:set_file_content with range request return 416. (#2010)
Co-authored-by: fenlog <bakurise@qq.com>
2024-12-24 09:38:59 -05:00
Sergey Bobrenok 8794792baa Treat out-of-range last_pos as the end of the content (#2009)
RFC-9110 '14.1.2. Byte Ranges':
A client can limit the number of bytes requested without knowing the
size of the selected representation. If the last-pos value is absent,
or if the value is greater than or equal to the current length of the
representation data, the byte range is interpreted as the remainder of
the representation (i.e., the server replaces the value of last-pos
with a value that is one less than the current length of the selected
representation).

https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6
2024-12-23 13:14:36 -05:00
yhirose b85768c1f3 Fix #2005 2024-12-16 17:43:50 -05:00
yhirose e6d71bd702 Add a unit test for Issue #2004 2024-12-12 18:15:22 -05:00
yhirose 258992a160 Changed to use non-blocking socket in is_ssl_peer_could_be_closed 2024-12-03 19:26:08 -05:00
yhirose a7bc00e330 Release v0.18.3 2024-12-03 06:33:00 -05:00
yhirose 11a40584e9 Fix #1998 2024-12-03 00:38:20 -05:00
yhirose 3e86bdb4d8 Fix #1997 (#2001) 2024-12-03 00:11:29 -05:00
Pavel P c817d65695 Fix casting uint64_t to size_t for 32-bit builds (#1999) 2024-12-02 11:09:52 -05:00
7 changed files with 377 additions and 70 deletions
+18 -6
View File
@@ -39,10 +39,10 @@ svr.listen("0.0.0.0", 8080);
#include "path/to/httplib.h"
// HTTP
httplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");
httplib::Client cli("http://yhirose.github.io");
// HTTPS
httplib::Client cli("https://cpp-httplib-server.yhirose.repl.co");
httplib::Client cli("https://yhirose.github.io");
auto res = cli.Get("/hi");
res->status;
@@ -125,6 +125,21 @@ int main(void)
res.set_content(req.body, "text/plain");
});
// If the handler takes time to finish, you can also poll the connection state
svr.Get("/task", [&](const Request& req, Response& res) {
const char * result = nullptr;
process.run(); // for example, starting an external process
while (result == nullptr) {
sleep(1);
if (req.is_connection_closed()) {
process.kill(); // kill the process
return;
}
result = process.stdout(); // != nullptr if the process finishes
}
res.set_content(result, "text/plain");
});
svr.Get("/stop", [&](const Request& req, Response& res) {
svr.stop();
});
@@ -916,9 +931,6 @@ From Docker Hub
```bash
> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server
...
> docker run --init --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server
Serving HTTP on 0.0.0.0 port 80 ...
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
@@ -956,7 +968,7 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
License
-------
MIT license (© 2024 Yuji Hirose)
MIT license (© 2025 Yuji Hirose)
Special Thanks To
-----------------
+1 -1
View File
@@ -1,7 +1,7 @@
//
// httplib.h
//
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// main.cc
//
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -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
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark issue
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client
server : server.cc ../httplib.h Makefile
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
+1 -1
View File
@@ -1,7 +1,7 @@
//
// server_and_client.cc
//
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// MIT License
//
+152 -45
View File
@@ -1,14 +1,14 @@
//
// httplib.h
//
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// MIT License
//
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.18.2"
#define CPPHTTPLIB_VERSION "0.18.7"
/*
* Configuration
@@ -218,7 +218,9 @@ using socket_t = SOCKET;
#include <csignal>
#include <pthread.h>
#include <sys/mman.h>
#ifndef __VMS
#include <sys/select.h>
#endif
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@@ -628,6 +630,7 @@ struct Request {
Ranges ranges;
Match matches;
std::unordered_map<std::string, std::string> path_params;
std::function<bool()> is_connection_closed = []() { return true; };
// for client
ResponseHandler response_handler;
@@ -2012,18 +2015,34 @@ inline void duration_to_sec_and_usec(const T &duration, U callback) {
callback(static_cast<time_t>(sec), static_cast<time_t>(usec));
}
inline bool is_numeric(const std::string &str) {
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
}
inline uint64_t get_header_value_u64(const Headers &headers,
const std::string &key, uint64_t def,
size_t id) {
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));
if (it != rng.second) {
return std::strtoull(it->second.data(), nullptr, 10);
if (is_numeric(it->second)) {
return std::strtoull(it->second.data(), nullptr, 10);
} else {
is_invalid_value = true;
}
}
return def;
}
inline uint64_t get_header_value_u64(const Headers &headers,
const std::string &key, uint64_t def,
size_t id) {
bool dummy = false;
return get_header_value_u64(headers, key, def, id, dummy);
}
} // namespace detail
inline uint64_t Request::get_header_value_u64(const std::string &key,
@@ -2041,8 +2060,6 @@ inline void default_socket_options(socket_t sock) {
#ifdef _WIN32
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
reinterpret_cast<const char *>(&opt), sizeof(opt));
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
reinterpret_cast<const char *>(&opt), sizeof(opt));
#else
#ifdef SO_REUSEPORT
setsockopt(sock, SOL_SOCKET, SO_REUSEPORT,
@@ -2506,6 +2523,60 @@ private:
bool is_open_empty_file = false;
};
// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5
namespace fields {
inline bool is_token_char(char c) {
return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
}
inline bool is_token(const std::string &s) {
if (s.empty()) { return false; }
for (auto c : s) {
if (!is_token_char(c)) { return false; }
}
return true;
}
inline bool is_field_name(const std::string &s) { return is_token(s); }
inline bool is_vchar(char c) { return c >= 33 && c <= 126; }
inline bool is_obs_text(char c) { return 128 <= static_cast<unsigned char>(c); }
inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); }
inline bool is_field_content(const std::string &s) {
if (s.empty()) { return true; }
if (s.size() == 1) {
return is_field_vchar(s[0]);
} else if (s.size() == 2) {
return is_field_vchar(s[0]) && is_field_vchar(s[1]);
} else {
size_t i = 0;
if (!is_field_vchar(s[i])) { return false; }
i++;
while (i < s.size() - 1) {
auto c = s[i++];
if (c == ' ' || c == '\t' || is_field_vchar(c)) {
} else {
return false;
}
}
return is_field_vchar(s[i]);
}
}
inline bool is_field_value(const std::string &s) { return is_field_content(s); }
} // namespace fields
} // namespace detail
// ----------------------------------------------------------------------------
@@ -4143,22 +4214,21 @@ 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 = case_ignore::equal(key, "Location")
? std::string(p, end)
: decode_url(std::string(p, end), false);
// auto val = (case_ignore::equal(key, "Location") ||
// case_ignore::equal(key, "Referer"))
// ? std::string(p, end)
// : decode_url(std::string(p, end), false);
auto val = std::string(p, end);
// NOTE: From RFC 9110:
// Field values containing CR, LF, or NUL characters are
// invalid and dangerous, due to the varying ways that
// implementations might parse and interpret those
// characters; a recipient of CR, LF, or NUL within a field
// value MUST either reject the message or replace each of
// those characters with SP before further processing or
// forwarding of that message.
static const std::string CR_LF_NUL("\r\n\0", 3);
if (val.find_first_of(CR_LF_NUL) != std::string::npos) { return false; }
if (!detail::fields::is_field_value(val)) { return false; }
if (case_ignore::equal(key, "Location") ||
case_ignore::equal(key, "Referer")) {
fn(key, val);
} else {
fn(key, decode_url(val, false));
}
fn(key, val);
return true;
}
@@ -4194,7 +4264,7 @@ inline bool read_headers(Stream &strm, Headers &headers) {
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
if (!parse_header(line_reader.ptr(), end,
[&](const std::string &key, std::string &val) {
[&](const std::string &key, const std::string &val) {
headers.emplace(key, val);
})) {
return false;
@@ -4386,8 +4456,14 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
} else if (!has_header(x.headers, "Content-Length")) {
ret = read_content_without_length(strm, out);
} else {
auto len = get_header_value_u64(x.headers, "Content-Length", 0, 0);
if (len > payload_max_length) {
auto is_invalid_value = false;
auto len = get_header_value_u64(
x.headers, "Content-Length",
(std::numeric_limits<uint64_t>::max)(), 0, is_invalid_value);
if (is_invalid_value) {
ret = false;
} else if (len > payload_max_length) {
exceed_payload_max_length = true;
skip_content_with_length(strm, len);
ret = false;
@@ -5156,7 +5232,18 @@ inline bool range_error(Request &req, Response &res) {
last_pos = contant_len - 1;
}
if (last_pos == -1) { last_pos = contant_len - 1; }
// NOTE: RFC-9110 '14.1.2. Byte Ranges':
// A client can limit the number of bytes requested without knowing the
// size of the selected representation. If the last-pos value is absent,
// or if the value is greater than or equal to the current length of the
// representation data, the byte range is interpreted as the remainder of
// the representation (i.e., the server replaces the value of last-pos
// with a value that is one less than the current length of the selected
// representation).
// https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6
if (last_pos == -1 || last_pos >= contant_len) {
last_pos = contant_len - 1;
}
// Range must be within content length
if (!(0 <= first_pos && first_pos <= last_pos &&
@@ -5294,10 +5381,14 @@ write_multipart_ranges_data(Stream &strm, const Request &req, Response &res,
inline bool expect_content(const Request &req) {
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
req.method == "PRI" || req.method == "DELETE") {
req.method == "DELETE") {
return true;
}
// TODO: check if Content-Length is set
if (req.has_header("Content-Length") &&
req.get_header_value_u64("Content-Length") > 0) {
return true;
}
if (is_chunked_transfer_encoding(req.headers)) { return true; }
return false;
}
@@ -5688,7 +5779,8 @@ inline size_t Request::get_header_value_count(const std::string &key) const {
inline void Request::set_header(const std::string &key,
const std::string &val) {
if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
if (detail::fields::is_field_name(key) &&
detail::fields::is_field_value(val)) {
headers.emplace(key, val);
}
}
@@ -5754,13 +5846,14 @@ inline size_t Response::get_header_value_count(const std::string &key) const {
inline void Response::set_header(const std::string &key,
const std::string &val) {
if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
if (detail::fields::is_field_name(key) &&
detail::fields::is_field_value(val)) {
headers.emplace(key, val);
}
}
inline void Response::set_redirect(const std::string &url, int stat) {
if (!detail::has_crlf(url)) {
if (detail::fields::is_field_value(url)) {
set_header("Location", url);
if (300 <= stat && stat < 400) {
this->status = stat;
@@ -7130,6 +7223,11 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
}
}
// Setup `is_connection_closed` method
req.is_connection_closed = [&]() {
return !detail::is_socket_alive(strm.socket());
};
// Routing
auto routed = false;
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
@@ -7172,14 +7270,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
: StatusCode::PartialContent_206;
}
if (detail::range_error(req, res)) {
res.body.clear();
res.content_length_ = 0;
res.content_provider_ = nullptr;
res.status = StatusCode::RangeNotSatisfiable_416;
return write_response(strm, close_connection, req, res);
}
// Serve file content by using a content provider
if (!res.file_content_path_.empty()) {
const auto &path = res.file_content_path_;
@@ -7206,6 +7296,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
});
}
if (detail::range_error(req, res)) {
res.body.clear();
res.content_length_ = 0;
res.content_provider_ = nullptr;
res.status = StatusCode::RangeNotSatisfiable_416;
return write_response(strm, close_connection, req, res);
}
return write_response_with_content(strm, close_connection, req, res);
} else {
if (res.status == -1) { res.status = StatusCode::NotFound_404; }
@@ -7418,6 +7516,10 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
inline bool ClientImpl::is_ssl_peer_could_be_closed(SSL *ssl) const {
detail::set_nonblocking(socket_.sock, true);
auto se = detail::scope_exit(
[&]() { detail::set_nonblocking(socket_.sock, false); });
char buf[1];
return !SSL_peek(ssl, buf, 1) &&
SSL_get_error(ssl, 0) == SSL_ERROR_ZERO_RETURN;
@@ -7962,7 +8064,9 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
// Body
if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
req.method != "CONNECT") {
auto redirect = 300 < res.status && res.status < 400 && follow_location_;
auto redirect = 300 < res.status && res.status < 400 &&
res.status != StatusCode::NotModified_304 &&
follow_location_;
if (req.response_handler && !redirect) {
if (!req.response_handler(res)) {
@@ -8002,16 +8106,18 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
error = Error::Read;
return false;
}
res.body.reserve(len);
res.body.reserve(static_cast<size_t>(len));
}
}
int dummy_status;
if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(),
dummy_status, std::move(progress), std::move(out),
decompress_)) {
if (error != Error::Canceled) { error = Error::Read; }
return false;
if (res.status != StatusCode::NotModified_304) {
int dummy_status;
if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(),
dummy_status, std::move(progress),
std::move(out), decompress_)) {
if (error != Error::Canceled) { error = Error::Read; }
return false;
}
}
}
@@ -8900,6 +9006,7 @@ inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock,
// best-efforts.
if (shutdown_gracefully) {
#ifdef _WIN32
(void)(sock);
SSL_shutdown(ssl);
#else
timeval tv;
+203 -15
View File
@@ -511,6 +511,15 @@ TEST(GetHeaderValueTest, RegularValueInt) {
EXPECT_EQ(100ull, val);
}
TEST(GetHeaderValueTest, RegularInvalidValueInt) {
Headers headers = {{"Content-Length", "x"}};
auto is_invalid_value = false;
auto val = detail::get_header_value_u64(headers, "Content-Length", 0, 0,
is_invalid_value);
EXPECT_EQ(0ull, val);
EXPECT_TRUE(is_invalid_value);
}
TEST(GetHeaderValueTest, Range) {
{
Headers headers = {make_range_header({{1, -1}})};
@@ -1854,6 +1863,32 @@ TEST(PathUrlEncodeTest, PathUrlEncode) {
}
}
TEST(PathUrlEncodeTest, IncludePercentEncodingLF) {
Server svr;
svr.Get("/", [](const Request &req, Response &) {
EXPECT_EQ("\x0A", req.get_param_value("something"));
});
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_url_encode(false);
auto res = cli.Get("/?something=%0A");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
}
}
TEST(BindServerTest, DISABLED_BindDualStack) {
Server svr;
@@ -2942,6 +2977,11 @@ protected:
res.status = 401;
res.set_header("WWW-Authenticate", "Basic realm=123456");
})
.Delete("/issue609",
[](const httplib::Request &, httplib::Response &res,
const httplib::ContentReader &) {
res.set_content("ok", "text/plain");
})
#if defined(CPPHTTPLIB_ZLIB_SUPPORT) || defined(CPPHTTPLIB_BROTLI_SUPPORT)
.Get("/compress",
[&](const Request & /*req*/, Response &res) {
@@ -3036,6 +3076,16 @@ TEST_F(ServerTest, GetFileContent) {
EXPECT_EQ("test.html", res->body);
}
TEST_F(ServerTest, GetFileContentWithRange) {
auto res = cli_.Get("/file_content", {{make_range_header({{1, 3}})}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
EXPECT_EQ("bytes 1-3/9", res->get_header_value("Content-Range"));
EXPECT_EQ(3, std::stoi(res->get_header_value("Content-Length")));
EXPECT_EQ("est", res->body);
}
TEST_F(ServerTest, GetFileContentWithContentType) {
auto res = cli_.Get("/file_content_with_content_type");
ASSERT_TRUE(res);
@@ -3794,12 +3844,14 @@ TEST_F(ServerTest, GetStreamedWithRangeError) {
TEST_F(ServerTest, GetRangeWithMaxLongLength) {
auto res = cli_.Get(
"/with-range",
{{"Range",
"bytes=0-" + std::to_string(std::numeric_limits<long>::max())}});
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
EXPECT_EQ("0", res->get_header_value("Content-Length"));
EXPECT_EQ(false, res->has_header("Content-Range"));
EXPECT_EQ(0U, res->body.size());
{{"Range", "bytes=0-" + std::to_string(std::numeric_limits<long>::max())},
{"Accept-Encoding", ""}});
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
EXPECT_EQ("7", res->get_header_value("Content-Length"));
EXPECT_EQ(true, res->has_header("Content-Range"));
EXPECT_EQ("bytes 0-6/7", res->get_header_value("Content-Range"));
EXPECT_EQ(std::string("abcdefg"), res->body);
}
TEST_F(ServerTest, GetRangeWithZeroToInfinite) {
@@ -4001,6 +4053,13 @@ TEST_F(ServerTest, Issue1772) {
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
}
TEST_F(ServerTest, Issue609) {
auto res = cli_.Delete("/issue609");
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(std::string("ok"), res->body);
}
TEST_F(ServerTest, GetStreamedChunked) {
auto res = cli_.Get("/streamed-chunked");
ASSERT_TRUE(res);
@@ -4889,8 +4948,10 @@ TEST(ServerRequestParsingTest, TrimWhitespaceFromHeaderValues) {
"Connection: close\r\n"
"\r\n";
ASSERT_TRUE(send_request(5, req));
EXPECT_EQ(header_value, "\v bar \x1B");
std::string res;
ASSERT_TRUE(send_request(5, req, &res));
EXPECT_EQ(header_value, "");
EXPECT_EQ("HTTP/1.1 400 Bad Request", res.substr(0, 24));
}
// Sends a raw request and verifies that there isn't a crash or exception.
@@ -5048,6 +5109,14 @@ TEST(ServerRequestParsingTest, InvalidFieldValueContains_CR_LF_NUL) {
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
}
TEST(ServerRequestParsingTest, InvalidFieldValueContains_LF) {
std::string out;
std::string request(
"GET /header_field_value_check HTTP/1.1\r\nTest: [\n\n\n]\r\n\r\n", 55);
test_raw_request(request, &out);
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
}
TEST(ServerRequestParsingTest, EmptyFieldValue) {
std::string out;
@@ -6132,6 +6201,18 @@ TEST(SSLClientTest, WildcardHostNameMatch_Online) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
TEST(SSLClientTest, Issue2004_Online) {
Client client("https://google.com");
client.set_follow_location(true);
auto res = client.Get("/");
ASSERT_TRUE(res);
ASSERT_EQ(StatusCode::OK_200, res->status);
auto body = res->body;
EXPECT_EQ(body.substr(0, 15), "<!doctype html>");
}
#if 0
TEST(SSLClientTest, SetInterfaceWithINET6) {
auto cli = std::make_shared<httplib::Client>("https://httpbin.org");
@@ -7472,9 +7553,9 @@ TEST(MultipartFormDataTest, CloseDelimiterWithoutCRLF) {
"text2"
"\r\n------------";
std::string resonse;
ASSERT_TRUE(send_request(1, req, &resonse));
ASSERT_EQ("200", resonse.substr(9, 3));
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
TEST(MultipartFormDataTest, ContentLength) {
@@ -7519,11 +7600,10 @@ TEST(MultipartFormDataTest, ContentLength) {
"text2"
"\r\n------------\r\n";
std::string resonse;
ASSERT_TRUE(send_request(1, req, &resonse));
ASSERT_EQ("200", resonse.substr(9, 3));
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("200", response.substr(9, 3));
}
#endif
TEST(TaskQueueTest, IncreaseAtomicInteger) {
@@ -7901,6 +7981,114 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
cli.Get("/test", {{"Test", "_\n\r_\n\r_"}});
}
TEST(InvalidHeaderCharsTest, is_field_name) {
EXPECT_TRUE(detail::fields::is_field_name("exampleToken"));
EXPECT_TRUE(detail::fields::is_field_name("token123"));
EXPECT_TRUE(detail::fields::is_field_name("!#$%&'*+-.^_`|~"));
EXPECT_FALSE(detail::fields::is_field_name("example token"));
EXPECT_FALSE(detail::fields::is_field_name(" example_token"));
EXPECT_FALSE(detail::fields::is_field_name("example_token "));
EXPECT_FALSE(detail::fields::is_field_name("token@123"));
EXPECT_FALSE(detail::fields::is_field_name(""));
EXPECT_FALSE(detail::fields::is_field_name("example\rtoken"));
EXPECT_FALSE(detail::fields::is_field_name("example\ntoken"));
EXPECT_FALSE(detail::fields::is_field_name(std::string("\0", 1)));
EXPECT_FALSE(detail::fields::is_field_name("example\ttoken"));
}
TEST(InvalidHeaderCharsTest, is_field_value) {
EXPECT_TRUE(detail::fields::is_field_value("exampleToken"));
EXPECT_TRUE(detail::fields::is_field_value("token123"));
EXPECT_TRUE(detail::fields::is_field_value("!#$%&'*+-.^_`|~"));
EXPECT_TRUE(detail::fields::is_field_value("example token"));
EXPECT_FALSE(detail::fields::is_field_value(" example_token"));
EXPECT_FALSE(detail::fields::is_field_value("example_token "));
EXPECT_TRUE(detail::fields::is_field_value("token@123"));
EXPECT_TRUE(detail::fields::is_field_value(""));
EXPECT_FALSE(detail::fields::is_field_value("example\rtoken"));
EXPECT_FALSE(detail::fields::is_field_value("example\ntoken"));
EXPECT_FALSE(detail::fields::is_field_value(std::string("\0", 1)));
EXPECT_TRUE(detail::fields::is_field_value("example\ttoken"));
EXPECT_TRUE(detail::fields::is_field_value("0"));
}
TEST(InvalidHeaderCharsTest, OnServer) {
Server svr;
svr.Get("/test_name", [&](const Request &req, Response &res) {
std::string header = "Not Set";
if (req.has_param("header")) { header = req.get_param_value("header"); }
res.set_header(header, "value");
res.set_content("Page Content Page Content", "text/plain");
});
svr.Get("/test_value", [&](const Request &req, Response &res) {
std::string header = "Not Set";
if (req.has_param("header")) { header = req.get_param_value("header"); }
res.set_header("X-Test", header);
res.set_content("Page Content Page Content", "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);
{
auto res = cli.Get(
R"(/test_name?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
ASSERT_TRUE(res);
EXPECT_EQ("Page Content Page Content", res->body);
EXPECT_FALSE(res->has_header("HEADER_KEY"));
}
{
auto res = cli.Get(
R"(/test_value?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
ASSERT_TRUE(res);
EXPECT_EQ("Page Content Page Content", res->body);
EXPECT_FALSE(res->has_header("HEADER_KEY"));
}
}
TEST(InvalidHeaderValueTest, InvalidContentLength) {
auto handled = false;
Server svr;
svr.Post("/test", [&](const Request &, Response &) { handled = true; });
thread t = thread([&] { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_FALSE(handled);
});
svr.wait_until_ready();
auto req = "POST /test HTTP/1.1\r\n"
"Content-Length: x\r\n"
"\r\n";
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("HTTP/1.1 400 Bad Request",
response.substr(0, response.find("\r\n")));
}
#ifndef _WIN32
TEST(Expect100ContinueTest, ServerClosesConnection) {
static constexpr char reject[] = "Unauthorized";