mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2f4728489 | |||
| d1b616286f | |||
| df74526f91 | |||
| 9f7ae0737a | |||
| 1ebb8412c5 | |||
| 7b69999c37 | |||
| c7e959a948 | |||
| ba5884e779 | |||
| cdaa5c48db | |||
| bab5c0e907 | |||
| 016838fd10 | |||
| 75053bf855 | |||
| ae3a6dd2a9 | |||
| 6d963fbe8d | |||
| 88f6245c84 | |||
| 0e7d2f9f93 |
+5
-5
@@ -14,8 +14,8 @@
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
@@ -79,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)
|
||||
@@ -206,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>
|
||||
@@ -233,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-1
@@ -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)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.12.0"
|
||||
#define CPPHTTPLIB_VERSION "0.12.1"
|
||||
|
||||
/*
|
||||
* 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>;
|
||||
@@ -454,6 +485,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;
|
||||
@@ -768,7 +800,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);
|
||||
@@ -822,6 +854,9 @@ enum class Error {
|
||||
UnsupportedMultipartBoundaryChars,
|
||||
Compression,
|
||||
ConnectionTimeout,
|
||||
|
||||
// For internal use only
|
||||
SSLPeerCouldBeClosed_,
|
||||
};
|
||||
|
||||
std::string to_string(const Error error);
|
||||
@@ -1094,8 +1129,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:
|
||||
@@ -1117,7 +1150,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_;
|
||||
@@ -1196,6 +1229,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,
|
||||
@@ -1787,6 +1823,8 @@ std::string params_to_query_str(const Params ¶ms);
|
||||
|
||||
void parse_query_text(const std::string &s, Params ¶ms);
|
||||
|
||||
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);
|
||||
@@ -3852,9 +3890,12 @@ inline void parse_query_text(const std::string &s, Params ¶ms) {
|
||||
|
||||
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);
|
||||
@@ -4387,15 +4428,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) {
|
||||
@@ -4406,16 +4447,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() {
|
||||
@@ -4690,6 +4822,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();
|
||||
@@ -5405,7 +5547,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;
|
||||
@@ -5425,14 +5567,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*/,
|
||||
@@ -5598,11 +5740,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();
|
||||
@@ -6149,7 +6287,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_);
|
||||
|
||||
@@ -6180,7 +6326,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;
|
||||
}
|
||||
@@ -6207,13 +6353,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) {
|
||||
@@ -6227,7 +6371,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; }
|
||||
@@ -6369,7 +6517,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_) {
|
||||
@@ -6619,6 +6767,17 @@ 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()) {
|
||||
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)) {
|
||||
@@ -7245,7 +7404,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_);
|
||||
@@ -7658,7 +7817,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_,
|
||||
@@ -7836,11 +7995,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_); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7985,7 +8147,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);
|
||||
|
||||
@@ -8001,14 +8163,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
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
+23
-12
@@ -1,20 +1,31 @@
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
find_package(GTest)
|
||||
|
||||
include(FetchContent)
|
||||
include(GoogleTest)
|
||||
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()
|
||||
|
||||
set(BUILD_GMOCK OFF)
|
||||
set(INSTALL_GTEST OFF)
|
||||
set(gtest_force_shared_crt ON)
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
gtest
|
||||
URL https://github.com/google/googletest/archive/main.tar.gz
|
||||
)
|
||||
FetchContent_MakeAvailable(gtest)
|
||||
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>")
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+164
-15
@@ -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,39 @@ 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");
|
||||
@@ -779,7 +812,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());
|
||||
@@ -1767,6 +1800,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, "");
|
||||
@@ -2611,6 +2678,23 @@ 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);
|
||||
@@ -3134,7 +3218,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();
|
||||
@@ -3283,7 +3367,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", "", ""},
|
||||
@@ -3298,7 +3382,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", "", ""},
|
||||
@@ -3714,10 +3798,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++.
|
||||
@@ -3820,6 +3904,32 @@ 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;
|
||||
|
||||
@@ -3975,7 +4085,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");
|
||||
@@ -3989,35 +4099,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;
|
||||
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user