mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Compare 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 |
@@ -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
|
run: vcpkg install gtest curl zlib brotli zstd
|
||||||
- name: Install OpenSSL
|
- name: Install OpenSSL
|
||||||
if: ${{ matrix.config.with_ssl }}
|
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 }}
|
- name: Configure CMake ${{ matrix.config.name }}
|
||||||
run: >
|
run: >
|
||||||
cmake -B build -S .
|
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)
|
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
|
||||||
# Defaults to static library
|
# 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)
|
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
|
# Necessary for Windows if building shared libs
|
||||||
# See https://stackoverflow.com/a/40743080
|
# See https://stackoverflow.com/a/40743080
|
||||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||||
endif()
|
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 some variables that are used in-tree and while building based on our options
|
||||||
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
|
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
|
||||||
set(HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN ${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN})
|
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_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
|
||||||
|
|
||||||
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
|
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
|
||||||
$<BUILD_INTERFACE:${_httplib_build_includedir}>
|
$<BUILD_INTERFACE:${_httplib_build_includedir}>
|
||||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||||
)
|
)
|
||||||
|
|
||||||
# Always require threads
|
# Always require threads
|
||||||
@@ -340,6 +347,6 @@ if(HTTPLIB_INSTALL)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(HTTPLIB_TEST)
|
if(HTTPLIB_TEST)
|
||||||
include(CTest)
|
include(CTest)
|
||||||
add_subdirectory(test)
|
add_subdirectory(test)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
+3
-1
@@ -8,4 +8,6 @@ FROM scratch
|
|||||||
COPY --from=builder /build/server /server
|
COPY --from=builder /build/server /server
|
||||||
COPY docker/html/index.html /html/index.html
|
COPY docker/html/index.html /html/index.html
|
||||||
EXPOSE 80
|
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("/");
|
auto res = cli.Get("/");
|
||||||
if (!res) {
|
if (!res) {
|
||||||
// Check the error type
|
// Check the error type
|
||||||
auto err = res.error();
|
const auto err = res.error();
|
||||||
|
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case httplib::Error::SSLConnection:
|
case httplib::Error::SSLConnection:
|
||||||
std::cout << "SSL connection failed, SSL error: "
|
std::cout << "SSL connection failed, SSL error: "
|
||||||
<< res->ssl_error() << std::endl;
|
<< res.ssl_error() << std::endl;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case httplib::Error::SSLLoadingCerts:
|
case httplib::Error::SSLLoadingCerts:
|
||||||
std::cout << "SSL cert loading failed, OpenSSL error: "
|
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;
|
break;
|
||||||
|
|
||||||
case httplib::Error::SSLServerVerification:
|
case httplib::Error::SSLServerVerification:
|
||||||
std::cout << "SSL verification failed, X509 error: "
|
std::cout << "SSL verification failed, X509 error: "
|
||||||
<< res->ssl_openssl_error() << std::endl;
|
<< res.ssl_openssl_error() << std::endl;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case httplib::Error::SSLServerHostnameVerification:
|
case httplib::Error::SSLServerHostnameVerification:
|
||||||
std::cout << "SSL hostname verification failed, X509 error: "
|
std::cout << "SSL hostname verification failed, X509 error: "
|
||||||
<< res->ssl_openssl_error() << std::endl;
|
<< res.ssl_openssl_error() << std::endl;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
|
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
|
### 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
|
```cpp
|
||||||
svr.set_logger([](const auto& req, const auto& res) {
|
svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
||||||
your_logger(req, 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
|
```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
|
// Log before compression - res.body contains uncompressed content
|
||||||
// Content-Encoding header is not yet set
|
// Content-Encoding header is not yet set
|
||||||
your_pre_compression_logger(req, res);
|
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
|
### 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
|
### GET with HTTP headers
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
|
|||||||
+256
-42
@@ -5,77 +5,291 @@
|
|||||||
// MIT License
|
// MIT License
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <signal.h>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include <httplib.h>
|
#include <httplib.h>
|
||||||
|
|
||||||
constexpr auto error_html = R"(<html>
|
using namespace httplib;
|
||||||
<head><title>{} {}</title></head>
|
|
||||||
<body>
|
|
||||||
<center><h1>404 Not Found</h1></center>
|
|
||||||
<hr><center>cpp-httplib/{}</center>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)";
|
|
||||||
|
|
||||||
void sigint_handler(int s) { exit(1); }
|
const auto SERVER_NAME =
|
||||||
|
std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION);
|
||||||
|
|
||||||
std::string time_local() {
|
Server svr;
|
||||||
auto p = std::chrono::system_clock::now();
|
|
||||||
auto t = std::chrono::system_clock::to_time_t(p);
|
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;
|
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();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string log(auto &req, auto &res) {
|
std::string get_error_time_format() {
|
||||||
auto remote_user = "-"; // TODO:
|
auto now = std::chrono::system_clock::now();
|
||||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||||
auto body_bytes_sent = res.get_header_value("Content-Length");
|
|
||||||
auto http_referer = "-"; // TODO:
|
|
||||||
auto http_user_agent = req.get_header_value("User-Agent", "-");
|
|
||||||
|
|
||||||
// NOTE: From NGINX default access log format
|
std::stringstream ss;
|
||||||
// log_format combined '$remote_addr - $remote_user [$time_local] '
|
ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
|
||||||
// '"$request" $status $body_bytes_sent '
|
return ss.str();
|
||||||
// '"$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, const char **argv) {
|
std::string get_client_ip(const Request &req) {
|
||||||
signal(SIGINT, sigint_handler);
|
// 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";
|
// Check for X-Real-IP header
|
||||||
auto host = "0.0.0.0";
|
auto real_ip = req.get_header_value("X-Real-IP");
|
||||||
auto port = 80;
|
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) {
|
// NGINX Combined log format:
|
||||||
auto body =
|
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||||
std::format(error_html, res.status, httplib::status_message(res.status),
|
// "$http_referer" "$http_user_agent"
|
||||||
CPPHTTPLIB_VERSION);
|
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(
|
svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
|
||||||
[](auto &req, auto &res) { std::cout << log(req, res) << std::endl; });
|
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::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;
|
return ret ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -39,12 +39,12 @@ endif
|
|||||||
deps = [dependency('threads')]
|
deps = [dependency('threads')]
|
||||||
args = []
|
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()
|
if openssl_dep.found()
|
||||||
deps += openssl_dep
|
deps += openssl_dep
|
||||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||||
if host_machine.system() == 'darwin'
|
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()
|
if macosx_keychain_dep.found()
|
||||||
deps += macosx_keychain_dep
|
deps += macosx_keychain_dep
|
||||||
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
|
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
|
||||||
@@ -52,15 +52,15 @@ if openssl_dep.found()
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))
|
zlib_dep = dependency('zlib', required: get_option('zlib'))
|
||||||
if zlib_dep.found()
|
if zlib_dep.found()
|
||||||
deps += zlib_dep
|
deps += zlib_dep
|
||||||
args += '-DCPPHTTPLIB_ZLIB_SUPPORT'
|
args += '-DCPPHTTPLIB_ZLIB_SUPPORT'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
brotli_deps = [dependency('libbrotlicommon', required: get_option('cpp-httplib_brotli'))]
|
brotli_deps = [dependency('libbrotlicommon', required: get_option('brotli'))]
|
||||||
brotli_deps += dependency('libbrotlidec', required: get_option('cpp-httplib_brotli'))
|
brotli_deps += dependency('libbrotlidec', required: get_option('brotli'))
|
||||||
brotli_deps += dependency('libbrotlienc', required: get_option('cpp-httplib_brotli'))
|
brotli_deps += dependency('libbrotlienc', required: get_option('brotli'))
|
||||||
|
|
||||||
brotli_found_all = true
|
brotli_found_all = true
|
||||||
foreach brotli_dep : brotli_deps
|
foreach brotli_dep : brotli_deps
|
||||||
@@ -74,7 +74,7 @@ if brotli_found_all
|
|||||||
args += '-DCPPHTTPLIB_BROTLI_SUPPORT'
|
args += '-DCPPHTTPLIB_BROTLI_SUPPORT'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
async_ns_opt = get_option('cpp-httplib_non_blocking_getaddrinfo')
|
async_ns_opt = get_option('non_blocking_getaddrinfo')
|
||||||
|
|
||||||
if host_machine.system() == 'windows'
|
if host_machine.system() == 'windows'
|
||||||
async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt)
|
async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt)
|
||||||
@@ -91,7 +91,7 @@ endif
|
|||||||
|
|
||||||
cpp_httplib_dep = dependency('', required: false)
|
cpp_httplib_dep = dependency('', required: false)
|
||||||
|
|
||||||
if get_option('cpp-httplib_compile')
|
if get_option('compile')
|
||||||
python3 = find_program('python3')
|
python3 = find_program('python3')
|
||||||
|
|
||||||
httplib_ch = custom_target(
|
httplib_ch = custom_target(
|
||||||
@@ -135,6 +135,6 @@ endif
|
|||||||
|
|
||||||
meson.override_dependency('cpp-httplib', cpp_httplib_dep)
|
meson.override_dependency('cpp-httplib', cpp_httplib_dep)
|
||||||
|
|
||||||
if get_option('cpp-httplib_test')
|
if get_option('test')
|
||||||
subdir('test')
|
subdir('test')
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -3200,6 +3200,11 @@ protected:
|
|||||||
[&](const Request & /*req*/, Response &res) {
|
[&](const Request & /*req*/, Response &res) {
|
||||||
res.set_content("abcdefg", "text/plain");
|
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",
|
.Get("/with-range-customized-response",
|
||||||
[&](const Request & /*req*/, Response &res) {
|
[&](const Request & /*req*/, Response &res) {
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
@@ -5800,6 +5805,8 @@ TEST_F(ServerTest, TooManyRedirect) {
|
|||||||
EXPECT_EQ(Error::ExceedRedirectCount, res.error());
|
EXPECT_EQ(Error::ExceedRedirectCount, res.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ServerTest, StartTime) { auto res = cli_.Get("/test-start-time"); }
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||||
TEST_F(ServerTest, Gzip) {
|
TEST_F(ServerTest, Gzip) {
|
||||||
Headers headers;
|
Headers headers;
|
||||||
@@ -7359,6 +7366,48 @@ TEST(KeepAliveTest, SSLClientReconnectionPost) {
|
|||||||
ASSERT_TRUE(result);
|
ASSERT_TRUE(result);
|
||||||
EXPECT_EQ(200, result->status);
|
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
|
#endif
|
||||||
|
|
||||||
TEST(ClientProblemDetectionTest, ContentProvider) {
|
TEST(ClientProblemDetectionTest, ContentProvider) {
|
||||||
@@ -8963,6 +9012,46 @@ TEST(HttpToHttpsRedirectTest, CertFile) {
|
|||||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
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) {
|
TEST(MultipartFormDataTest, LargeData) {
|
||||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user