mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d60dc8839 | |||
| b713a3a651 | |||
| 7e8db1dc68 | |||
| 316add860b | |||
| 79ce6f1745 | |||
| 09fdf4eacd | |||
| 143b2dd15a | |||
| e2c4e9d95c | |||
| d87082f04b | |||
| cc14855ba0 | |||
| 56c418745f | |||
| 4ce9911837 | |||
| 559c407552 | |||
| a2f4e29a7b | |||
| b8cf739d27 | |||
| aec2f9521d | |||
| 7b55ecdc59 | |||
| e9575bcb78 | |||
| 308aeb187b | |||
| 05d18f2bc5 | |||
| 3da4a0ac69 | |||
| 9d12b3f20e | |||
| 852a374748 | |||
| 3b5bab3308 | |||
| 69e75f4a67 | |||
| b0fd4befb1 | |||
| 3e80666a74 | |||
| 3e4567bae8 | |||
| db075d8cf9 | |||
| 16df0ef37e | |||
| f1a2ac5108 | |||
| e5743b358d | |||
| 1184bbe4cb | |||
| 9dcffda7ae | |||
| e5903635e2 | |||
| 510b4eaaae |
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: install brotli library on ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt-get install -y libbrotli-dev
|
||||
run: sudo apt update && sudo apt-get install -y libbrotli-dev
|
||||
- name: install brotli library on macOS
|
||||
if: matrix.os == 'macOS-latest'
|
||||
run: brew install brotli
|
||||
|
||||
+4
-3
@@ -66,6 +66,7 @@ if(Git_FOUND)
|
||||
# Gets the latest tag as a string like "v0.6.6"
|
||||
# Can silently fail if git isn't on the system
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=0
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE _raw_version_string
|
||||
ERROR_VARIABLE _git_tag_error
|
||||
)
|
||||
@@ -235,9 +236,9 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
|
||||
# Set the definitions to enable optional features
|
||||
target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:"CPPHTTPLIB_BROTLI_SUPPORT">
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_ZLIB}>:"CPPHTTPLIB_ZLIB_SUPPORT">
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:"CPPHTTPLIB_OPENSSL_SUPPORT">
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:CPPHTTPLIB_BROTLI_SUPPORT>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_ZLIB}>:CPPHTTPLIB_ZLIB_SUPPORT>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
|
||||
)
|
||||
|
||||
# Cmake's find_package search path is different based on the system
|
||||
|
||||
@@ -7,8 +7,41 @@ A C++11 single-file header-only cross platform HTTP/HTTPS library.
|
||||
|
||||
It's extremely easy to setup. Just include **httplib.h** file in your code!
|
||||
|
||||
Server Example
|
||||
--------------
|
||||
NOTE: This is a 'blocking' HTTP library. If you are looking for a 'non-blocking' library, this is not the one that you want.
|
||||
|
||||
Simple examples
|
||||
---------------
|
||||
|
||||
#### Server
|
||||
|
||||
```c++
|
||||
httplib::Server svr;
|
||||
|
||||
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
```
|
||||
|
||||
#### Client
|
||||
|
||||
```c++
|
||||
httplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
|
||||
res->status; // 200
|
||||
res->body; // "Hello World!"
|
||||
```
|
||||
|
||||
### Try out the examples on Repl.it!
|
||||
|
||||
1. Run server at https://repl.it/@yhirose/cpp-httplib-server
|
||||
2. Run client at https://repl.it/@yhirose/cpp-httplib-client
|
||||
|
||||
Server
|
||||
------
|
||||
|
||||
```c++
|
||||
#include <httplib.h>
|
||||
@@ -197,7 +230,7 @@ svr.Get("/stream", [&](const Request &req, Response &res) {
|
||||
// prepare data...
|
||||
sink.write(data.data(), data.size());
|
||||
} else {
|
||||
done(); // No more data
|
||||
sink.done(); // No more data
|
||||
}
|
||||
return true; // return 'false' if you want to cancel the process.
|
||||
});
|
||||
@@ -264,13 +297,18 @@ Please see [Server example](https://github.com/yhirose/cpp-httplib/blob/master/e
|
||||
|
||||
### Default thread pool support
|
||||
|
||||
`ThreadPool` is used as a **default** task queue, and the default thread count is 8, or `std::thread::hardware_concurrency()`. You can change it with `CPPHTTPLIB_THREAD_POOL_COUNT`.
|
||||
|
||||
`ThreadPool` is used as a default task queue, and the default thread count is set to value from `std::thread::hardware_concurrency()`.
|
||||
If you want to set the thread count at runtime, there is no convenient way... But here is how.
|
||||
|
||||
You can change the thread count by setting `CPPHTTPLIB_THREAD_POOL_COUNT`.
|
||||
```cpp
|
||||
svr.new_task_queue = [] { return new ThreadPool(12); };
|
||||
```
|
||||
|
||||
### Override the default thread pool with yours
|
||||
|
||||
You can supply your own thread pool implementation according to your need.
|
||||
|
||||
```cpp
|
||||
class YourThreadPoolTaskQueue : public TaskQueue {
|
||||
public:
|
||||
@@ -295,8 +333,8 @@ svr.new_task_queue = [] {
|
||||
};
|
||||
```
|
||||
|
||||
Client Example
|
||||
--------------
|
||||
Client
|
||||
------
|
||||
|
||||
```c++
|
||||
#include <httplib.h>
|
||||
@@ -564,9 +602,9 @@ NOTE: cpp-httplib currently supports only version 1.1.1.
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
|
||||
SSLServer svr("./cert.pem", "./key.pem");
|
||||
httplib::SSLServer svr("./cert.pem", "./key.pem");
|
||||
|
||||
SSLClient cli("localhost", 8080);
|
||||
httplib::SSLClient cli("localhost", 1234); // or `httplib::Client cli("https://localhost:1234");`
|
||||
cli.set_ca_cert_path("./ca-bundle.crt");
|
||||
cli.enable_server_certificate_verification(true);
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
|
||||
#CXX = clang++
|
||||
CXXFLAGS = -ggdb -O0 -std=c++11 -DGTEST_USE_OWN_TR1_TUPLE -I.. -I. -Wall -Wextra -Wtype-limits -Wconversion
|
||||
CXXFLAGS = -ggdb -O0 -std=c++11 -DGTEST_USE_OWN_TR1_TUPLE -I.. -I. -Wall -Wextra -Wtype-limits -Wconversion #-fsanitize=address
|
||||
|
||||
OPENSSL_DIR = /usr/local/opt/openssl@1.1
|
||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
|
||||
|
||||
+184
-15
@@ -44,6 +44,12 @@ TEST(StartupTest, WSAStartup) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(TrimTests, TrimStringTests) {
|
||||
EXPECT_EQ("abc", detail::trim_copy("abc"));
|
||||
EXPECT_EQ("abc", detail::trim_copy(" abc "));
|
||||
EXPECT_TRUE(detail::trim_copy("").empty());
|
||||
}
|
||||
|
||||
TEST(SplitTest, ParseQueryString) {
|
||||
string s = "key1=val1&key2=val2&key3=val3";
|
||||
Params dic;
|
||||
@@ -66,6 +72,23 @@ TEST(SplitTest, ParseQueryString) {
|
||||
EXPECT_EQ("val3", dic.find("key3")->second);
|
||||
}
|
||||
|
||||
TEST(SplitTest, ParseInvalidQueryTests) {
|
||||
|
||||
{
|
||||
string s = " ";
|
||||
Params dict;
|
||||
detail::parse_query_text(s, dict);
|
||||
EXPECT_TRUE(dict.empty());
|
||||
}
|
||||
|
||||
{
|
||||
string s = " = =";
|
||||
Params dict;
|
||||
detail::parse_query_text(s, dict);
|
||||
EXPECT_TRUE(dict.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseQueryTest, ParseQueryString) {
|
||||
string s = "key1=val1&key2=val2&key3=val3";
|
||||
Params dic;
|
||||
@@ -1064,7 +1087,7 @@ protected:
|
||||
})
|
||||
.Post("/multipart",
|
||||
[&](const Request &req, Response & /*res*/) {
|
||||
EXPECT_EQ(5u, req.files.size());
|
||||
EXPECT_EQ(6u, req.files.size());
|
||||
ASSERT_TRUE(!req.has_file("???"));
|
||||
ASSERT_TRUE(req.body.empty());
|
||||
|
||||
@@ -1093,6 +1116,13 @@ protected:
|
||||
EXPECT_EQ("application/octet-stream", file.content_type);
|
||||
EXPECT_EQ(0u, file.content.size());
|
||||
}
|
||||
|
||||
{
|
||||
const auto &file = req.get_file_value("file4");
|
||||
EXPECT_TRUE(file.filename.empty());
|
||||
EXPECT_EQ(0u, file.content.size());
|
||||
EXPECT_EQ("application/json tmp-string", file.content_type);
|
||||
}
|
||||
})
|
||||
.Post("/empty",
|
||||
[&](const Request &req, Response &res) {
|
||||
@@ -1303,8 +1333,11 @@ protected:
|
||||
|
||||
virtual void TearDown() {
|
||||
svr_.stop();
|
||||
for (auto &t : request_threads_) {
|
||||
t.join();
|
||||
if (!request_threads_.empty()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
for (auto &t : request_threads_) {
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
t_.join();
|
||||
}
|
||||
@@ -1785,7 +1818,7 @@ TEST_F(ServerTest, MultipartFormData) {
|
||||
{"file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain"},
|
||||
{"file2", "{\n \"world\", true\n}\n", "world.json", "application/json"},
|
||||
{"file3", "", "", "application/octet-stream"},
|
||||
};
|
||||
{"file4", "", "", " application/json tmp-string "}};
|
||||
|
||||
auto res = cli_.Post("/multipart", items);
|
||||
|
||||
@@ -2021,7 +2054,6 @@ TEST_F(ServerTest, SlowRequest) {
|
||||
std::thread([=]() { auto res = cli_.Get("/slow"); }));
|
||||
request_threads_.push_back(
|
||||
std::thread([=]() { auto res = cli_.Get("/slow"); }));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, SlowPost) {
|
||||
@@ -2038,9 +2070,14 @@ TEST_F(ServerTest, SlowPost) {
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(200, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, SlowPostFail) {
|
||||
char buffer[64 * 1024];
|
||||
memset(buffer, 0x42, sizeof(buffer));
|
||||
|
||||
cli_.set_write_timeout(0, 0);
|
||||
res = cli_.Post(
|
||||
auto res = cli_.Post(
|
||||
"/slowpost", 64 * 1024 * 1024,
|
||||
[&](size_t /*offset*/, size_t /*length*/, DataSink &sink) {
|
||||
sink.write(buffer, sizeof(buffer));
|
||||
@@ -2157,6 +2194,47 @@ TEST_F(ServerTest, GetStreamedChunkedWithGzip2) {
|
||||
EXPECT_EQ(200, res->status);
|
||||
EXPECT_EQ(std::string("123456789"), res->body);
|
||||
}
|
||||
|
||||
TEST(GzipDecompressor, ChunkedDecompression) {
|
||||
std::string data;
|
||||
for (size_t i = 0; i < 32 * 1024; ++i) {
|
||||
data.push_back(static_cast<char>('a' + i % 26));
|
||||
}
|
||||
|
||||
std::string compressed_data;
|
||||
{
|
||||
httplib::detail::gzip_compressor compressor;
|
||||
bool result = compressor.compress(
|
||||
data.data(), data.size(),
|
||||
/*last=*/true, [&](const char *data, size_t size) {
|
||||
compressed_data.insert(compressed_data.size(), data, size);
|
||||
return true;
|
||||
});
|
||||
ASSERT_TRUE(result);
|
||||
}
|
||||
|
||||
std::string decompressed_data;
|
||||
{
|
||||
httplib::detail::gzip_decompressor decompressor;
|
||||
|
||||
// Chunk size is chosen specificaly 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();
|
||||
chunk_begin += chunk_size) {
|
||||
size_t current_chunk_size =
|
||||
std::min(compressed_data.size() - chunk_begin, chunk_size);
|
||||
bool result = decompressor.decompress(
|
||||
compressed_data.data() + chunk_begin, current_chunk_size,
|
||||
[&](const char *data, size_t size) {
|
||||
decompressed_data.insert(decompressed_data.size(), data, size);
|
||||
return true;
|
||||
});
|
||||
ASSERT_TRUE(result);
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(data, decompressed_data);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
@@ -2252,6 +2330,41 @@ TEST_F(ServerTest, PostMulitpartFilsContentReceiver) {
|
||||
EXPECT_EQ(200, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PostMulitpartPlusBoundary) {
|
||||
MultipartFormDataItems items = {
|
||||
{"text1", "text default", "", ""},
|
||||
{"text2", "aωb", "", ""},
|
||||
{"file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain"},
|
||||
{"file2", "{\n \"world\", true\n}\n", "world.json", "application/json"},
|
||||
{"file3", "", "", "application/octet-stream"},
|
||||
};
|
||||
|
||||
auto boundary = std::string("+++++");
|
||||
|
||||
std::string body;
|
||||
|
||||
for (const auto &item : items) {
|
||||
body += "--" + boundary + "\r\n";
|
||||
body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
|
||||
if (!item.filename.empty()) {
|
||||
body += "; filename=\"" + item.filename + "\"";
|
||||
}
|
||||
body += "\r\n";
|
||||
if (!item.content_type.empty()) {
|
||||
body += "Content-Type: " + item.content_type + "\r\n";
|
||||
}
|
||||
body += "\r\n";
|
||||
body += item.content + "\r\n";
|
||||
}
|
||||
body += "--" + boundary + "--\r\n";
|
||||
|
||||
std::string content_type = "multipart/form-data; boundary=" + boundary;
|
||||
auto res = cli_.Post("/content_receiver", body, content_type.c_str());
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(200, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, PostContentReceiverGzip) {
|
||||
cli_.set_compress(true);
|
||||
auto res = cli_.Post("/content_receiver", "content", "text/plain");
|
||||
@@ -2733,15 +2846,14 @@ TEST(StreamingTest, NoContentLengthStreaming) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/stream", [](const Request & /*req*/, Response &res) {
|
||||
res.set_content_provider(
|
||||
"text/plain", [](size_t offset, DataSink &sink) {
|
||||
if (offset < 6) {
|
||||
sink.os << (offset < 3 ? "a" : "b");
|
||||
} else {
|
||||
sink.done();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
res.set_content_provider("text/plain", [](size_t offset, DataSink &sink) {
|
||||
if (offset < 6) {
|
||||
sink.os << (offset < 3 ? "a" : "b");
|
||||
} else {
|
||||
sink.done();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
|
||||
@@ -2879,6 +2991,50 @@ TEST(KeepAliveTest, ReadTimeout) {
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
TEST(KeepAliveTest, ReadTimeoutSSL) {
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.Get("/a", [&](const Request & /*req*/, Response &res) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
res.set_content("a", "text/plain");
|
||||
});
|
||||
|
||||
svr.Get("/b", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_content("b", "text/plain");
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr]() {
|
||||
svr.listen("localhost", PORT);
|
||||
});
|
||||
while (!svr.is_running()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
// Give GET time to get a few messages.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
|
||||
SSLClient cli("localhost", PORT);
|
||||
cli.enable_server_certificate_verification(false);
|
||||
cli.set_keep_alive(true);
|
||||
cli.set_read_timeout(1);
|
||||
|
||||
auto resa = cli.Get("/a");
|
||||
ASSERT_TRUE(!resa);
|
||||
EXPECT_EQ(Error::Read, resa.error());
|
||||
|
||||
auto resb = cli.Get("/b");
|
||||
ASSERT_TRUE(resb);
|
||||
EXPECT_EQ(200, resb->status);
|
||||
EXPECT_EQ("b", resb->body);
|
||||
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
}
|
||||
#endif
|
||||
|
||||
class ServerTestWithAI_PASSIVE : public ::testing::Test {
|
||||
protected:
|
||||
ServerTestWithAI_PASSIVE()
|
||||
@@ -3013,6 +3169,19 @@ TEST_F(PayloadMaxLengthTest, ExceedLimit) {
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
TEST(SSLClientTest, UpdateCAStore) {
|
||||
httplib::SSLClient httplib_client("www.google.com");
|
||||
auto ca_store_1 = X509_STORE_new();
|
||||
X509_STORE_load_locations(ca_store_1, "/etc/ssl/certs/ca-certificates.crt",
|
||||
nullptr);
|
||||
httplib_client.set_ca_cert_store(ca_store_1);
|
||||
|
||||
auto ca_store_2 = X509_STORE_new();
|
||||
X509_STORE_load_locations(ca_store_2, "/etc/ssl/certs/ca-certificates.crt",
|
||||
nullptr);
|
||||
httplib_client.set_ca_cert_store(ca_store_2);
|
||||
}
|
||||
|
||||
TEST(SSLClientTest, ServerNameIndication) {
|
||||
SSLClient cli("httpbin.org", 443);
|
||||
auto res = cli.Get("/get");
|
||||
|
||||
Reference in New Issue
Block a user