Compare commits

...

32 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
yhirose f2f4728489 Release v0.12.1 2023-03-10 17:53:19 -05:00
yhirose d1b616286f Add note for macOS regarding system certs 2023-03-10 17:48:19 -05:00
Mathieu Gaillard df74526f91 Fix multipart Content-Type headers with both boundary and charset parameters (#1516)
* Fix multipart Content-Type headers with both boundary and charset parameters

* Improve code readability

* Add missing forward declaration

---------

Co-authored-by: Mathieu Gaillard <gaillard@adobe.com>
2023-03-08 23:57:17 -05:00
Joel Rosdahl 9f7ae0737a Fix typos (#1517) 2023-03-08 17:03:20 -05:00
yhirose 1ebb8412c5 Use SSL_ERROR_ZERO_RETURN to check if the SSL peer is closed. 2023-03-07 08:15:16 -05:00
Petr Menšík 7b69999c37 Reuse gtest on system (#1493)
* Reuse gtest on the system

Try to use gtest on the system if found. Avoid using recent CMake
features.

* Set CMP0135 only when using FetchContent

* Add /bigobj option for MSVC

* Support also cmake between 3.14 and 3.20

Older versions provided only GTest::Main target, not currently used
gtest_main. Provide backward compatibility also to old cmake versions.

* Remove redundant variable checking

---------

Co-authored-by: Jiwoo Park <jiwoo_90@naver.com>
2023-03-05 21:35:35 -05:00
yhirose c7e959a948 Fix #1481 2023-03-04 18:14:00 -05:00
yhirose ba5884e779 Fix #1481 (#1513) 2023-03-03 23:45:19 -05:00
yhirose cdaa5c48db Code cleanup 2023-03-03 22:41:57 -05:00
Alexandre Bouvier bab5c0e907 cmake: fix find_dependency (#1509) 2023-03-01 08:38:58 -05:00
Alexandre Bouvier 016838fd10 cmake: support components (#1504) 2023-02-25 09:56:56 -05:00
yhirose 75053bf855 Fix #1498 (#1501)
* Fix #1498

* Fixed build error
2023-02-22 13:19:36 -05:00
yhirose ae3a6dd2a9 Fixed an issue with example/Makefile on MacOS 2023-02-21 22:00:10 -05:00
Sergey Kazmin 6d963fbe8d Support loading system certs from Keychein on MacOS (#1474)
* Support loading system certs from Keychein on MacOS

* review improvements: add deps to meson.build and improve conditional expressions in cmake

* fix tabs

* fix tabs

* review improvements

* fix after review

* additionally load root certs from the system root keychain

* cmake fix

* fix

* small refactoring

* small refactoring

---------

Co-authored-by: Sergey Kazmin <sergey.kazmin@kaspersky.com>
2023-02-17 12:06:55 -05:00
jingTian-z 88f6245c84 feat: Add Request::get_file_multi_value func. (#1495)
Support to get multiple values of a key.

perf: Rename function names, variable names etc.
2023-02-16 21:51:06 -05:00
yhirose 0e7d2f9f93 Resolve #1482 2023-02-14 11:40:47 -05:00
yhirose 4e6ded1f36 Release v0.12.0 2023-02-07 10:27:40 -05:00
yhirose d663588491 Removed is_writable() from DataSink (Resolve #1478, too) (#1483) 2023-02-04 13:53:42 -05:00
yhirose 439caf5b79 Fix #1479 2023-02-01 19:11:11 -05:00
yhirose c4ba43ca6f Removed incorrect comment 2023-01-24 09:07:42 -05:00
Jiwoo Park 20cba2ecd9 Support CTest (#1468)
* Add test/CMakeLists.txt to enable testing with CTest

Add HTTPLIB_TEST option
Downlaod GoogleTest source using FetchContent module
Generate cert files to test httplib with OpenSSL

* Generate cert2.pem with a new certificate request

* Use the latest GoogleTest library
2023-01-21 03:43:22 -05:00
yhirose 0ff2e16d69 Issue 52666: cpp-httplib:server_fuzzer: Timeout in server_fuzzer 2023-01-21 01:09:19 -05:00
Ray Beck 51607ec752 add to_human_string (#1467)
* add to_human_string

* replace to_string with to_human_string

* fix test
2023-01-19 07:01:34 -05:00
14 changed files with 772 additions and 135 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: |
+12 -6
View File
@@ -8,13 +8,14 @@
* HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on)
* HTTPLIB_REQUIRE_BROTLI (default off)
* HTTPLIB_COMPILE (default off)
* HTTPLIB_TEST (default off)
* BROTLI_USE_STATIC_LIBS - tells Cmake to use the static Brotli libs (only works if you have them installed).
* OPENSSL_USE_STATIC_LIBS - tells Cmake to use the static OpenSSL libs (only works if you have them installed).
-------------------------------------------------------------------------------
After installation with Cmake, a find_package(httplib) is available.
This creates a httplib::httplib target (if found).
After installation with Cmake, a find_package(httplib COMPONENTS OpenSSL ZLIB Brotli) is available.
This creates a httplib::httplib target (if found and if listed components are supported).
It can be linked like so:
target_link_libraries(your_exe httplib::httplib)
@@ -78,7 +79,7 @@ set(_HTTPLIB_OPENSSL_MIN_VER "1.1.1")
# Allow for a build to require OpenSSL to pass, instead of just being optional
option(HTTPLIB_REQUIRE_OPENSSL "Requires OpenSSL to be found & linked, or fails build." OFF)
option(HTTPLIB_REQUIRE_ZLIB "Requires ZLIB to be found & linked, or fails build." OFF)
# Allow for a build to casually enable OpenSSL/ZLIB support, but silenty continue if not found.
# Allow for a build to casually enable OpenSSL/ZLIB support, but silently continue if not found.
# Make these options so their automatic use can be specifically disabled (as needed)
option(HTTPLIB_USE_OPENSSL_IF_AVAILABLE "Uses OpenSSL (if available) to enable HTTPS support." ON)
option(HTTPLIB_USE_ZLIB_IF_AVAILABLE "Uses ZLIB (if available) to enable Zlib compression support." ON)
@@ -88,7 +89,7 @@ option(HTTPLIB_COMPILE "If ON, uses a Python script to split the header into a c
if(HTTPLIB_COMPILE)
set(HTTPLIB_IS_COMPILED TRUE)
endif()
option(HTTPLIB_TEST "Enables testing and builds tests" OFF)
option(HTTPLIB_REQUIRE_BROTLI "Requires Brotli to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_BROTLI_IF_AVAILABLE "Uses Brotli (if available) to enable Brotli decompression support." ON)
# Defaults to static library
@@ -205,6 +206,8 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Windows>:crypt32>
$<$<PLATFORM_ID:Windows>:cryptui>
# Needed for API from MacOS Security framework
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>>:-framework CoreFoundation -framework Security>"
# Can't put multiple targets in a single generator expression or it bugs out.
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
@@ -232,8 +235,6 @@ configure_package_config_file("${PROJECT_NAME}Config.cmake.in"
INSTALL_DESTINATION "${_TARGET_INSTALL_CMAKEDIR}"
# Passes the includedir install path
PATH_VARS CMAKE_INSTALL_FULL_INCLUDEDIR
# There aren't any components, so don't use the macro
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
if(HTTPLIB_COMPILE)
@@ -278,3 +279,8 @@ install(EXPORT httplibTargets
NAMESPACE ${PROJECT_NAME}::
DESTINATION ${_TARGET_INSTALL_CMAKEDIR}
)
if(HTTPLIB_TEST)
include(CTest)
add_subdirectory(test)
endif()
+27 -1
View File
@@ -54,6 +54,7 @@ SSL Support
SSL support is available with `CPPHTTPLIB_OPENSSL_SUPPORT`. `libssl` and `libcrypto` should be linked.
NOTE: cpp-httplib currently supports only version 1.1.1 and 3.0.
NOTE for macOS: cpp-httplib now uses system certs. `CoreFoundation` and `Security` should be linked with `-framework`.
```c++
#define CPPHTTPLIB_OPENSSL_SUPPORT
@@ -346,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.
@@ -493,6 +515,10 @@ auto res = cli.Get("/hi", headers);
```
or
```c++
auto res = cli.Get("/hi", {{"Accept-Encoding", "gzip, deflate"}});
```
or
```c++
cli.set_default_headers({
{ "Accept-Encoding", "gzip, deflate" }
});
@@ -815,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
-----------------
+8 -1
View File
@@ -8,12 +8,19 @@ OPENSSL_DIR = $(PREFIX)/opt/openssl@1.1
#OPENSSL_DIR = $(PREFIX)/opt/openssl@3
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
ifneq ($(OS), Windows_NT)
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S), Darwin)
OPENSSL_SUPPORT += -framework CoreFoundation -framework Security
endif
endif
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
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark issue
server : server.cc ../httplib.h Makefile
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
+1 -1
View File
@@ -19,7 +19,7 @@ public:
unique_lock<mutex> lk(m_);
int id = id_;
cv_.wait(lk, [&] { return cid_ == id; });
if (sink->is_writable()) { sink->write(message_.data(), message_.size()); }
sink->write(message_.data(), message_.size());
}
void send_event(const string &message) {
+315 -88
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.11.4"
#define CPPHTTPLIB_VERSION "0.12.2"
/*
* Configuration
@@ -239,7 +239,10 @@ using socket_t = int;
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "cryptui.lib")
#endif
#endif //_WIN32
#elif defined(__APPLE__) // _WIN32
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#endif // __APPLE__
#include <openssl/err.h>
#include <openssl/evp.h>
@@ -308,6 +311,34 @@ struct ci {
}
};
// This is based on
// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189".
template <typename EF> struct scope_exit {
explicit scope_exit(EF &&f)
: exit_function(std::move(f)), execute_on_destruction{true} {}
scope_exit(scope_exit &&rhs)
: exit_function(std::move(rhs.exit_function)),
execute_on_destruction{rhs.execute_on_destruction} {
rhs.release();
}
~scope_exit() {
if (execute_on_destruction) { this->exit_function(); }
}
void release() { this->execute_on_destruction = false; }
private:
scope_exit(const scope_exit &) = delete;
void operator=(const scope_exit &) = delete;
scope_exit &operator=(scope_exit &&) = delete;
EF exit_function;
bool execute_on_destruction;
};
} // namespace detail
using Headers = std::multimap<std::string, std::string, detail::ci>;
@@ -340,7 +371,7 @@ public:
std::function<bool(const char *data, size_t data_len)> write;
std::function<void()> done;
std::function<bool()> is_writable;
std::function<void(const Headers &trailer)> done_with_trailer;
std::ostream os;
private:
@@ -455,6 +486,7 @@ struct Request {
bool has_file(const std::string &key) const;
MultipartFormData get_file_value(const std::string &key) const;
std::vector<MultipartFormData> get_file_values(const std::string &key) const;
// private members...
size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;
@@ -769,7 +801,7 @@ private:
ContentReceiver multipart_receiver);
bool read_content_core(Stream &strm, Request &req, Response &res,
ContentReceiver receiver,
MultipartContentHeader mulitpart_header,
MultipartContentHeader multipart_header,
ContentReceiver multipart_receiver);
virtual bool process_and_close_socket(socket_t sock);
@@ -823,6 +855,9 @@ enum class Error {
UnsupportedMultipartBoundaryChars,
Compression,
ConnectionTimeout,
// For internal use only
SSLPeerCouldBeClosed_,
};
std::string to_string(const Error error);
@@ -1095,8 +1130,6 @@ protected:
bool is_open() const { return sock != INVALID_SOCKET; }
};
Result send_(Request &&req);
virtual bool create_and_connect_socket(Socket &socket, Error &error);
// All of:
@@ -1118,7 +1151,7 @@ protected:
void copy_settings(const ClientImpl &rhs);
// Socket endoint information
// Socket endpoint information
const std::string host_;
const int port_;
const std::string host_and_port_;
@@ -1197,6 +1230,9 @@ protected:
Logger logger_;
private:
bool send_(Request &req, Response &res, Error &error);
Result send_(Request &&req);
socket_t create_client_socket(Error &error) const;
bool read_response_line(Stream &strm, const Request &req, Response &res);
bool write_request(Stream &strm, Request &req, bool close_connection,
@@ -1665,20 +1701,20 @@ Server::set_idle_interval(const std::chrono::duration<Rep, Period> &duration) {
inline std::string to_string(const Error error) {
switch (error) {
case Error::Success: return "Success";
case Error::Connection: return "Connection";
case Error::BindIPAddress: return "BindIPAddress";
case Error::Read: return "Read";
case Error::Write: return "Write";
case Error::ExceedRedirectCount: return "ExceedRedirectCount";
case Error::Canceled: return "Canceled";
case Error::SSLConnection: return "SSLConnection";
case Error::SSLLoadingCerts: return "SSLLoadingCerts";
case Error::SSLServerVerification: return "SSLServerVerification";
case Error::Success: return "Success (no error)";
case Error::Connection: return "Could not establish connection";
case Error::BindIPAddress: return "Failed to bind IP address";
case Error::Read: return "Failed to read connection";
case Error::Write: return "Failed to write connection";
case Error::ExceedRedirectCount: return "Maximum redirect count exceeded";
case Error::Canceled: return "Connection handling canceled";
case Error::SSLConnection: return "SSL connection failed";
case Error::SSLLoadingCerts: return "SSL certificate loading failed";
case Error::SSLServerVerification: return "SSL server verification failed";
case Error::UnsupportedMultipartBoundaryChars:
return "UnsupportedMultipartBoundaryChars";
case Error::Compression: return "Compression";
case Error::ConnectionTimeout: return "ConnectionTimeout";
return "Unsupported HTTP multipart boundary characters";
case Error::Compression: return "Compression failed";
case Error::ConnectionTimeout: return "Connection timed out";
case Error::Unknown: return "Unknown";
default: break;
}
@@ -1788,6 +1824,9 @@ 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_range_header(const std::string &s, Ranges &ranges);
int close_socket(socket_t sock);
@@ -2939,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) {
@@ -3354,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.
@@ -3377,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;
}
@@ -3475,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];
@@ -3501,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;
@@ -3579,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 {
@@ -3632,7 +3703,7 @@ inline bool write_content(Stream &strm, const ContentProvider &content_provider,
data_sink.write = [&](const char *d, size_t l) -> bool {
if (ok) {
if (write_data(strm, d, l)) {
if (strm.is_writable() && write_data(strm, d, l)) {
offset += l;
} else {
ok = false;
@@ -3641,14 +3712,14 @@ inline bool write_content(Stream &strm, const ContentProvider &content_provider,
return ok;
};
data_sink.is_writable = [&](void) { return ok && strm.is_writable(); };
while (offset < end_offset && !is_shutting_down()) {
if (!content_provider(offset, end_offset - offset, data_sink)) {
if (!strm.is_writable()) {
error = Error::Write;
return false;
} else if (!content_provider(offset, end_offset - offset, data_sink)) {
error = Error::Canceled;
return false;
}
if (!ok) {
} else if (!ok) {
error = Error::Write;
return false;
}
@@ -3680,18 +3751,21 @@ write_content_without_length(Stream &strm,
data_sink.write = [&](const char *d, size_t l) -> bool {
if (ok) {
offset += l;
if (!write_data(strm, d, l)) { ok = false; }
if (!strm.is_writable() || !write_data(strm, d, l)) { ok = false; }
}
return ok;
};
data_sink.done = [&](void) { data_available = false; };
data_sink.is_writable = [&](void) { return ok && strm.is_writable(); };
while (data_available && !is_shutting_down()) {
if (!content_provider(offset, 0, data_sink)) { return false; }
if (!ok) { return false; }
if (!strm.is_writable()) {
return false;
} else if (!content_provider(offset, 0, data_sink)) {
return false;
} else if (!ok) {
return false;
}
}
return true;
}
@@ -3720,7 +3794,10 @@ write_content_chunked(Stream &strm, const ContentProvider &content_provider,
// 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())) { ok = false; }
if (!strm.is_writable() ||
!write_data(strm, chunk.data(), chunk.size())) {
ok = false;
}
}
} else {
ok = false;
@@ -3729,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;
@@ -3747,26 +3824,46 @@ 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.is_writable = [&](void) { return ok && strm.is_writable(); };
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()) {
if (!content_provider(offset, 0, data_sink)) {
if (!strm.is_writable()) {
error = Error::Write;
return false;
} else if (!content_provider(offset, 0, data_sink)) {
error = Error::Canceled;
return false;
}
if (!ok) {
} else if (!ok) {
error = Error::Write;
return false;
}
@@ -3847,9 +3944,12 @@ inline void parse_query_text(const std::string &s, Params &params) {
inline bool parse_multipart_boundary(const std::string &content_type,
std::string &boundary) {
auto pos = content_type.find("boundary=");
auto boundary_keyword = "boundary=";
auto pos = content_type.find(boundary_keyword);
if (pos == std::string::npos) { return false; }
boundary = content_type.substr(pos + 9);
auto end = content_type.find(';', pos);
auto beg = pos + strlen(boundary_keyword);
boundary = content_type.substr(beg, end - beg);
if (boundary.length() >= 2 && boundary.front() == '"' &&
boundary.back() == '"') {
boundary = boundary.substr(1, boundary.size() - 2);
@@ -3960,6 +4060,9 @@ public:
if (std::regex_match(header, m, re_content_disposition)) {
file_.name = m[1];
file_.filename = m[2];
} else {
is_valid_ = false;
return false;
}
}
buf_erase(pos + crlf_.size());
@@ -4379,15 +4482,15 @@ inline std::string SHA_512(const std::string &s) {
}
#endif
#ifdef _WIN32
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#ifdef _WIN32
// NOTE: This code came up with the following stackoverflow post:
// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
inline bool load_system_certs_on_windows(X509_STORE *store) {
auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT");
if (!hStore) { return false; }
auto result = false;
PCCERT_CONTEXT pContext = NULL;
while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
nullptr) {
@@ -4398,16 +4501,107 @@ inline bool load_system_certs_on_windows(X509_STORE *store) {
if (x509) {
X509_STORE_add_cert(store, x509);
X509_free(x509);
result = true;
}
}
CertFreeCertificateContext(pContext);
CertCloseStore(hStore, 0);
return result;
}
#elif defined(__APPLE__)
template <typename T>
using CFObjectPtr =
std::unique_ptr<typename std::remove_pointer<T>::type, void (*)(CFTypeRef)>;
inline void cf_object_ptr_deleter(CFTypeRef obj) {
if (obj) { CFRelease(obj); }
}
inline bool retrieve_certs_from_keychain(CFObjectPtr<CFArrayRef> &certs) {
CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef};
CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll,
kCFBooleanTrue};
CFObjectPtr<CFDictionaryRef> query(
CFDictionaryCreate(nullptr, reinterpret_cast<const void **>(keys), values,
sizeof(keys) / sizeof(keys[0]),
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks),
cf_object_ptr_deleter);
if (!query) { return false; }
CFTypeRef security_items = nullptr;
if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess ||
CFArrayGetTypeID() != CFGetTypeID(security_items)) {
return false;
}
certs.reset(reinterpret_cast<CFArrayRef>(security_items));
return true;
}
inline bool retrieve_root_certs_from_keychain(CFObjectPtr<CFArrayRef> &certs) {
CFArrayRef root_security_items = nullptr;
if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) {
return false;
}
certs.reset(root_security_items);
return true;
}
inline bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) {
auto result = false;
for (int i = 0; i < CFArrayGetCount(certs); ++i) {
const auto cert = reinterpret_cast<const __SecCertificate *>(
CFArrayGetValueAtIndex(certs, i));
if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; }
CFDataRef cert_data = nullptr;
if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) !=
errSecSuccess) {
continue;
}
CFObjectPtr<CFDataRef> cert_data_ptr(cert_data, cf_object_ptr_deleter);
auto encoded_cert = static_cast<const unsigned char *>(
CFDataGetBytePtr(cert_data_ptr.get()));
auto x509 =
d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get()));
if (x509) {
X509_STORE_add_cert(store, x509);
X509_free(x509);
result = true;
}
}
return result;
}
inline bool load_system_certs_on_apple(X509_STORE *store) {
auto result = false;
CFObjectPtr<CFArrayRef> certs(nullptr, cf_object_ptr_deleter);
if (retrieve_certs_from_keychain(certs) && certs) {
result = add_certs_to_x509_store(certs.get(), store);
}
if (retrieve_root_certs_from_keychain(certs) && certs) {
result = add_certs_to_x509_store(certs.get(), store) || result;
}
return result;
}
#endif
#endif
#ifdef _WIN32
class WSInit {
public:
WSInit() {
@@ -4682,6 +4876,16 @@ inline MultipartFormData Request::get_file_value(const std::string &key) const {
return MultipartFormData();
}
inline std::vector<MultipartFormData>
Request::get_file_values(const std::string &key) const {
std::vector<MultipartFormData> values;
auto rng = files.equal_range(key);
for (auto it = rng.first; it != rng.second; it++) {
values.push_back(it->second);
}
return values;
}
// Response implementation
inline bool Response::has_header(const std::string &key) const {
return headers.find(key) != headers.end();
@@ -5397,7 +5601,7 @@ inline bool Server::read_content_with_content_receiver(
inline bool Server::read_content_core(Stream &strm, Request &req, Response &res,
ContentReceiver receiver,
MultipartContentHeader mulitpart_header,
MultipartContentHeader multipart_header,
ContentReceiver multipart_receiver) {
detail::MultipartFormDataParser multipart_form_data_parser;
ContentReceiverWithProgress out;
@@ -5417,14 +5621,14 @@ inline bool Server::read_content_core(Stream &strm, Request &req, Response &res,
while (pos < n) {
auto read_size = (std::min)<size_t>(1, n - pos);
auto ret = multipart_form_data_parser.parse(
buf + pos, read_size, multipart_receiver, mulitpart_header);
buf + pos, read_size, multipart_receiver, multipart_header);
if (!ret) { return false; }
pos += read_size;
}
return true;
*/
return multipart_form_data_parser.parse(buf, n, multipart_receiver,
mulitpart_header);
multipart_header);
};
} else {
out = [receiver](const char *buf, size_t n, uint64_t /*off*/,
@@ -5590,11 +5794,7 @@ inline bool Server::listen_internal() {
#endif
}
#if __cplusplus > 201703L
task_queue->enqueue([=, this]() { process_and_close_socket(sock); });
#else
task_queue->enqueue([=]() { process_and_close_socket(sock); });
#endif
task_queue->enqueue([this, sock]() { process_and_close_socket(sock); });
}
task_queue->shutdown();
@@ -6141,7 +6341,15 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
std::lock_guard<std::recursive_mutex> request_mutex_guard(request_mutex_);
auto ret = send_(req, res, error);
if (error == Error::SSLPeerCouldBeClosed_) {
assert(!ret);
ret = send_(req, res, error);
}
return ret;
}
inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
{
std::lock_guard<std::mutex> guard(socket_mutex_);
@@ -6172,7 +6380,7 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
if (is_ssl()) {
auto &scli = static_cast<SSLClient &>(*this);
if (!proxy_host_.empty() && proxy_port_ != -1) {
bool success = false;
auto success = false;
if (!scli.connect_with_proxy(socket_, res, success, error)) {
return success;
}
@@ -6199,13 +6407,11 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
}
}
auto ret = false;
auto close_connection = !keep_alive_;
auto ret = process_socket(socket_, [&](Stream &strm) {
return handle_request(strm, req, res, close_connection, error);
});
// Briefly lock mutex in order to mark that a request is no longer ongoing
{
auto se = detail::scope_exit<std::function<void(void)>>([&]() {
// Briefly lock mutex in order to mark that a request is no longer ongoing
std::lock_guard<std::mutex> guard(socket_mutex_);
socket_requests_in_flight_ -= 1;
if (socket_requests_in_flight_ <= 0) {
@@ -6219,7 +6425,11 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) {
shutdown_socket(socket_);
close_socket(socket_);
}
}
});
ret = process_socket(socket_, [&](Stream &strm) {
return handle_request(strm, req, res, close_connection, error);
});
if (!ret) {
if (error == Error::Success) { error = Error::Unknown; }
@@ -6307,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; }
@@ -6323,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()) {
@@ -6335,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);
}
}
}
@@ -6361,7 +6574,7 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
auto is_shutting_down = []() { return false; };
if (req.is_chunked_content_provider_) {
// TODO: Brotli suport
// TODO: Brotli support
std::unique_ptr<detail::compressor> compressor;
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
if (compress_) {
@@ -6378,7 +6591,7 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
return detail::write_content(strm, req.content_provider_, 0,
req.content_length_, is_shutting_down, error);
}
} // namespace httplib
}
inline bool ClientImpl::write_request(Stream &strm, Request &req,
bool close_connection, Error &error) {
@@ -6541,8 +6754,6 @@ inline std::unique_ptr<Response> ClientImpl::send_with_content_provider(
return ok;
};
data_sink.is_writable = [&](void) { return ok && true; };
while (ok && offset < content_length) {
if (!content_provider(offset, content_length - offset, data_sink)) {
error = Error::Canceled;
@@ -6613,6 +6824,20 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
// Send request
if (!write_request(strm, req, close_connection, error)) { return false; }
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
if (is_ssl()) {
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
// Receive response and headers
if (!read_response_line(strm, req, res) ||
!detail::read_headers(strm, res.headers)) {
@@ -6714,7 +6939,6 @@ inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider(
bool has_data = true;
cur_sink.write = sink.write;
cur_sink.done = [&]() { has_data = false; };
cur_sink.is_writable = sink.is_writable;
if (!provider_items[cur_item].provider(offset - cur_start, cur_sink))
return false;
@@ -7240,7 +7464,7 @@ inline void ClientImpl::stop() {
return;
}
// Otherwise, sitll holding the mutex, we can shut everything down ourselves
// Otherwise, still holding the mutex, we can shut everything down ourselves
shutdown_ssl(socket_, true);
shutdown_socket(socket_);
close_socket(socket_);
@@ -7653,7 +7877,7 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) {
},
[](SSL * /*ssl2*/) { return true; });
bool ret = false;
auto ret = false;
if (ssl) {
ret = detail::process_server_socket_ssl(
svr_sock_, ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_,
@@ -7831,11 +8055,14 @@ inline bool SSLClient::load_certs() {
ret = false;
}
} else {
auto loaded = false;
#ifdef _WIN32
detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
#else
SSL_CTX_set_default_verify_paths(ctx_);
loaded =
detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
#elif defined(__APPLE__)
loaded = detail::load_system_certs_on_apple(SSL_CTX_get_cert_store(ctx_));
#endif
if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); }
}
});
@@ -7980,7 +8207,7 @@ SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
if (alt_names) {
auto dsn_matched = false;
auto ip_mached = false;
auto ip_matched = false;
auto count = sk_GENERAL_NAME_num(alt_names);
@@ -7996,14 +8223,14 @@ SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
case GEN_IPADD:
if (!memcmp(&addr6, name, addr_len) ||
!memcmp(&addr, name, addr_len)) {
ip_mached = true;
ip_matched = true;
}
break;
}
}
}
if (dsn_matched || ip_mached) { ret = true; }
if (dsn_matched || ip_matched) { ret = true; }
}
GENERAL_NAMES_free((STACK_OF(GENERAL_NAME) *)alt_names);
+17 -8
View File
@@ -12,19 +12,19 @@ set(HTTPLIB_VERSION @PROJECT_VERSION@)
include(CMakeFindDependencyMacro)
# We add find_dependency calls here to not make the end-user have to call them.
find_dependency(Threads REQUIRED)
find_dependency(Threads)
if(@HTTPLIB_IS_USING_OPENSSL@)
# OpenSSL COMPONENTS were added in Cmake v3.11
if(CMAKE_VERSION VERSION_LESS "3.11")
find_dependency(OpenSSL @_HTTPLIB_OPENSSL_MIN_VER@ REQUIRED)
find_dependency(OpenSSL @_HTTPLIB_OPENSSL_MIN_VER@)
else()
# Once the COMPONENTS were added, they were made optional when not specified.
# Since we use both, we need to search for both.
find_dependency(OpenSSL @_HTTPLIB_OPENSSL_MIN_VER@ COMPONENTS Crypto SSL REQUIRED)
find_dependency(OpenSSL @_HTTPLIB_OPENSSL_MIN_VER@ COMPONENTS Crypto SSL)
endif()
endif()
if(@HTTPLIB_IS_USING_ZLIB@)
find_dependency(ZLIB REQUIRED)
find_dependency(ZLIB)
endif()
if(@HTTPLIB_IS_USING_BROTLI@)
@@ -32,7 +32,7 @@ if(@HTTPLIB_IS_USING_BROTLI@)
# Note that the FindBrotli.cmake file is installed in the same dir as this file.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")
set(BROTLI_USE_STATIC_LIBS @BROTLI_USE_STATIC_LIBS@)
find_dependency(Brotli COMPONENTS common encoder decoder REQUIRED)
find_dependency(Brotli COMPONENTS common encoder decoder)
endif()
# Mildly useful for end-users
@@ -42,10 +42,19 @@ set_and_check(HTTPLIB_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_FULL_INCLUDEDIR@")
# This is helpful if you're using Cmake's pre-compiled header feature
set_and_check(HTTPLIB_HEADER_PATH "@PACKAGE_CMAKE_INSTALL_FULL_INCLUDEDIR@/httplib.h")
# Brings in the target library
include("${CMAKE_CURRENT_LIST_DIR}/httplibTargets.cmake")
# Consider each library support as a "component"
set(httplib_OpenSSL_FOUND @HTTPLIB_IS_USING_OPENSSL@)
set(httplib_ZLIB_FOUND @HTTPLIB_IS_USING_ZLIB@)
set(httplib_Brotli_FOUND @HTTPLIB_IS_USING_BROTLI@)
# Ouputs a "found httplib /usr/include/httplib.h" message when using find_package(httplib)
check_required_components(httplib)
# Brings in the target library, but only if all required components are found
if(NOT DEFINED httplib_FOUND OR httplib_FOUND)
include("${CMAKE_CURRENT_LIST_DIR}/httplibTargets.cmake")
endif()
# Outputs a "found httplib /usr/include/httplib.h" message when using find_package(httplib)
include(FindPackageMessage)
if(TARGET httplib::httplib)
set(HTTPLIB_FOUND TRUE)
+3
View File
@@ -34,6 +34,9 @@ openssl_dep = dependency('openssl', version: '>=1.1.1', required: get_option('cp
if openssl_dep.found()
deps += openssl_dep
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
if host_machine.system() == 'darwin'
deps += dependency('appleframeworks', modules: ['CoreFoundation', 'Security'])
endif
endif
zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))
+104
View File
@@ -0,0 +1,104 @@
find_package(GTest)
if(GTest_FOUND)
if(NOT TARGET GTest::gtest_main AND TARGET GTest::Main)
# CMake <3.20
add_library(GTest::gtest_main INTERFACE IMPORTED)
target_link_libraries(GTest::gtest_main INTERFACE GTest::Main)
endif()
else()
if(POLICY CMP0135)
cmake_policy(SET CMP0135 NEW)
endif()
include(FetchContent)
set(BUILD_GMOCK OFF)
set(INSTALL_GTEST OFF)
set(gtest_force_shared_crt ON)
FetchContent_Declare(
gtest
URL https://github.com/google/googletest/archive/main.tar.gz
)
FetchContent_MakeAvailable(gtest)
endif()
add_executable(httplib-test test.cc)
target_compile_options(httplib-test PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8;/bigobj>")
target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main)
gtest_discover_tests(httplib-test)
execute_process(
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_LIST_DIR}/www www
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_LIST_DIR}/www2 www2
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_LIST_DIR}/www3 www3
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_LIST_DIR}/ca-bundle.crt ca-bundle.crt
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_LIST_DIR}/image.jpg image.jpg
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
if(HTTPLIB_IS_USING_OPENSSL)
find_program(OPENSSL_COMMAND
NAMES openssl
PATHS ${OPENSSL_INCLUDE_DIR}/../bin
REQUIRED
)
execute_process(
COMMAND ${OPENSSL_COMMAND} genrsa 2048
OUTPUT_FILE key.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} req -new -batch -config ${CMAKE_CURRENT_LIST_DIR}/test.conf -key key.pem
COMMAND ${OPENSSL_COMMAND} x509 -days 3650 -req -signkey key.pem
OUTPUT_FILE cert.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} req -x509 -new -config ${CMAKE_CURRENT_LIST_DIR}/test.conf -key key.pem -sha256 -days 3650 -nodes -out cert2.pem -extensions SAN
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} genrsa 2048
OUTPUT_FILE rootCA.key.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} req -x509 -new -batch -config ${CMAKE_CURRENT_LIST_DIR}/test.rootCA.conf -key rootCA.key.pem -days 1024
OUTPUT_FILE rootCA.cert.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} genrsa 2048
OUTPUT_FILE client.key.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} req -new -batch -config ${CMAKE_CURRENT_LIST_DIR}/test.conf -key client.key.pem
COMMAND ${OPENSSL_COMMAND} x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial
OUTPUT_FILE client.cert.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} genrsa -passout pass:test123! 2048
OUTPUT_FILE key_encrypted.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
execute_process(
COMMAND ${OPENSSL_COMMAND} req -new -batch -config ${CMAKE_CURRENT_LIST_DIR}/test.conf -key key_encrypted.pem
COMMAND ${OPENSSL_COMMAND} x509 -days 3650 -req -signkey key_encrypted.pem
OUTPUT_FILE cert_encrypted.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
endif()
+7
View File
@@ -8,6 +8,13 @@ OPENSSL_DIR = $(PREFIX)/opt/openssl@1.1
#OPENSSL_DIR = $(PREFIX)/opt/openssl@3
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
ifneq ($(OS), Windows_NT)
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S), Darwin)
OPENSSL_SUPPORT += -framework CoreFoundation -framework Security
endif
endif
ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
BROTLI_DIR = $(PREFIX)/opt/brotli
@@ -11,7 +11,7 @@
#include <vector>
// Forward declare the "fuzz target" interface.
// We deliberately keep this inteface simple and header-free.
// We deliberately keep this interface simple and header-free.
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
// It reads all files passed as parameters and feeds their contents
+274 -26
View File
@@ -46,7 +46,7 @@ MultipartFormData &get_file_value(MultipartFormDataItems &files,
return *it;
#else
if (it != files.end()) { return *it; }
throw std::runtime_error("invalid mulitpart form data name error");
throw std::runtime_error("invalid multipart form data name error");
#endif
}
@@ -169,6 +169,40 @@ TEST(ParamsToQueryTest, ConvertParamsToQuery) {
EXPECT_EQ(detail::params_to_query_str(dic), "key1=val1&key2=val2&key3=val3");
}
TEST(ParseMultipartBoundaryTest, DefaultValue) {
string content_type = "multipart/form-data; boundary=something";
string boundary;
auto ret = detail::parse_multipart_boundary(content_type, boundary);
EXPECT_TRUE(ret);
EXPECT_EQ(boundary, "something");
}
TEST(ParseMultipartBoundaryTest, ValueWithQuote) {
string content_type = "multipart/form-data; boundary=\"gc0pJq0M:08jU534c0p\"";
string boundary;
auto ret = detail::parse_multipart_boundary(content_type, boundary);
EXPECT_TRUE(ret);
EXPECT_EQ(boundary, "gc0pJq0M:08jU534c0p");
}
TEST(ParseMultipartBoundaryTest, ValueWithCharset) {
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);
EXPECT_EQ(boundary, "THIS_STRING_SEPARATES");
}
TEST(ParseMultipartBoundaryTest, ValueWithQuotesAndCharset) {
string content_type =
"multipart/mixed; boundary=\"cpp-httplib-multipart-data\"; charset=UTF-8";
string boundary;
auto ret = detail::parse_multipart_boundary(content_type, boundary);
EXPECT_TRUE(ret);
EXPECT_EQ(boundary, "cpp-httplib-multipart-data");
}
TEST(GetHeaderValueTest, DefaultValue) {
Headers headers = {{"Dummy", "Dummy"}};
auto val = detail::get_header_value(headers, "Content-Type", 0, "text/plain");
@@ -596,7 +630,7 @@ TEST(ConnectionErrorTest, InvalidHostCheckResultErrorToString) {
ASSERT_TRUE(!res);
stringstream s;
s << "error code: " << res.error();
EXPECT_EQ("error code: Connection (2)", s.str());
EXPECT_EQ("error code: Could not establish connection (2)", s.str());
}
TEST(ConnectionErrorTest, InvalidPort) {
@@ -779,7 +813,7 @@ TEST(DigestAuthTest, FromHTTPWatch_Online) {
}
// NOTE: Until httpbin.org fixes issue #46, the following test is commented
// out. Plese see https://httpbin.org/digest-auth/auth/hello/world
// out. Please see https://httpbin.org/digest-auth/auth/hello/world
// cli.set_digest_auth("bad", "world");
// for (auto path : paths) {
// auto res = cli.Get(path.c_str());
@@ -1650,7 +1684,6 @@ protected:
[&](const Request & /*req*/, Response &res) {
res.set_chunked_content_provider(
"text/plain", [](size_t /*offset*/, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
sink.os << "123";
sink.os << "456";
sink.os << "789";
@@ -1664,7 +1697,6 @@ protected:
res.set_chunked_content_provider(
"text/plain",
[i](size_t /*offset*/, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
switch (*i) {
case 0: sink.os << "123"; break;
case 1: sink.os << "456"; break;
@@ -1679,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(
@@ -1694,7 +1750,6 @@ protected:
res.set_content_provider(
data->size(), "text/plain",
[data](size_t offset, size_t length, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
size_t DATA_CHUNK_SIZE = 4;
const auto &d = *data;
auto out_len =
@@ -1714,8 +1769,6 @@ protected:
res.set_content_provider(
size_t(-1), "text/plain",
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
if (!sink.is_writable()) return false;
sink.os << "data_chunk";
return true;
});
@@ -1772,6 +1825,40 @@ protected:
EXPECT_EQ("application/json tmp-string", file.content_type);
}
})
.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 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 &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);
}
})
.Post("/empty",
[&](const Request &req, Response &res) {
EXPECT_EQ(req.body, "");
@@ -2616,6 +2703,24 @@ TEST_F(ServerTest, MultipartFormData) {
EXPECT_EQ(200, res->status);
}
TEST_F(ServerTest, MultipartFormDataMultiFileValues) {
MultipartFormDataItems items = {
{"text", "default text", "", ""},
{"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"},
};
auto res = cli_.Post("/multipart/multi_file_values", items);
ASSERT_TRUE(res);
EXPECT_EQ(200, res->status);
}
TEST_F(ServerTest, CaseInsensitiveHeaderName) {
auto res = cli_.Get("/hi");
ASSERT_TRUE(res);
@@ -2841,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";
@@ -2952,7 +3066,6 @@ TEST_F(ServerTest, PutWithContentProvider) {
auto res = cli_.Put(
"/put", 3,
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
sink.os << "PUT";
return true;
},
@@ -2979,7 +3092,6 @@ TEST_F(ServerTest, PutWithContentProviderWithoutLength) {
auto res = cli_.Put(
"/put",
[](size_t /*offset*/, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
sink.os << "PUT";
sink.done();
return true;
@@ -3006,7 +3118,6 @@ TEST_F(ServerTest, PutWithContentProviderWithGzip) {
auto res = cli_.Put(
"/put", 3,
[](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
sink.os << "PUT";
return true;
},
@@ -3035,7 +3146,6 @@ TEST_F(ServerTest, PutWithContentProviderWithoutLengthWithGzip) {
auto res = cli_.Put(
"/put",
[](size_t /*offset*/, DataSink &sink) {
EXPECT_TRUE(sink.is_writable());
sink.os << "PUT";
sink.done();
return true;
@@ -3143,7 +3253,7 @@ TEST(GzipDecompressor, ChunkedDecompression) {
{
httplib::detail::gzip_decompressor decompressor;
// Chunk size is chosen specificaly to have a decompressed chunk size equal
// Chunk size is chosen specifically to have a decompressed chunk size equal
// to 16384 bytes 16384 bytes is the size of decompressor output buffer
size_t chunk_size = 130;
for (size_t chunk_begin = 0; chunk_begin < compressed_data.size();
@@ -3292,7 +3402,7 @@ TEST_F(ServerTest, PostContentReceiver) {
ASSERT_EQ("content", res->body);
}
TEST_F(ServerTest, PostMulitpartFilsContentReceiver) {
TEST_F(ServerTest, PostMultipartFileContentReceiver) {
MultipartFormDataItems items = {
{"text1", "text default", "", ""},
{"text2", "aωb", "", ""},
@@ -3307,7 +3417,7 @@ TEST_F(ServerTest, PostMulitpartFilsContentReceiver) {
EXPECT_EQ(200, res->status);
}
TEST_F(ServerTest, PostMulitpartPlusBoundary) {
TEST_F(ServerTest, PostMultipartPlusBoundary) {
MultipartFormDataItems items = {
{"text1", "text default", "", ""},
{"text2", "aωb", "", ""},
@@ -3723,10 +3833,10 @@ TEST(ServerRequestParsingTest, ReadHeadersRegexComplexity2) {
"&&&%%%");
}
TEST(ServerRequestParsingTest, ExcessiveWhitespaceInUnparseableHeaderLine) {
TEST(ServerRequestParsingTest, ExcessiveWhitespaceInUnparsableHeaderLine) {
// Make sure this doesn't crash the server.
// In a previous version of the header line regex, the "\r" rendered the line
// unparseable and the regex engine repeatedly backtracked, trying to look for
// unparsable and the regex engine repeatedly backtracked, trying to look for
// a new position where the leading white space ended and the field value
// began.
// The crash occurs with libc++ but not libstdc++.
@@ -3829,6 +3939,31 @@ TEST(ServerStopTest, StopServerWithChunkedTransmission) {
ASSERT_FALSE(svr.is_running());
}
TEST(ServerStopTest, ClientAccessAfterServerDown) {
httplib::Server svr;
svr.Post("/hi", [&](const httplib::Request & /*req*/,
httplib::Response &res) { res.status = 200; });
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
while (!svr.is_running()) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
Client cli(HOST, PORT);
auto res = cli.Post("/hi", "data", "text/plain");
ASSERT_TRUE(res);
EXPECT_EQ(200, res->status);
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
res = cli.Post("/hi", "data", "text/plain");
ASSERT_FALSE(res);
}
TEST(StreamingTest, NoContentLengthStreaming) {
Server svr;
@@ -3984,7 +4119,7 @@ TEST(KeepAliveTest, ReadTimeout) {
cli.set_read_timeout(std::chrono::seconds(1));
auto resa = cli.Get("/a");
ASSERT_TRUE(!resa);
ASSERT_FALSE(resa);
EXPECT_EQ(Error::Read, resa.error());
auto resb = cli.Get("/b");
@@ -3998,35 +4133,74 @@ TEST(KeepAliveTest, ReadTimeout) {
}
TEST(KeepAliveTest, Issue1041) {
const auto resourcePath = "/hi";
Server svr;
svr.set_keep_alive_timeout(3);
svr.Get(resourcePath, [](const httplib::Request &, httplib::Response &res) {
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
res.set_content("Hello World!", "text/plain");
});
auto a2 = std::async(std::launch::async, [&svr] { svr.listen(HOST, PORT); });
auto f = std::async(std::launch::async, [&svr] { svr.listen(HOST, PORT); });
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Client cli(HOST, PORT);
cli.set_keep_alive(true);
auto result = cli.Get(resourcePath);
auto result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
std::this_thread::sleep_for(std::chrono::seconds(5));
result = cli.Get(resourcePath);
result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
svr.stop();
a2.wait();
f.wait();
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
TEST(KeepAliveTest, SSLClientReconnection) {
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.set_keep_alive_timeout(1);
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
res.set_content("Hello World!", "text/plain");
});
auto f = std::async(std::launch::async, [&svr] { svr.listen(HOST, PORT); });
std::this_thread::sleep_for(std::chrono::milliseconds(200));
SSLClient cli(HOST, PORT);
cli.enable_server_certificate_verification(false);
cli.set_keep_alive(true);
auto result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
std::this_thread::sleep_for(std::chrono::seconds(2));
// Recoonect
result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
result = cli.Get("/hi");
ASSERT_TRUE(result);
EXPECT_EQ(200, result->status);
svr.stop();
f.wait();
}
#endif
TEST(ClientProblemDetectionTest, ContentProvider) {
Server svr;
@@ -4442,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);
@@ -5344,6 +5518,49 @@ TEST(MultipartFormDataTest, DataProviderItems) {
t.join();
}
TEST(MultipartFormDataTest, BadHeader) {
Server svr;
svr.Post("/post", [&](const Request & /*req*/, Response &res) {
res.set_content("ok", "text/plain");
});
thread t = thread([&] { svr.listen(HOST, PORT); });
while (!svr.is_running()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
const std::string body =
"This is the preamble. It is to be ignored, though it\r\n"
"is a handy place for composition agents to include an\r\n"
"explanatory note to non-MIME conformant readers.\r\n"
"\r\n"
"\r\n"
"--simple boundary\r\n"
"Content-Disposition: form-data; name=\"field1\"\r\n"
": BAD...\r\n"
"\r\n"
"value1\r\n"
"--simple boundary\r\n"
"Content-Disposition: form-data; name=\"field2\"; "
"filename=\"example.txt\"\r\n"
"\r\n"
"value2\r\n"
"--simple boundary--\r\n"
"This is the epilogue. It is also to be ignored.\r\n";
std::string content_type =
R"(multipart/form-data; boundary="simple boundary")";
Client cli(HOST, PORT);
auto res = cli.Post("/post", body, content_type.c_str());
ASSERT_TRUE(res);
EXPECT_EQ(400, res->status);
svr.stop();
t.join();
}
TEST(MultipartFormDataTest, WithPreamble) {
Server svr;
svr.Post("/post", [&](const Request & /*req*/, Response &res) {
@@ -5795,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());
}
+1 -1
View File
@@ -193,7 +193,7 @@ void DigestAuthTestFromHTTPWatch(T& cli) {
}
// NOTE: Until httpbin.org fixes issue #46, the following test is commented
// out. Plese see https://httpbin.org/digest-auth/auth/hello/world
// out. Please see https://httpbin.org/digest-auth/auth/hello/world
// cli.set_digest_auth("bad", "world");
// for (auto path : paths) {
// auto res = cli.Get(path.c_str());