mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb8e45383e | |||
| a1df576e4f | |||
| 7fb0254794 | |||
| c82d1e52cc | |||
| 846151b605 | |||
| e44e31dd5b | |||
| f7b9501662 | |||
| e12fe4cbbb | |||
| 49d2e1f135 | |||
| 8191fd8e6c | |||
| d73395e1dc | |||
| 64d001162b | |||
| bb00a23116 | |||
| 63d6e9b91b | |||
| 66eed5681a | |||
| 8ecdb11979 | |||
| 894fcc8e02 | |||
| 7f43f0f3ff | |||
| 87e03dd1ce | |||
| e5cacb465d | |||
| ee8371f753 | |||
| 081723f983 | |||
| b61f36579c | |||
| 33f53aa458 | |||
| 412ab5f063 | |||
| 11e02e901c | |||
| 65a8f4cf44 | |||
| 27d128bbb4 | |||
| 070f9bec58 | |||
| f817032513 | |||
| 17abe221c0 | |||
| 4a7a81e039 | |||
| 37fd4eb643 | |||
| 865b0e4c03 | |||
| b324921c1a | |||
| 63f72caf30 | |||
| 99ac17b90a |
@@ -172,6 +172,11 @@ if(HTTPLIB_COMPILE)
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
|
||||
)
|
||||
set_target_properties(${PROJECT_NAME}
|
||||
PROPERTIES
|
||||
VERSION ${${PROJECT_NAME}_VERSION}
|
||||
SOVERSION ${${PROJECT_NAME}_VERSION_MAJOR}
|
||||
)
|
||||
else()
|
||||
# This is for header-only.
|
||||
set(_INTERFACE_OR_PUBLIC INTERFACE)
|
||||
|
||||
@@ -183,6 +183,15 @@ The followings are built-in mappings:
|
||||
| webm | video/webm | zip | application/zip |
|
||||
| mp3 | audio/mp3 | wasm | application/wasm |
|
||||
|
||||
### File request handler
|
||||
|
||||
```cpp
|
||||
// The handler is called right before the response is sent to a client
|
||||
svr.set_file_request_handler([](const Request &req, Response &res) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
NOTE: These static file server methods are not thread-safe.
|
||||
|
||||
### Logging
|
||||
@@ -303,7 +312,7 @@ Without content length:
|
||||
svr.Get("/stream", [&](const Request &req, Response &res) {
|
||||
res.set_content_provider(
|
||||
"text/plain", // Content type
|
||||
[&](size_t offset, size_t length, DataSink &sink) {
|
||||
[&](size_t offset, DataSink &sink) {
|
||||
if (/* there is still data */) {
|
||||
std::vector<char> data;
|
||||
// prepare data...
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM alpine as builder
|
||||
WORKDIR /src/example
|
||||
RUN apk add g++ make openssl-dev zlib-dev brotli-dev
|
||||
COPY ./httplib.h /src
|
||||
COPY ./example/hello.cc /src/example
|
||||
COPY ./example/Makefile /src/example
|
||||
RUN make hello
|
||||
|
||||
FROM alpine
|
||||
RUN apk --no-cache add brotli libstdc++
|
||||
COPY --from=builder /src/example/hello /bin/hello
|
||||
CMD ["/bin/hello"]
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#CXX = clang++
|
||||
CXXFLAGS = -std=c++11 -I.. -Wall -Wextra -pthread
|
||||
CXXFLAGS = -O2 -std=c++11 -I.. -Wall -Wextra -pthread
|
||||
|
||||
PREFIX = /usr/local
|
||||
#PREFIX = $(shell brew --prefix)
|
||||
|
||||
+1
-1
@@ -15,5 +15,5 @@ int main(void) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.listen("localhost", 8080);
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_HEADER_MAX_LENGTH
|
||||
#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT
|
||||
#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20
|
||||
#endif
|
||||
@@ -213,6 +217,15 @@ using socket_t = int;
|
||||
#include <thread>
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
// these are defined in wincrypt.h and it breaks compilation if BoringSSL is
|
||||
// used
|
||||
#ifdef _WIN32
|
||||
#undef X509_NAME
|
||||
#undef X509_CERT_PAIR
|
||||
#undef X509_EXTENSIONS
|
||||
#undef PKCS7_SIGNER_INFO
|
||||
#endif
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/md5.h>
|
||||
#include <openssl/ssl.h>
|
||||
@@ -786,6 +799,7 @@ enum class Error {
|
||||
SSLServerVerification,
|
||||
UnsupportedMultipartBoundaryChars,
|
||||
Compression,
|
||||
ConnectionTimeout,
|
||||
};
|
||||
|
||||
std::string to_string(const Error error);
|
||||
@@ -1370,7 +1384,8 @@ class SSLServer : public Server {
|
||||
public:
|
||||
SSLServer(const char *cert_path, const char *private_key_path,
|
||||
const char *client_ca_cert_file_path = nullptr,
|
||||
const char *client_ca_cert_dir_path = nullptr);
|
||||
const char *client_ca_cert_dir_path = nullptr,
|
||||
const char *private_key_password = nullptr);
|
||||
|
||||
SSLServer(X509 *cert, EVP_PKEY *private_key,
|
||||
X509_STORE *client_ca_cert_store = nullptr);
|
||||
@@ -1581,6 +1596,7 @@ inline std::string to_string(const Error error) {
|
||||
case Error::UnsupportedMultipartBoundaryChars:
|
||||
return "UnsupportedMultipartBoundaryChars";
|
||||
case Error::Compression: return "Compression";
|
||||
case Error::ConnectionTimeout: return "ConnectionTimeout";
|
||||
case Error::Unknown: return "Unknown";
|
||||
default: break;
|
||||
}
|
||||
@@ -1644,6 +1660,10 @@ Client::set_write_timeout(const std::chrono::duration<Rep, Period> &duration) {
|
||||
* .h + .cc.
|
||||
*/
|
||||
|
||||
std::string hosted_at(const char *hostname);
|
||||
|
||||
void hosted_at(const char *hostname, std::vector<std::string> &addrs);
|
||||
|
||||
std::string append_query_params(const char *path, const Params ¶ms);
|
||||
|
||||
std::pair<std::string, std::string> make_range_header(Ranges ranges);
|
||||
@@ -1657,6 +1677,8 @@ namespace detail {
|
||||
|
||||
std::string encode_query_param(const std::string &value);
|
||||
|
||||
std::string decode_url(const std::string &s, bool convert_plus_to_space);
|
||||
|
||||
void read_file(const std::string &path, std::string &out);
|
||||
|
||||
std::string trim_copy(const std::string &s);
|
||||
@@ -1936,8 +1958,12 @@ inline std::string base64_encode(const std::string &in) {
|
||||
}
|
||||
|
||||
inline bool is_file(const std::string &path) {
|
||||
#ifdef _WIN32
|
||||
return _access_s(path.c_str(), 0) == 0;
|
||||
#else
|
||||
struct stat st;
|
||||
return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool is_dir(const std::string &path) {
|
||||
@@ -2290,7 +2316,8 @@ inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) {
|
||||
inline Error wait_until_socket_is_ready(socket_t sock, time_t sec,
|
||||
time_t usec) {
|
||||
#ifdef CPPHTTPLIB_USE_POLL
|
||||
struct pollfd pfd_read;
|
||||
pfd_read.fd = sock;
|
||||
@@ -2300,17 +2327,21 @@ inline bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) {
|
||||
|
||||
auto poll_res = handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); });
|
||||
|
||||
if (poll_res == 0) { return Error::ConnectionTimeout; }
|
||||
|
||||
if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,
|
||||
reinterpret_cast<char *>(&error), &len);
|
||||
return res >= 0 && !error;
|
||||
auto successful = res >= 0 && !error;
|
||||
return successful ? Error::Success : Error::Connection;
|
||||
}
|
||||
return false;
|
||||
|
||||
return Error::Connection;
|
||||
#else
|
||||
#ifndef _WIN32
|
||||
if (sock >= FD_SETSIZE) { return false; }
|
||||
if (sock >= FD_SETSIZE) { return Error::Connection; }
|
||||
#endif
|
||||
|
||||
fd_set fdsr;
|
||||
@@ -2328,14 +2359,17 @@ inline bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) {
|
||||
return select(static_cast<int>(sock + 1), &fdsr, &fdsw, &fdse, &tv);
|
||||
});
|
||||
|
||||
if (ret == 0) { return Error::ConnectionTimeout; }
|
||||
|
||||
if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
return getsockopt(sock, SOL_SOCKET, SO_ERROR,
|
||||
reinterpret_cast<char *>(&error), &len) >= 0 &&
|
||||
!error;
|
||||
auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,
|
||||
reinterpret_cast<char *>(&error), &len);
|
||||
auto successful = res >= 0 && !error;
|
||||
return successful ? Error::Success : Error::Connection;
|
||||
}
|
||||
return false;
|
||||
return Error::Connection;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2480,25 +2514,28 @@ socket_t create_socket(const char *host, const char *ip, int port,
|
||||
SocketOptions socket_options,
|
||||
BindOrConnect bind_or_connect) {
|
||||
// Get address info
|
||||
const char *node = nullptr;
|
||||
struct addrinfo hints;
|
||||
struct addrinfo *result;
|
||||
|
||||
memset(&hints, 0, sizeof(struct addrinfo));
|
||||
hints.ai_family = address_family;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = socket_flags;
|
||||
hints.ai_protocol = 0;
|
||||
|
||||
// Ask getaddrinfo to convert IP in c-string to address
|
||||
if (ip[0] != '\0') {
|
||||
node = ip;
|
||||
// Ask getaddrinfo to convert IP in c-string to address
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_flags = AI_NUMERICHOST;
|
||||
} else {
|
||||
node = host;
|
||||
hints.ai_family = address_family;
|
||||
hints.ai_flags = socket_flags;
|
||||
}
|
||||
|
||||
auto service = std::to_string(port);
|
||||
|
||||
if (ip[0] != '\0' ? getaddrinfo(ip, service.c_str(), &hints, &result)
|
||||
: getaddrinfo(host, service.c_str(), &hints, &result)) {
|
||||
if (getaddrinfo(node, service.c_str(), &hints, &result)) {
|
||||
#if defined __linux__ && !defined __ANDROID__
|
||||
res_init();
|
||||
#endif
|
||||
@@ -2658,27 +2695,43 @@ inline socket_t create_client_socket(
|
||||
::connect(sock2, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen));
|
||||
|
||||
if (ret < 0) {
|
||||
if (is_connection_error() ||
|
||||
!wait_until_socket_is_ready(sock2, connection_timeout_sec,
|
||||
connection_timeout_usec)) {
|
||||
if (is_connection_error()) {
|
||||
error = Error::Connection;
|
||||
return false;
|
||||
}
|
||||
error = wait_until_socket_is_ready(sock2, connection_timeout_sec,
|
||||
connection_timeout_usec);
|
||||
if (error != Error::Success) { return false; }
|
||||
}
|
||||
|
||||
set_nonblocking(sock2, false);
|
||||
|
||||
{
|
||||
#ifdef _WIN32
|
||||
auto timeout = static_cast<uint32_t>(read_timeout_sec * 1000 +
|
||||
read_timeout_usec / 1000);
|
||||
setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,
|
||||
sizeof(timeout));
|
||||
#else
|
||||
timeval tv;
|
||||
tv.tv_sec = static_cast<long>(read_timeout_sec);
|
||||
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(read_timeout_usec);
|
||||
setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv));
|
||||
#endif
|
||||
}
|
||||
{
|
||||
|
||||
#ifdef _WIN32
|
||||
auto timeout = static_cast<uint32_t>(write_timeout_sec * 1000 +
|
||||
write_timeout_usec / 1000);
|
||||
setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout,
|
||||
sizeof(timeout));
|
||||
#else
|
||||
timeval tv;
|
||||
tv.tv_sec = static_cast<long>(write_timeout_sec);
|
||||
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(write_timeout_usec);
|
||||
setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv));
|
||||
#endif
|
||||
}
|
||||
|
||||
error = Error::Success;
|
||||
@@ -2694,7 +2747,7 @@ inline socket_t create_client_socket(
|
||||
return sock;
|
||||
}
|
||||
|
||||
inline void get_remote_ip_and_port(const struct sockaddr_storage &addr,
|
||||
inline bool get_remote_ip_and_port(const struct sockaddr_storage &addr,
|
||||
socklen_t addr_len, std::string &ip,
|
||||
int &port) {
|
||||
if (addr.ss_family == AF_INET) {
|
||||
@@ -2702,14 +2755,19 @@ inline void get_remote_ip_and_port(const struct sockaddr_storage &addr,
|
||||
} else if (addr.ss_family == AF_INET6) {
|
||||
port =
|
||||
ntohs(reinterpret_cast<const struct sockaddr_in6 *>(&addr)->sin6_port);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<char, NI_MAXHOST> ipstr{};
|
||||
if (!getnameinfo(reinterpret_cast<const struct sockaddr *>(&addr), addr_len,
|
||||
ipstr.data(), static_cast<socklen_t>(ipstr.size()), nullptr,
|
||||
0, NI_NUMERICHOST)) {
|
||||
ip = ipstr.data();
|
||||
if (getnameinfo(reinterpret_cast<const struct sockaddr *>(&addr), addr_len,
|
||||
ipstr.data(), static_cast<socklen_t>(ipstr.size()), nullptr,
|
||||
0, NI_NUMERICHOST)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ip = ipstr.data();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
|
||||
@@ -2755,10 +2813,12 @@ find_content_type(const std::string &path,
|
||||
default: return nullptr;
|
||||
case "css"_t: return "text/css";
|
||||
case "csv"_t: return "text/csv";
|
||||
case "txt"_t: return "text/plain";
|
||||
case "vtt"_t: return "text/vtt";
|
||||
case "htm"_t:
|
||||
case "html"_t: return "text/html";
|
||||
case "js"_t:
|
||||
case "mjs"_t: return "text/javascript";
|
||||
case "txt"_t: return "text/plain";
|
||||
case "vtt"_t: return "text/vtt";
|
||||
|
||||
case "apng"_t: return "image/apng";
|
||||
case "avif"_t: return "image/avif";
|
||||
@@ -2790,8 +2850,6 @@ find_content_type(const std::string &path,
|
||||
case "7z"_t: return "application/x-7z-compressed";
|
||||
case "atom"_t: return "application/atom+xml";
|
||||
case "pdf"_t: return "application/pdf";
|
||||
case "js"_t:
|
||||
case "mjs"_t: return "application/javascript";
|
||||
case "json"_t: return "application/json";
|
||||
case "rss"_t: return "application/rss+xml";
|
||||
case "tar"_t: return "application/x-tar";
|
||||
@@ -2876,14 +2934,21 @@ inline const char *status_message(int status) {
|
||||
}
|
||||
|
||||
inline bool can_compress_content_type(const std::string &content_type) {
|
||||
return (!content_type.rfind("text/", 0) &&
|
||||
content_type != "text/event-stream") ||
|
||||
content_type == "image/svg+xml" ||
|
||||
content_type == "application/javascript" ||
|
||||
content_type == "application/json" ||
|
||||
content_type == "application/xml" ||
|
||||
content_type == "application/protobuf" ||
|
||||
content_type == "application/xhtml+xml";
|
||||
using udl::operator""_t;
|
||||
|
||||
auto tag = str2tag(content_type);
|
||||
|
||||
switch (tag) {
|
||||
case "image/svg+xml"_t:
|
||||
case "application/javascript"_t:
|
||||
case "application/json"_t:
|
||||
case "application/xml"_t:
|
||||
case "application/protobuf"_t:
|
||||
case "application/xhtml+xml"_t: return true;
|
||||
|
||||
default:
|
||||
return !content_type.rfind("text/", 0) && tag != "text/event-stream"_t;
|
||||
}
|
||||
}
|
||||
|
||||
inline EncodingType encoding_type(const Request &req, const Response &res) {
|
||||
@@ -2962,7 +3027,6 @@ inline bool gzip_compressor::compress(const char *data, size_t data_length,
|
||||
assert((flush == Z_FINISH && ret == Z_STREAM_END) ||
|
||||
(flush == Z_NO_FLUSH && ret == Z_OK));
|
||||
assert(strm_.avail_in == 0);
|
||||
|
||||
} while (data_length > 0);
|
||||
|
||||
return true;
|
||||
@@ -3171,15 +3235,26 @@ inline bool read_headers(Stream &strm, Headers &headers) {
|
||||
if (!line_reader.getline()) { return false; }
|
||||
|
||||
// Check if the line ends with CRLF.
|
||||
auto line_terminator_len = 2;
|
||||
if (line_reader.end_with_crlf()) {
|
||||
// Blank line indicates end of headers.
|
||||
if (line_reader.size() == 2) { break; }
|
||||
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
||||
} else {
|
||||
// Blank line indicates end of headers.
|
||||
if (line_reader.size() == 1) { break; }
|
||||
line_terminator_len = 1;
|
||||
}
|
||||
#else
|
||||
} else {
|
||||
continue; // Skip invalid line.
|
||||
}
|
||||
#endif
|
||||
|
||||
// Exclude CRLF
|
||||
auto end = line_reader.ptr() + line_reader.size() - 2;
|
||||
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
|
||||
|
||||
// Exclude line terminator
|
||||
auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
|
||||
|
||||
parse_header(line_reader.ptr(), end,
|
||||
[&](std::string &&key, std::string &&val) {
|
||||
@@ -3363,7 +3438,7 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
||||
if (!ret) { status = exceed_payload_max_length ? 413 : 400; }
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
|
||||
ssize_t write_len = 0;
|
||||
@@ -3677,10 +3752,11 @@ public:
|
||||
bool parse(const char *buf, size_t n, const ContentReceiver &content_callback,
|
||||
const MultipartContentHeader &header_callback) {
|
||||
|
||||
// TODO: support 'filename*'
|
||||
static const std::regex re_content_disposition(
|
||||
"^Content-Disposition:\\s*form-data;\\s*name=\"(.*?)\"(?:;\\s*filename="
|
||||
"\"(.*?)\")?\\s*$",
|
||||
R"~(^Content-Disposition:\s*form-data;\s*name="(.*?)"(?:;\s*filename="(.*?)")?(?:;\s*filename\*=\S+)?\s*$)~",
|
||||
std::regex_constants::icase);
|
||||
|
||||
static const std::string dash_ = "--";
|
||||
static const std::string crlf_ = "\r\n";
|
||||
|
||||
@@ -3703,6 +3779,7 @@ public:
|
||||
}
|
||||
case 2: { // Headers
|
||||
auto pos = buf_find(crlf_);
|
||||
if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
|
||||
while (pos < buf_size()) {
|
||||
// Empty line
|
||||
if (pos == 0) {
|
||||
@@ -3866,7 +3943,7 @@ private:
|
||||
|
||||
void buf_append(const char *data, size_t n) {
|
||||
auto remaining_size = buf_size();
|
||||
if (remaining_size > 0) {
|
||||
if (remaining_size > 0 && buf_spos_ > 0) {
|
||||
for (size_t i = 0; i < remaining_size; i++) {
|
||||
buf_[i] = buf_[buf_spos_ + i];
|
||||
}
|
||||
@@ -3874,9 +3951,7 @@ private:
|
||||
buf_spos_ = 0;
|
||||
buf_epos_ = remaining_size;
|
||||
|
||||
if (remaining_size + n > buf_.size()) {
|
||||
buf_.resize(remaining_size + n);
|
||||
}
|
||||
if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); }
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
buf_[buf_epos_ + i] = data[i];
|
||||
@@ -3909,6 +3984,7 @@ inline std::string make_multipart_data_boundary() {
|
||||
// platforms, but due to lack of support in the c++ standard library,
|
||||
// doing better requires either some ugly hacks or breaking portability.
|
||||
std::random_device seed_gen;
|
||||
|
||||
// Request 128 bits of entropy for initialization
|
||||
std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()};
|
||||
std::mt19937 engine(seed_sequence);
|
||||
@@ -4191,14 +4267,16 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
|
||||
}
|
||||
}
|
||||
|
||||
auto field =
|
||||
"Digest username=\"" + username + "\", realm=\"" + auth.at("realm") +
|
||||
"\", nonce=\"" + auth.at("nonce") + "\", uri=\"" + req.path +
|
||||
"\", algorithm=" + algo +
|
||||
(qop.empty() ? ", response=\""
|
||||
: ", qop=" + qop + ", nc=\"" + nc + "\", cnonce=\"" +
|
||||
cnonce + "\", response=\"") +
|
||||
response + "\"";
|
||||
auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : "";
|
||||
|
||||
auto field = "Digest username=\"" + username + "\", realm=\"" +
|
||||
auth.at("realm") + "\", nonce=\"" + auth.at("nonce") +
|
||||
"\", uri=\"" + req.path + "\", algorithm=" + algo +
|
||||
(qop.empty() ? ", response=\""
|
||||
: ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" +
|
||||
cnonce + "\", response=\"") +
|
||||
response + "\"" +
|
||||
(opaque.empty() ? "" : ", opaque=\"" + opaque + "\"");
|
||||
|
||||
auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
|
||||
return std::make_pair(key, field);
|
||||
@@ -4268,6 +4346,41 @@ private:
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline std::string hosted_at(const char *hostname) {
|
||||
std::vector<std::string> addrs;
|
||||
hosted_at(hostname, addrs);
|
||||
if (addrs.empty()) { return std::string(); }
|
||||
return addrs[0];
|
||||
}
|
||||
|
||||
inline void hosted_at(const char *hostname, std::vector<std::string> &addrs) {
|
||||
struct addrinfo hints;
|
||||
struct addrinfo *result;
|
||||
|
||||
memset(&hints, 0, sizeof(struct addrinfo));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = 0;
|
||||
|
||||
if (getaddrinfo(hostname, nullptr, &hints, &result)) {
|
||||
#if defined __linux__ && !defined __ANDROID__
|
||||
res_init();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto rp = result; rp; rp = rp->ai_next) {
|
||||
const auto &addr =
|
||||
*reinterpret_cast<struct sockaddr_storage *>(rp->ai_addr);
|
||||
std::string ip;
|
||||
int dummy = -1;
|
||||
if (detail::get_remote_ip_and_port(addr, sizeof(struct sockaddr_storage),
|
||||
ip, dummy)) {
|
||||
addrs.push_back(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string append_query_params(const char *path, const Params ¶ms) {
|
||||
std::string path_with_query = path;
|
||||
const static std::regex re("[^?]+\\?.*");
|
||||
@@ -4853,6 +4966,14 @@ inline bool Server::parse_request_line(const char *s, Request &req) {
|
||||
if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { return false; }
|
||||
|
||||
{
|
||||
// Skip URL fragment
|
||||
for (size_t i = 0; i < req.target.size(); i++) {
|
||||
if (req.target[i] == '#') {
|
||||
req.target.erase(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t count = 0;
|
||||
|
||||
detail::split(req.target.data(), req.target.data() + req.target.size(), '?',
|
||||
@@ -5224,16 +5345,31 @@ inline bool Server::listen_internal() {
|
||||
}
|
||||
|
||||
{
|
||||
#ifdef _WIN32
|
||||
auto timeout = static_cast<uint32_t>(read_timeout_sec_ * 1000 +
|
||||
read_timeout_usec_ / 1000);
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,
|
||||
sizeof(timeout));
|
||||
#else
|
||||
timeval tv;
|
||||
tv.tv_sec = static_cast<long>(read_timeout_sec_);
|
||||
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(read_timeout_usec_);
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv));
|
||||
#endif
|
||||
}
|
||||
{
|
||||
|
||||
#ifdef _WIN32
|
||||
auto timeout = static_cast<uint32_t>(write_timeout_sec_ * 1000 +
|
||||
write_timeout_usec_ / 1000);
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout,
|
||||
sizeof(timeout));
|
||||
#else
|
||||
timeval tv;
|
||||
tv.tv_sec = static_cast<long>(write_timeout_sec_);
|
||||
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(write_timeout_usec_);
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if __cplusplus > 201703L
|
||||
@@ -5676,6 +5812,7 @@ inline socket_t ClientImpl::create_client_socket(Error &error) const {
|
||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||
write_timeout_usec_, interface_, error);
|
||||
}
|
||||
|
||||
// Check is custom IP specified for host_
|
||||
std::string ip;
|
||||
auto it = addr_map_.find(host_);
|
||||
@@ -5736,7 +5873,11 @@ inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
|
||||
|
||||
if (!line_reader.getline()) { return false; }
|
||||
|
||||
#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
||||
const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n");
|
||||
#else
|
||||
const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n");
|
||||
#endif
|
||||
|
||||
std::cmatch m;
|
||||
if (!std::regex_match(line_reader.ptr(), m, re)) {
|
||||
@@ -6028,9 +6169,11 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
|
||||
if (!req.has_header("Accept")) { req.headers.emplace("Accept", "*/*"); }
|
||||
|
||||
#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
|
||||
if (!req.has_header("User-Agent")) {
|
||||
req.headers.emplace("User-Agent", "cpp-httplib/0.10.0");
|
||||
req.headers.emplace("User-Agent", "cpp-httplib/0.10.3");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (req.body.empty()) {
|
||||
if (req.content_provider_) {
|
||||
@@ -6123,11 +6266,6 @@ inline std::unique_ptr<Response> ClientImpl::send_with_content_provider(
|
||||
ContentProviderWithoutLength content_provider_without_length,
|
||||
const char *content_type, Error &error) {
|
||||
|
||||
// Request req;
|
||||
// req.method = method;
|
||||
// req.headers = headers;
|
||||
// req.path = path;
|
||||
|
||||
if (content_type) { req.headers.emplace("Content-Type", content_type); }
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
@@ -7063,9 +7201,9 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
|
||||
auto err = SSL_get_error(ssl_, ret);
|
||||
int n = 1000;
|
||||
#ifdef _WIN32
|
||||
while (--n >= 0 &&
|
||||
(err == SSL_ERROR_WANT_READ ||
|
||||
err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT)) {
|
||||
while (--n >= 0 && (err == SSL_ERROR_WANT_READ ||
|
||||
(err == SSL_ERROR_SYSCALL &&
|
||||
WSAGetLastError() == WSAETIMEDOUT))) {
|
||||
#else
|
||||
while (--n >= 0 && err == SSL_ERROR_WANT_READ) {
|
||||
#endif
|
||||
@@ -7093,9 +7231,9 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) {
|
||||
auto err = SSL_get_error(ssl_, ret);
|
||||
int n = 1000;
|
||||
#ifdef _WIN32
|
||||
while (--n >= 0 &&
|
||||
(err == SSL_ERROR_WANT_WRITE ||
|
||||
err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT)) {
|
||||
while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE ||
|
||||
(err == SSL_ERROR_SYSCALL &&
|
||||
WSAGetLastError() == WSAETIMEDOUT))) {
|
||||
#else
|
||||
while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) {
|
||||
#endif
|
||||
@@ -7128,7 +7266,8 @@ static SSLInit sslinit_;
|
||||
// SSL HTTP server implementation
|
||||
inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
|
||||
const char *client_ca_cert_file_path,
|
||||
const char *client_ca_cert_dir_path) {
|
||||
const char *client_ca_cert_dir_path,
|
||||
const char *private_key_password) {
|
||||
ctx_ = SSL_CTX_new(TLS_server_method());
|
||||
|
||||
if (ctx_) {
|
||||
@@ -7138,6 +7277,12 @@ inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path,
|
||||
|
||||
SSL_CTX_set_min_proto_version(ctx_, TLS1_1_VERSION);
|
||||
|
||||
// add default password callback before opening encrypted private key
|
||||
if (private_key_password != nullptr && (private_key_password[0] != '\0')) {
|
||||
SSL_CTX_set_default_passwd_cb_userdata(ctx_,
|
||||
(char *)private_key_password);
|
||||
}
|
||||
|
||||
if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 ||
|
||||
SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) !=
|
||||
1) {
|
||||
|
||||
+12
-3
@@ -19,12 +19,13 @@ project(
|
||||
# Check just in case downstream decides to edit the source
|
||||
# and add a project version
|
||||
version = meson.project_version()
|
||||
python = import('python').find_installation('python3')
|
||||
python3 = find_program('python3')
|
||||
if version == 'undefined'
|
||||
# Meson doesn't have regular expressions, but since it is implemented
|
||||
# in python we can be sure we can use it to parse the file manually
|
||||
version = run_command(
|
||||
python, '-c', 'import re; raw_version = re.search("User\-Agent.*cpp\-httplib/([0-9]+\.?)+", open("httplib.h").read()).group(0); print(re.search("([0-9]+\\.?)+", raw_version).group(0))'
|
||||
python3, '-c', 'import re; raw_version = re.search("User\-Agent.*cpp\-httplib/([0-9]+\.?)+", open("httplib.h").read()).group(0); print(re.search("([0-9]+\\.?)+", raw_version).group(0))',
|
||||
check: true
|
||||
).stdout().strip()
|
||||
endif
|
||||
|
||||
@@ -68,7 +69,7 @@ if get_option('cpp-httplib_compile')
|
||||
'split',
|
||||
input: 'httplib.h',
|
||||
output: ['httplib.cc', 'httplib.h'],
|
||||
command: [python, files('split.py'), '--out', meson.current_build_dir()],
|
||||
command: [python3, files('split.py'), '--out', meson.current_build_dir()],
|
||||
install: true,
|
||||
install_dir: [false, get_option('includedir')]
|
||||
)
|
||||
@@ -92,6 +93,14 @@ if get_option('cpp-httplib_compile')
|
||||
else
|
||||
install_headers('httplib.h')
|
||||
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: include_directories('.'))
|
||||
|
||||
import('pkgconfig').generate(
|
||||
name: 'cpp-httplib',
|
||||
description: 'A C++ HTTP/HTTPS server and client library',
|
||||
install_dir: join_paths(get_option('datadir'), 'pkgconfig'),
|
||||
url: 'https://github.com/yhirose/cpp-httplib',
|
||||
version: version
|
||||
)
|
||||
endif
|
||||
|
||||
if meson.version().version_compare('>=0.54.0')
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
option('cpp-httplib_openssl', type: 'feature', value: 'auto', description: 'Enable OpenSSL support')
|
||||
option('cpp-httplib_zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
|
||||
option('cpp-httplib_brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
|
||||
option('cpp-httplib_compile', type: 'boolean', value: false, description: 'Split the header into a compilable header & source file (requires Python 3)')
|
||||
option('cpp-httplib_compile', type: 'boolean', value: false, description: 'Split the header into a compilable header & source file')
|
||||
option('cpp-httplib_test', type: 'boolean', value: false, description: 'Build tests')
|
||||
|
||||
@@ -65,6 +65,8 @@ cert.pem:
|
||||
openssl req -x509 -new -batch -config test.rootCA.conf -key rootCA.key.pem -days 1024 > rootCA.cert.pem
|
||||
openssl genrsa 2048 > client.key.pem
|
||||
openssl req -new -batch -config test.conf -key client.key.pem | openssl x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial > client.cert.pem
|
||||
openssl genrsa -passout pass:test123! 2048 > key_encrypted.pem
|
||||
openssl req -new -batch -config test.conf -key key_encrypted.pem | openssl x509 -days 3650 -req -signkey key_encrypted.pem > cert_encrypted.pem
|
||||
#c_rehash .
|
||||
|
||||
clean:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -33,6 +33,13 @@ cert2_pem = custom_target(
|
||||
command: [openssl, 'req', '-x509', '-config', test_conf, '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-out', '@OUTPUT@', '-extensions', 'SAN']
|
||||
)
|
||||
|
||||
cert_encrypted_pem = custom_target(
|
||||
'cert_encrypted_pem',
|
||||
input: key_encrypted_pem,
|
||||
output: 'cert_encrypted.pem',
|
||||
command: [openssl, 'req', '-x509', '-config', test_conf, '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-out', '@OUTPUT@', '-extensions', 'SAN']
|
||||
)
|
||||
|
||||
rootca_key_pem = custom_target(
|
||||
'rootca_key_pem',
|
||||
output: 'rootCA.key.pem',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
docker-compose down --rmi all
|
||||
@@ -1 +0,0 @@
|
||||
docker-compose up -d
|
||||
+161
-3
@@ -18,6 +18,9 @@
|
||||
#define CLIENT_CA_CERT_DIR "."
|
||||
#define CLIENT_CERT_FILE "./client.cert.pem"
|
||||
#define CLIENT_PRIVATE_KEY_FILE "./client.key.pem"
|
||||
#define SERVER_ENCRYPTED_CERT_FILE "./cert_encrypted.pem"
|
||||
#define SERVER_ENCRYPTED_PRIVATE_KEY_FILE "./key_encrypted.pem"
|
||||
#define SERVER_ENCRYPTED_PRIVATE_KEY_PASS "test123!"
|
||||
|
||||
using namespace std;
|
||||
using namespace httplib;
|
||||
@@ -58,6 +61,14 @@ TEST(StartupTest, WSAStartup) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(DecodeURLTest, PercentCharacter) {
|
||||
EXPECT_EQ(
|
||||
detail::decode_url(
|
||||
R"(descrip=Gastos%20%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%B1%C3%91%206)",
|
||||
false),
|
||||
R"(descrip=Gastos áéíóúñÑ 6)");
|
||||
}
|
||||
|
||||
TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest) {
|
||||
string unescapedCharacters = "-_.!~*'()";
|
||||
|
||||
@@ -393,6 +404,31 @@ TEST(ChunkedEncodingTest, FromHTTPWatch_Online) {
|
||||
EXPECT_EQ(out, res->body);
|
||||
}
|
||||
|
||||
TEST(HostnameToIPConversionTest, HTTPWatch_Online) {
|
||||
auto host = "www.httpwatch.com";
|
||||
|
||||
auto ip = hosted_at(host);
|
||||
EXPECT_EQ("191.236.16.12", ip);
|
||||
|
||||
std::vector<std::string> addrs;
|
||||
hosted_at(host, addrs);
|
||||
EXPECT_EQ(1u, addrs.size());
|
||||
}
|
||||
|
||||
#if 0 // It depends on each test environment...
|
||||
TEST(HostnameToIPConversionTest, YouTube_Online) {
|
||||
auto host = "www.youtube.com";
|
||||
|
||||
std::vector<std::string> addrs;
|
||||
hosted_at(host, addrs);
|
||||
|
||||
EXPECT_EQ(20u, addrs.size());
|
||||
|
||||
auto it = std::find(addrs.begin(), addrs.end(), "2607:f8b0:4006:809::200e");
|
||||
EXPECT_TRUE(it != addrs.end());
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(ChunkedEncodingTest, WithContentReceiver_Online) {
|
||||
auto host = "www.httpwatch.com";
|
||||
|
||||
@@ -574,10 +610,11 @@ TEST(ConnectionErrorTest, InvalidPort) {
|
||||
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_TRUE(!res);
|
||||
EXPECT_EQ(Error::Connection, res.error());
|
||||
EXPECT_TRUE(Error::Connection == res.error() ||
|
||||
Error::ConnectionTimeout == res.error());
|
||||
}
|
||||
|
||||
TEST(ConnectionErrorTest, Timeout) {
|
||||
TEST(ConnectionErrorTest, Timeout_Online) {
|
||||
auto host = "google.com";
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
@@ -589,9 +626,14 @@ TEST(ConnectionErrorTest, Timeout) {
|
||||
#endif
|
||||
cli.set_connection_timeout(std::chrono::seconds(2));
|
||||
|
||||
// only probe one address type so that the error reason
|
||||
// correlates to the timed-out IPv4, not the unsupported
|
||||
// IPv6 connection attempt
|
||||
cli.set_address_family(AF_INET);
|
||||
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_TRUE(!res);
|
||||
EXPECT_TRUE(res.error() == Error::Connection);
|
||||
EXPECT_EQ(Error::ConnectionTimeout, res.error());
|
||||
}
|
||||
|
||||
TEST(CancelTest, NoCancel_Online) {
|
||||
@@ -1156,6 +1198,17 @@ TEST(BindServerTest, BindAndListenSeparatelySSL) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
TEST(BindServerTest, BindAndListenSeparatelySSLEncryptedKey) {
|
||||
SSLServer svr(SERVER_ENCRYPTED_CERT_FILE, SERVER_ENCRYPTED_PRIVATE_KEY_FILE,
|
||||
nullptr, nullptr, SERVER_ENCRYPTED_PRIVATE_KEY_PASS);
|
||||
int port = svr.bind_to_any_port("0.0.0.0");
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
ASSERT_TRUE(port > 0);
|
||||
svr.stop();
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(ErrorHandlerTest, ContentLength) {
|
||||
Server svr;
|
||||
|
||||
@@ -1363,6 +1416,35 @@ TEST(InvalidFormatTest, StatusCode) {
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
}
|
||||
|
||||
TEST(URLFragmentTest, WithFragment) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/hi",
|
||||
[](const Request &req, Response &/*res*/) {
|
||||
EXPECT_TRUE(req.target == "/hi");
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
|
||||
{
|
||||
Client cli(HOST, PORT);
|
||||
|
||||
auto res = cli.Get("/hi#key1=val1=key2=val2");
|
||||
EXPECT_TRUE(res);
|
||||
EXPECT_EQ(200, res->status);
|
||||
|
||||
res = cli.Get("/hi%23key1=val1=key2=val2");
|
||||
EXPECT_TRUE(res);
|
||||
EXPECT_EQ(404, res->status);
|
||||
}
|
||||
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
}
|
||||
|
||||
class ServerTest : public ::testing::Test {
|
||||
protected:
|
||||
ServerTest()
|
||||
@@ -1630,6 +1712,11 @@ protected:
|
||||
EXPECT_EQ("0", req.get_header_value("Content-Length"));
|
||||
res.set_content("empty-no-content-type", "text/plain");
|
||||
})
|
||||
.Post("/post-large",
|
||||
[&](const Request &req, Response &res) {
|
||||
EXPECT_EQ(req.body, LARGE_DATA);
|
||||
res.set_content(req.body, "text/plain");
|
||||
})
|
||||
.Put("/empty-no-content-type",
|
||||
[&](const Request &req, Response &res) {
|
||||
EXPECT_EQ(req.body, "");
|
||||
@@ -2035,6 +2122,13 @@ TEST_F(ServerTest, PostEmptyContentWithNoContentType) {
|
||||
ASSERT_EQ("empty-no-content-type", res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PostLarge) {
|
||||
auto res = cli_.Post("/post-large", LARGE_DATA, "text/plain");
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_EQ(200, res->status);
|
||||
EXPECT_EQ(LARGE_DATA, res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PutEmptyContentWithNoContentType) {
|
||||
auto res = cli_.Put("/empty-no-content-type");
|
||||
ASSERT_TRUE(res);
|
||||
@@ -4726,4 +4820,68 @@ TEST(HttpToHttpsRedirectTest, CertFile) {
|
||||
t.join();
|
||||
t2.join();
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, LargeData) {
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
|
||||
svr.Post("/post", [&](const Request &req, Response & /*res*/,
|
||||
const ContentReader &content_reader) {
|
||||
if (req.is_multipart_form_data()) {
|
||||
MultipartFormDataItems files;
|
||||
content_reader(
|
||||
[&](const MultipartFormData &file) {
|
||||
files.push_back(file);
|
||||
return true;
|
||||
},
|
||||
[&](const char *data, size_t data_length) {
|
||||
files.back().content.append(data, data_length);
|
||||
return true;
|
||||
});
|
||||
|
||||
EXPECT_TRUE(std::string(files[0].name) == "document");
|
||||
EXPECT_EQ(size_t(1024 * 1024 * 2), files[0].content.size());
|
||||
EXPECT_TRUE(files[0].filename == "2MB_data");
|
||||
EXPECT_TRUE(files[0].content_type == "application/octet-stream");
|
||||
|
||||
EXPECT_TRUE(files[1].name == "hello");
|
||||
EXPECT_TRUE(files[1].content == "world");
|
||||
EXPECT_TRUE(files[1].filename == "");
|
||||
EXPECT_TRUE(files[1].content_type == "");
|
||||
} else {
|
||||
std::string body;
|
||||
content_reader([&](const char *data, size_t data_length) {
|
||||
body.append(data, data_length);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8080); });
|
||||
while (!svr.is_running()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
|
||||
{
|
||||
std::string data(1024 * 1024 * 2, '.');
|
||||
std::stringstream buffer;
|
||||
buffer << data;
|
||||
|
||||
Client cli("https://localhost:8080");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
|
||||
MultipartFormDataItems items{
|
||||
{"document", buffer.str(), "2MB_data", "application/octet-stream"},
|
||||
{"hello", "world", "", ""},
|
||||
};
|
||||
|
||||
auto res = cli.Post("/post", items);
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_EQ(200, res->status);
|
||||
}
|
||||
|
||||
svr.stop();
|
||||
t.join();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user