mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a1f379e75 | |||
| fd8da4d8e4 | |||
| 365cbe37fa | |||
| 4a7aae5469 | |||
| fd324e1412 | |||
| 366eda72dc | |||
| c216dc94d2 | |||
| 61893a00a4 |
@@ -285,6 +285,22 @@ svr.set_post_routing_handler([](const auto& req, auto& res) {
|
||||
});
|
||||
```
|
||||
|
||||
### Pre request handler
|
||||
|
||||
```cpp
|
||||
svr.set_pre_request_handler([](const auto& req, auto& res) {
|
||||
if (req.matched_route == "/user/:user") {
|
||||
auto user = req.path_params.at("user");
|
||||
if (user != "john") {
|
||||
res.status = StatusCode::Forbidden_403;
|
||||
res.set_content("error", "text/html");
|
||||
return Server::HandlerResponse::Handled;
|
||||
}
|
||||
}
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
```
|
||||
|
||||
### 'multipart/form-data' POST data
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.20.1"
|
||||
#define CPPHTTPLIB_VERSION "0.21.0"
|
||||
|
||||
/*
|
||||
* Configuration
|
||||
@@ -192,8 +192,13 @@ using ssize_t = long;
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<afunix.h>)
|
||||
// afunix.h uses types declared in winsock2.h, so has to be included after it.
|
||||
#include <afunix.h>
|
||||
#define CPPHTTPLIB_HAVE_AFUNIX_H 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef WSA_FLAG_NO_HANDLE_INHERIT
|
||||
#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
|
||||
@@ -539,6 +544,7 @@ struct MultipartFormData {
|
||||
std::string content;
|
||||
std::string filename;
|
||||
std::string content_type;
|
||||
Headers headers;
|
||||
};
|
||||
using MultipartFormDataItems = std::vector<MultipartFormData>;
|
||||
using MultipartFormDataMap = std::multimap<std::string, MultipartFormData>;
|
||||
@@ -631,6 +637,7 @@ using Ranges = std::vector<Range>;
|
||||
struct Request {
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string matched_route;
|
||||
Params params;
|
||||
Headers headers;
|
||||
std::string body;
|
||||
@@ -882,10 +889,16 @@ namespace detail {
|
||||
|
||||
class MatcherBase {
|
||||
public:
|
||||
MatcherBase(std::string pattern) : pattern_(pattern) {}
|
||||
virtual ~MatcherBase() = default;
|
||||
|
||||
const std::string &pattern() const { return pattern_; }
|
||||
|
||||
// Match request path and populate its matches and
|
||||
virtual bool match(Request &request) const = 0;
|
||||
|
||||
private:
|
||||
std::string pattern_;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -937,7 +950,8 @@ private:
|
||||
*/
|
||||
class RegexMatcher final : public MatcherBase {
|
||||
public:
|
||||
RegexMatcher(const std::string &pattern) : regex_(pattern) {}
|
||||
RegexMatcher(const std::string &pattern)
|
||||
: MatcherBase(pattern), regex_(pattern) {}
|
||||
|
||||
bool match(Request &request) const override;
|
||||
|
||||
@@ -1004,9 +1018,12 @@ public:
|
||||
}
|
||||
|
||||
Server &set_exception_handler(ExceptionHandler handler);
|
||||
|
||||
Server &set_pre_routing_handler(HandlerWithResponse handler);
|
||||
Server &set_post_routing_handler(Handler handler);
|
||||
|
||||
Server &set_pre_request_handler(HandlerWithResponse handler);
|
||||
|
||||
Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
|
||||
Server &set_logger(Logger logger);
|
||||
|
||||
@@ -1087,8 +1104,7 @@ private:
|
||||
bool listen_internal();
|
||||
|
||||
bool routing(Request &req, Response &res, Stream &strm);
|
||||
bool handle_file_request(const Request &req, Response &res,
|
||||
bool head = false);
|
||||
bool handle_file_request(const Request &req, Response &res);
|
||||
bool dispatch_request(Request &req, Response &res,
|
||||
const Handlers &handlers) const;
|
||||
bool dispatch_request_for_content_reader(
|
||||
@@ -1149,6 +1165,7 @@ private:
|
||||
ExceptionHandler exception_handler_;
|
||||
HandlerWithResponse pre_routing_handler_;
|
||||
Handler post_routing_handler_;
|
||||
HandlerWithResponse pre_request_handler_;
|
||||
Expect100ContinueHandler expect_100_continue_handler_;
|
||||
|
||||
Logger logger_;
|
||||
@@ -2066,7 +2083,9 @@ template <size_t N> inline constexpr size_t str_len(const char (&)[N]) {
|
||||
}
|
||||
|
||||
inline bool is_numeric(const std::string &str) {
|
||||
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
|
||||
return !str.empty() &&
|
||||
std::all_of(str.cbegin(), str.cend(),
|
||||
[](unsigned char c) { return std::isdigit(c); });
|
||||
}
|
||||
|
||||
inline uint64_t get_header_value_u64(const Headers &headers,
|
||||
@@ -3550,6 +3569,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
|
||||
hints.ai_flags = socket_flags;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H)
|
||||
if (hints.ai_family == AF_UNIX) {
|
||||
const auto addrlen = host.length();
|
||||
if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; }
|
||||
@@ -3594,6 +3614,7 @@ socket_t create_socket(const std::string &host, const std::string &ip, int port,
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto service = std::to_string(port);
|
||||
|
||||
@@ -5025,6 +5046,16 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// parse and emplace space trimmed headers into a map
|
||||
if (!parse_header(
|
||||
header.data(), header.data() + header.size(),
|
||||
[&](const std::string &key, const std::string &val) {
|
||||
file_.headers.emplace(key, val);
|
||||
})) {
|
||||
is_valid_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr const char header_content_type[] = "Content-Type:";
|
||||
|
||||
if (start_with_case_ignore(header, header_content_type)) {
|
||||
@@ -5124,6 +5155,7 @@ private:
|
||||
file_.name.clear();
|
||||
file_.filename.clear();
|
||||
file_.content_type.clear();
|
||||
file_.headers.clear();
|
||||
}
|
||||
|
||||
bool start_with_case_ignore(const std::string &a, const char *b) const {
|
||||
@@ -6216,7 +6248,8 @@ inline time_t BufferStream::duration() const { return 0; }
|
||||
|
||||
inline const std::string &BufferStream::get_buffer() const { return buffer; }
|
||||
|
||||
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
|
||||
inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern)
|
||||
: MatcherBase(pattern) {
|
||||
constexpr const char marker[] = "/:";
|
||||
|
||||
// One past the last ending position of a path param substring
|
||||
@@ -6467,6 +6500,11 @@ inline Server &Server::set_post_routing_handler(Handler handler) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Server &Server::set_pre_request_handler(HandlerWithResponse handler) {
|
||||
pre_request_handler_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Server &Server::set_logger(Logger logger) {
|
||||
logger_ = std::move(logger);
|
||||
return *this;
|
||||
@@ -6878,8 +6916,7 @@ Server::read_content_core(Stream &strm, Request &req, Response &res,
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool Server::handle_file_request(const Request &req, Response &res,
|
||||
bool head) {
|
||||
inline bool Server::handle_file_request(const Request &req, Response &res) {
|
||||
for (const auto &entry : base_dirs_) {
|
||||
// Prefix match
|
||||
if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) {
|
||||
@@ -6912,7 +6949,7 @@ inline bool Server::handle_file_request(const Request &req, Response &res,
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!head && file_request_handler_) {
|
||||
if (req.method != "HEAD" && file_request_handler_) {
|
||||
file_request_handler_(req, res);
|
||||
}
|
||||
|
||||
@@ -7046,9 +7083,8 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
|
||||
}
|
||||
|
||||
// File handler
|
||||
auto is_head_request = req.method == "HEAD";
|
||||
if ((req.method == "GET" || is_head_request) &&
|
||||
handle_file_request(req, res, is_head_request)) {
|
||||
if ((req.method == "GET" || req.method == "HEAD") &&
|
||||
handle_file_request(req, res)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7123,7 +7159,11 @@ inline bool Server::dispatch_request(Request &req, Response &res,
|
||||
const auto &handler = x.second;
|
||||
|
||||
if (matcher->match(req)) {
|
||||
handler(req, res);
|
||||
req.matched_route = matcher->pattern();
|
||||
if (!pre_request_handler_ ||
|
||||
pre_request_handler_(req, res) != HandlerResponse::Handled) {
|
||||
handler(req, res);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7250,7 +7290,11 @@ inline bool Server::dispatch_request_for_content_reader(
|
||||
const auto &handler = x.second;
|
||||
|
||||
if (matcher->match(req)) {
|
||||
handler(req, res, content_reader);
|
||||
req.matched_route = matcher->pattern();
|
||||
if (!pre_request_handler_ ||
|
||||
pre_request_handler_(req, res) != HandlerResponse::Handled) {
|
||||
handler(req, res, content_reader);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ if get_option('cpp-httplib_compile')
|
||||
soversion: version.split('.')[0] + '.' + version.split('.')[1],
|
||||
install: true
|
||||
)
|
||||
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, link_with: lib, sources: httplib_ch[1])
|
||||
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, link_with: lib, sources: httplib_ch[1], version: version)
|
||||
|
||||
import('pkgconfig').generate(
|
||||
lib,
|
||||
@@ -98,7 +98,7 @@ if get_option('cpp-httplib_compile')
|
||||
)
|
||||
else
|
||||
install_headers('httplib.h')
|
||||
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: '.')
|
||||
cpp_httplib_dep = declare_dependency(compile_args: args, dependencies: deps, include_directories: '.', version: version)
|
||||
|
||||
import('pkgconfig').generate(
|
||||
name: 'cpp-httplib',
|
||||
|
||||
+155
-1
@@ -2263,7 +2263,7 @@ TEST(NoContentTest, ContentLength) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RoutingHandlerTest, PreRoutingHandler) {
|
||||
TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) {
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
@@ -2354,6 +2354,63 @@ TEST(RoutingHandlerTest, PreRoutingHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RequestHandlerTest, PreRequestHandler) {
|
||||
auto route_path = "/user/:user";
|
||||
|
||||
Server svr;
|
||||
|
||||
svr.Get("/hi", [](const Request &, Response &res) {
|
||||
res.set_content("hi", "text/plain");
|
||||
});
|
||||
|
||||
svr.Get(route_path, [](const Request &req, Response &res) {
|
||||
res.set_content(req.path_params.at("user"), "text/plain");
|
||||
});
|
||||
|
||||
svr.set_pre_request_handler([&](const Request &req, Response &res) {
|
||||
if (req.matched_route == route_path) {
|
||||
auto user = req.path_params.at("user");
|
||||
if (user != "john") {
|
||||
res.status = StatusCode::Forbidden_403;
|
||||
res.set_content("error", "text/html");
|
||||
return Server::HandlerResponse::Handled;
|
||||
}
|
||||
}
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, PORT);
|
||||
{
|
||||
auto res = cli.Get("/hi");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("hi", res->body);
|
||||
}
|
||||
|
||||
{
|
||||
auto res = cli.Get("/user/john");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("john", res->body);
|
||||
}
|
||||
|
||||
{
|
||||
auto res = cli.Get("/user/invalid-user");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::Forbidden_403, res->status);
|
||||
EXPECT_EQ("error", res->body);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(InvalidFormatTest, StatusCode) {
|
||||
Server svr;
|
||||
|
||||
@@ -5732,6 +5789,41 @@ TEST(KeepAliveTest, ReadTimeout) {
|
||||
EXPECT_EQ("b", resb->body);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, MaxCount) {
|
||||
size_t keep_alive_max_count = 3;
|
||||
|
||||
Server svr;
|
||||
svr.set_keep_alive_max_count(keep_alive_max_count);
|
||||
|
||||
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, PORT);
|
||||
cli.set_keep_alive(true);
|
||||
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
auto result = cli.Get("/hi");
|
||||
ASSERT_TRUE(result);
|
||||
EXPECT_EQ(StatusCode::OK_200, result->status);
|
||||
|
||||
if (i == keep_alive_max_count - 1) {
|
||||
EXPECT_EQ("close", result->get_header_value("Connection"));
|
||||
} else {
|
||||
EXPECT_FALSE(result->has_header("Connection"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, Issue1041) {
|
||||
Server svr;
|
||||
svr.set_keep_alive_timeout(3);
|
||||
@@ -7918,6 +8010,68 @@ TEST(MultipartFormDataTest, ContentLength) {
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("200", response.substr(9, 3));
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, AccessPartHeaders) {
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &req, Response &) {
|
||||
ASSERT_EQ(2u, req.files.size());
|
||||
|
||||
auto it = req.files.begin();
|
||||
ASSERT_EQ("text1", it->second.name);
|
||||
ASSERT_EQ("text1", it->second.content);
|
||||
ASSERT_EQ(1, it->second.headers.count("Content-Length"));
|
||||
auto content_length = it->second.headers.find("CONTENT-length");
|
||||
ASSERT_EQ("5", content_length->second);
|
||||
ASSERT_EQ(3, it->second.headers.size());
|
||||
|
||||
++it;
|
||||
ASSERT_EQ("text2", it->second.name);
|
||||
ASSERT_EQ("text2", it->second.content);
|
||||
auto &headers = it->second.headers;
|
||||
ASSERT_EQ(3, headers.size());
|
||||
auto custom_header = headers.find("x-whatever");
|
||||
ASSERT_TRUE(custom_header != headers.end());
|
||||
ASSERT_NE("customvalue", custom_header->second);
|
||||
ASSERT_EQ("CustomValue", custom_header->second);
|
||||
ASSERT_TRUE(headers.find("X-Test") == headers.end()); // text1 header
|
||||
|
||||
handled = true;
|
||||
});
|
||||
|
||||
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
ASSERT_TRUE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Content-Type: multipart/form-data;boundary=--------\r\n"
|
||||
"Content-Length: 232\r\n"
|
||||
"\r\n----------\r\n"
|
||||
"Content-Disposition: form-data; name=\"text1\"\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"X-Test: 1\r\n"
|
||||
"\r\n"
|
||||
"text1"
|
||||
"\r\n----------\r\n"
|
||||
"Content-Disposition: form-data; name=\"text2\"\r\n"
|
||||
"Content-Type: text/plain\r\n"
|
||||
"X-Whatever: CustomValue\r\n"
|
||||
"\r\n"
|
||||
"text2"
|
||||
"\r\n------------\r\n"
|
||||
"That should be disregarded. Not even read";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("200", response.substr(9, 3));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(TaskQueueTest, IncreaseAtomicInteger) {
|
||||
|
||||
Reference in New Issue
Block a user