mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89c932f313 | |||
| eb5a65e0df | |||
| 7a3b92bbd9 | |||
| eb11032797 | |||
| 54e75dc8ef | |||
| b20b5fdd1f | |||
| f4cc542d4b | |||
| 4285d33992 | |||
| 92b4f53012 | |||
| b8e21eac89 | |||
| 3fae5f1473 | |||
| fe7fe15d2e | |||
| fbd6ce7a3f | |||
| dffce89514 | |||
| 3f44c80fd3 | |||
| a2bb6f6c1e | |||
| 7012e765e1 | |||
| b1c1fa2dc6 | |||
| fbee136dca | |||
| 70cca55caf | |||
| cdaed14925 | |||
| b52d7d8411 | |||
| acf28a362d | |||
| 0b3758ec36 | |||
| a5d4c143e5 | |||
| c0c36f021d | |||
| 8e8a23e3d2 | |||
| 890a2dd85d | |||
| ca5fe354fb | |||
| dd98d2a24d | |||
| 1f110b54d8 | |||
| 7b6867bcdf | |||
| 8b28577ec6 | |||
| 55b38907dc | |||
| 53ea9e8bb4 |
@@ -0,0 +1,51 @@
|
||||
name: Release Docker Image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history and tags
|
||||
|
||||
- name: Extract tag (manual)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
id: set_tag_manual
|
||||
run: |
|
||||
# Checkout the latest tag and set output
|
||||
git fetch --tags
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0)
|
||||
git checkout $LATEST_TAG
|
||||
echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract tag (release)
|
||||
if: github.event_name == 'release'
|
||||
id: set_tag_release
|
||||
run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64 # Build for both amd64 and arm64
|
||||
# Use extracted tag without leading 'v'
|
||||
tags: |
|
||||
yhirose4dockerhub/cpp-httplib-server:latest
|
||||
yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag_manual.outputs.tag || steps.set_tag_release.outputs.tag }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
run: vcpkg install gtest curl zlib brotli zstd
|
||||
- name: Install OpenSSL
|
||||
if: ${{ matrix.config.with_ssl }}
|
||||
run: choco install openssl
|
||||
run: choco install openssl --version 3.5.2 # workaround for chocolatey issue with the latest OpenSSL
|
||||
- name: Configure CMake ${{ matrix.config.name }}
|
||||
run: >
|
||||
cmake -B build -S .
|
||||
|
||||
+12
-5
@@ -111,12 +111,19 @@ option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build.
|
||||
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
|
||||
# Defaults to static library
|
||||
option(BUILD_SHARED_LIBS "Build the library as a shared library instead of static. Has no effect if using header-only." OFF)
|
||||
if (BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
|
||||
if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
|
||||
# Necessary for Windows if building shared libs
|
||||
# See https://stackoverflow.com/a/40743080
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
|
||||
message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
|
||||
endif()
|
||||
if(CMAKE_SIZEOF_VOID_P LESS 8)
|
||||
message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.")
|
||||
endif()
|
||||
|
||||
# Set some variables that are used in-tree and while building based on our options
|
||||
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
|
||||
set(HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN ${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN})
|
||||
@@ -243,8 +250,8 @@ add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
|
||||
target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
|
||||
|
||||
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
)
|
||||
|
||||
# Always require threads
|
||||
@@ -340,6 +347,6 @@ if(HTTPLIB_INSTALL)
|
||||
endif()
|
||||
|
||||
if(HTTPLIB_TEST)
|
||||
include(CTest)
|
||||
add_subdirectory(test)
|
||||
include(CTest)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
+3
-1
@@ -8,4 +8,6 @@ FROM scratch
|
||||
COPY --from=builder /build/server /server
|
||||
COPY docker/html/index.html /html/index.html
|
||||
EXPOSE 80
|
||||
CMD ["/server"]
|
||||
|
||||
ENTRYPOINT ["/server"]
|
||||
CMD ["--host", "0.0.0.0", "--port", "80", "--mount", "/:./html"]
|
||||
|
||||
@@ -98,32 +98,31 @@ httplib::Client cli("https://example.com");
|
||||
auto res = cli.Get("/");
|
||||
if (!res) {
|
||||
// Check the error type
|
||||
auto err = res.error();
|
||||
const auto err = res.error();
|
||||
|
||||
switch (err) {
|
||||
case httplib::Error::SSLConnection:
|
||||
std::cout << "SSL connection failed, SSL error: "
|
||||
<< res->ssl_error() << std::endl;
|
||||
<< res.ssl_error() << std::endl;
|
||||
break;
|
||||
|
||||
case httplib::Error::SSLLoadingCerts:
|
||||
std::cout << "SSL cert loading failed, OpenSSL error: "
|
||||
<< std::hex << res->ssl_openssl_error() << std::endl;
|
||||
<< std::hex << res.ssl_openssl_error() << std::endl;
|
||||
break;
|
||||
|
||||
case httplib::Error::SSLServerVerification:
|
||||
std::cout << "SSL verification failed, X509 error: "
|
||||
<< res->ssl_openssl_error() << std::endl;
|
||||
<< res.ssl_openssl_error() << std::endl;
|
||||
break;
|
||||
|
||||
case httplib::Error::SSLServerHostnameVerification:
|
||||
std::cout << "SSL hostname verification failed, X509 error: "
|
||||
<< res->ssl_openssl_error() << std::endl;
|
||||
<< res.ssl_openssl_error() << std::endl;
|
||||
break;
|
||||
|
||||
default:
|
||||
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -269,23 +268,47 @@ svr.set_file_request_handler([](const Request &req, Response &res) {
|
||||
|
||||
### Logging
|
||||
|
||||
cpp-httplib provides separate logging capabilities for access logs and error logs, similar to web servers like Nginx and Apache.
|
||||
|
||||
#### Access Logging
|
||||
|
||||
Access loggers capture successful HTTP requests and responses:
|
||||
|
||||
```cpp
|
||||
svr.set_logger([](const auto& req, const auto& res) {
|
||||
your_logger(req, res);
|
||||
svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
||||
std::cout << req.method << " " << req.path << " -> " << res.status << std::endl;
|
||||
});
|
||||
```
|
||||
|
||||
You can also set a pre-compression logger to capture request/response data before compression is applied. This is useful for debugging and monitoring purposes when you need to see the original, uncompressed response content:
|
||||
#### Pre-compression Logging
|
||||
|
||||
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
||||
|
||||
```cpp
|
||||
svr.set_pre_compression_logger([](const auto& req, const auto& res) {
|
||||
svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Response& res) {
|
||||
// Log before compression - res.body contains uncompressed content
|
||||
// Content-Encoding header is not yet set
|
||||
your_pre_compression_logger(req, res);
|
||||
});
|
||||
```
|
||||
|
||||
The pre-compression logger is only called when compression would be applied. For responses without compression, only the regular logger is called.
|
||||
The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called.
|
||||
|
||||
#### Error Logging
|
||||
|
||||
Error loggers capture failed requests and connection issues. Unlike access loggers, error loggers only receive the Error and Request information, as errors typically occur before a meaningful Response can be generated.
|
||||
|
||||
```cpp
|
||||
svr.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
|
||||
std::cerr << httplib::to_string(err) << " while processing request";
|
||||
if (req) {
|
||||
std::cerr << ", client: " << req->get_header_value("X-Forwarded-For")
|
||||
<< ", request: '" << req->method << " " << req->path << " " << req->version << "'"
|
||||
<< ", host: " << req->get_header_value("Host");
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
});
|
||||
```
|
||||
|
||||
### Error handler
|
||||
|
||||
@@ -718,6 +741,51 @@ enum Error {
|
||||
};
|
||||
```
|
||||
|
||||
### Client Logging
|
||||
|
||||
#### Access Logging
|
||||
|
||||
```cpp
|
||||
cli.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start_time).count();
|
||||
std::cout << "✓ " << req.method << " " << req.path
|
||||
<< " -> " << res.status << " (" << res.body.size() << " bytes, "
|
||||
<< duration << "ms)" << std::endl;
|
||||
});
|
||||
```
|
||||
|
||||
#### Error Logging
|
||||
|
||||
```cpp
|
||||
cli.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
|
||||
std::cerr << "✗ ";
|
||||
if (req) {
|
||||
std::cerr << req->method << " " << req->path << " ";
|
||||
}
|
||||
std::cerr << "failed: " << httplib::to_string(err);
|
||||
|
||||
// Add specific guidance based on error type
|
||||
switch (err) {
|
||||
case httplib::Error::Connection:
|
||||
std::cerr << " (verify server is running and reachable)";
|
||||
break;
|
||||
case httplib::Error::SSLConnection:
|
||||
std::cerr << " (check SSL certificate and TLS configuration)";
|
||||
break;
|
||||
case httplib::Error::ConnectionTimeout:
|
||||
std::cerr << " (increase timeout or check network latency)";
|
||||
break;
|
||||
case httplib::Error::Read:
|
||||
std::cerr << " (server may have closed connection prematurely)";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
});
|
||||
```
|
||||
|
||||
### GET with HTTP headers
|
||||
|
||||
```c++
|
||||
@@ -871,7 +939,7 @@ auto res = cli.Post(
|
||||
httplib::Client cli(url, port);
|
||||
|
||||
// prints: 0 / 000 bytes => 50% complete
|
||||
auto res = cli.Get("/", [](uint64_t len, uint64_t total) {
|
||||
auto res = cli.Get("/", [](size_t len, size_t total) {
|
||||
printf("%lld / %lld bytes => %d%% complete\n",
|
||||
len, total,
|
||||
(int)(len*100/total));
|
||||
@@ -990,6 +1058,25 @@ auto res = cli.Get("/already%20encoded/path"); // Use pre-encoded paths
|
||||
- `true` (default): Automatically encodes spaces, plus signs, newlines, and other special characters
|
||||
- `false`: Sends paths as-is without encoding (useful for pre-encoded URLs)
|
||||
|
||||
### Performance Note for Local Connections
|
||||
|
||||
> [!WARNING]
|
||||
> On Windows systems with improperly configured IPv6 settings, using "localhost" as the hostname may cause significant connection delays (up to 2 seconds per request) due to DNS resolution issues. This affects both client and server operations. For better performance when connecting to local services, use "127.0.0.1" instead of "localhost".
|
||||
>
|
||||
> See: https://github.com/yhirose/cpp-httplib/issues/366#issuecomment-593004264
|
||||
|
||||
```cpp
|
||||
// May be slower on Windows due to DNS resolution delays
|
||||
httplib::Client cli("localhost", 8080);
|
||||
httplib::Server svr;
|
||||
svr.listen("localhost", 8080);
|
||||
|
||||
// Faster alternative for local connections
|
||||
httplib::Client cli("127.0.0.1", 8080);
|
||||
httplib::Server svr;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
```
|
||||
|
||||
Compression
|
||||
-----------
|
||||
|
||||
@@ -1065,6 +1152,8 @@ cli.set_address_family(AF_UNIX);
|
||||
|
||||
"my-socket.sock" can be a relative path or an absolute path. Your application must have the appropriate permissions for the path. You can also use an abstract socket address on Linux. To use an abstract socket address, prepend a null byte ('\x00') to the path.
|
||||
|
||||
This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior:
|
||||
|
||||
|
||||
URI Encoding/Decoding Utilities
|
||||
-------------------------------
|
||||
@@ -1141,6 +1230,24 @@ Serving HTTP on 0.0.0.0 port 80 ...
|
||||
NOTE
|
||||
----
|
||||
|
||||
### Regular Expression Stack Overflow
|
||||
|
||||
> [!CAUTION]
|
||||
> When using complex regex patterns in route handlers, be aware that certain patterns may cause stack overflow during pattern matching. This is a known issue with `std::regex` implementations and affects the `dispatch_request()` method.
|
||||
>
|
||||
> ```cpp
|
||||
> // This pattern can cause stack overflow with large input
|
||||
> svr.Get(".*", handler);
|
||||
> ```
|
||||
>
|
||||
> Consider using simpler patterns or path parameters to avoid this issue:
|
||||
>
|
||||
> ```cpp
|
||||
> // Safer alternatives
|
||||
> svr.Get("/users/:id", handler); // Path parameters
|
||||
> svr.Get(R"(/api/v\d+/.*)", handler); // More specific patterns
|
||||
> ```
|
||||
|
||||
### g++
|
||||
|
||||
g++ 4.8 and below cannot build this library since `<regex>` in the versions are [broken](https://stackoverflow.com/questions/12530406/is-gcc-4-8-or-earlier-buggy-about-regular-expressions).
|
||||
|
||||
+256
-42
@@ -5,77 +5,291 @@
|
||||
// MIT License
|
||||
//
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <format>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <signal.h>
|
||||
#include <sstream>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
constexpr auto error_html = R"(<html>
|
||||
<head><title>{} {}</title></head>
|
||||
<body>
|
||||
<center><h1>404 Not Found</h1></center>
|
||||
<hr><center>cpp-httplib/{}</center>
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
using namespace httplib;
|
||||
|
||||
void sigint_handler(int s) { exit(1); }
|
||||
const auto SERVER_NAME =
|
||||
std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION);
|
||||
|
||||
std::string time_local() {
|
||||
auto p = std::chrono::system_clock::now();
|
||||
auto t = std::chrono::system_clock::to_time_t(p);
|
||||
Server svr;
|
||||
|
||||
void signal_handler(int signal) {
|
||||
if (signal == SIGINT || signal == SIGTERM) {
|
||||
std::cout << "\nReceived signal, shutting down gracefully...\n";
|
||||
svr.stop();
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_time_format() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&t), "%d/%b/%Y:%H:%M:%S %z");
|
||||
ss << std::put_time(std::localtime(&time_t), "%d/%b/%Y:%H:%M:%S %z");
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string log(auto &req, auto &res) {
|
||||
auto remote_user = "-"; // TODO:
|
||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
||||
auto body_bytes_sent = res.get_header_value("Content-Length");
|
||||
auto http_referer = "-"; // TODO:
|
||||
auto http_user_agent = req.get_header_value("User-Agent", "-");
|
||||
std::string get_error_time_format() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
|
||||
// NOTE: From NGINX default access log format
|
||||
// log_format combined '$remote_addr - $remote_user [$time_local] '
|
||||
// '"$request" $status $body_bytes_sent '
|
||||
// '"$http_referer" "$http_user_agent"';
|
||||
return std::format(R"({} - {} [{}] "{}" {} {} "{}" "{}")", req.remote_addr,
|
||||
remote_user, time_local(), request, res.status,
|
||||
body_bytes_sent, http_referer, http_user_agent);
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
signal(SIGINT, sigint_handler);
|
||||
std::string get_client_ip(const Request &req) {
|
||||
// Check for X-Forwarded-For header first (common in reverse proxy setups)
|
||||
auto forwarded_for = req.get_header_value("X-Forwarded-For");
|
||||
if (!forwarded_for.empty()) {
|
||||
// Get the first IP if there are multiple
|
||||
auto comma_pos = forwarded_for.find(',');
|
||||
if (comma_pos != std::string::npos) {
|
||||
return forwarded_for.substr(0, comma_pos);
|
||||
}
|
||||
return forwarded_for;
|
||||
}
|
||||
|
||||
auto base_dir = "./html";
|
||||
auto host = "0.0.0.0";
|
||||
auto port = 80;
|
||||
// Check for X-Real-IP header
|
||||
auto real_ip = req.get_header_value("X-Real-IP");
|
||||
if (!real_ip.empty()) { return real_ip; }
|
||||
|
||||
httplib::Server svr;
|
||||
// Fallback to remote address (though cpp-httplib doesn't provide this
|
||||
// directly) For demonstration, we'll use a placeholder
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
svr.set_error_handler([](auto & /*req*/, auto &res) {
|
||||
auto body =
|
||||
std::format(error_html, res.status, httplib::status_message(res.status),
|
||||
CPPHTTPLIB_VERSION);
|
||||
// NGINX Combined log format:
|
||||
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||
// "$http_referer" "$http_user_agent"
|
||||
void nginx_access_logger(const Request &req, const Response &res) {
|
||||
auto remote_addr = get_client_ip(req);
|
||||
std::string remote_user =
|
||||
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
||||
auto time_local = get_time_format();
|
||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
||||
auto status = res.status;
|
||||
auto body_bytes_sent = res.body.size();
|
||||
auto http_referer = req.get_header_value("Referer");
|
||||
if (http_referer.empty()) http_referer = "-";
|
||||
auto http_user_agent = req.get_header_value("User-Agent");
|
||||
if (http_user_agent.empty()) http_user_agent = "-";
|
||||
|
||||
res.set_content(body, "text/html");
|
||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||
remote_addr, remote_user, time_local, request,
|
||||
status, body_bytes_sent, http_referer,
|
||||
http_user_agent)
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
// NGINX Error log format:
|
||||
// YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request",
|
||||
// host: "host"
|
||||
void nginx_error_logger(const Error &err, const Request *req) {
|
||||
auto time_local = get_error_time_format();
|
||||
std::string level = "error";
|
||||
|
||||
if (req) {
|
||||
auto client_ip = get_client_ip(*req);
|
||||
auto request =
|
||||
std::format("{} {} {}", req->method, req->path, req->version);
|
||||
auto host = req->get_header_value("Host");
|
||||
if (host.empty()) host = "-";
|
||||
|
||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||
"\"{}\", host: \"{}\"",
|
||||
time_local, level, to_string(err), client_ip,
|
||||
request, host)
|
||||
<< std::endl;
|
||||
} else {
|
||||
// If no request context, just log the error
|
||||
std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err))
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void print_usage(const char *program_name) {
|
||||
std::cout << "Usage: " << program_name << " [OPTIONS]" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " --host <hostname> Server hostname (default: localhost)"
|
||||
<< std::endl;
|
||||
std::cout << " --port <port> Server port (default: 8080)"
|
||||
<< std::endl;
|
||||
std::cout << " --mount <mount:path> Mount point and document root"
|
||||
<< std::endl;
|
||||
std::cout << " Format: mount_point:document_root"
|
||||
<< std::endl;
|
||||
std::cout << " (default: /:./html)" << std::endl;
|
||||
std::cout << " --version Show version information"
|
||||
<< std::endl;
|
||||
std::cout << " --help Show this help message" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Examples:" << std::endl;
|
||||
std::cout << " " << program_name
|
||||
<< " --host localhost --port 8080 --mount /:./html" << std::endl;
|
||||
std::cout << " " << program_name
|
||||
<< " --host 0.0.0.0 --port 3000 --mount /api:./api" << std::endl;
|
||||
}
|
||||
|
||||
struct ServerConfig {
|
||||
std::string hostname = "localhost";
|
||||
int port = 8080;
|
||||
std::string mount_point = "/";
|
||||
std::string document_root = "./html";
|
||||
};
|
||||
|
||||
enum class ParseResult { SUCCESS, HELP_REQUESTED, VERSION_REQUESTED, ERROR };
|
||||
|
||||
ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::HELP_REQUESTED;
|
||||
} else if (strcmp(argv[i], "--host") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "Error: --host requires a hostname argument" << std::endl;
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
config.hostname = argv[++i];
|
||||
} else if (strcmp(argv[i], "--port") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "Error: --port requires a port number argument"
|
||||
<< std::endl;
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
config.port = std::atoi(argv[++i]);
|
||||
if (config.port <= 0 || config.port > 65535) {
|
||||
std::cerr << "Error: Invalid port number. Must be between 1 and 65535"
|
||||
<< std::endl;
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
} else if (strcmp(argv[i], "--mount") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr
|
||||
<< "Error: --mount requires mount_point:document_root argument"
|
||||
<< std::endl;
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
std::string mount_arg = argv[++i];
|
||||
auto colon_pos = mount_arg.find(':');
|
||||
if (colon_pos == std::string::npos) {
|
||||
std::cerr << "Error: --mount argument must be in format "
|
||||
"mount_point:document_root"
|
||||
<< std::endl;
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
config.mount_point = mount_arg.substr(0, colon_pos);
|
||||
config.document_root = mount_arg.substr(colon_pos + 1);
|
||||
|
||||
if (config.mount_point.empty() || config.document_root.empty()) {
|
||||
std::cerr
|
||||
<< "Error: Both mount_point and document_root must be non-empty"
|
||||
<< std::endl;
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
} else if (strcmp(argv[i], "--version") == 0) {
|
||||
std::cout << CPPHTTPLIB_VERSION << std::endl;
|
||||
return ParseResult::VERSION_REQUESTED;
|
||||
} else {
|
||||
std::cerr << "Error: Unknown option '" << argv[i] << "'" << std::endl;
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
}
|
||||
return ParseResult::SUCCESS;
|
||||
}
|
||||
|
||||
bool setup_server(Server &svr, const ServerConfig &config) {
|
||||
svr.set_logger(nginx_access_logger);
|
||||
svr.set_error_logger(nginx_error_logger);
|
||||
|
||||
auto ret = svr.set_mount_point(config.mount_point, config.document_root);
|
||||
if (!ret) {
|
||||
std::cerr
|
||||
<< std::format(
|
||||
"Error: Cannot mount '{}' to '{}'. Directory may not exist.",
|
||||
config.mount_point, config.document_root)
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
svr.set_file_extension_and_mimetype_mapping("html", "text/html");
|
||||
svr.set_file_extension_and_mimetype_mapping("htm", "text/html");
|
||||
svr.set_file_extension_and_mimetype_mapping("css", "text/css");
|
||||
svr.set_file_extension_and_mimetype_mapping("js", "text/javascript");
|
||||
svr.set_file_extension_and_mimetype_mapping("json", "application/json");
|
||||
svr.set_file_extension_and_mimetype_mapping("xml", "application/xml");
|
||||
svr.set_file_extension_and_mimetype_mapping("png", "image/png");
|
||||
svr.set_file_extension_and_mimetype_mapping("jpg", "image/jpeg");
|
||||
svr.set_file_extension_and_mimetype_mapping("jpeg", "image/jpeg");
|
||||
svr.set_file_extension_and_mimetype_mapping("gif", "image/gif");
|
||||
svr.set_file_extension_and_mimetype_mapping("svg", "image/svg+xml");
|
||||
svr.set_file_extension_and_mimetype_mapping("ico", "image/x-icon");
|
||||
svr.set_file_extension_and_mimetype_mapping("pdf", "application/pdf");
|
||||
svr.set_file_extension_and_mimetype_mapping("zip", "application/zip");
|
||||
svr.set_file_extension_and_mimetype_mapping("txt", "text/plain");
|
||||
|
||||
svr.set_error_handler([](const Request & /*req*/, Response &res) {
|
||||
if (res.status == 404) {
|
||||
res.set_content(
|
||||
std::format(
|
||||
"<html><head><title>404 Not Found</title></head>"
|
||||
"<body><h1>404 Not Found</h1>"
|
||||
"<p>The requested resource was not found on this server.</p>"
|
||||
"<hr><p>{}</p></body></html>",
|
||||
SERVER_NAME),
|
||||
"text/html");
|
||||
}
|
||||
});
|
||||
|
||||
svr.set_logger(
|
||||
[](auto &req, auto &res) { std::cout << log(req, res) << std::endl; });
|
||||
svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
|
||||
res.set_header("Server", SERVER_NAME);
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
|
||||
svr.set_mount_point("/", base_dir);
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
std::cout << std::format("Serving HTTP on {0} port {1} ...", host, port)
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
ServerConfig config;
|
||||
|
||||
auto result = parse_command_line(argc, argv, config);
|
||||
switch (result) {
|
||||
case ParseResult::HELP_REQUESTED:
|
||||
case ParseResult::VERSION_REQUESTED: return 0;
|
||||
case ParseResult::ERROR: return 1;
|
||||
case ParseResult::SUCCESS: break;
|
||||
}
|
||||
|
||||
if (!setup_server(svr, config)) { return 1; }
|
||||
|
||||
std::cout << "Serving HTTP on " << config.hostname << ":" << config.port
|
||||
<< std::endl;
|
||||
std::cout << "Mount point: " << config.mount_point << " -> "
|
||||
<< config.document_root << std::endl;
|
||||
std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl;
|
||||
|
||||
auto ret = svr.listen(host, port);
|
||||
auto ret = svr.listen(config.hostname, config.port);
|
||||
|
||||
std::cout << "Server has been shut down." << std::endl;
|
||||
|
||||
return ret ? 0 : 1;
|
||||
}
|
||||
|
||||
+17
-9
@@ -18,6 +18,14 @@ project(
|
||||
|
||||
cxx = meson.get_compiler('cpp')
|
||||
|
||||
if cxx.sizeof('void *') != 8
|
||||
if host_machine.system() == 'windows'
|
||||
error('unsupported architecture: cpp-httplib doesn\'t support 32-bit Windows. Please use a 64-bit compiler.')
|
||||
else
|
||||
warning('cpp-httplib doesn\'t support 32-bit platforms. Please use a 64-bit compiler.')
|
||||
endif
|
||||
endif
|
||||
|
||||
# Check just in case downstream decides to edit the source
|
||||
# and add a project version
|
||||
version = meson.project_version()
|
||||
@@ -31,12 +39,12 @@ endif
|
||||
deps = [dependency('threads')]
|
||||
args = []
|
||||
|
||||
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('cpp-httplib_openssl'))
|
||||
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('openssl'))
|
||||
if openssl_dep.found()
|
||||
deps += openssl_dep
|
||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||
if host_machine.system() == 'darwin'
|
||||
macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('cpp-httplib_macosx_keychain'))
|
||||
macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
|
||||
if macosx_keychain_dep.found()
|
||||
deps += macosx_keychain_dep
|
||||
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
|
||||
@@ -44,15 +52,15 @@ if openssl_dep.found()
|
||||
endif
|
||||
endif
|
||||
|
||||
zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))
|
||||
zlib_dep = dependency('zlib', required: get_option('zlib'))
|
||||
if zlib_dep.found()
|
||||
deps += zlib_dep
|
||||
args += '-DCPPHTTPLIB_ZLIB_SUPPORT'
|
||||
endif
|
||||
|
||||
brotli_deps = [dependency('libbrotlicommon', required: get_option('cpp-httplib_brotli'))]
|
||||
brotli_deps += dependency('libbrotlidec', required: get_option('cpp-httplib_brotli'))
|
||||
brotli_deps += dependency('libbrotlienc', required: get_option('cpp-httplib_brotli'))
|
||||
brotli_deps = [dependency('libbrotlicommon', required: get_option('brotli'))]
|
||||
brotli_deps += dependency('libbrotlidec', required: get_option('brotli'))
|
||||
brotli_deps += dependency('libbrotlienc', required: get_option('brotli'))
|
||||
|
||||
brotli_found_all = true
|
||||
foreach brotli_dep : brotli_deps
|
||||
@@ -66,7 +74,7 @@ if brotli_found_all
|
||||
args += '-DCPPHTTPLIB_BROTLI_SUPPORT'
|
||||
endif
|
||||
|
||||
async_ns_opt = get_option('cpp-httplib_non_blocking_getaddrinfo')
|
||||
async_ns_opt = get_option('non_blocking_getaddrinfo')
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt)
|
||||
@@ -83,7 +91,7 @@ endif
|
||||
|
||||
cpp_httplib_dep = dependency('', required: false)
|
||||
|
||||
if get_option('cpp-httplib_compile')
|
||||
if get_option('compile')
|
||||
python3 = find_program('python3')
|
||||
|
||||
httplib_ch = custom_target(
|
||||
@@ -127,6 +135,6 @@ endif
|
||||
|
||||
meson.override_dependency('cpp-httplib', cpp_httplib_dep)
|
||||
|
||||
if get_option('cpp-httplib_test')
|
||||
if get_option('test')
|
||||
subdir('test')
|
||||
endif
|
||||
|
||||
+179
-4
@@ -173,6 +173,48 @@ TEST_F(UnixSocketTest, abstract) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(UnixSocketTest, HostHeaderAutoSet) {
|
||||
httplib::Server svr;
|
||||
std::string received_host_header;
|
||||
|
||||
svr.Get(pattern_, [&](const httplib::Request &req, httplib::Response &res) {
|
||||
// Capture the Host header sent by the client
|
||||
auto host_iter = req.headers.find("Host");
|
||||
if (host_iter != req.headers.end()) {
|
||||
received_host_header = host_iter->second;
|
||||
}
|
||||
res.set_content(content_, "text/plain");
|
||||
});
|
||||
|
||||
std::thread t{[&] {
|
||||
ASSERT_TRUE(svr.set_address_family(AF_UNIX).listen(pathname_, 80));
|
||||
}};
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
ASSERT_TRUE(svr.is_running());
|
||||
|
||||
// Test that Host header is automatically set to "localhost" for Unix socket
|
||||
// connections
|
||||
httplib::Client cli{pathname_};
|
||||
cli.set_address_family(AF_UNIX);
|
||||
ASSERT_TRUE(cli.is_valid());
|
||||
|
||||
const auto &result = cli.Get(pattern_);
|
||||
ASSERT_TRUE(result) << "error: " << result.error();
|
||||
|
||||
const auto &resp = result.value();
|
||||
EXPECT_EQ(resp.status, StatusCode::OK_200);
|
||||
EXPECT_EQ(resp.body, content_);
|
||||
|
||||
// Verify that Host header was automatically set to "localhost"
|
||||
EXPECT_EQ(received_host_header, "localhost");
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST(SocketStream, wait_writable_UNIX) {
|
||||
int fds[2];
|
||||
@@ -259,9 +301,8 @@ TEST(StartupTest, WSAStartup) {
|
||||
|
||||
TEST(DecodePathTest, PercentCharacter) {
|
||||
EXPECT_EQ(
|
||||
detail::decode_path(
|
||||
R"(descrip=Gastos%20%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%B1%C3%91%206)",
|
||||
false),
|
||||
decode_path_component(
|
||||
R"(descrip=Gastos%20%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%B1%C3%91%206)"),
|
||||
u8"descrip=Gastos áéíóúñÑ 6");
|
||||
}
|
||||
|
||||
@@ -271,7 +312,7 @@ TEST(DecodePathTest, PercentCharacterNUL) {
|
||||
expected.push_back('\0');
|
||||
expected.push_back('x');
|
||||
|
||||
EXPECT_EQ(detail::decode_path("x%00x", false), expected);
|
||||
EXPECT_EQ(decode_path_component("x%00x"), expected);
|
||||
}
|
||||
|
||||
TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest) {
|
||||
@@ -3159,6 +3200,11 @@ protected:
|
||||
[&](const Request & /*req*/, Response &res) {
|
||||
res.set_content("abcdefg", "text/plain");
|
||||
})
|
||||
.Get("/test-start-time",
|
||||
[&](const Request &req, Response & /*res*/) {
|
||||
EXPECT_NE(req.start_time_,
|
||||
std::chrono::steady_clock::time_point::min());
|
||||
})
|
||||
.Get("/with-range-customized-response",
|
||||
[&](const Request & /*req*/, Response &res) {
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
@@ -5759,6 +5805,8 @@ TEST_F(ServerTest, TooManyRedirect) {
|
||||
EXPECT_EQ(Error::ExceedRedirectCount, res.error());
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, StartTime) { auto res = cli_.Get("/test-start-time"); }
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
TEST_F(ServerTest, Gzip) {
|
||||
Headers headers;
|
||||
@@ -7318,6 +7366,48 @@ TEST(KeepAliveTest, SSLClientReconnectionPost) {
|
||||
ASSERT_TRUE(result);
|
||||
EXPECT_EQ(200, result->status);
|
||||
}
|
||||
|
||||
TEST(SNI_AutoDetectionTest, SNI_Logic) {
|
||||
{
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.Get("/sni", [&](const Request &req, Response &res) {
|
||||
std::string expected;
|
||||
if (req.ssl) {
|
||||
if (const char *sni =
|
||||
SSL_get_servername(req.ssl, TLSEXT_NAMETYPE_host_name)) {
|
||||
expected = sni;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(expected, req.get_param_value("expected"));
|
||||
res.set_content("ok", "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();
|
||||
|
||||
{
|
||||
SSLClient cli("localhost", PORT);
|
||||
cli.enable_server_certificate_verification(false);
|
||||
auto res = cli.Get("/sni?expected=localhost");
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
{
|
||||
SSLClient cli("::1", PORT);
|
||||
cli.enable_server_certificate_verification(false);
|
||||
auto res = cli.Get("/sni?expected=");
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(ClientProblemDetectionTest, ContentProvider) {
|
||||
@@ -8922,6 +9012,46 @@ TEST(HttpToHttpsRedirectTest, CertFile) {
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(SSLClientRedirectTest, CertFile) {
|
||||
SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(ssl_svr1.is_valid());
|
||||
ssl_svr1.Get("/index", [&](const Request &, Response &res) {
|
||||
res.set_redirect("https://127.0.0.1:1235/index");
|
||||
ssl_svr1.stop();
|
||||
});
|
||||
|
||||
SSLServer ssl_svr2(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(ssl_svr2.is_valid());
|
||||
ssl_svr2.Get("/index", [&](const Request &, Response &res) {
|
||||
res.set_content("test", "text/plain");
|
||||
ssl_svr2.stop();
|
||||
});
|
||||
|
||||
thread t = thread([&]() { ASSERT_TRUE(ssl_svr1.listen("127.0.0.1", PORT)); });
|
||||
thread t2 =
|
||||
thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", 1235)); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
t2.join();
|
||||
t.join();
|
||||
ASSERT_FALSE(ssl_svr1.is_running());
|
||||
});
|
||||
|
||||
ssl_svr1.wait_until_ready();
|
||||
ssl_svr2.wait_until_ready();
|
||||
|
||||
SSLClient cli("127.0.0.1", PORT);
|
||||
std::string cert;
|
||||
read_file(SERVER_CERT2_FILE, cert);
|
||||
cli.load_ca_cert_store(cert.c_str(), cert.size());
|
||||
cli.enable_server_certificate_verification(true);
|
||||
cli.set_follow_location(true);
|
||||
cli.set_connection_timeout(30);
|
||||
|
||||
auto res = cli.Get("/index");
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, LargeData) {
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
|
||||
@@ -9900,6 +10030,51 @@ TEST(RedirectTest, RedirectToUrlWithQueryParameters) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RedirectTest, RedirectToUrlWithPlusInQueryParameters) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [](const Request & /*req*/, Response &res) {
|
||||
res.set_redirect(R"(/hello?key=AByz09+~-._%20%26%3F%C3%BC%2B)");
|
||||
});
|
||||
|
||||
svr.Get("/hello", [](const Request &req, Response &res) {
|
||||
res.set_content(req.get_param_value("key"), "text/plain");
|
||||
});
|
||||
|
||||
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);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("AByz09 ~-._ &?ü+", res->body);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
TEST(RedirectTest, Issue2185_Online) {
|
||||
SSLClient client("github.com");
|
||||
client.set_follow_location(true);
|
||||
|
||||
auto res = client.Get("/Coollab-Art/Coollab/releases/download/1.1.1_UI-Scale/"
|
||||
"Coollab-Windows.zip");
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(9920427U, res->body.size());
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(VulnerabilityTest, CRLFInjection) {
|
||||
Server svr;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user