mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-03 13:38:36 +00:00
Compare commits
25
Commits
v0.56.0
...
086a648364
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086a648364 | ||
|
|
639391ad7f | ||
|
|
0715c2739e | ||
|
|
3330d0eb06 | ||
|
|
c1c2b1f4b4 | ||
|
|
e11dbec7b3 | ||
|
|
174bce5ccf | ||
|
|
57c4f7f385 | ||
|
|
2fb2dbbe1e | ||
|
|
8b6ab24159 | ||
|
|
5a202d3d5f | ||
|
|
4f3f9ef19b | ||
|
|
6d59d1e2df | ||
|
|
9386b25dd7 | ||
|
|
91c55a4385 | ||
|
|
ad88645a83 | ||
|
|
82722fcb13 | ||
|
|
8b872605e0 | ||
|
|
52f214bf2e | ||
|
|
09c02f1335 | ||
|
|
2e5480ad65 | ||
|
|
4cb363e3f2 | ||
|
|
deb520e26b | ||
|
|
f37a5b1407 | ||
|
|
f15992c7ed |
No files matched your search
+21
-3
@@ -15,7 +15,7 @@
|
|||||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||||
* HTTPLIB_REQUIRE_ZSTD (default off)
|
* HTTPLIB_REQUIRE_ZSTD (default off)
|
||||||
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
||||||
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
|
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on when supported)
|
||||||
* HTTPLIB_COMPILE (default off)
|
* HTTPLIB_COMPILE (default off)
|
||||||
* HTTPLIB_INSTALL (default on)
|
* HTTPLIB_INSTALL (default on)
|
||||||
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
||||||
@@ -181,6 +181,24 @@ if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
|
|||||||
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
||||||
endif()
|
endif()
|
||||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
||||||
|
if(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO AND WIN32)
|
||||||
|
include(CheckCXXSymbolExists)
|
||||||
|
|
||||||
|
set(_httplib_cmake_required_definitions ${CMAKE_REQUIRED_DEFINITIONS})
|
||||||
|
set(_httplib_cmake_required_libraries ${CMAKE_REQUIRED_LIBRARIES})
|
||||||
|
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_WIN32_WINNT=0x0A00)
|
||||||
|
list(APPEND CMAKE_REQUIRED_LIBRARIES ws2_32)
|
||||||
|
check_cxx_symbol_exists(GetAddrInfoExCancel "winsock2.h;ws2tcpip.h" HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||||
|
set(CMAKE_REQUIRED_DEFINITIONS ${_httplib_cmake_required_definitions})
|
||||||
|
set(CMAKE_REQUIRED_LIBRARIES ${_httplib_cmake_required_libraries})
|
||||||
|
unset(_httplib_cmake_required_definitions)
|
||||||
|
unset(_httplib_cmake_required_libraries)
|
||||||
|
|
||||||
|
if(NOT HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||||
|
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO FALSE)
|
||||||
|
message(WARNING "GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
||||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||||
@@ -367,7 +385,7 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
|||||||
# Needed for API from MacOS Security framework
|
# Needed for API from MacOS Security framework
|
||||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
||||||
# Needed for non-blocking getaddrinfo on MacOS
|
# Needed for non-blocking getaddrinfo on MacOS
|
||||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||||
# Can't put multiple targets in a single generator expression or it bugs out.
|
# 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::common>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
||||||
@@ -390,7 +408,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
|||||||
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
||||||
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
||||||
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
$<$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
||||||
)
|
)
|
||||||
|
|
||||||
# CMake configuration files installation directory
|
# CMake configuration files installation directory
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ sse.on_error([](httplib::Error err) { });
|
|||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Set reconnect interval (default: 3000ms)
|
// Set reconnect interval (default: 3000ms, minimum: 100ms)
|
||||||
sse.set_reconnect_interval(5000);
|
sse.set_reconnect_interval(5000);
|
||||||
|
|
||||||
// Set max reconnect attempts (default: 0 = unlimited)
|
// Set max reconnect attempts (default: 0 = unlimited)
|
||||||
|
|||||||
@@ -343,6 +343,18 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The check above runs after the handshake, so the client sees a successful upgrade followed by a close frame. To refuse the upgrade itself with an HTTP status, use a pre-routing or pre-request handler. Both run before the `101 Switching Protocols` response, and `req.matched_route` is available in the pre-request handler:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
if (req.matched_route == "/ws" && req.get_header_value("Authorization").empty()) {
|
||||||
|
res.status = httplib::StatusCode::Unauthorized_401;
|
||||||
|
return httplib::Server::HandlerResponse::Handled; // not upgraded
|
||||||
|
}
|
||||||
|
return httplib::Server::HandlerResponse::Unhandled;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Custom Headers and Timeouts
|
### Custom Headers and Timeouts
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -347,6 +347,25 @@ int port = svr.bind_to_any_port("0.0.0.0");
|
|||||||
svr.listen_after_bind();
|
svr.listen_after_bind();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Port sharing and exclusive binding
|
||||||
|
|
||||||
|
By default, the server socket enables address/port reuse: `SO_REUSEPORT` where it is available (Linux, macOS), and `SO_REUSEADDR` otherwise (Windows). A restarted server can bind again immediately, but binding to a port that another server is already listening on also succeeds, and connections are distributed between them.
|
||||||
|
|
||||||
|
If you want `listen()` to fail when the port is already in use, replace the default socket options with `set_socket_options`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Setting only `SO_REUSEADDR` is not enough on Windows. There, `SO_REUSEADDR` allows two sockets that both set it to bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
|
||||||
|
|
||||||
### Static File Server
|
### Static File Server
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -433,6 +452,9 @@ svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `req.path` is percent-decoded and may contain control characters such as CR/LF. Escape request data before writing it to a log file (see [docker/main.cc](docker/main.cc) for an example).
|
||||||
|
|
||||||
#### Pre-compression Logging
|
#### Pre-compression Logging
|
||||||
|
|
||||||
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
||||||
@@ -545,14 +567,15 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
|||||||
|
|
||||||
```
|
```
|
||||||
Request received
|
Request received
|
||||||
|
│
|
||||||
|
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
|
||||||
|
│ └─ returns a status other than 100 → stop here
|
||||||
│
|
│
|
||||||
├─ pre_routing_handler route not matched yet, body not read
|
├─ pre_routing_handler route not matched yet, body not read
|
||||||
│ └─ returns Handled → stop here
|
│ └─ returns Handled → stop here
|
||||||
│
|
│
|
||||||
├─ file_request_handler (GET/HEAD, static file serving)
|
├─ file_request_handler (GET/HEAD, static file serving)
|
||||||
│
|
│
|
||||||
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
|
|
||||||
│
|
|
||||||
├─ route matching → req.matched_route is set
|
├─ route matching → req.matched_route is set
|
||||||
│
|
│
|
||||||
├─ pre_request_handler route matched, body NOT read yet
|
├─ pre_request_handler route matched, body NOT read yet
|
||||||
@@ -568,6 +591,10 @@ Request received
|
|||||||
|
|
||||||
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
|
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
|
||||||
|
|
||||||
|
For a request with `Expect: 100-continue`, the `100 Continue` response is not sent until the body is about to be read. A request rejected before that point (by `pre_routing_handler`, `pre_request_handler`, or because no route matched) gets its final response without `100 Continue`, so the client never sends the body.
|
||||||
|
|
||||||
|
A WebSocket upgrade request that matches a route registered with `svr.WebSocket()` takes a shorter path: `pre_routing_handler`, then route matching (`req.matched_route` is set), then `pre_request_handler`, then the WebSocket handler. If either hook returns `Handled`, its response is sent as a regular HTTP response and the connection is not upgraded. Once the connection is upgraded, `post_routing_handler` does not run.
|
||||||
|
|
||||||
### Response user data
|
### Response user data
|
||||||
|
|
||||||
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||||
@@ -827,7 +854,9 @@ svr.Get("/content", [&](const Request &req, Response &res) {
|
|||||||
|
|
||||||
### 'Expect: 100-continue' handler
|
### 'Expect: 100-continue' handler
|
||||||
|
|
||||||
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.
|
By default, the server accepts an `Expect: 100-continue` header and sends a `100 Continue` response when it starts reading the request body. If the request is answered without reading the body, `100 Continue` is not sent and the connection is closed after the response.
|
||||||
|
|
||||||
|
The handler runs before `pre_routing_handler`. Returning `100` lets the request proceed; returning any other status sends that status as the final response and closes the connection.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Send a '417 Expectation Failed' response.
|
// Send a '417 Expectation Failed' response.
|
||||||
|
|||||||
+26
-6
@@ -48,6 +48,23 @@ std::string get_error_time_format() {
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape a value for a log line the way NGINX does: '"', '\\', control
|
||||||
|
// bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
|
||||||
|
// (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
|
||||||
|
// writing them verbatim would let a client forge extra log lines.
|
||||||
|
std::string escape_log(const std::string &s) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (unsigned char c : s) {
|
||||||
|
if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
|
||||||
|
out += std::format("\\x{:02X}", c);
|
||||||
|
} else {
|
||||||
|
out += static_cast<char>(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// NGINX Combined log format:
|
// NGINX Combined log format:
|
||||||
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||||
// "$http_referer" "$http_user_agent"
|
// "$http_referer" "$http_user_agent"
|
||||||
@@ -55,7 +72,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
std::string remote_user =
|
std::string remote_user =
|
||||||
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
||||||
auto time_local = get_time_format();
|
auto time_local = get_time_format();
|
||||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
// $request is the original request line, so log the raw target rather than
|
||||||
|
// the percent-decoded req.path.
|
||||||
|
auto request = std::format("{} {} {}", req.method, req.target, req.version);
|
||||||
auto status = res.status;
|
auto status = res.status;
|
||||||
auto body_bytes_sent = res.body.size();
|
auto body_bytes_sent = res.body.size();
|
||||||
auto http_referer = req.get_header_value("Referer");
|
auto http_referer = req.get_header_value("Referer");
|
||||||
@@ -64,9 +83,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
if (http_user_agent.empty()) http_user_agent = "-";
|
if (http_user_agent.empty()) http_user_agent = "-";
|
||||||
|
|
||||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||||
req.remote_addr, remote_user, time_local, request,
|
req.remote_addr, remote_user, time_local,
|
||||||
status, body_bytes_sent, http_referer,
|
escape_log(request), status, body_bytes_sent,
|
||||||
http_user_agent)
|
escape_log(http_referer), escape_log(http_user_agent))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +98,15 @@ void nginx_error_logger(const Error &err, const Request *req) {
|
|||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
auto request =
|
auto request =
|
||||||
std::format("{} {} {}", req->method, req->path, req->version);
|
std::format("{} {} {}", req->method, req->target, req->version);
|
||||||
auto host = req->get_header_value("Host");
|
auto host = req->get_header_value("Host");
|
||||||
if (host.empty()) host = "-";
|
if (host.empty()) host = "-";
|
||||||
|
|
||||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||||
"\"{}\", host: \"{}\"",
|
"\"{}\", host: \"{}\"",
|
||||||
time_local, level, to_string(err),
|
time_local, level, to_string(err),
|
||||||
req->remote_addr, request, host)
|
req->remote_addr, escape_log(request),
|
||||||
|
escape_log(host))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
} else {
|
} else {
|
||||||
// If no request context, just log the error
|
// If no request context, just log the error
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.56.0"
|
version = "0.58.0"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ svr.set_pre_request_handler(
|
|||||||
|
|
||||||
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
|
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
|
||||||
|
|
||||||
|
The pre-request handler also runs for routes registered with `svr.WebSocket()`. It is called before the `101 Switching Protocols` response, so returning `Handled` sends your HTTP response (such as a 403) and the connection is never upgraded.
|
||||||
|
|
||||||
## Return values
|
## Return values
|
||||||
|
|
||||||
Same as pre-routing — return `HandlerResponse`.
|
Same as pre-routing — return `HandlerResponse`.
|
||||||
|
|||||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
|||||||
|
|
||||||
## Check the return values
|
## Check the return values
|
||||||
|
|
||||||
`bind_to_port()` returns `false` on failure — typically when the port is already taken. Always check it.
|
`bind_to_port()` returns `false` on failure, for example when you don't have permission to bind to the port. Always check it.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
std::cerr << "port already in use" << std::endl;
|
std::cerr << "bind failed" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
||||||
|
|
||||||
|
## Detect a port that's already in use
|
||||||
|
|
||||||
|
With the default settings, you can actually bind to a port another server is already using. That's because cpp-httplib sets `SO_REUSEPORT` (Linux, macOS) or `SO_REUSEADDR` (Windows) on the server socket. A restarted server can bind again right away. The flip side is that a second server on the same port starts without an error, and connections get split between the two.
|
||||||
|
|
||||||
|
To make `bind_to_port()` fail on a port in use, replace the socket options with `set_socket_options()`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
|
std::cerr << "port already in use" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_socket_options()` replaces the defaults entirely. Setting `SO_REUSEADDR` on Linux and macOS keeps the "restarted server can bind again right away" behavior.
|
||||||
|
|
||||||
|
> **Note:** `SO_REUSEADDR` alone isn't enough on Windows. Two sockets that both set it can bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
|
||||||
|
|
||||||
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](../s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](../s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
||||||
@@ -107,6 +107,8 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A check inside the handler runs after the handshake has completed. To refuse the connection with an HTTP status such as 401 before it is upgraded, use `set_pre_request_handler()` instead. It also runs for WebSocket routes. See [S11. Authenticate per route with a pre-request handler](../../cookbook/s11-pre-request).
|
||||||
|
|
||||||
## Using WSS
|
## Using WSS
|
||||||
|
|
||||||
WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`.
|
WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`.
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ svr.set_pre_request_handler(
|
|||||||
|
|
||||||
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`)です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
|
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`)です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
|
||||||
|
|
||||||
|
`svr.WebSocket()`で登録したルートでも、Pre-requestハンドラは呼ばれます。呼ばれるのは`101 Switching Protocols`を返す前なので、`Handled`を返すとそのHTTPレスポンス(403など)がそのまま返り、WebSocketへのアップグレードは行われません。
|
||||||
|
|
||||||
## 戻り値の意味
|
## 戻り値の意味
|
||||||
|
|
||||||
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
||||||
|
|||||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
|||||||
|
|
||||||
## 戻り値のチェック
|
## 戻り値のチェック
|
||||||
|
|
||||||
`bind_to_port()`は失敗すると`false`を返します。ポートが既に使われている場合などです。必ずチェックしてください。
|
`bind_to_port()`は失敗すると`false`を返します。ポートにbindする権限が無い場合などです。必ずチェックしてください。
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
std::cerr << "port already in use" << std::endl;
|
std::cerr << "bind failed" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
||||||
|
|
||||||
|
## 使用中のポートを検出する
|
||||||
|
|
||||||
|
実は、デフォルトの設定では、ほかのサーバーが使っているポートにもbindできてしまいます。cpp-httplibがサーバーソケットに`SO_REUSEPORT`(Linux、macOS)か`SO_REUSEADDR`(Windows)を設定しているからです。再起動したサーバーはすぐにbindし直せます。その代わり、同じポートで2つ目のサーバーを起動してもエラーにならず、接続が両方に振り分けられます。
|
||||||
|
|
||||||
|
使用中のポートで`bind_to_port()`を失敗させたいときは、`set_socket_options()`でソケットオプションを差し替えてください。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
|
std::cerr << "port already in use" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_socket_options()`はデフォルトの設定を丸ごと置き換えます。Linux、macOSで`SO_REUSEADDR`を設定しているのは、再起動したサーバーがすぐにbindし直せるようにするためです。
|
||||||
|
|
||||||
|
> **Note:** Windowsでは`SO_REUSEADDR`だけでは足りません。お互いに`SO_REUSEADDR`を設定したソケット同士は、同じポートにbindできてしまいます。`SO_EXCLUSIVEADDRUSE`を使ってください。
|
||||||
|
|
||||||
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](../s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](../s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
||||||
@@ -107,6 +107,8 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ハンドラー内のチェックは、ハンドシェイクが完了した後に行われます。アップグレードする前に401などのHTTPステータスで接続を拒否したい場合は、`set_pre_request_handler()`を使ってください。WebSocketのルートでも呼ばれます。詳しくは[S11. Pre-request handlerでルート単位の認証を行う](../../cookbook/s11-pre-request)を参照してください。
|
||||||
|
|
||||||
## WSSで使う
|
## WSSで使う
|
||||||
|
|
||||||
HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。
|
HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.56.0"
|
#define CPPHTTPLIB_VERSION "0.58.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x003800"
|
#define CPPHTTPLIB_VERSION_NUM "0x003a00"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -939,6 +939,48 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolves a relative-path or query-only Location value against the path of
|
||||||
|
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
|
||||||
|
// references starting with '/' are returned unchanged.
|
||||||
|
inline std::string resolve_relative_location(const std::string &location,
|
||||||
|
const std::string &base) {
|
||||||
|
if (location.empty() || location[0] == '/') { return location; }
|
||||||
|
|
||||||
|
// A ':' in the first segment means the value has a scheme.
|
||||||
|
if (location.find(':') < location.find_first_of("/?#")) { return location; }
|
||||||
|
|
||||||
|
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; }
|
||||||
|
|
||||||
|
auto base_path = base.substr(0, base.find_first_of("?#"));
|
||||||
|
if (location[0] == '?') { return base_path + location; }
|
||||||
|
|
||||||
|
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
|
||||||
|
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
|
||||||
|
|
||||||
|
// Remove "." and ".." segments from the merged path.
|
||||||
|
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
|
||||||
|
std::string path;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < path_end) {
|
||||||
|
auto next = (std::min)(merged.find('/', i + 1), path_end);
|
||||||
|
auto segment = merged.substr(i + 1, next - i - 1);
|
||||||
|
auto is_last = next == path_end;
|
||||||
|
if (segment == "." || segment == "..") {
|
||||||
|
if (segment == "..") {
|
||||||
|
path.erase((std::min)(path.rfind('/'), path.size()));
|
||||||
|
}
|
||||||
|
if (is_last) { path += '/'; }
|
||||||
|
} else {
|
||||||
|
path += '/';
|
||||||
|
path += segment;
|
||||||
|
}
|
||||||
|
i = next;
|
||||||
|
}
|
||||||
|
if (path.empty()) { path = "/"; }
|
||||||
|
|
||||||
|
return path + merged.substr(path_end);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
enum class SSLVerifierResponse {
|
enum class SSLVerifierResponse {
|
||||||
@@ -1756,6 +1798,7 @@ struct Request {
|
|||||||
|
|
||||||
// private members...
|
// private members...
|
||||||
bool body_consumed_ = false;
|
bool body_consumed_ = false;
|
||||||
|
bool expect_100_continue_pending_ = false;
|
||||||
size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;
|
size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT;
|
||||||
size_t content_length_ = 0;
|
size_t content_length_ = 0;
|
||||||
ContentProvider content_provider_;
|
ContentProvider content_provider_;
|
||||||
@@ -1841,6 +1884,7 @@ struct Response {
|
|||||||
ContentProvider content_provider_;
|
ContentProvider content_provider_;
|
||||||
ContentProviderResourceReleaser content_provider_resource_releaser_;
|
ContentProviderResourceReleaser content_provider_resource_releaser_;
|
||||||
bool is_chunked_content_provider_ = false;
|
bool is_chunked_content_provider_ = false;
|
||||||
|
bool is_file_content_provider_ = false;
|
||||||
bool content_provider_success_ = false;
|
bool content_provider_success_ = false;
|
||||||
std::string file_content_path_;
|
std::string file_content_path_;
|
||||||
std::string file_content_content_type_;
|
std::string file_content_content_type_;
|
||||||
@@ -2985,7 +3029,7 @@ private:
|
|||||||
bool read_response_line(Stream &strm, const Request &req, Response &res,
|
bool read_response_line(Stream &strm, const Request &req, Response &res,
|
||||||
bool skip_100_continue = true) const;
|
bool skip_100_continue = true) const;
|
||||||
bool write_request(Stream &strm, Request &req, bool close_connection,
|
bool write_request(Stream &strm, Request &req, bool close_connection,
|
||||||
Error &error, bool skip_body = false);
|
Error &error, bool skip_body, bool &rejected_locally);
|
||||||
bool write_request_body(Stream &strm, Request &req, Error &error);
|
bool write_request_body(Stream &strm, Request &req, Error &error);
|
||||||
void prepare_default_headers(Request &r, bool for_stream,
|
void prepare_default_headers(Request &r, bool for_stream,
|
||||||
const std::string &ct);
|
const std::string &ct);
|
||||||
@@ -3913,6 +3957,7 @@ bool is_field_vchar(char c);
|
|||||||
bool is_field_content(const std::string &s);
|
bool is_field_content(const std::string &s);
|
||||||
bool is_field_value(const std::string &s);
|
bool is_field_value(const std::string &s);
|
||||||
bool is_field_valid(const std::string &name, const std::string &value);
|
bool is_field_valid(const std::string &name, const std::string &value);
|
||||||
|
bool is_request_target(const std::string &s);
|
||||||
|
|
||||||
} // namespace fields
|
} // namespace fields
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
@@ -4869,7 +4914,8 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
|||||||
msg.id = value;
|
msg.id = value;
|
||||||
} else if (field == "retry") {
|
} else if (field == "retry") {
|
||||||
// Parse retry interval in milliseconds
|
// Parse retry interval in milliseconds
|
||||||
{
|
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||||
|
if (detail::is_numeric(value)) {
|
||||||
int v = 0;
|
int v = 0;
|
||||||
auto res =
|
auto res =
|
||||||
detail::from_chars(value.data(), value.data() + value.size(), v);
|
detail::from_chars(value.data(), value.data() + value.size(), v);
|
||||||
@@ -5005,11 +5051,15 @@ inline bool SSEClient::should_reconnect(int count) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void SSEClient::wait_for_reconnect() {
|
inline void SSEClient::wait_for_reconnect() {
|
||||||
// Use small increments to check running_ flag frequently
|
// Use small increments to check running_ flag frequently.
|
||||||
|
// Always wait at least one increment, so that a zero interval (e.g.
|
||||||
|
// "retry: 0" from the server) cannot cause a busy reconnect loop.
|
||||||
|
const auto step_ms = 100;
|
||||||
|
auto interval_ms = (std::max)(reconnect_interval_ms_, step_ms);
|
||||||
auto waited = 0;
|
auto waited = 0;
|
||||||
while (running_.load() && waited < reconnect_interval_ms_) {
|
while (running_.load() && waited < interval_ms) {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(step_ms));
|
||||||
waited += 100;
|
waited += step_ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5777,15 +5827,15 @@ inline std::string encode_path(const std::string &s) {
|
|||||||
switch (s[i]) {
|
switch (s[i]) {
|
||||||
case ' ': result += "%20"; break;
|
case ' ': result += "%20"; break;
|
||||||
case '+': result += "%2B"; break;
|
case '+': result += "%2B"; break;
|
||||||
case '\r': result += "%0D"; break;
|
|
||||||
case '\n': result += "%0A"; break;
|
|
||||||
case '\'': result += "%27"; break;
|
case '\'': result += "%27"; break;
|
||||||
case ',': result += "%2C"; break;
|
case ',': result += "%2C"; break;
|
||||||
// case ':': result += "%3A"; break; // ok? probably...
|
// case ':': result += "%3A"; break; // ok? probably...
|
||||||
case ';': result += "%3B"; break;
|
case ';': result += "%3B"; break;
|
||||||
default:
|
default:
|
||||||
auto c = static_cast<uint8_t>(s[i]);
|
auto c = static_cast<uint8_t>(s[i]);
|
||||||
if (c >= 0x80) {
|
// Control characters (incl. CR/LF) and non-ASCII bytes are not allowed
|
||||||
|
// in a request-target as-is.
|
||||||
|
if (c < 0x20 || c == 0x7f || c >= 0x80) {
|
||||||
result += '%';
|
result += '%';
|
||||||
char hex[4];
|
char hex[4];
|
||||||
auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
|
auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
|
||||||
@@ -5910,6 +5960,13 @@ inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
|
|||||||
// to look up.
|
// to look up.
|
||||||
split(trailer_header.data(), trailer_header.data() + trailer_header.size(),
|
split(trailer_header.data(), trailer_header.data() + trailer_header.size(),
|
||||||
',', [&](const char *b, const char *e) {
|
',', [&](const char *b, const char *e) {
|
||||||
|
// A legitimate message declares only a handful of trailers. Cap the
|
||||||
|
// set so a peer cannot grow it without bound: an oversized set only
|
||||||
|
// arises from an attempt to force many colliding names into
|
||||||
|
// quadratic lookups (case_ignore::hash is unkeyed).
|
||||||
|
if (declared_trailers.size() >= CPPHTTPLIB_HEADER_MAX_COUNT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
std::string key(b, e);
|
std::string key(b, e);
|
||||||
if (prohibited_trailers.find(key) == prohibited_trailers.end()) {
|
if (prohibited_trailers.find(key) == prohibited_trailers.end()) {
|
||||||
declared_trailers.insert(key);
|
declared_trailers.insert(key);
|
||||||
@@ -5920,6 +5977,8 @@ inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
|
|||||||
size_t trailer_header_count = 0;
|
size_t trailer_header_count = 0;
|
||||||
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
|
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
|
||||||
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
|
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
|
||||||
|
// Count every received trailer field, not only the declared ones stored in
|
||||||
|
// dest, so undeclared fields cannot keep this loop running past the limit.
|
||||||
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
|
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
|
||||||
|
|
||||||
constexpr auto line_terminator_len = 2;
|
constexpr auto line_terminator_len = 2;
|
||||||
@@ -5932,12 +5991,13 @@ inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
|
|||||||
if (declared_trailers.find(key) !=
|
if (declared_trailers.find(key) !=
|
||||||
declared_trailers.end()) {
|
declared_trailers.end()) {
|
||||||
dest.emplace(key, val);
|
dest.emplace(key, val);
|
||||||
trailer_header_count++;
|
|
||||||
}
|
}
|
||||||
})) {
|
})) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trailer_header_count++;
|
||||||
|
|
||||||
if (!line_reader.getline()) { return false; }
|
if (!line_reader.getline()) { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7569,7 +7629,8 @@ inline bool parse_quality(const char *b, const char *e, std::string &token,
|
|||||||
|
|
||||||
double v = 0.0;
|
double v = 0.0;
|
||||||
auto res = from_chars(pb + r.first, pb + r.second, v);
|
auto res = from_chars(pb + r.first, pb + r.second, v);
|
||||||
if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
|
if (res.ec != std::errc{} || res.ptr != pb + r.second ||
|
||||||
|
v < 0.0 || v > 1.0) {
|
||||||
invalid = true;
|
invalid = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -8506,12 +8567,11 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
|||||||
|
|
||||||
inline ssize_t write_request_line(Stream &strm, const std::string &method,
|
inline ssize_t write_request_line(Stream &strm, const std::string &method,
|
||||||
const std::string &path) {
|
const std::string &path) {
|
||||||
// A request target must not carry CR/LF (or other control octets); otherwise
|
// Neither the method nor the request target may carry CR/LF, SP or other
|
||||||
// a value smuggled into it splits the request line and injects headers or a
|
// control octets; otherwise a value smuggled into either splits the request
|
||||||
// whole request. The same field-value check already guards header values in
|
// line and injects headers or a whole request.
|
||||||
// check_and_write_headers and the request target in
|
if (!fields::is_token(method)) { return -1; }
|
||||||
// perform_websocket_handshake; apply it here too.
|
if (!fields::is_request_target(path)) { return -1; }
|
||||||
if (!fields::is_field_value(path)) { return -1; }
|
|
||||||
|
|
||||||
std::string s = method;
|
std::string s = method;
|
||||||
s += ' ';
|
s += ' ';
|
||||||
@@ -8733,9 +8793,9 @@ inline bool compress_content_provider(const ContentProvider &content_provider,
|
|||||||
return cmp.compress(nullptr, 0, true, append);
|
return cmp.compress(nullptr, 0, true, append);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serves `m` as the response body. `set_content_provider()` clears the coding,
|
// Serves `m` as the response body. `set_content_provider()` clears the coding
|
||||||
// so recording it has to come after; keeping both here means a third
|
// and the file flag, so recording them has to come after; keeping all of it
|
||||||
// file-serving path cannot get that order wrong.
|
// here means a third file-serving path cannot get that order wrong.
|
||||||
inline void set_file_content_provider(Response &res,
|
inline void set_file_content_provider(Response &res,
|
||||||
const std::shared_ptr<mmap> &m,
|
const std::shared_ptr<mmap> &m,
|
||||||
const std::string &content_type,
|
const std::string &content_type,
|
||||||
@@ -8747,6 +8807,7 @@ inline void set_file_content_provider(Response &res,
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
res.is_file_content_provider_ = true;
|
||||||
res.content_coding_ = encoding;
|
res.content_coding_ = encoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10164,6 +10225,12 @@ inline bool is_field_valid(const std::string &name, const std::string &value) {
|
|||||||
return is_field_name(name) && is_field_value(value);
|
return is_field_name(name) && is_field_value(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 9112 §2.2/§3.2: the request-target has no SP, HTAB or other control
|
||||||
|
// characters (incl. bare CR). obs-text (raw UTF-8) is allowed.
|
||||||
|
inline bool is_request_target(const std::string &s) {
|
||||||
|
return std::all_of(s.begin(), s.end(), is_field_vchar);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fields
|
} // namespace fields
|
||||||
|
|
||||||
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||||
@@ -10631,10 +10698,9 @@ inline bool match_hostname(const std::string &pattern,
|
|||||||
// Verify certificate using Windows CertGetCertificateChain API.
|
// Verify certificate using Windows CertGetCertificateChain API.
|
||||||
// This provides real-time certificate validation with Windows Update
|
// This provides real-time certificate validation with Windows Update
|
||||||
// integration, independent of the TLS backend (OpenSSL or MbedTLS).
|
// integration, independent of the TLS backend (OpenSSL or MbedTLS).
|
||||||
inline bool
|
inline bool verify_cert_with_windows_schannel(
|
||||||
verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
const std::vector<unsigned char> &der_cert, const std::string &hostname,
|
||||||
const std::string &hostname,
|
bool verify_hostname, uint64_t &out_error, tls::const_session_t session) {
|
||||||
bool verify_hostname, uint64_t &out_error) {
|
|
||||||
if (der_cert.empty()) { return false; }
|
if (der_cert.empty()) { return false; }
|
||||||
|
|
||||||
out_error = 0;
|
out_error = 0;
|
||||||
@@ -10652,6 +10718,24 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
|||||||
auto cert_guard =
|
auto cert_guard =
|
||||||
scope_exit([&] { CertFreeCertificateContext(cert_context); });
|
scope_exit([&] { CertFreeCertificateContext(cert_context); });
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||||
|
// Add the intermediates the server sent. Without them CryptoAPI follows the
|
||||||
|
// leaf's AIA URL, which may lead to an issuer under an untrusted root.
|
||||||
|
auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr);
|
||||||
|
auto store_guard = scope_exit([&] { CertCloseStore(store, 0); });
|
||||||
|
auto sk = SSL_get_peer_cert_chain(static_cast<const SSL *>(session));
|
||||||
|
for (int i = 1; sk && i < sk_X509_num(sk); i++) {
|
||||||
|
std::vector<unsigned char> der;
|
||||||
|
tls::get_cert_der(sk_X509_value(sk, i), der);
|
||||||
|
CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(),
|
||||||
|
static_cast<DWORD>(der.size()),
|
||||||
|
CERT_STORE_ADD_USE_EXISTING, nullptr);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)session;
|
||||||
|
auto store = cert_context->hCertStore;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Setup chain parameters
|
// Setup chain parameters
|
||||||
CERT_CHAIN_PARA chain_para = {};
|
CERT_CHAIN_PARA chain_para = {};
|
||||||
chain_para.cbSize = sizeof(chain_para);
|
chain_para.cbSize = sizeof(chain_para);
|
||||||
@@ -10659,7 +10743,7 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
|||||||
// Build certificate chain with revocation checking
|
// Build certificate chain with revocation checking
|
||||||
PCCERT_CHAIN_CONTEXT chain_context = nullptr;
|
PCCERT_CHAIN_CONTEXT chain_context = nullptr;
|
||||||
auto chain_result = CertGetCertificateChain(
|
auto chain_result = CertGetCertificateChain(
|
||||||
nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para,
|
nullptr, cert_context, nullptr, store, &chain_para,
|
||||||
CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
|
CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
|
||||||
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
|
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
|
||||||
nullptr, &chain_context);
|
nullptr, &chain_context);
|
||||||
@@ -10882,8 +10966,8 @@ inline bool setup_client_tls_session(
|
|||||||
if (get_cert_der(server_cert, der)) {
|
if (get_cert_der(server_cert, der)) {
|
||||||
uint64_t wincrypt_error = 0;
|
uint64_t wincrypt_error = 0;
|
||||||
if (!verify_cert_with_windows_schannel(
|
if (!verify_cert_with_windows_schannel(
|
||||||
der, host, options.server_hostname_verification,
|
der, host, options.server_hostname_verification, wincrypt_error,
|
||||||
wincrypt_error)) {
|
session)) {
|
||||||
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11647,6 +11731,7 @@ inline void Response::set_content_provider(
|
|||||||
if (in_length > 0) { content_provider_ = std::move(provider); }
|
if (in_length > 0) { content_provider_ = std::move(provider); }
|
||||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||||
is_chunked_content_provider_ = false;
|
is_chunked_content_provider_ = false;
|
||||||
|
is_file_content_provider_ = false;
|
||||||
content_coding_ = detail::EncodingType::None;
|
content_coding_ = detail::EncodingType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11658,6 +11743,7 @@ inline void Response::set_content_provider(
|
|||||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||||
is_chunked_content_provider_ = false;
|
is_chunked_content_provider_ = false;
|
||||||
|
is_file_content_provider_ = false;
|
||||||
content_coding_ = detail::EncodingType::None;
|
content_coding_ = detail::EncodingType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11669,6 +11755,7 @@ inline void Response::set_chunked_content_provider(
|
|||||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||||
is_chunked_content_provider_ = true;
|
is_chunked_content_provider_ = true;
|
||||||
|
is_file_content_provider_ = false;
|
||||||
content_coding_ = detail::EncodingType::None;
|
content_coding_ = detail::EncodingType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13235,6 +13322,8 @@ inline bool Server::parse_request_line(const char *s, Request &req) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!detail::fields::is_request_target(req.target)) { return false; }
|
||||||
|
|
||||||
{
|
{
|
||||||
// Skip URL fragment
|
// Skip URL fragment
|
||||||
for (size_t i = 0; i < req.target.size(); i++) {
|
for (size_t i = 0; i < req.target.size(); i++) {
|
||||||
@@ -13288,7 +13377,10 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
// Prepare additional headers
|
// Prepare additional headers
|
||||||
if (close_connection ||
|
if (close_connection ||
|
||||||
detail::has_header_token(req.headers, "Connection", "close") ||
|
detail::has_header_token(req.headers, "Connection", "close") ||
|
||||||
400 <= res.status) { // Don't leave connections open after errors
|
400 <= res.status || // Don't leave connections open after errors
|
||||||
|
// The client withholds the body until `100 Continue`, which was never
|
||||||
|
// sent, so whether and when the body follows is unknown.
|
||||||
|
(req.expect_100_continue_pending_ && detail::has_framed_body(req))) {
|
||||||
res.set_header("Connection", "close");
|
res.set_header("Connection", "close");
|
||||||
} else {
|
} else {
|
||||||
std::string s = "timeout=";
|
std::string s = "timeout=";
|
||||||
@@ -13319,9 +13411,27 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
||||||
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
|
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
|
||||||
|
|
||||||
// Combine small body with headers to reduce write syscalls
|
// Combine a small body with the headers so the whole response leaves in a
|
||||||
if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) {
|
// single write. A large body is written on its own instead: a copy of it
|
||||||
bstrm.write(res.body.data(), res.body.size());
|
// costs more than the extra write saves.
|
||||||
|
auto send_body = req.method != "HEAD";
|
||||||
|
auto body_is_separate = false;
|
||||||
|
auto provider_done = false;
|
||||||
|
if (send_body && !res.body.empty() && !res.content_provider_) {
|
||||||
|
if (res.body.size() < CPPHTTPLIB_SEND_BUFSIZ) {
|
||||||
|
bstrm.write(res.body.data(), res.body.size());
|
||||||
|
} else {
|
||||||
|
body_is_separate = true;
|
||||||
|
}
|
||||||
|
} else if (send_body && res.content_provider_ &&
|
||||||
|
res.is_file_content_provider_ &&
|
||||||
|
res.content_length_ < CPPHTTPLIB_SEND_BUFSIZ) {
|
||||||
|
// A small file is read into the same buffer. Other providers may produce
|
||||||
|
// their data over time, so they are never held back.
|
||||||
|
if (!write_content_with_provider(bstrm, req, res, boundary, content_type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
provider_done = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log before writing to avoid race condition with client-side code that
|
// Log before writing to avoid race condition with client-side code that
|
||||||
@@ -13332,17 +13442,20 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
auto &data = bstrm.get_buffer();
|
auto &data = bstrm.get_buffer();
|
||||||
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
|
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
|
||||||
|
|
||||||
// Streaming body
|
if (body_is_separate) {
|
||||||
auto ret = true;
|
return detail::write_data(strm, res.body.data(), res.body.size());
|
||||||
if (req.method != "HEAD" && res.content_provider_) {
|
|
||||||
if (write_content_with_provider(strm, req, res, boundary, content_type)) {
|
|
||||||
res.content_provider_success_ = true;
|
|
||||||
} else {
|
|
||||||
ret = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
// Streaming body
|
||||||
|
if (send_body && res.content_provider_) {
|
||||||
|
if (!provider_done &&
|
||||||
|
!write_content_with_provider(strm, req, res, boundary, content_type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
res.content_provider_success_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool
|
inline bool
|
||||||
@@ -13539,6 +13652,13 @@ inline bool Server::read_content_core(
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// The client is waiting for this before it sends the body.
|
||||||
|
if (req.expect_100_continue_pending_) {
|
||||||
|
req.expect_100_continue_pending_ = false;
|
||||||
|
detail::write_response_line(strm, StatusCode::Continue_100);
|
||||||
|
strm.write("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,
|
if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr,
|
||||||
out, true)) {
|
out, true)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -14277,8 +14397,21 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
res.version = "HTTP/1.1";
|
res.version = "HTTP/1.1";
|
||||||
res.headers = default_headers_;
|
res.headers = default_headers_;
|
||||||
|
|
||||||
// Request line and headers
|
// RFC 9112 §9.6: a server that sends the "close" connection option must
|
||||||
|
// close the connection after that response, whichever path wrote it (an
|
||||||
|
// error status, a handler, or a rejected request). Reading on would also
|
||||||
|
// parse whatever the client sent next on a connection it considers done.
|
||||||
|
auto honor_connection_close = detail::scope_exit([&] {
|
||||||
|
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
||||||
|
connection_closed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request line and headers. A rejected message leaves the rest of it (and
|
||||||
|
// any body) unread, so the connection cannot be reused: the leftover bytes
|
||||||
|
// would be parsed as the next request.
|
||||||
if (!parse_request_line(line_reader.ptr(), req)) {
|
if (!parse_request_line(line_reader.ptr(), req)) {
|
||||||
|
connection_closed = true;
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
output_error_log(Error::InvalidRequestLine, &req);
|
output_error_log(Error::InvalidRequestLine, &req);
|
||||||
return write_response(strm, close_connection, req, res);
|
return write_response(strm, close_connection, req, res);
|
||||||
@@ -14286,20 +14419,28 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
|
|
||||||
// Request headers
|
// Request headers
|
||||||
if (!detail::read_headers(strm, req.headers)) {
|
if (!detail::read_headers(strm, req.headers)) {
|
||||||
|
connection_closed = true;
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
output_error_log(Error::InvalidHeaders, &req);
|
output_error_log(Error::InvalidHeaders, &req);
|
||||||
return write_response(strm, close_connection, req, res);
|
return write_response(strm, close_connection, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
// RFC 9112 §6.3: Reject requests whose framing is invalid or ambiguous,
|
||||||
// otherwise let an intermediary and this parser disagree on where the body
|
// which would otherwise let an intermediary and this parser disagree on
|
||||||
// ends and enable request smuggling. Two cases: a non-zero Content-Length
|
// where the body ends and enable request smuggling. Three cases: a
|
||||||
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
// Content-Length that is not a valid decimal length (e.g. "42, 42", "+42"
|
||||||
// compatibility with existing clients), and a Transfer-Encoding whose final
|
// or empty), which would otherwise be read as "no body"; a non-zero
|
||||||
// coding is not chunked, which leaves the body length undeterminable. The
|
// Content-Length alongside any Transfer-Encoding (Content-Length: 0 is
|
||||||
// latter must not fall through to the "no body" path, or the body bytes are
|
// tolerated for compatibility with existing clients); and a
|
||||||
// parsed as the next request on a persistent connection.
|
// Transfer-Encoding whose final coding is not chunked, which leaves the body
|
||||||
if (detail::has_conflicting_content_length(req.headers) ||
|
// length undeterminable. None of them may fall through to the "no body"
|
||||||
|
// path, or the body bytes are parsed as the next request on a persistent
|
||||||
|
// connection.
|
||||||
|
auto is_invalid_content_length = false;
|
||||||
|
detail::get_header_value_u64(req.headers, "Content-Length", 0, 0,
|
||||||
|
is_invalid_content_length);
|
||||||
|
if (is_invalid_content_length ||
|
||||||
|
detail::has_conflicting_content_length(req.headers) ||
|
||||||
(req.has_header("Transfer-Encoding") &&
|
(req.has_header("Transfer-Encoding") &&
|
||||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||||
connection_closed = true;
|
connection_closed = true;
|
||||||
@@ -14376,19 +14517,20 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
// case-insensitive, and a 100-continue expectation in an HTTP/1.0 request
|
// case-insensitive, and a 100-continue expectation in an HTTP/1.0 request
|
||||||
// must be ignored. An expectation we do not recognize is left alone; the
|
// must be ignored. An expectation we do not recognize is left alone; the
|
||||||
// 417 the section allows for one is a MAY, not a requirement.
|
// 417 the section allows for one is a MAY, not a requirement.
|
||||||
|
//
|
||||||
|
// `100 Continue` itself is deferred until the body is actually read (see
|
||||||
|
// read_content_core), so a request rejected by a later handler never
|
||||||
|
// invites the client to send a body nobody will read.
|
||||||
if (req.version != "HTTP/1.0" &&
|
if (req.version != "HTTP/1.0" &&
|
||||||
detail::has_header_token(req.headers, "Expect", "100-continue")) {
|
detail::has_header_token(req.headers, "Expect", "100-continue")) {
|
||||||
int status = StatusCode::Continue_100;
|
int status = StatusCode::Continue_100;
|
||||||
if (expect_100_continue_handler_) {
|
if (expect_100_continue_handler_) {
|
||||||
status = expect_100_continue_handler_(req, res);
|
status = expect_100_continue_handler_(req, res);
|
||||||
}
|
}
|
||||||
switch (status) {
|
if (status == StatusCode::Continue_100) {
|
||||||
case StatusCode::Continue_100:
|
req.expect_100_continue_pending_ = true;
|
||||||
case StatusCode::ExpectationFailed_417:
|
} else {
|
||||||
detail::write_response_line(strm, status);
|
if (res.status == -1) { res.status = status; }
|
||||||
strm.write("\r\n");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
connection_closed = true;
|
connection_closed = true;
|
||||||
return write_response(strm, true, req, res);
|
return write_response(strm, true, req, res);
|
||||||
}
|
}
|
||||||
@@ -14401,18 +14543,25 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
};
|
};
|
||||||
|
|
||||||
// WebSocket upgrade
|
// WebSocket upgrade
|
||||||
// Check pre_routing_handler_ before upgrading so that authentication
|
// Run pre_routing_handler_ and pre_request_handler_ before upgrading so
|
||||||
// and other middleware can reject the request with an HTTP response
|
// that authentication and other middleware can reject the request with an
|
||||||
// (e.g., 401) before the protocol switches.
|
// HTTP response (e.g., 401) before the protocol switches.
|
||||||
if (detail::is_websocket_upgrade(req)) {
|
if (detail::is_websocket_upgrade(req)) {
|
||||||
if (pre_routing_handler_ &&
|
if (pre_routing_handler_ &&
|
||||||
pre_routing_handler_(req, res) == HandlerResponse::Handled) {
|
pre_routing_handler_(req, res) == HandlerResponse::Handled) {
|
||||||
if (res.status == -1) { res.status = StatusCode::OK_200; }
|
if (res.status == -1) { res.status = StatusCode::OK_200; }
|
||||||
return write_response(strm, close_connection, req, res);
|
return write_response_with_content(strm, close_connection, req, res);
|
||||||
}
|
}
|
||||||
// Find matching WebSocket handler
|
// Find matching WebSocket handler
|
||||||
for (const auto &entry : websocket_handlers_) {
|
for (const auto &entry : websocket_handlers_) {
|
||||||
if (entry.matcher->match(req)) {
|
if (entry.matcher->match(req)) {
|
||||||
|
req.matched_route = entry.matcher->pattern();
|
||||||
|
if (pre_request_handler_ &&
|
||||||
|
pre_request_handler_(req, res) == HandlerResponse::Handled) {
|
||||||
|
if (res.status == -1) { res.status = StatusCode::OK_200; }
|
||||||
|
return write_response_with_content(strm, close_connection, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
// Compute accept key
|
// Compute accept key
|
||||||
auto client_key = req.get_header_value("Sec-WebSocket-Key");
|
auto client_key = req.get_header_value("Sec-WebSocket-Key");
|
||||||
auto accept_key = detail::websocket_accept_key(client_key);
|
auto accept_key = detail::websocket_accept_key(client_key);
|
||||||
@@ -14564,17 +14713,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
// keep-alive. Without framing there is no body to drain — reading would
|
// keep-alive. Without framing there is no body to drain — reading would
|
||||||
// consume the next request (issue #2450). If the response has committed the
|
// consume the next request (issue #2450). If the response has committed the
|
||||||
// connection to close, there is no next request to protect.
|
// connection to close, there is no next request to protect.
|
||||||
if (!req.body_consumed_ && detail::has_framed_body(req)) {
|
if (!req.body_consumed_ && detail::has_framed_body(req) &&
|
||||||
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
!detail::has_header_token(res.headers, "Connection", "close")) {
|
||||||
|
int dummy_status;
|
||||||
|
if (!detail::read_content(
|
||||||
|
strm, req, payload_max_length_, dummy_status, nullptr,
|
||||||
|
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
||||||
connection_closed = true;
|
connection_closed = true;
|
||||||
} else {
|
|
||||||
int dummy_status;
|
|
||||||
if (!detail::read_content(
|
|
||||||
strm, req, payload_max_length_, dummy_status, nullptr,
|
|
||||||
[](const char *, size_t, size_t, size_t) { return true; },
|
|
||||||
false)) {
|
|
||||||
connection_closed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15088,16 +15233,30 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
|||||||
prepare_default_headers(req, true, content_type);
|
prepare_default_headers(req, true, content_type);
|
||||||
|
|
||||||
auto &strm = *handle.stream_;
|
auto &strm = *handle.stream_;
|
||||||
if (detail::write_request_line(strm, req.method, req.path) < 0) {
|
|
||||||
handle.error = Error::Write;
|
|
||||||
handle.response.reset();
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detail::check_and_write_headers(strm, req.headers, header_writer_,
|
// Build the request line and headers in memory first, like write_request()
|
||||||
handle.error)) {
|
// does, so that a rejected header leaves nothing on the wire.
|
||||||
handle.response.reset();
|
{
|
||||||
return handle;
|
detail::BufferStream bstrm;
|
||||||
|
|
||||||
|
if (detail::write_request_line(bstrm, req.method, req.path) < 0) {
|
||||||
|
handle.error = Error::Write;
|
||||||
|
handle.response.reset();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||||
|
handle.error)) {
|
||||||
|
handle.response.reset();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &data = bstrm.get_buffer();
|
||||||
|
if (!detail::write_data(strm, data.data(), data.size())) {
|
||||||
|
handle.error = Error::Write;
|
||||||
|
handle.response.reset();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.empty()) {
|
if (!body.empty()) {
|
||||||
@@ -15272,22 +15431,45 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
|
|||||||
stream_line_reader lr(strm, line_buf, sizeof(line_buf));
|
stream_line_reader lr(strm, line_buf, sizeof(line_buf));
|
||||||
if (!lr.getline()) { return -1; }
|
if (!lr.getline()) { return -1; }
|
||||||
|
|
||||||
|
// Everything below is bounded by eol rather than by the buffer's NUL, so
|
||||||
|
// the line terminator is never mistaken for line content.
|
||||||
|
const char *eol = lr.ptr() + lr.size();
|
||||||
|
if (lr.end_with_crlf()) {
|
||||||
|
eol -= 2;
|
||||||
|
} else if (eol != lr.ptr() && eol[-1] == '\n') {
|
||||||
|
// Only reachable under CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR, where
|
||||||
|
// getline() ends the line on a bare LF. That LF is the terminator, so it
|
||||||
|
// has to come off here or the check below would reject the line.
|
||||||
|
eol -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
// RFC 9112 §7.1: chunk-size = 1*HEXDIG
|
// RFC 9112 §7.1: chunk-size = 1*HEXDIG
|
||||||
const char *p = lr.ptr();
|
const char *p = lr.ptr();
|
||||||
int v = 0;
|
int v = 0;
|
||||||
if (!is_hex(*p, v)) { return -1; }
|
if (p == eol || !is_hex(*p, v)) { return -1; }
|
||||||
|
|
||||||
size_t chunk_len = 0;
|
size_t chunk_len = 0;
|
||||||
constexpr size_t chunk_len_max = (std::numeric_limits<size_t>::max)();
|
constexpr size_t chunk_len_max = (std::numeric_limits<size_t>::max)();
|
||||||
for (; is_hex(*p, v); ++p) {
|
for (; p < eol && is_hex(*p, v); ++p) {
|
||||||
if (chunk_len > (chunk_len_max >> 4)) { return -1; }
|
if (chunk_len > (chunk_len_max >> 4)) { return -1; }
|
||||||
chunk_len = (chunk_len << 4) | static_cast<size_t>(v);
|
chunk_len = (chunk_len << 4) | static_cast<size_t>(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (is_space_or_tab(*p)) {
|
while (p < eol && is_space_or_tab(*p)) {
|
||||||
++p;
|
++p;
|
||||||
}
|
}
|
||||||
if (*p != '\0' && *p != ';' && *p != '\r' && *p != '\n') { return -1; }
|
|
||||||
|
// RFC 9112 §7.1.1: only a chunk-ext may sit between the size and the line
|
||||||
|
// terminator, and it is built from tokens and quoted-strings, so it never
|
||||||
|
// holds a CR, LF or any other control character. getline() reads up to the
|
||||||
|
// CRLF, so a bare LF left in here would be swallowed as extension text
|
||||||
|
// while an intermediary that ends the line on it delimits the chunks
|
||||||
|
// differently, and the two disagree on where the body ends (request
|
||||||
|
// smuggling).
|
||||||
|
if (p < eol && *p != ';') { return -1; }
|
||||||
|
for (; p < eol; ++p) {
|
||||||
|
if (!is_space_or_tab(*p) && !fields::is_field_vchar(*p)) { return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
if (chunk_len == 0) {
|
if (chunk_len == 0) {
|
||||||
chunk_remaining = 0;
|
chunk_remaining = 0;
|
||||||
@@ -15438,7 +15620,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
|||||||
if (location.empty()) { return false; }
|
if (location.empty()) { return false; }
|
||||||
|
|
||||||
detail::UrlComponents uc;
|
detail::UrlComponents uc;
|
||||||
if (!detail::parse_url(location, uc)) { return false; }
|
if (!detail::parse_url(detail::resolve_relative_location(location, req.path),
|
||||||
|
uc)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Only follow http/https redirects
|
// Only follow http/https redirects
|
||||||
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
||||||
@@ -15627,7 +15812,9 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
|
|||||||
|
|
||||||
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||||
bool close_connection, Error &error,
|
bool close_connection, Error &error,
|
||||||
bool skip_body) {
|
bool skip_body, bool &rejected_locally) {
|
||||||
|
rejected_locally = false;
|
||||||
|
|
||||||
// Prepare additional headers
|
// Prepare additional headers
|
||||||
if (close_connection) {
|
if (close_connection) {
|
||||||
if (!req.has_header("Connection")) {
|
if (!req.has_header("Connection")) {
|
||||||
@@ -15716,15 +15903,18 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
|
|
||||||
// Write request line and headers
|
// Write request line and headers
|
||||||
if (detail::write_request_line(bstrm, req.method, path_with_query) < 0) {
|
if (detail::write_request_line(bstrm, req.method, path_with_query) < 0) {
|
||||||
// A rejected target (e.g. CR/LF smuggled in via a decoded redirect
|
// A rejected method (not a token, e.g. carrying CR/LF) or target (e.g.
|
||||||
// Location under set_path_encode(false)) must fail the request cleanly
|
// CR/LF smuggled in via a decoded redirect Location under
|
||||||
// instead of emitting a request-line-less, header-injecting request.
|
// set_path_encode(false)) must fail the request cleanly instead of
|
||||||
|
// emitting a request-line-less, header-injecting request.
|
||||||
error = Error::Write;
|
error = Error::Write;
|
||||||
|
rejected_locally = true;
|
||||||
output_error_log(error, &req);
|
output_error_log(error, &req);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||||
error)) {
|
error)) {
|
||||||
|
rejected_locally = true;
|
||||||
output_error_log(error, &req);
|
output_error_log(error, &req);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -15993,8 +16183,16 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
|||||||
detail::has_header_token(req.headers, "Expect", "100-continue");
|
detail::has_header_token(req.headers, "Expect", "100-continue");
|
||||||
|
|
||||||
// Send request (skip body if using Expect: 100-continue)
|
// Send request (skip body if using Expect: 100-continue)
|
||||||
|
auto rejected_locally = false;
|
||||||
auto write_request_success =
|
auto write_request_success =
|
||||||
write_request(strm, req, close_connection, error, expect_100_continue);
|
write_request(strm, req, close_connection, error, expect_100_continue,
|
||||||
|
rejected_locally);
|
||||||
|
|
||||||
|
// A failed write normally still reads the response below, since the server
|
||||||
|
// may have answered early (e.g. 413/414) and closed while the body was being
|
||||||
|
// sent. A request rejected before any byte reached the socket gets no such
|
||||||
|
// response, and waiting for one would block until the read timeout.
|
||||||
|
if (rejected_locally) { return false; }
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
if (is_ssl() && !expect_100_continue) {
|
if (is_ssl() && !expect_100_continue) {
|
||||||
@@ -19312,11 +19510,11 @@ inline void shutdown(session_t session, bool graceful) {
|
|||||||
|
|
||||||
auto ssl = static_cast<SSL *>(session);
|
auto ssl = static_cast<SSL *>(session);
|
||||||
if (graceful) {
|
if (graceful) {
|
||||||
// First call sends close_notify
|
// Send close_notify without waiting for the peer's. The connection is
|
||||||
if (SSL_shutdown(ssl) == 0) {
|
// closed right after this, so a unidirectional shutdown is enough, and an
|
||||||
// Second call waits for peer's close_notify
|
// idle peer that never answers would otherwise hold this thread until the
|
||||||
SSL_shutdown(ssl);
|
// read timeout. The other backends do not wait either.
|
||||||
}
|
SSL_shutdown(ssl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -164,14 +164,18 @@ if [ "$DRY_RUN" -eq 1 ]; then
|
|||||||
echo "==> Dry run complete. No changes were made."
|
echo "==> Dry run complete. No changes were made."
|
||||||
else
|
else
|
||||||
echo "==> Updating httplib.h..."
|
echo "==> Updating httplib.h..."
|
||||||
sed -i '' "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
# `-i.bak` is the in-place form GNU and BSD sed both accept (`-i ''` is
|
||||||
sed -i '' "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
# BSD-only: GNU sed reads the '' as the script).
|
||||||
|
sed -i.bak "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
||||||
|
sed -i.bak "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
||||||
|
rm -f httplib.h.bak
|
||||||
echo " CPPHTTPLIB_VERSION = \"$NEW_VERSION\""
|
echo " CPPHTTPLIB_VERSION = \"$NEW_VERSION\""
|
||||||
echo " CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
|
echo " CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Updating docs-src/config.toml..."
|
echo "==> Updating docs-src/config.toml..."
|
||||||
sed -i '' "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
|
sed -i.bak "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
|
||||||
|
rm -f docs-src/config.toml.bak
|
||||||
echo " version = \"$NEW_VERSION\""
|
echo " version = \"$NEW_VERSION\""
|
||||||
|
|
||||||
# --- Step 6: Commit, tag, and push ---
|
# --- Step 6: Commit, tag, and push ---
|
||||||
|
|||||||
+944
-45
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user