mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5814e121df | |||
| 7adbccbaf7 | |||
| eb10c22db1 | |||
| 708f860e3a | |||
| eb30f15363 | |||
| 4941d5b56b | |||
| 9bbb4741b4 | |||
| 282f2feb77 | |||
| 60a1f00618 | |||
| 9104054ca5 | |||
| d69f144a99 | |||
| 929dfbd348 | |||
| 3047183fd9 | |||
| ef5e4044f1 | |||
| 3779800322 | |||
| 986a20fb7d | |||
| 8311e1105f | |||
| ba6845925d | |||
| 343a0fc073 | |||
| 54f8a4d0f3 | |||
| 9c36aae4b7 | |||
| b766025a83 | |||
| 9b5f76f833 | |||
| d647f484a4 | |||
| 8794792baa | |||
| b85768c1f3 | |||
| e6d71bd702 | |||
| 258992a160 | |||
| a7bc00e330 | |||
| 11a40584e9 | |||
| 3e86bdb4d8 | |||
| c817d65695 |
@@ -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,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
@@ -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
@@ -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,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
|
||||
//
|
||||
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user