mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb8e45383e | |||
| a1df576e4f | |||
| 7fb0254794 | |||
| c82d1e52cc | |||
| 846151b605 | |||
| e44e31dd5b | |||
| f7b9501662 | |||
| e12fe4cbbb | |||
| 49d2e1f135 | |||
| 8191fd8e6c |
@@ -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
|
||||
|
||||
@@ -1384,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);
|
||||
@@ -2812,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";
|
||||
@@ -2847,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";
|
||||
@@ -2933,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) {
|
||||
@@ -3019,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;
|
||||
@@ -3431,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;
|
||||
@@ -3745,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";
|
||||
|
||||
@@ -4958,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(), '?',
|
||||
@@ -7250,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_) {
|
||||
@@ -7260,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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
+113
-5
@@ -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;
|
||||
@@ -607,7 +610,8 @@ 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_Online) {
|
||||
@@ -1194,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;
|
||||
|
||||
@@ -1401,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()
|
||||
@@ -1669,10 +1713,10 @@ protected:
|
||||
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");
|
||||
})
|
||||
[&](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, "");
|
||||
@@ -4776,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