mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc6faf5c17 | |||
| 71bb17fb0e | |||
| 094bf112bb | |||
| fbec2a3466 | |||
| c3fa06112b | |||
| f73e694f0c | |||
| 191bfb2ea4 | |||
| ad5839f0d1 | |||
| 02dfb97fd6 | |||
| a38a076571 | |||
| 0e1b52b23e | |||
| c0469eba96 | |||
| 7dec57d1eb | |||
| b85aa76bd2 | |||
| cea018f2cd | |||
| 1111219f17 | |||
| a7e1d14b15 | |||
| 6eff49e1fb | |||
| bd95e67c23 | |||
| 2e2e47bab1 | |||
| 59905c7f0d | |||
| 8d03ef1615 | |||
| 23a1d79a66 | |||
| 781c55f120 | |||
| 40f7985e02 | |||
| f85f30a637 | |||
| 6da7f0c61c | |||
| 2ba0e7a797 | |||
| ded82448aa | |||
| 98048a033a | |||
| 7ae794a6bf | |||
| 385adefb11 | |||
| b7c2f04318 | |||
| d23cf77cd0 | |||
| 5304464a53 | |||
| db98efee5a | |||
| cdf0d33258 | |||
| 25688258ad | |||
| f0990ca96d | |||
| 0461cb770c | |||
| 51b704b902 | |||
| 7eb03e81fc | |||
| 6a6d4161d1 | |||
| 63b07ada43 | |||
| 2de4c59bc2 | |||
| b7097f1386 | |||
| 681d388247 | |||
| ae94d64f67 | |||
| 3401877d3d | |||
| bce08e62f9 | |||
| f4ecb96e54 | |||
| c23764269d | |||
| f441cd2a44 | |||
| c3613c6977 | |||
| 87c2b4e584 | |||
| c795ad1c32 | |||
| 3e0fa33559 | |||
| 27b73f050e | |||
| dbd5ca4bf2 | |||
| 143019a38c | |||
| 1d36013fc3 | |||
| 8bba34eebc | |||
| 0a9102ff6b | |||
| c1fa5e1710 | |||
| 84796738fc | |||
| adf58bf474 | |||
| 337fbb0793 | |||
| 9e7861b0b4 | |||
| 27ee115a60 | |||
| 59882752aa | |||
| 61e9f7ce8f | |||
| 1acf18876f | |||
| 4b2b851dbb | |||
| 551f96d4a2 | |||
| eacc1ca98e | |||
| ac9ebb0ee3 | |||
| 11eed05ce7 | |||
| f3bba0646a | |||
| 2da189f88c | |||
| 6e0f211cff | |||
| 318a3fe425 | |||
| 2d8d524178 | |||
| afa88dbe70 | |||
| 08133b593b | |||
| 8aedbf4547 | |||
| cde29362ef | |||
| bae40fcdf2 | |||
| db561f5552 | |||
| 35c52c1ab9 | |||
| 23ff9a5605 | |||
| 41be1e24e3 | |||
| 6e52d0a057 | |||
| f72b4582e6 | |||
| 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 | |||
| ecfd84c171 | |||
| b5b2a1d1c8 | |||
| 17ba303889 | |||
| 802743264c | |||
| 9dbaed75ef | |||
| c551e97297 | |||
| 4ff7a1c858 | |||
| 082acacd45 | |||
| 52163ed982 | |||
| af7a69bcf6 | |||
| 145fc8b021 | |||
| af73377611 | |||
| a3f5569196 | |||
| a636a094bf | |||
| cb85e573de | |||
| 120405beac | |||
| ceff2c1154 | |||
| 0c08c378d7 | |||
| 9a0571513e | |||
| cfb56c0b78 | |||
| 083fe43ad3 | |||
| 9e36247665 | |||
| fd03483237 | |||
| d5409ab541 | |||
| 0b875e0747 | |||
| 292f9a6c55 | |||
| 7c303bb871 | |||
| ea850cbfa7 | |||
| e6ff3d7ac2 | |||
| e1ab5a604b | |||
| 696c02f2bc | |||
| a183dba9fc | |||
| d37373a983 | |||
| b2bf172393 | |||
| 1729aa8c1f | |||
| aabd0634ae | |||
| de5a255ac6 |
@@ -29,6 +29,7 @@ jobs:
|
||||
git
|
||||
libbrotli-dev
|
||||
libssl-dev
|
||||
libzstd-dev
|
||||
meson
|
||||
pkg-config
|
||||
python3
|
||||
|
||||
@@ -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 }}
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
clang-format --version
|
||||
cd test && make style_check
|
||||
|
||||
ubuntu:
|
||||
build-error-check-on-32bit:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
@@ -53,10 +53,28 @@ jobs:
|
||||
- arch_flags: -m32
|
||||
arch_suffix: :i386
|
||||
name: (32-bit)
|
||||
- arch_flags:
|
||||
arch_suffix:
|
||||
name: (64-bit)
|
||||
name: ubuntu ${{ matrix.config.name }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install libraries
|
||||
run: |
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libc6-dev${{ matrix.config.arch_suffix }} libstdc++-13-dev${{ matrix.config.arch_suffix }} \
|
||||
libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \
|
||||
zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \
|
||||
libzstd-dev${{ matrix.config.arch_suffix }}
|
||||
- name: build and run tests (expect failure)
|
||||
run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
|
||||
continue-on-error: true
|
||||
|
||||
ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -138,6 +156,7 @@ jobs:
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake
|
||||
-DHTTPLIB_TEST=ON
|
||||
-DHTTPLIB_COMPILE=${{ matrix.config.compiled && 'ON' || 'OFF' }}
|
||||
-DHTTPLIB_USE_OPENSSL_IF_AVAILABLE=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
|
||||
-DHTTPLIB_REQUIRE_ZLIB=ON
|
||||
-DHTTPLIB_REQUIRE_BROTLI=ON
|
||||
-DHTTPLIB_REQUIRE_ZSTD=ON
|
||||
@@ -146,7 +165,10 @@ jobs:
|
||||
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
|
||||
- name: Run tests ${{ matrix.config.name }}
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release
|
||||
run: ctest --output-on-failure --test-dir build -C Release -E "BenchmarkTest"
|
||||
- name: Run benchmark tests with retry ${{ matrix.config.name }}
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5
|
||||
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: No Exceptions Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test-no-exceptions:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev
|
||||
|
||||
- name: Run tests with CPPHTTPLIB_NO_EXCEPTIONS
|
||||
run: |
|
||||
cd test && make EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS"
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Proxy Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev netcat-openbsd
|
||||
|
||||
- name: Run proxy tests
|
||||
run: |
|
||||
cd test && make proxy
|
||||
+20
-2
@@ -1,16 +1,34 @@
|
||||
tags
|
||||
|
||||
# Ignore executables (no extension) but not source files
|
||||
example/server
|
||||
!example/server.*
|
||||
example/client
|
||||
!example/client.*
|
||||
example/hello
|
||||
!example/hello.*
|
||||
example/simplecli
|
||||
!example/simplecli.*
|
||||
example/simplesvr
|
||||
!example/simplesvr.*
|
||||
example/benchmark
|
||||
!example/benchmark.*
|
||||
example/redirect
|
||||
example/sse*
|
||||
!example/redirect.*
|
||||
example/ssecli
|
||||
!example/ssecli.*
|
||||
example/ssecli-stream
|
||||
!example/ssecli-stream.*
|
||||
example/ssesvr
|
||||
!example/ssesvr.*
|
||||
example/upload
|
||||
!example/upload.*
|
||||
example/one_time_request
|
||||
!example/one_time_request.*
|
||||
example/server_and_client
|
||||
!example/server_and_client.*
|
||||
example/accept_header
|
||||
!example/accept_header.*
|
||||
example/*.pem
|
||||
test/httplib.cc
|
||||
test/httplib.h
|
||||
@@ -42,4 +60,4 @@ ipch
|
||||
*.pyc
|
||||
.*
|
||||
!/.gitattributes
|
||||
!/.travis.yml
|
||||
!/.github
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v18.1.8 # 最新バージョンを使用
|
||||
hooks:
|
||||
- id: clang-format
|
||||
files: \.(cpp|cc|h)$
|
||||
args: [-i] # インプレースで修正
|
||||
+49
-17
@@ -1,6 +1,6 @@
|
||||
#[[
|
||||
Build options:
|
||||
* BUILD_SHARED_LIBS (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
||||
* Standard BUILD_SHARED_LIBS is supported and sets HTTPLIB_SHARED default value.
|
||||
* HTTPLIB_USE_OPENSSL_IF_AVAILABLE (default on)
|
||||
* HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on)
|
||||
* HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on)
|
||||
@@ -10,8 +10,10 @@
|
||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||
* HTTPLIB_REQUIRE_ZSTD (default off)
|
||||
* HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN (default on)
|
||||
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
|
||||
* HTTPLIB_COMPILE (default off)
|
||||
* HTTPLIB_INSTALL (default on)
|
||||
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
||||
* HTTPLIB_TEST (default off)
|
||||
* BROTLI_USE_STATIC_LIBS - tells Cmake to use the static Brotli libs (only works if you have them installed).
|
||||
* OPENSSL_USE_STATIC_LIBS - tells Cmake to use the static OpenSSL libs (only works if you have them installed).
|
||||
@@ -49,6 +51,7 @@
|
||||
* HTTPLIB_IS_USING_BROTLI - a bool for if Brotli support is enabled.
|
||||
* HTTPLIB_IS_USING_ZSTD - a bool for if ZSTD support is enabled.
|
||||
* HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN - a bool for if support of loading system certs from the Apple Keychain is enabled.
|
||||
* HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO - a bool for if nonblocking getaddrinfo is enabled.
|
||||
* HTTPLIB_IS_COMPILED - a bool for if the library is compiled, or otherwise header-only.
|
||||
* HTTPLIB_INCLUDE_DIR - the root path to httplib's header (e.g. /usr/include).
|
||||
* HTTPLIB_LIBRARY - the full path to the library if compiled (e.g. /usr/lib/libhttplib.so).
|
||||
@@ -104,19 +107,43 @@ option(HTTPLIB_TEST "Enables testing and builds tests" OFF)
|
||||
option(HTTPLIB_REQUIRE_BROTLI "Requires Brotli to be found & linked, or fails build." OFF)
|
||||
option(HTTPLIB_USE_BROTLI_IF_AVAILABLE "Uses Brotli (if available) to enable Brotli decompression support." ON)
|
||||
option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system certs from the Apple Keychain." ON)
|
||||
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
|
||||
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
|
||||
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)
|
||||
# Necessary for Windows if building shared libs
|
||||
# See https://stackoverflow.com/a/40743080
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
# Defaults to static library but respects standard BUILD_SHARED_LIBS if set
|
||||
include(CMakeDependentOption)
|
||||
cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only."
|
||||
"${BUILD_SHARED_LIBS}" HTTPLIB_COMPILE OFF
|
||||
)
|
||||
if(HTTPLIB_SHARED)
|
||||
set(HTTPLIB_LIB_TYPE SHARED)
|
||||
if(WIN32)
|
||||
# Necessary for Windows if building shared libs
|
||||
# See https://stackoverflow.com/a/40743080
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
endif()
|
||||
else()
|
||||
set(HTTPLIB_LIB_TYPE STATIC)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
|
||||
if(CMAKE_SYSTEM_VERSION)
|
||||
if(${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()
|
||||
else()
|
||||
set(CMAKE_SYSTEM_VERSION "10.0.19041.0")
|
||||
message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.")
|
||||
endif()
|
||||
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})
|
||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
||||
|
||||
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||
@@ -158,8 +185,11 @@ elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
|
||||
set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND})
|
||||
endif()
|
||||
|
||||
# NOTE:
|
||||
# zstd < 1.5.6 does not provide the CMake imported target `zstd::libzstd`.
|
||||
# Older versions must be consumed via their pkg-config file.
|
||||
if(HTTPLIB_REQUIRE_ZSTD)
|
||||
find_package(zstd)
|
||||
find_package(zstd 1.5.6 CONFIG)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
@@ -167,7 +197,7 @@ if(HTTPLIB_REQUIRE_ZSTD)
|
||||
endif()
|
||||
set(HTTPLIB_IS_USING_ZSTD TRUE)
|
||||
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
|
||||
find_package(zstd QUIET)
|
||||
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PKG_CONFIG_FOUND)
|
||||
@@ -212,8 +242,7 @@ if(HTTPLIB_COMPILE)
|
||||
|
||||
# split.py puts output in "out"
|
||||
set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out")
|
||||
# This will automatically be either static or shared based on the value of BUILD_SHARED_LIBS
|
||||
add_library(${PROJECT_NAME} "${_httplib_build_includedir}/httplib.cc")
|
||||
add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc")
|
||||
target_sources(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
|
||||
@@ -239,18 +268,20 @@ 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
|
||||
target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
# Always require threads
|
||||
Threads::Threads
|
||||
# Needed for Windows libs on Mingw, as the pragma comment(lib, "xyz") aren't triggered.
|
||||
$<$<PLATFORM_ID:Windows>:ws2_32>
|
||||
$<$<PLATFORM_ID:Windows>:crypt32>
|
||||
# Needed for API from MacOS Security framework
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CoreFoundation -framework Security>"
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
||||
# Needed for non-blocking getaddrinfo on MacOS
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||
# 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::encoder>
|
||||
@@ -269,6 +300,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_ZSTD}>:CPPHTTPLIB_ZSTD_SUPPORT>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>:CPPHTTPLIB_OPENSSL_SUPPORT>
|
||||
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_CERTS_FROM_MACOSX_KEYCHAIN}>>:CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN>
|
||||
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
||||
)
|
||||
|
||||
# CMake configuration files installation directory
|
||||
@@ -335,6 +367,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"]
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
# SSEClient - Server-Sent Events Client
|
||||
|
||||
A simple, EventSource-like SSE client for C++11.
|
||||
|
||||
## Features
|
||||
|
||||
- **Auto-reconnect**: Automatically reconnects on connection loss
|
||||
- **Last-Event-ID**: Sends last received ID on reconnect for resumption
|
||||
- **retry field**: Respects server's reconnect interval
|
||||
- **Event types**: Supports custom event types via `on_event()`
|
||||
- **Async support**: Run in background thread with `start_async()`
|
||||
- **C++11 compatible**: No C++14/17/20 features required
|
||||
|
||||
## Quick Start
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
httplib::sse::SSEClient sse(cli, "/events");
|
||||
|
||||
sse.on_message([](const httplib::sse::SSEMessage &msg) {
|
||||
std::cout << "Event: " << msg.event << std::endl;
|
||||
std::cout << "Data: " << msg.data << std::endl;
|
||||
});
|
||||
|
||||
sse.start(); // Blocking, with auto-reconnect
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### SSEMessage
|
||||
|
||||
```cpp
|
||||
struct SSEMessage {
|
||||
std::string event; // Event type (default: "message")
|
||||
std::string data; // Event payload
|
||||
std::string id; // Event ID
|
||||
};
|
||||
```
|
||||
|
||||
### SSEClient
|
||||
|
||||
#### Constructor
|
||||
|
||||
```cpp
|
||||
// Basic
|
||||
SSEClient(Client &client, const std::string &path);
|
||||
|
||||
// With custom headers
|
||||
SSEClient(Client &client, const std::string &path, const Headers &headers);
|
||||
```
|
||||
|
||||
#### Event Handlers
|
||||
|
||||
```cpp
|
||||
// Called for all events (or events without a specific handler)
|
||||
sse.on_message([](const SSEMessage &msg) { });
|
||||
|
||||
// Called for specific event types
|
||||
sse.on_event("update", [](const SSEMessage &msg) { });
|
||||
sse.on_event("delete", [](const SSEMessage &msg) { });
|
||||
|
||||
// Called when connection is established
|
||||
sse.on_open([]() { });
|
||||
|
||||
// Called on connection errors
|
||||
sse.on_error([](httplib::Error err) { });
|
||||
```
|
||||
|
||||
#### Configuration
|
||||
|
||||
```cpp
|
||||
// Set reconnect interval (default: 3000ms)
|
||||
sse.set_reconnect_interval(5000);
|
||||
|
||||
// Set max reconnect attempts (default: 0 = unlimited)
|
||||
sse.set_max_reconnect_attempts(10);
|
||||
```
|
||||
|
||||
#### Control
|
||||
|
||||
```cpp
|
||||
// Blocking start with auto-reconnect
|
||||
sse.start();
|
||||
|
||||
// Non-blocking start (runs in background thread)
|
||||
sse.start_async();
|
||||
|
||||
// Stop the client (thread-safe)
|
||||
sse.stop();
|
||||
```
|
||||
|
||||
#### State
|
||||
|
||||
```cpp
|
||||
bool connected = sse.is_connected();
|
||||
const std::string &id = sse.last_event_id();
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
httplib::sse::SSEClient sse(cli, "/events");
|
||||
|
||||
sse.on_message([](const httplib::sse::SSEMessage &msg) {
|
||||
std::cout << msg.data << std::endl;
|
||||
});
|
||||
|
||||
sse.start();
|
||||
```
|
||||
|
||||
### With Custom Event Types
|
||||
|
||||
```cpp
|
||||
httplib::sse::SSEClient sse(cli, "/events");
|
||||
|
||||
sse.on_event("notification", [](const httplib::sse::SSEMessage &msg) {
|
||||
std::cout << "Notification: " << msg.data << std::endl;
|
||||
});
|
||||
|
||||
sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
|
||||
std::cout << "Update: " << msg.data << std::endl;
|
||||
});
|
||||
|
||||
sse.start();
|
||||
```
|
||||
|
||||
### Async with Stop
|
||||
|
||||
```cpp
|
||||
httplib::sse::SSEClient sse(cli, "/events");
|
||||
|
||||
sse.on_message([](const httplib::sse::SSEMessage &msg) {
|
||||
std::cout << msg.data << std::endl;
|
||||
});
|
||||
|
||||
sse.start_async(); // Returns immediately
|
||||
|
||||
// ... do other work ...
|
||||
|
||||
sse.stop(); // Stop when done
|
||||
```
|
||||
|
||||
### With Custom Headers (e.g., Authentication)
|
||||
|
||||
```cpp
|
||||
httplib::Headers headers = {
|
||||
{"Authorization", "Bearer token123"}
|
||||
};
|
||||
|
||||
httplib::sse::SSEClient sse(cli, "/events", headers);
|
||||
sse.start();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```cpp
|
||||
sse.on_error([](httplib::Error err) {
|
||||
std::cerr << "Error: " << httplib::to_string(err) << std::endl;
|
||||
});
|
||||
|
||||
sse.set_reconnect_interval(1000);
|
||||
sse.set_max_reconnect_attempts(5);
|
||||
|
||||
sse.start();
|
||||
```
|
||||
|
||||
## SSE Protocol
|
||||
|
||||
The client parses SSE format according to the [W3C specification](https://html.spec.whatwg.org/multipage/server-sent-events.html):
|
||||
|
||||
```
|
||||
event: custom-type
|
||||
id: 123
|
||||
data: {"message": "hello"}
|
||||
|
||||
data: simple message
|
||||
|
||||
: this is a comment (ignored)
|
||||
```
|
||||
@@ -0,0 +1,317 @@
|
||||
# cpp-httplib Streaming API
|
||||
|
||||
This document describes the streaming extensions for cpp-httplib, providing an iterator-style API for handling HTTP responses incrementally with **true socket-level streaming**.
|
||||
|
||||
> **Important Notes**:
|
||||
>
|
||||
> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed after the response is fully read. For connection reuse, use `Client::Get()`.
|
||||
> - **Single iteration only**: The `next()` method can only iterate through the body once.
|
||||
> - **Result is not thread-safe**: While `stream::Get()` can be called from multiple threads simultaneously, the returned `stream::Result` must be used from a single thread only.
|
||||
|
||||
## Overview
|
||||
|
||||
The streaming API allows you to process HTTP response bodies chunk by chunk using an iterator-style pattern. Data is read directly from the network socket, enabling low-memory processing of large responses. This is particularly useful for:
|
||||
|
||||
- **LLM/AI streaming responses** (e.g., ChatGPT, Claude, Ollama)
|
||||
- **Server-Sent Events (SSE)**
|
||||
- **Large file downloads** with progress tracking
|
||||
- **Reverse proxy implementations**
|
||||
|
||||
## Quick Start
|
||||
|
||||
```cpp
|
||||
#include "httplib.h"
|
||||
|
||||
int main() {
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
|
||||
// Get streaming response
|
||||
auto result = httplib::stream::Get(cli, "/stream");
|
||||
|
||||
if (result) {
|
||||
// Process response body in chunks
|
||||
while (result.next()) {
|
||||
std::cout.write(result.data(), result.size());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Layers
|
||||
|
||||
cpp-httplib provides multiple API layers for different use cases:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SSEClient (planned) │ ← SSE-specific, parsed events
|
||||
│ - on_message(), on_event() │
|
||||
│ - Auto-reconnect, Last-Event-ID │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ stream::Get() / stream::Result │ ← Iterator-based streaming
|
||||
│ - while (result.next()) { ... } │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ open_stream() / StreamHandle │ ← General-purpose streaming
|
||||
│ - handle.read(buf, len) │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Client::Get() │ ← Traditional, full buffering
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Use Case | Recommended API |
|
||||
|----------|----------------|
|
||||
| SSE with auto-reconnect | SSEClient (planned) or `ssecli-stream.cc` example |
|
||||
| LLM streaming (JSON Lines) | `stream::Get()` |
|
||||
| Large file download | `stream::Get()` or `open_stream()` |
|
||||
| Reverse proxy | `open_stream()` |
|
||||
| Small responses with Keep-Alive | `Client::Get()` |
|
||||
|
||||
## API Reference
|
||||
|
||||
### Low-Level API: `StreamHandle`
|
||||
|
||||
The `StreamHandle` struct provides direct control over streaming responses. It takes ownership of the socket connection and reads data directly from the network.
|
||||
|
||||
> **Note:** When using `open_stream()`, the connection is dedicated to streaming and **Keep-Alive is not supported**. For Keep-Alive connections, use `client.Get()` instead.
|
||||
|
||||
```cpp
|
||||
// Open a stream (takes ownership of socket)
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
auto handle = cli.open_stream("GET", "/path");
|
||||
|
||||
// Check validity
|
||||
if (handle.is_valid()) {
|
||||
// Access response headers immediately
|
||||
int status = handle.response->status;
|
||||
auto content_type = handle.response->get_header_value("Content-Type");
|
||||
|
||||
// Read body incrementally
|
||||
char buf[4096];
|
||||
ssize_t n;
|
||||
while ((n = handle.read(buf, sizeof(buf))) > 0) {
|
||||
process(buf, n);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### StreamHandle Members
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `response` | `std::unique_ptr<Response>` | HTTP response with headers |
|
||||
| `error` | `Error` | Error code if request failed |
|
||||
| `is_valid()` | `bool` | Returns true if response is valid |
|
||||
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket |
|
||||
| `get_read_error()` | `Error` | Get the last read error |
|
||||
| `has_read_error()` | `bool` | Check if a read error occurred |
|
||||
|
||||
### High-Level API: `stream::Get()` and `stream::Result`
|
||||
|
||||
The `httplib.h` header provides a more ergonomic iterator-style API.
|
||||
|
||||
```cpp
|
||||
#include "httplib.h"
|
||||
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
cli.set_follow_location(true);
|
||||
...
|
||||
|
||||
// Simple GET
|
||||
auto result = httplib::stream::Get(cli, "/path");
|
||||
|
||||
// GET with custom headers
|
||||
httplib::Headers headers = {{"Authorization", "Bearer token"}};
|
||||
auto result = httplib::stream::Get(cli, "/path", headers);
|
||||
|
||||
// Process the response
|
||||
if (result) {
|
||||
while (result.next()) {
|
||||
process(result.data(), result.size());
|
||||
}
|
||||
}
|
||||
|
||||
// Or read entire body at once
|
||||
auto result2 = httplib::stream::Get(cli, "/path");
|
||||
if (result2) {
|
||||
std::string body = result2.read_all();
|
||||
}
|
||||
```
|
||||
|
||||
#### stream::Result Members
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `operator bool()` | `bool` | Returns true if response is valid |
|
||||
| `is_valid()` | `bool` | Same as `operator bool()` |
|
||||
| `status()` | `int` | HTTP status code |
|
||||
| `headers()` | `const Headers&` | Response headers |
|
||||
| `get_header_value(key, def)` | `std::string` | Get header value (with optional default) |
|
||||
| `has_header(key)` | `bool` | Check if header exists |
|
||||
| `next()` | `bool` | Read next chunk, returns false when done |
|
||||
| `data()` | `const char*` | Pointer to current chunk data |
|
||||
| `size()` | `size_t` | Size of current chunk |
|
||||
| `read_all()` | `std::string` | Read entire remaining body into string |
|
||||
| `error()` | `Error` | Get the connection/request error |
|
||||
| `read_error()` | `Error` | Get the last read error |
|
||||
| `has_read_error()` | `bool` | Check if a read error occurred |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: SSE (Server-Sent Events) Client
|
||||
|
||||
```cpp
|
||||
#include "httplib.h"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
httplib::Client cli("http://localhost:1234");
|
||||
|
||||
auto result = httplib::stream::Get(cli, "/events");
|
||||
if (!result) { return 1; }
|
||||
|
||||
while (result.next()) {
|
||||
std::cout.write(result.data(), result.size());
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
For a complete SSE client with auto-reconnection and event parsing, see `example/ssecli-stream.cc`.
|
||||
|
||||
### Example 2: LLM Streaming Response
|
||||
|
||||
```cpp
|
||||
#include "httplib.h"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
httplib::Client cli("http://localhost:11434"); // Ollama
|
||||
|
||||
auto result = httplib::stream::Get(cli, "/api/generate");
|
||||
|
||||
if (result && result.status() == 200) {
|
||||
while (result.next()) {
|
||||
std::cout.write(result.data(), result.size());
|
||||
std::cout.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for connection errors
|
||||
if (result.read_error() != httplib::Error::Success) {
|
||||
std::cerr << "Connection lost\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Large File Download with Progress
|
||||
|
||||
```cpp
|
||||
#include "httplib.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
httplib::Client cli("http://example.com");
|
||||
auto result = httplib::stream::Get(cli, "/large-file.zip");
|
||||
|
||||
if (!result || result.status() != 200) {
|
||||
std::cerr << "Download failed\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::ofstream file("download.zip", std::ios::binary);
|
||||
size_t total = 0;
|
||||
|
||||
while (result.next()) {
|
||||
file.write(result.data(), result.size());
|
||||
total += result.size();
|
||||
std::cout << "\rDownloaded: " << (total / 1024) << " KB" << std::flush;
|
||||
}
|
||||
|
||||
std::cout << "\nComplete!\n";
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Reverse Proxy Streaming
|
||||
|
||||
```cpp
|
||||
#include "httplib.h"
|
||||
|
||||
httplib::Server svr;
|
||||
|
||||
svr.Get("/proxy/(.*)", [](const httplib::Request& req, httplib::Response& res) {
|
||||
httplib::Client upstream("http://backend:8080");
|
||||
auto handle = upstream.open_stream("/" + req.matches[1].str());
|
||||
|
||||
if (!handle.is_valid()) {
|
||||
res.status = 502;
|
||||
return;
|
||||
}
|
||||
|
||||
res.status = handle.response->status;
|
||||
res.set_chunked_content_provider(
|
||||
handle.response->get_header_value("Content-Type"),
|
||||
[handle = std::move(handle)](size_t, httplib::DataSink& sink) mutable {
|
||||
char buf[8192];
|
||||
auto n = handle.read(buf, sizeof(buf));
|
||||
if (n > 0) {
|
||||
sink.write(buf, static_cast<size_t>(n));
|
||||
return true;
|
||||
}
|
||||
sink.done();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
svr.listen("0.0.0.0", 3000);
|
||||
```
|
||||
|
||||
## Comparison with Existing APIs
|
||||
|
||||
| Feature | `Client::Get()` | `open_stream()` | `stream::Get()` |
|
||||
|---------|----------------|-----------------|----------------|
|
||||
| Headers available | After complete | Immediately | Immediately |
|
||||
| Body reading | All at once | Direct from socket | Iterator-based |
|
||||
| Memory usage | Full body in RAM | Minimal (controlled) | Minimal (controlled) |
|
||||
| Keep-Alive support | ✅ Yes | ❌ No | ❌ No |
|
||||
| Compression | Auto-handled | Auto-handled | Auto-handled |
|
||||
| Best for | Small responses, Keep-Alive | Low-level streaming | Easy streaming |
|
||||
|
||||
## Features
|
||||
|
||||
- **True socket-level streaming**: Data is read directly from the network socket
|
||||
- **Low memory footprint**: Only the current chunk is held in memory
|
||||
- **Compression support**: Automatic decompression for gzip, brotli, and zstd
|
||||
- **Chunked transfer**: Full support for chunked transfer encoding
|
||||
- **SSL/TLS support**: Works with HTTPS connections
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Keep-Alive Behavior
|
||||
|
||||
The streaming API (`stream::Get()` / `open_stream()`) takes ownership of the socket connection for the duration of the stream. This means:
|
||||
|
||||
- **Keep-Alive is not supported** for streaming connections
|
||||
- The socket is closed when `StreamHandle` is destroyed
|
||||
- For Keep-Alive scenarios, use the standard `client.Get()` API instead
|
||||
|
||||
```cpp
|
||||
// Use for streaming (no Keep-Alive)
|
||||
auto result = httplib::stream::Get(cli, "/large-stream");
|
||||
while (result.next()) { /* ... */ }
|
||||
|
||||
// Use for Keep-Alive connections
|
||||
auto res = cli.Get("/api/data"); // Connection can be reused
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
|
||||
- [example/ssecli-stream.cc](./example/ssecli-stream.cc) - SSE client with auto-reconnection
|
||||
@@ -5,7 +5,7 @@ cpp-httplib
|
||||
|
||||
A C++11 single-file header-only cross platform HTTP/HTTPS library.
|
||||
|
||||
It's extremely easy to setup. Just include the **httplib.h** file in your code!
|
||||
It's extremely easy to set up. Just include the **httplib.h** file in your code!
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
|
||||
@@ -44,9 +44,10 @@ httplib::Client cli("http://yhirose.github.io");
|
||||
// HTTPS
|
||||
httplib::Client cli("https://yhirose.github.io");
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
res->status;
|
||||
res->body;
|
||||
if (auto res = cli.Get("/hi")) {
|
||||
res->status;
|
||||
res->body;
|
||||
}
|
||||
```
|
||||
|
||||
SSL Support
|
||||
@@ -85,6 +86,48 @@ cli.enable_server_hostname_verification(false);
|
||||
> [!NOTE]
|
||||
> When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.
|
||||
|
||||
### SSL Error Handling
|
||||
|
||||
When SSL operations fail, cpp-httplib provides detailed error information through two separate error fields:
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include "path/to/httplib.h"
|
||||
|
||||
httplib::Client cli("https://example.com");
|
||||
|
||||
auto res = cli.Get("/");
|
||||
if (!res) {
|
||||
// Check the error type
|
||||
const auto err = res.error();
|
||||
|
||||
switch (err) {
|
||||
case httplib::Error::SSLConnection:
|
||||
std::cout << "SSL connection failed, SSL error: "
|
||||
<< 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;
|
||||
break;
|
||||
|
||||
case httplib::Error::SSLServerVerification:
|
||||
std::cout << "SSL verification failed, X509 error: "
|
||||
<< 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;
|
||||
break;
|
||||
|
||||
default:
|
||||
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Server
|
||||
------
|
||||
|
||||
@@ -148,7 +191,7 @@ int main(void)
|
||||
}
|
||||
```
|
||||
|
||||
`Post`, `Put`, `Delete` and `Options` methods are also supported.
|
||||
`Post`, `Put`, `Patch`, `Delete` and `Options` methods are also supported.
|
||||
|
||||
### Bind a socket to multiple interfaces and any available port
|
||||
|
||||
@@ -187,7 +230,7 @@ svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c");
|
||||
svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h");
|
||||
```
|
||||
|
||||
The followings are built-in mappings:
|
||||
The following are built-in mappings:
|
||||
|
||||
| Extension | MIME Type | Extension | MIME Type |
|
||||
| :--------- | :-------------------------- | :--------- | :-------------------------- |
|
||||
@@ -201,7 +244,7 @@ The followings are built-in mappings:
|
||||
| bmp | image/bmp | 7z | application/x-7z-compressed |
|
||||
| gif | image/gif | atom | application/atom+xml |
|
||||
| png | image/png | pdf | application/pdf |
|
||||
| svg | image/svg+xml | mjs, js | application/javascript |
|
||||
| svg | image/svg+xml | mjs, js | text/javascript |
|
||||
| webp | image/webp | json | application/json |
|
||||
| ico | image/x-icon | rss | application/rss+xml |
|
||||
| tif | image/tiff | tar | application/x-tar |
|
||||
@@ -226,9 +269,45 @@ 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;
|
||||
});
|
||||
```
|
||||
|
||||
#### 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 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 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;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -301,16 +380,80 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
||||
});
|
||||
```
|
||||
|
||||
### 'multipart/form-data' POST data
|
||||
### Form data handling
|
||||
|
||||
#### URL-encoded form data ('application/x-www-form-urlencoded')
|
||||
|
||||
```cpp
|
||||
svr.Post("/multipart", [&](const auto& req, auto& res) {
|
||||
auto size = req.files.size();
|
||||
auto ret = req.has_file("name1");
|
||||
const auto& file = req.get_file_value("name1");
|
||||
// file.filename;
|
||||
// file.content_type;
|
||||
// file.content;
|
||||
svr.Post("/form", [&](const auto& req, auto& res) {
|
||||
// URL query parameters and form-encoded data are accessible via req.params
|
||||
std::string username = req.get_param_value("username");
|
||||
std::string password = req.get_param_value("password");
|
||||
|
||||
// Handle multiple values with same name
|
||||
auto interests = req.get_param_values("interests");
|
||||
|
||||
// Check existence
|
||||
if (req.has_param("newsletter")) {
|
||||
// Handle newsletter subscription
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 'multipart/form-data' POST data
|
||||
|
||||
```cpp
|
||||
svr.Post("/multipart", [&](const Request& req, Response& res) {
|
||||
// Access text fields (from form inputs without files)
|
||||
std::string username = req.form.get_field("username");
|
||||
std::string bio = req.form.get_field("bio");
|
||||
|
||||
// Access uploaded files
|
||||
if (req.form.has_file("avatar")) {
|
||||
const auto& file = req.form.get_file("avatar");
|
||||
std::cout << "Uploaded file: " << file.filename
|
||||
<< " (" << file.content_type << ") - "
|
||||
<< file.content.size() << " bytes" << std::endl;
|
||||
|
||||
// Access additional headers if needed
|
||||
for (const auto& header : file.headers) {
|
||||
std::cout << "Header: " << header.first << " = " << header.second << std::endl;
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
std::ofstream ofs(file.filename, std::ios::binary);
|
||||
ofs << file.content;
|
||||
}
|
||||
|
||||
// Handle multiple values with same name
|
||||
auto tags = req.form.get_fields("tags"); // e.g., multiple checkboxes
|
||||
for (const auto& tag : tags) {
|
||||
std::cout << "Tag: " << tag << std::endl;
|
||||
}
|
||||
|
||||
auto documents = req.form.get_files("documents"); // multiple file upload
|
||||
for (const auto& doc : documents) {
|
||||
std::cout << "Document: " << doc.filename
|
||||
<< " (" << doc.content.size() << " bytes)" << std::endl;
|
||||
}
|
||||
|
||||
// Check existence before accessing
|
||||
if (req.form.has_field("newsletter")) {
|
||||
std::cout << "Newsletter subscription: " << req.form.get_field("newsletter") << std::endl;
|
||||
}
|
||||
|
||||
// Get counts for validation
|
||||
if (req.form.get_field_count("tags") > 5) {
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
res.set_content("Too many tags", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// Summary
|
||||
std::cout << "Received " << req.form.fields.size() << " text fields and "
|
||||
<< req.form.files.size() << " files" << std::endl;
|
||||
|
||||
res.set_content("Upload successful", "text/plain");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -321,16 +464,29 @@ svr.Post("/content_receiver",
|
||||
[&](const Request &req, Response &res, const ContentReader &content_reader) {
|
||||
if (req.is_multipart_form_data()) {
|
||||
// NOTE: `content_reader` is blocking until every form data field is read
|
||||
MultipartFormDataItems files;
|
||||
// This approach allows streaming processing of large files
|
||||
std::vector<FormData> items;
|
||||
content_reader(
|
||||
[&](const MultipartFormData &file) {
|
||||
files.push_back(file);
|
||||
[&](const FormData &item) {
|
||||
items.push_back(item);
|
||||
return true;
|
||||
},
|
||||
[&](const char *data, size_t data_length) {
|
||||
files.back().content.append(data, data_length);
|
||||
items.back().content.append(data, data_length);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Process the received items
|
||||
for (const auto& item : items) {
|
||||
if (item.filename.empty()) {
|
||||
// Text field
|
||||
std::cout << "Field: " << item.name << " = " << item.content << std::endl;
|
||||
} else {
|
||||
// File
|
||||
std::cout << "File: " << item.name << " (" << item.filename << ") - "
|
||||
<< item.content.size() << " bytes" << std::endl;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::string body;
|
||||
content_reader([&](const char *data, size_t data_length) {
|
||||
@@ -451,7 +607,7 @@ svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
|
||||
### Keep-Alive connection
|
||||
|
||||
```cpp
|
||||
svr.set_keep_alive_max_count(2); // Default is 5
|
||||
svr.set_keep_alive_max_count(2); // Default is 100
|
||||
svr.set_keep_alive_timeout(10); // Default is 5
|
||||
```
|
||||
|
||||
@@ -566,7 +722,7 @@ httplib::SSLClient cli("localhost");
|
||||
Here is the list of errors from `Result::error()`.
|
||||
|
||||
```c++
|
||||
enum Error {
|
||||
enum class Error {
|
||||
Success = 0,
|
||||
Unknown,
|
||||
Connection,
|
||||
@@ -578,12 +734,77 @@ enum Error {
|
||||
SSLConnection,
|
||||
SSLLoadingCerts,
|
||||
SSLServerVerification,
|
||||
SSLServerHostnameVerification,
|
||||
UnsupportedMultipartBoundaryChars,
|
||||
Compression,
|
||||
ConnectionTimeout,
|
||||
ProxyConnection,
|
||||
ConnectionClosed,
|
||||
Timeout,
|
||||
ResourceExhaustion,
|
||||
TooManyFormDataFiles,
|
||||
ExceedMaxPayloadSize,
|
||||
ExceedUriMaxLength,
|
||||
ExceedMaxSocketDescriptorCount,
|
||||
InvalidRequestLine,
|
||||
InvalidHTTPMethod,
|
||||
InvalidHTTPVersion,
|
||||
InvalidHeaders,
|
||||
MultipartParsing,
|
||||
OpenFile,
|
||||
Listen,
|
||||
GetSockName,
|
||||
UnsupportedAddressFamily,
|
||||
HTTPParsing,
|
||||
InvalidRangeHeader,
|
||||
};
|
||||
```
|
||||
|
||||
### 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++
|
||||
@@ -634,7 +855,7 @@ auto res = cli.Post("/post", params);
|
||||
### POST with Multipart Form Data
|
||||
|
||||
```c++
|
||||
httplib::MultipartFormDataItems items = {
|
||||
httplib::UploadFormDataItems items = {
|
||||
{ "text1", "text default", "", "" },
|
||||
{ "text2", "aωb", "", "" },
|
||||
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
|
||||
@@ -651,6 +872,12 @@ auto res = cli.Post("/multipart", items);
|
||||
res = cli.Put("/resource/foo", "text", "text/plain");
|
||||
```
|
||||
|
||||
### PATCH
|
||||
|
||||
```c++
|
||||
res = cli.Patch("/resource/foo", "text", "text/plain");
|
||||
```
|
||||
|
||||
### DELETE
|
||||
|
||||
```c++
|
||||
@@ -672,7 +899,7 @@ cli.set_read_timeout(5, 0); // 5 seconds
|
||||
cli.set_write_timeout(5, 0); // 5 seconds
|
||||
|
||||
// This method works the same as curl's `--max-time` option
|
||||
svr.set_max_timeout(5000); // 5 seconds
|
||||
cli.set_max_timeout(5000); // 5 seconds
|
||||
```
|
||||
|
||||
### Receive content with a content receiver
|
||||
@@ -737,7 +964,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));
|
||||
@@ -785,7 +1012,7 @@ cli.set_proxy_bearer_token_auth("pass");
|
||||
### Range
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("httpbin.org");
|
||||
httplib::Client cli("httpcan.org");
|
||||
|
||||
auto res = cli.Get("/range/32", {
|
||||
httplib::make_range_header({{1, 10}}) // 'Range: bytes=1-10'
|
||||
@@ -836,6 +1063,45 @@ res->status; // 200
|
||||
cli.set_interface("eth0"); // Interface name, IP address or host name
|
||||
```
|
||||
|
||||
### Automatic Path Encoding
|
||||
|
||||
The client automatically encodes special characters in URL paths by default:
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("https://example.com");
|
||||
|
||||
// Automatic path encoding (default behavior)
|
||||
cli.set_path_encode(true);
|
||||
auto res = cli.Get("/path with spaces/file.txt"); // Automatically encodes spaces
|
||||
|
||||
// Disable automatic path encoding
|
||||
cli.set_path_encode(false);
|
||||
auto res = cli.Get("/already%20encoded/path"); // Use pre-encoded paths
|
||||
```
|
||||
|
||||
- `set_path_encode(bool on)` - Controls automatic encoding of special characters in URL 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
|
||||
-----------
|
||||
|
||||
@@ -846,6 +1112,7 @@ The server can apply compression to the following MIME type contents:
|
||||
* application/javascript
|
||||
* application/json
|
||||
* application/xml
|
||||
* application/protobuf
|
||||
* application/xhtml+xml
|
||||
|
||||
### Zlib Support
|
||||
@@ -857,13 +1124,18 @@ The server can apply compression to the following MIME type contents:
|
||||
Brotli compression is available with `CPPHTTPLIB_BROTLI_SUPPORT`. Necessary libraries should be linked.
|
||||
Please see https://github.com/google/brotli for more detail.
|
||||
|
||||
### Zstd Support
|
||||
|
||||
Zstd compression is available with `CPPHTTPLIB_ZSTD_SUPPORT`. Necessary libraries should be linked.
|
||||
Please see https://github.com/facebook/zstd for more detail.
|
||||
|
||||
### Default `Accept-Encoding` value
|
||||
|
||||
The default `Accept-Encoding` value contains all possible compression types. So, the following two examples are same.
|
||||
|
||||
```c++
|
||||
res = cli.Get("/resource/foo");
|
||||
res = cli.Get("/resource/foo", {{"Accept-Encoding", "gzip, deflate, br"}});
|
||||
res = cli.Get("/resource/foo", {{"Accept-Encoding", "br, gzip, deflate, zstd"}});
|
||||
```
|
||||
|
||||
If we don't want a response without compression, we have to set `Accept-Encoding` to an empty string. This behavior is similar to curl.
|
||||
@@ -903,8 +1175,86 @@ httplib::Client cli("./my-socket.sock");
|
||||
cli.set_address_family(AF_UNIX);
|
||||
```
|
||||
|
||||
"my-socket.sock" can be a relative path or an absolute path. You 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.
|
||||
"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
|
||||
-------------------------------
|
||||
|
||||
cpp-httplib provides utility functions for URI encoding and decoding:
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
|
||||
std::string url = "https://example.com/search?q=hello world";
|
||||
std::string encoded = httplib::encode_uri(url);
|
||||
std::string decoded = httplib::decode_uri(encoded);
|
||||
|
||||
std::string param = "hello world";
|
||||
std::string encoded_component = httplib::encode_uri_component(param);
|
||||
std::string decoded_component = httplib::decode_uri_component(encoded_component);
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
- `encode_uri(const std::string &value)` - Encodes a full URI, preserving reserved characters like `://`, `?`, `&`, `=`
|
||||
- `decode_uri(const std::string &value)` - Decodes a URI-encoded string
|
||||
- `encode_uri_component(const std::string &value)` - Encodes a URI component (query parameter, path segment), encoding all reserved characters
|
||||
- `decode_uri_component(const std::string &value)` - Decodes a URI component
|
||||
|
||||
Use `encode_uri()` for full URLs and `encode_uri_component()` for individual query parameters or path segments.
|
||||
|
||||
Stream API
|
||||
----------
|
||||
|
||||
Process large responses without loading everything into memory.
|
||||
|
||||
```c++
|
||||
httplib::Client cli("localhost", 8080);
|
||||
cli.set_follow_location(true);
|
||||
...
|
||||
|
||||
auto result = httplib::stream::Get(cli, "/large-file");
|
||||
if (result) {
|
||||
while (result.next()) {
|
||||
process(result.data(), result.size()); // Process each chunk as it arrives
|
||||
}
|
||||
}
|
||||
|
||||
// Or read the entire body at once
|
||||
auto result2 = httplib::stream::Get(cli, "/file");
|
||||
if (result2) {
|
||||
std::string body = result2.read_all();
|
||||
}
|
||||
```
|
||||
|
||||
All HTTP methods are supported: `stream::Get`, `Post`, `Put`, `Patch`, `Delete`, `Head`, `Options`.
|
||||
|
||||
See [README-stream.md](README-stream.md) for more details.
|
||||
|
||||
SSE Client
|
||||
----------
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
|
||||
int main() {
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
httplib::sse::SSEClient sse(cli, "/events");
|
||||
|
||||
sse.on_message([](const httplib::sse::SSEMessage &msg) {
|
||||
std::cout << "Event: " << msg.event << std::endl;
|
||||
std::cout << "Data: " << msg.data << std::endl;
|
||||
});
|
||||
|
||||
sse.start(); // Blocking, with auto-reconnect
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
See [README-sse.md](README-sse.md) for more details.
|
||||
|
||||
Split httplib.h into .h and .cc
|
||||
-------------------------------
|
||||
@@ -954,6 +1304,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).
|
||||
@@ -982,7 +1350,7 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
|
||||
License
|
||||
-------
|
||||
|
||||
MIT license (© 2025 Yuji Hirose)
|
||||
MIT license (© 2026 Yuji Hirose)
|
||||
|
||||
Special Thanks To
|
||||
-----------------
|
||||
|
||||
+2
-32
@@ -20,36 +20,6 @@ run : server
|
||||
server : cpp-httplib/main.cpp ../httplib.h
|
||||
@g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib/main.cpp
|
||||
|
||||
# cpp-httplib v0.19.0
|
||||
bench-v19: server-v19
|
||||
@echo "---------------------\n cpp-httplib v0.19.0\n---------------------\n"
|
||||
@./server-v19 & export PID=$$!; $(BENCH); kill $${PID}
|
||||
@echo ""
|
||||
|
||||
monitor-v19: server-v19
|
||||
@./server-v19 & export PID=$$!; $(MONITOR); kill $${PID}
|
||||
|
||||
run-v19 : server-v19
|
||||
@./server-v19
|
||||
|
||||
server-v19 : cpp-httplib-v19/main.cpp cpp-httplib-v19/httplib.h
|
||||
@g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib-v19/main.cpp
|
||||
|
||||
# cpp-httplib v0.18.0
|
||||
bench-v18: server-v18
|
||||
@echo "---------------------\n cpp-httplib v0.18.0\n---------------------\n"
|
||||
@./server-v18 & export PID=$$!; $(BENCH); kill $${PID}
|
||||
@echo ""
|
||||
|
||||
monitor-v18: server-v18
|
||||
@./server-v18 & export PID=$$!; $(MONITOR); kill $${PID}
|
||||
|
||||
run-v18 : server-v18
|
||||
@./server-v18
|
||||
|
||||
server-v18 : cpp-httplib-v18/main.cpp cpp-httplib-v18/httplib.h
|
||||
@g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib-v18/main.cpp
|
||||
|
||||
# crow
|
||||
bench-crow: server-crow
|
||||
@echo "-------------\n Crow v1.2.0\n-------------\n"
|
||||
@@ -66,9 +36,9 @@ server-crow : crow/main.cpp
|
||||
@g++ -o $@ $(CXXFLAGS) crow/main.cpp
|
||||
|
||||
# misc
|
||||
build: server server-v18 server-v19 server-crow
|
||||
build: server server-crow
|
||||
|
||||
bench-all: bench-crow bench bench-v19 bench-v18
|
||||
bench-all: bench-crow bench
|
||||
|
||||
issue:
|
||||
bombardier -c 10 -d 30s localhost:8080
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
#include "./httplib.h"
|
||||
using namespace httplib;
|
||||
|
||||
int main() {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [](const Request &, Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
#include "./httplib.h"
|
||||
using namespace httplib;
|
||||
|
||||
int main() {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [](const Request &, Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
rm -f httplib.h
|
||||
wget https://raw.githubusercontent.com/yhirose/cpp-httplib/v$1/httplib.h
|
||||
@@ -86,7 +86,9 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
|
||||
# Check if the target was already found by Pkgconf
|
||||
if(TARGET PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
|
||||
# ALIAS since we don't want the PkgConfig namespace on the Cmake library (for end-users)
|
||||
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
|
||||
if (NOT TARGET Brotli::${_component_name})
|
||||
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
|
||||
endif()
|
||||
|
||||
# Tells HANDLE_COMPONENTS we found the component
|
||||
set(Brotli_${_component_name}_FOUND TRUE)
|
||||
@@ -139,7 +141,10 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
|
||||
# Tells HANDLE_COMPONENTS we found the component
|
||||
set(Brotli_${_component_name}_FOUND TRUE)
|
||||
|
||||
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
|
||||
if (NOT TARGET Brotli::${_component_name})
|
||||
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
|
||||
endif()
|
||||
|
||||
# Attach the literal library and include dir to the IMPORTED target for the end-user
|
||||
set_target_properties("Brotli::${_component_name}" PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Brotli_INCLUDE_DIR}"
|
||||
|
||||
@@ -7,6 +7,7 @@ set(HTTPLIB_IS_USING_OPENSSL @HTTPLIB_IS_USING_OPENSSL@)
|
||||
set(HTTPLIB_IS_USING_ZLIB @HTTPLIB_IS_USING_ZLIB@)
|
||||
set(HTTPLIB_IS_COMPILED @HTTPLIB_COMPILE@)
|
||||
set(HTTPLIB_IS_USING_BROTLI @HTTPLIB_IS_USING_BROTLI@)
|
||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO @HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO@)
|
||||
set(HTTPLIB_VERSION @PROJECT_VERSION@)
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
+265
-43
@@ -1,81 +1,303 @@
|
||||
//
|
||||
// main.cc
|
||||
//
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// 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);
|
||||
// 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) {
|
||||
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 = "-";
|
||||
|
||||
auto base_dir = "./html";
|
||||
auto host = "0.0.0.0";
|
||||
auto port = 80;
|
||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||
req.remote_addr, remote_user, time_local, request,
|
||||
status, body_bytes_sent, http_referer,
|
||||
http_user_agent)
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
httplib::Server svr;
|
||||
// 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";
|
||||
|
||||
svr.set_error_handler([](auto & /*req*/, auto &res) {
|
||||
auto body =
|
||||
std::format(error_html, res.status, httplib::status_message(res.status),
|
||||
CPPHTTPLIB_VERSION);
|
||||
if (req) {
|
||||
auto request =
|
||||
std::format("{} {} {}", req->method, req->path, req->version);
|
||||
auto host = req->get_header_value("Host");
|
||||
if (host.empty()) host = "-";
|
||||
|
||||
res.set_content(body, "text/html");
|
||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||
"\"{}\", host: \"{}\"",
|
||||
time_local, level, to_string(err),
|
||||
req->remote_addr, 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 << " --trusted-proxy <ip> Add trusted proxy IP address"
|
||||
<< std::endl;
|
||||
std::cout << " (can be specified multiple times)"
|
||||
<< 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;
|
||||
std::cout << " " << program_name
|
||||
<< " --trusted-proxy 192.168.1.100 --trusted-proxy 10.0.0.1"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
struct ServerConfig {
|
||||
std::string hostname = "localhost";
|
||||
int port = 8080;
|
||||
std::string mount_point = "/";
|
||||
std::string document_root = "./html";
|
||||
std::vector<std::string> trusted_proxies;
|
||||
};
|
||||
|
||||
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 if (strcmp(argv[i], "--trusted-proxy") == 0) {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "Error: --trusted-proxy requires an IP address argument"
|
||||
<< std::endl;
|
||||
print_usage(argv[0]);
|
||||
return ParseResult::ERROR;
|
||||
}
|
||||
config.trusted_proxies.push_back(argv[++i]);
|
||||
} 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);
|
||||
|
||||
// Set trusted proxies if specified
|
||||
if (!config.trusted_proxies.empty()) {
|
||||
svr.set_trusted_proxies(config.trusted_proxies);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
auto ret = svr.listen(host, port);
|
||||
if (!config.trusted_proxies.empty()) {
|
||||
std::cout << "Trusted proxies: ";
|
||||
for (size_t i = 0; i < config.trusted_proxies.size(); ++i) {
|
||||
if (i > 0) std::cout << ", ";
|
||||
std::cout << config.trusted_proxies[i];
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl;
|
||||
|
||||
auto ret = svr.listen(config.hostname, config.port);
|
||||
|
||||
std::cout << "Server has been shut down." << std::endl;
|
||||
|
||||
return ret ? 0 : 1;
|
||||
}
|
||||
|
||||
+5
-2
@@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = $(PREFIX)/opt/brotli
|
||||
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client
|
||||
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header
|
||||
|
||||
server : server.cc ../httplib.h Makefile
|
||||
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
@@ -56,9 +56,12 @@ one_time_request : one_time_request.cc ../httplib.h Makefile
|
||||
server_and_client : server_and_client.cc ../httplib.h Makefile
|
||||
$(CXX) -o server_and_client $(CXXFLAGS) server_and_client.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
|
||||
accept_header : accept_header.cc ../httplib.h Makefile
|
||||
$(CXX) -o accept_header $(CXXFLAGS) accept_header.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
|
||||
pem:
|
||||
openssl genrsa 2048 > key.pem
|
||||
openssl req -new -key key.pem | openssl x509 -days 3650 -req -signkey key.pem > cert.pem
|
||||
|
||||
clean:
|
||||
rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client *.pem
|
||||
rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header *.pem
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "httplib.h"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
using namespace httplib;
|
||||
|
||||
// Example usage of parse_accept_header function
|
||||
std::cout << "=== Accept Header Parser Example ===" << std::endl;
|
||||
|
||||
// Example 1: Simple Accept header
|
||||
std::string accept1 = "text/html,application/json,text/plain";
|
||||
std::vector<std::string> result1;
|
||||
if (detail::parse_accept_header(accept1, result1)) {
|
||||
std::cout << "\nExample 1: " << accept1 << std::endl;
|
||||
std::cout << "Parsed order:" << std::endl;
|
||||
for (size_t i = 0; i < result1.size(); ++i) {
|
||||
std::cout << " " << (i + 1) << ". " << result1[i] << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "\nExample 1: Failed to parse Accept header" << std::endl;
|
||||
}
|
||||
|
||||
// Example 2: Accept header with quality values
|
||||
std::string accept2 = "text/html;q=0.9,application/json;q=1.0,text/plain;q=0.8";
|
||||
std::vector<std::string> result2;
|
||||
if (detail::parse_accept_header(accept2, result2)) {
|
||||
std::cout << "\nExample 2: " << accept2 << std::endl;
|
||||
std::cout << "Parsed order (sorted by priority):" << std::endl;
|
||||
for (size_t i = 0; i < result2.size(); ++i) {
|
||||
std::cout << " " << (i + 1) << ". " << result2[i] << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "\nExample 2: Failed to parse Accept header" << std::endl;
|
||||
}
|
||||
|
||||
// Example 3: Browser-like Accept header
|
||||
std::string accept3 = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
|
||||
std::vector<std::string> result3;
|
||||
if (detail::parse_accept_header(accept3, result3)) {
|
||||
std::cout << "\nExample 3: " << accept3 << std::endl;
|
||||
std::cout << "Parsed order:" << std::endl;
|
||||
for (size_t i = 0; i < result3.size(); ++i) {
|
||||
std::cout << " " << (i + 1) << ". " << result3[i] << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "\nExample 3: Failed to parse Accept header" << std::endl;
|
||||
}
|
||||
|
||||
// Example 4: Invalid Accept header examples
|
||||
std::cout << "\n=== Invalid Accept Header Examples ===" << std::endl;
|
||||
|
||||
std::vector<std::string> invalid_examples = {
|
||||
"text/html;q=1.5,application/json", // q > 1.0
|
||||
"text/html;q=-0.1,application/json", // q < 0.0
|
||||
"text/html;q=invalid,application/json", // invalid q value
|
||||
"invalidtype,application/json", // invalid media type
|
||||
",application/json" // empty entry
|
||||
};
|
||||
|
||||
for (const auto& invalid_accept : invalid_examples) {
|
||||
std::vector<std::string> temp_result;
|
||||
std::cout << "\nTesting invalid: " << invalid_accept << std::endl;
|
||||
if (detail::parse_accept_header(invalid_accept, temp_result)) {
|
||||
std::cout << " Unexpectedly succeeded!" << std::endl;
|
||||
} else {
|
||||
std::cout << " Correctly rejected as invalid" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Example 4: Server usage example
|
||||
std::cout << "\n=== Server Usage Example ===" << std::endl;
|
||||
Server svr;
|
||||
|
||||
svr.Get("/api/data", [](const Request& req, Response& res) {
|
||||
// Get Accept header
|
||||
auto accept_header = req.get_header_value("Accept");
|
||||
if (accept_header.empty()) {
|
||||
accept_header = "*/*"; // Default if no Accept header
|
||||
}
|
||||
|
||||
// Parse accept header to get preferred content types
|
||||
std::vector<std::string> preferred_types;
|
||||
if (!detail::parse_accept_header(accept_header, preferred_types)) {
|
||||
// Invalid Accept header
|
||||
res.status = 400; // Bad Request
|
||||
res.set_content("Invalid Accept header", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Client Accept header: " << accept_header << std::endl;
|
||||
std::cout << "Preferred types in order:" << std::endl;
|
||||
for (size_t i = 0; i < preferred_types.size(); ++i) {
|
||||
std::cout << " " << (i + 1) << ". " << preferred_types[i] << std::endl;
|
||||
}
|
||||
|
||||
// Choose response format based on client preference
|
||||
std::string response_content;
|
||||
std::string content_type;
|
||||
|
||||
for (const auto& type : preferred_types) {
|
||||
if (type == "application/json" || type == "application/*" || type == "*/*") {
|
||||
response_content = "{\"message\": \"Hello, World!\", \"data\": [1, 2, 3]}";
|
||||
content_type = "application/json";
|
||||
break;
|
||||
} else if (type == "text/html" || type == "text/*") {
|
||||
response_content = "<html><body><h1>Hello, World!</h1><p>Data: 1, 2, 3</p></body></html>";
|
||||
content_type = "text/html";
|
||||
break;
|
||||
} else if (type == "text/plain") {
|
||||
response_content = "Hello, World!\nData: 1, 2, 3";
|
||||
content_type = "text/plain";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (response_content.empty()) {
|
||||
// No supported content type found
|
||||
res.status = 406; // Not Acceptable
|
||||
res.set_content("No acceptable content type found", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
res.set_content(response_content, content_type);
|
||||
std::cout << "Responding with: " << content_type << std::endl;
|
||||
});
|
||||
|
||||
std::cout << "Server configured. You can test it with:" << std::endl;
|
||||
std::cout << " curl -H \"Accept: application/json\" http://localhost:8080/api/data" << std::endl;
|
||||
std::cout << " curl -H \"Accept: text/html\" http://localhost:8080/api/data" << std::endl;
|
||||
std::cout << " curl -H \"Accept: text/plain\" http://localhost:8080/api/data" << std::endl;
|
||||
std::cout << " curl -H \"Accept: text/html;q=0.9,application/json;q=1.0\" http://localhost:8080/api/data" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ struct StopWatch {
|
||||
int main(void) {
|
||||
string body(1024 * 5, 'a');
|
||||
|
||||
httplib::Client cli("httpbin.org", 80);
|
||||
httplib::Client cli("httpcan.org", 80);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
StopWatch sw(to_string(i).c_str());
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// client.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// hello.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// redirect.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// sample.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// server_and_client.cc
|
||||
//
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// simplecli.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
+18
-5
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// simplesvr.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
@@ -27,13 +27,26 @@ string dump_headers(const Headers &headers) {
|
||||
return s;
|
||||
}
|
||||
|
||||
string dump_multipart_files(const MultipartFormDataMap &files) {
|
||||
string dump_multipart_formdata(const MultipartFormData &form) {
|
||||
string s;
|
||||
char buf[BUFSIZ];
|
||||
|
||||
s += "--------------------------------\n";
|
||||
|
||||
for (const auto &x : files) {
|
||||
for (const auto &x : form.fields) {
|
||||
const auto &name = x.first;
|
||||
const auto &field = x.second;
|
||||
|
||||
snprintf(buf, sizeof(buf), "name: %s\n", name.c_str());
|
||||
s += buf;
|
||||
|
||||
snprintf(buf, sizeof(buf), "text length: %zu\n", field.content.size());
|
||||
s += buf;
|
||||
|
||||
s += "----------------\n";
|
||||
}
|
||||
|
||||
for (const auto &x : form.files) {
|
||||
const auto &name = x.first;
|
||||
const auto &file = x.second;
|
||||
|
||||
@@ -77,7 +90,7 @@ string log(const Request &req, const Response &res) {
|
||||
s += buf;
|
||||
|
||||
s += dump_headers(req.headers);
|
||||
s += dump_multipart_files(req.files);
|
||||
s += dump_multipart_formdata(req.form);
|
||||
|
||||
s += "--------------------------------\n";
|
||||
|
||||
@@ -101,7 +114,7 @@ int main(int argc, const char **argv) {
|
||||
#endif
|
||||
|
||||
svr.Post("/multipart", [](const Request &req, Response &res) {
|
||||
auto body = dump_headers(req.headers) + dump_multipart_files(req.files);
|
||||
auto body = dump_headers(req.headers) + dump_multipart_formdata(req.form);
|
||||
|
||||
res.set_content(body, "text/plain");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//
|
||||
// ssecli-stream.cc
|
||||
//
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
// SSE (Server-Sent Events) client example using Streaming API
|
||||
// with automatic reconnection support (similar to JavaScript's EventSource)
|
||||
//
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// SSE Event Parser
|
||||
//------------------------------------------------------------------------------
|
||||
// Parses SSE events from the stream according to the SSE specification.
|
||||
// SSE format:
|
||||
// event: <event-type> (optional, defaults to "message")
|
||||
// data: <payload> (can have multiple lines)
|
||||
// id: <event-id> (optional, used for reconnection)
|
||||
// retry: <milliseconds> (optional, reconnection interval)
|
||||
// <blank line> (signals end of event)
|
||||
//
|
||||
struct SSEEvent {
|
||||
std::string event = "message"; // Event type (default: "message")
|
||||
std::string data; // Event payload
|
||||
std::string id; // Event ID for Last-Event-ID header
|
||||
|
||||
void clear() {
|
||||
event = "message";
|
||||
data.clear();
|
||||
id.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// Parse a single SSE field line (e.g., "data: hello")
|
||||
// Returns true if this line ends an event (blank line)
|
||||
bool parse_sse_line(const std::string &line, SSEEvent &event, int &retry_ms) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty() || line == "\r") { return true; }
|
||||
|
||||
// Find the colon separator
|
||||
auto colon_pos = line.find(':');
|
||||
if (colon_pos == std::string::npos) {
|
||||
// Line with no colon is treated as field name with empty value
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string field = line.substr(0, colon_pos);
|
||||
std::string value;
|
||||
|
||||
// Value starts after colon, skip optional single space
|
||||
if (colon_pos + 1 < line.size()) {
|
||||
size_t value_start = colon_pos + 1;
|
||||
if (line[value_start] == ' ') { value_start++; }
|
||||
value = line.substr(value_start);
|
||||
// Remove trailing \r if present
|
||||
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
|
||||
}
|
||||
|
||||
// Handle known fields
|
||||
if (field == "event") {
|
||||
event.event = value;
|
||||
} else if (field == "data") {
|
||||
// Multiple data lines are concatenated with newlines
|
||||
if (!event.data.empty()) { event.data += "\n"; }
|
||||
event.data += value;
|
||||
} else if (field == "id") {
|
||||
// Empty id is valid (clears the last event ID)
|
||||
event.id = value;
|
||||
} else if (field == "retry") {
|
||||
// Parse retry interval in milliseconds
|
||||
try {
|
||||
retry_ms = std::stoi(value);
|
||||
} catch (...) {
|
||||
// Invalid retry value, ignore
|
||||
}
|
||||
}
|
||||
// Unknown fields are ignored per SSE spec
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Main - SSE Client with Auto-Reconnection
|
||||
//------------------------------------------------------------------------------
|
||||
int main(void) {
|
||||
// Configuration
|
||||
const std::string host = "http://localhost:1234";
|
||||
const std::string path = "/event1";
|
||||
|
||||
httplib::Client cli(host);
|
||||
|
||||
// State for reconnection (persists across connections)
|
||||
std::string last_event_id; // Sent as Last-Event-ID header on reconnect
|
||||
int retry_ms = 3000; // Reconnection delay (server can override via retry:)
|
||||
int connection_count = 0;
|
||||
|
||||
std::cout << "SSE Client starting...\n";
|
||||
std::cout << "Target: " << host << path << "\n";
|
||||
std::cout << "Press Ctrl+C to exit\n\n";
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Main reconnection loop
|
||||
// This mimics JavaScript's EventSource behavior:
|
||||
// - Automatically reconnects on connection failure
|
||||
// - Sends Last-Event-ID header to resume from last received event
|
||||
// - Respects server's retry interval
|
||||
//----------------------------------------------------------------------------
|
||||
while (true) {
|
||||
connection_count++;
|
||||
std::cout << "[Connection #" << connection_count << "] Connecting...\n";
|
||||
|
||||
// Build headers, including Last-Event-ID if we have one
|
||||
httplib::Headers headers;
|
||||
if (!last_event_id.empty()) {
|
||||
headers.emplace("Last-Event-ID", last_event_id);
|
||||
std::cout << "[Connection #" << connection_count
|
||||
<< "] Resuming from event ID: " << last_event_id << "\n";
|
||||
}
|
||||
|
||||
// Open streaming connection
|
||||
auto result = httplib::stream::Get(cli, path, headers);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Connection error handling
|
||||
//--------------------------------------------------------------------------
|
||||
if (!result) {
|
||||
std::cerr << "[Connection #" << connection_count
|
||||
<< "] Failed: " << httplib::to_string(result.error()) << "\n";
|
||||
std::cerr << "Reconnecting in " << retry_ms << "ms...\n\n";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.status() != 200) {
|
||||
std::cerr << "[Connection #" << connection_count
|
||||
<< "] HTTP error: " << result.status() << "\n";
|
||||
|
||||
// For certain errors, don't reconnect
|
||||
if (result.status() == 204 || // No Content - server wants us to stop
|
||||
result.status() == 404 || // Not Found
|
||||
result.status() == 401 || // Unauthorized
|
||||
result.status() == 403) { // Forbidden
|
||||
std::cerr << "Permanent error, not reconnecting.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cerr << "Reconnecting in " << retry_ms << "ms...\n\n";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify Content-Type (optional but recommended)
|
||||
auto content_type = result.get_header_value("Content-Type");
|
||||
if (content_type.find("text/event-stream") == std::string::npos) {
|
||||
std::cerr << "[Warning] Content-Type is not text/event-stream: "
|
||||
<< content_type << "\n";
|
||||
}
|
||||
|
||||
std::cout << "[Connection #" << connection_count << "] Connected!\n\n";
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Event receiving loop
|
||||
// Reads chunks from the stream and parses SSE events
|
||||
//--------------------------------------------------------------------------
|
||||
std::string buffer;
|
||||
SSEEvent current_event;
|
||||
int event_count = 0;
|
||||
|
||||
// Read data from stream using httplib::stream API
|
||||
while (result.next()) {
|
||||
buffer.append(result.data(), result.size());
|
||||
|
||||
// Process complete lines in the buffer
|
||||
size_t line_start = 0;
|
||||
size_t newline_pos;
|
||||
|
||||
while ((newline_pos = buffer.find('\n', line_start)) !=
|
||||
std::string::npos) {
|
||||
std::string line = buffer.substr(line_start, newline_pos - line_start);
|
||||
line_start = newline_pos + 1;
|
||||
|
||||
// Parse the line and check if event is complete
|
||||
bool event_complete = parse_sse_line(line, current_event, retry_ms);
|
||||
|
||||
if (event_complete && !current_event.data.empty()) {
|
||||
// Event received - process it
|
||||
event_count++;
|
||||
|
||||
std::cout << "--- Event #" << event_count << " ---\n";
|
||||
std::cout << "Type: " << current_event.event << "\n";
|
||||
std::cout << "Data: " << current_event.data << "\n";
|
||||
if (!current_event.id.empty()) {
|
||||
std::cout << "ID: " << current_event.id << "\n";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
// Update last_event_id for reconnection
|
||||
// Note: Empty id clears the last event ID per SSE spec
|
||||
if (!current_event.id.empty()) { last_event_id = current_event.id; }
|
||||
|
||||
current_event.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep unprocessed data in buffer
|
||||
buffer.erase(0, line_start);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Connection ended - check why
|
||||
//--------------------------------------------------------------------------
|
||||
if (result.read_error() != httplib::Error::Success) {
|
||||
std::cerr << "\n[Connection #" << connection_count
|
||||
<< "] Error: " << httplib::to_string(result.read_error())
|
||||
<< "\n";
|
||||
} else {
|
||||
std::cout << "\n[Connection #" << connection_count
|
||||
<< "] Stream ended normally\n";
|
||||
}
|
||||
|
||||
std::cout << "Received " << event_count << " events in this connection\n";
|
||||
std::cout << "Reconnecting in " << retry_ms << "ms...\n\n";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
+43
-7
@@ -1,21 +1,57 @@
|
||||
//
|
||||
// ssecli.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
|
||||
int main(void) {
|
||||
httplib::Client("http://localhost:1234")
|
||||
.Get("/event1", [&](const char *data, size_t data_length) {
|
||||
std::cout << string(data, data_length);
|
||||
return true;
|
||||
});
|
||||
// Global SSEClient pointer for signal handling
|
||||
httplib::sse::SSEClient *g_sse = nullptr;
|
||||
|
||||
void signal_handler(int) {
|
||||
if (g_sse) { g_sse->stop(); }
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Configuration
|
||||
const string host = "http://localhost:1234";
|
||||
const string path = "/event1";
|
||||
|
||||
cout << "SSE Client using httplib::sse::SSEClient\n";
|
||||
cout << "Connecting to: " << host << path << "\n";
|
||||
cout << "Press Ctrl+C to exit\n\n";
|
||||
|
||||
httplib::Client cli(host);
|
||||
httplib::sse::SSEClient sse(cli, path);
|
||||
|
||||
// Set up signal handler for graceful shutdown
|
||||
g_sse = &sse;
|
||||
signal(SIGINT, signal_handler);
|
||||
|
||||
// Event handlers
|
||||
sse.on_open([]() { cout << "[Connected]\n\n"; });
|
||||
|
||||
sse.on_message([](const httplib::sse::SSEMessage &msg) {
|
||||
cout << "Event: " << msg.event << "\n";
|
||||
cout << "Data: " << msg.data << "\n";
|
||||
if (!msg.id.empty()) { cout << "ID: " << msg.id << "\n"; }
|
||||
cout << "\n";
|
||||
});
|
||||
|
||||
sse.on_error([](httplib::Error err) {
|
||||
cerr << "[Error] " << httplib::to_string(err) << "\n";
|
||||
});
|
||||
|
||||
// Start with auto-reconnect (blocking)
|
||||
sse.start();
|
||||
|
||||
cout << "\n[Disconnected]\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
+11
-6
@@ -14,11 +14,18 @@ class EventDispatcher {
|
||||
public:
|
||||
EventDispatcher() {}
|
||||
|
||||
void wait_event(DataSink *sink) {
|
||||
bool wait_event(DataSink *sink) {
|
||||
unique_lock<mutex> lk(m_);
|
||||
int id = id_;
|
||||
cv_.wait(lk, [&] { return cid_ == id; });
|
||||
|
||||
// Wait with timeout to prevent hanging if client disconnects
|
||||
if (!cv_.wait_for(lk, std::chrono::seconds(5),
|
||||
[&] { return cid_ == id; })) {
|
||||
return false; // Timeout occurred
|
||||
}
|
||||
|
||||
sink->write(message_.data(), message_.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void send_event(const string &message) {
|
||||
@@ -71,8 +78,7 @@ int main(void) {
|
||||
cout << "connected to event1..." << endl;
|
||||
res.set_chunked_content_provider("text/event-stream",
|
||||
[&](size_t /*offset*/, DataSink &sink) {
|
||||
ed.wait_event(&sink);
|
||||
return true;
|
||||
return ed.wait_event(&sink);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,8 +86,7 @@ int main(void) {
|
||||
cout << "connected to event2..." << endl;
|
||||
res.set_chunked_content_provider("text/event-stream",
|
||||
[&](size_t /*offset*/, DataSink &sink) {
|
||||
ed.wait_event(&sink);
|
||||
return true;
|
||||
return ed.wait_event(&sink);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
//
|
||||
// upload.cc
|
||||
//
|
||||
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
@@ -37,8 +37,8 @@ int main(void) {
|
||||
});
|
||||
|
||||
svr.Post("/post", [](const Request &req, Response &res) {
|
||||
auto image_file = req.get_file_value("image_file");
|
||||
auto text_file = req.get_file_value("text_file");
|
||||
const auto &image_file = req.form.get_file("image_file");
|
||||
const auto &text_file = req.form.get_file("text_file");
|
||||
|
||||
cout << "image file length: " << image_file.content.length() << endl
|
||||
<< "image file name: " << image_file.filename << endl
|
||||
|
||||
+40
-10
@@ -13,14 +13,23 @@ project(
|
||||
'b_lto=true',
|
||||
'warning_level=3'
|
||||
],
|
||||
meson_version: '>=0.62.0'
|
||||
meson_version: '>=0.63.0'
|
||||
)
|
||||
|
||||
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()
|
||||
if version == 'undefined'
|
||||
cxx = meson.get_compiler('cpp')
|
||||
version = cxx.get_define('CPPHTTPLIB_VERSION',
|
||||
prefix: '#include <httplib.h>',
|
||||
include_directories: include_directories('.')).strip('"')
|
||||
@@ -30,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: ['CFNetwork', 'CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
|
||||
if macosx_keychain_dep.found()
|
||||
deps += macosx_keychain_dep
|
||||
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
|
||||
@@ -43,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
|
||||
@@ -65,9 +74,30 @@ if brotli_found_all
|
||||
args += '-DCPPHTTPLIB_BROTLI_SUPPORT'
|
||||
endif
|
||||
|
||||
zstd_dep = dependency('libzstd', required: get_option('zstd'))
|
||||
if zstd_dep.found()
|
||||
deps += zstd_dep
|
||||
args += '-DCPPHTTPLIB_ZSTD_SUPPORT'
|
||||
endif
|
||||
|
||||
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)
|
||||
elif host_machine.system() == 'darwin'
|
||||
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation'], required: async_ns_opt)
|
||||
else
|
||||
async_ns_dep = cxx.find_library('anl', required: async_ns_opt)
|
||||
endif
|
||||
|
||||
if async_ns_dep.found()
|
||||
deps += async_ns_dep
|
||||
args += '-DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO'
|
||||
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(
|
||||
@@ -111,6 +141,6 @@ endif
|
||||
|
||||
meson.override_dependency('cpp-httplib', cpp_httplib_dep)
|
||||
|
||||
if get_option('cpp-httplib_test')
|
||||
if get_option('test')
|
||||
subdir('test')
|
||||
endif
|
||||
|
||||
+17
-6
@@ -2,9 +2,20 @@
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
option('cpp-httplib_openssl', type: 'feature', value: 'auto', description: 'Enable OpenSSL support')
|
||||
option('cpp-httplib_zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
|
||||
option('cpp-httplib_brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
|
||||
option('cpp-httplib_macosx_keychain', type: 'feature', value: 'auto', description: 'Enable loading certs from the Keychain on Apple devices')
|
||||
option('cpp-httplib_compile', type: 'boolean', value: false, description: 'Split the header into a compilable header & source file (requires python3)')
|
||||
option('cpp-httplib_test', type: 'boolean', value: false, description: 'Build tests')
|
||||
option('openssl', type: 'feature', value: 'auto', description: 'Enable OpenSSL support')
|
||||
option('zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
|
||||
option('brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
|
||||
option('zstd', type: 'feature', value: 'auto', description: 'Enable zstd support')
|
||||
option('macosx_keychain', type: 'feature', value: 'auto', description: 'Enable loading certs from the Keychain on Apple devices')
|
||||
option('non_blocking_getaddrinfo', type: 'feature', value: 'auto', description: 'Enable asynchronous name lookup')
|
||||
option('compile', type: 'boolean', value: false, description: 'Split the header into a compilable header & source file (requires python3)')
|
||||
option('test', type: 'boolean', value: false, description: 'Build tests')
|
||||
|
||||
# Old option names
|
||||
option('cpp-httplib_openssl', type: 'feature', deprecated: 'openssl')
|
||||
option('cpp-httplib_zlib', type: 'feature', deprecated: 'zlib')
|
||||
option('cpp-httplib_brotli', type: 'feature', deprecated: 'brotli')
|
||||
option('cpp-httplib_macosx_keychain', type: 'feature', deprecated: 'macosx_keychain')
|
||||
option('cpp-httplib_non_blocking_getaddrinfo', type: 'feature', deprecated: 'non_blocking_getaddrinfo')
|
||||
option('cpp-httplib_compile', type: 'boolean', value: false, deprecated: 'compile')
|
||||
option('cpp-httplib_test', type: 'boolean', value: false, deprecated: 'test')
|
||||
|
||||
@@ -2,66 +2,72 @@
|
||||
|
||||
"""This script splits httplib.h into .h and .cc parts."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from typing import List
|
||||
|
||||
border = '// ----------------------------------------------------------------------------'
|
||||
|
||||
args_parser = argparse.ArgumentParser(description=__doc__)
|
||||
args_parser.add_argument(
|
||||
"-e", "--extension", help="extension of the implementation file (default: cc)",
|
||||
default="cc"
|
||||
)
|
||||
args_parser.add_argument(
|
||||
"-o", "--out", help="where to write the files (default: out)", default="out"
|
||||
)
|
||||
args = args_parser.parse_args()
|
||||
def main() -> None:
|
||||
"""Main entry point for the script."""
|
||||
BORDER: str = '// ----------------------------------------------------------------------------'
|
||||
|
||||
cur_dir = os.path.dirname(sys.argv[0])
|
||||
lib_name = 'httplib'
|
||||
header_name = '/' + lib_name + '.h'
|
||||
source_name = '/' + lib_name + '.' + args.extension
|
||||
# get the input file
|
||||
in_file = cur_dir + header_name
|
||||
# get the output file
|
||||
h_out = args.out + header_name
|
||||
cc_out = args.out + source_name
|
||||
args_parser: ArgumentParser = ArgumentParser(description=__doc__)
|
||||
args_parser.add_argument(
|
||||
"-e", "--extension", help="extension of the implementation file (default: cc)",
|
||||
default="cc"
|
||||
)
|
||||
args_parser.add_argument(
|
||||
"-o", "--out", help="where to write the files (default: out)", default="out"
|
||||
)
|
||||
args: Namespace = args_parser.parse_args()
|
||||
|
||||
# if the modification time of the out file is after the in file,
|
||||
# don't split (as it is already finished)
|
||||
do_split = True
|
||||
cur_dir: str = os.path.dirname(sys.argv[0])
|
||||
if not cur_dir:
|
||||
cur_dir = '.'
|
||||
lib_name: str = 'httplib'
|
||||
header_name: str = f"/{lib_name}.h"
|
||||
source_name: str = f"/{lib_name}.{args.extension}"
|
||||
# get the input file
|
||||
in_file: str = cur_dir + header_name
|
||||
# get the output file
|
||||
h_out: str = args.out + header_name
|
||||
cc_out: str = args.out + source_name
|
||||
|
||||
if os.path.exists(h_out):
|
||||
in_time = os.path.getmtime(in_file)
|
||||
out_time = os.path.getmtime(h_out)
|
||||
do_split = in_time > out_time
|
||||
# if the modification time of the out file is after the in file,
|
||||
# don't split (as it is already finished)
|
||||
do_split: bool = True
|
||||
|
||||
if do_split:
|
||||
with open(in_file) as f:
|
||||
lines = f.readlines()
|
||||
if os.path.exists(h_out):
|
||||
in_time: float = os.path.getmtime(in_file)
|
||||
out_time: float = os.path.getmtime(h_out)
|
||||
do_split: bool = in_time > out_time
|
||||
|
||||
if do_split:
|
||||
with open(in_file) as f:
|
||||
lines: List[str] = f.readlines()
|
||||
|
||||
python_version = sys.version_info[0]
|
||||
if python_version < 3:
|
||||
os.makedirs(args.out)
|
||||
else:
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
in_implementation = False
|
||||
cc_out = args.out + source_name
|
||||
with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
|
||||
fc.write('#include "httplib.h"\n')
|
||||
fc.write('namespace httplib {\n')
|
||||
for line in lines:
|
||||
is_border_line = border in line
|
||||
if is_border_line:
|
||||
in_implementation = not in_implementation
|
||||
elif in_implementation:
|
||||
fc.write(line.replace('inline ', ''))
|
||||
else:
|
||||
fh.write(line)
|
||||
fc.write('} // namespace httplib\n')
|
||||
in_implementation: bool = False
|
||||
cc_out: str = args.out + source_name
|
||||
with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
|
||||
fc.write('#include "httplib.h"\n')
|
||||
fc.write('namespace httplib {\n')
|
||||
for line in lines:
|
||||
is_border_line: bool = BORDER in line
|
||||
if is_border_line:
|
||||
in_implementation: bool = not in_implementation
|
||||
elif in_implementation:
|
||||
fc.write(line.replace('inline ', ''))
|
||||
else:
|
||||
fh.write(line)
|
||||
fc.write('} // namespace httplib\n')
|
||||
|
||||
print("Wrote {} and {}".format(h_out, cc_out))
|
||||
else:
|
||||
print("{} and {} are up to date".format(h_out, cc_out))
|
||||
print(f"Wrote {h_out} and {cc_out}")
|
||||
else:
|
||||
print(f"{h_out} and {cc_out} are up to date")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+32
-5
@@ -1,5 +1,5 @@
|
||||
CXX = clang++
|
||||
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow $(EXTRA_CXXFLAGS) # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS -fsanitize=address
|
||||
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow $(EXTRA_CXXFLAGS) -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS -fsanitize=address
|
||||
|
||||
PREFIX ?= $(shell brew --prefix)
|
||||
|
||||
@@ -9,7 +9,7 @@ OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPEN
|
||||
ifneq ($(OS), Windows_NT)
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S), Darwin)
|
||||
OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework CoreFoundation -framework Security
|
||||
OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -21,7 +21,18 @@ BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_
|
||||
ZSTD_DIR = $(PREFIX)/opt/zstd
|
||||
ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -I$(ZSTD_DIR)/include -L$(ZSTD_DIR)/lib -lzstd
|
||||
|
||||
TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) -pthread -lcurl
|
||||
LIBS = -lpthread -lcurl
|
||||
ifneq ($(OS), Windows_NT)
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S), Darwin)
|
||||
LIBS += -framework CoreFoundation -framework CFNetwork
|
||||
endif
|
||||
ifneq ($(UNAME_S), Darwin)
|
||||
LIBS += -lanl
|
||||
endif
|
||||
endif
|
||||
|
||||
TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
|
||||
|
||||
# By default, use standalone_fuzz_target_runner.
|
||||
# This runner does no fuzzing, but simply executes the inputs
|
||||
@@ -40,7 +51,23 @@ all : test test_split
|
||||
./test
|
||||
|
||||
proxy : test_proxy
|
||||
./test_proxy
|
||||
@echo "Starting proxy server..."
|
||||
cd proxy && \
|
||||
docker compose up -d
|
||||
@echo "Waiting for proxy to be ready..."
|
||||
@until nc -z localhost 3128 && nc -z localhost 3129; do sleep 1; done
|
||||
@echo "Proxy servers are ready, waiting additional 5 seconds for full startup..."
|
||||
@sleep 5
|
||||
@echo "Checking proxy server status..."
|
||||
@cd proxy && docker compose ps
|
||||
@echo "Checking proxy server logs..."
|
||||
@cd proxy && docker compose logs --tail=20
|
||||
@echo "Running proxy tests..."
|
||||
./test_proxy; \
|
||||
exit_code=$$?; \
|
||||
echo "Stopping proxy server..."; \
|
||||
cd proxy && docker compose down; \
|
||||
exit $$exit_code
|
||||
|
||||
test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS)
|
||||
@@ -86,7 +113,7 @@ fuzz_test: server_fuzzer
|
||||
|
||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||
server_fuzzer : fuzzing/server_fuzzer.cc ../httplib.h standalone_fuzz_target_runner.o
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) $< $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(LIB_FUZZING_ENGINE) $(ZSTD_SUPPORT) $(LIBS)
|
||||
@file $@
|
||||
|
||||
# Standalone fuzz runner, which just reads inputs from fuzzing/corpus/ dir and
|
||||
|
||||
@@ -20,7 +20,7 @@ all : server_fuzzer
|
||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||
server_fuzzer : server_fuzzer.cc ../../httplib.h
|
||||
# $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
||||
|
||||
clean:
|
||||
|
||||
@@ -993,7 +993,7 @@ void UniversalTersePrint(const T& value, ::std::ostream* os) {
|
||||
// NUL-terminated string.
|
||||
template <typename T>
|
||||
void UniversalPrint(const T& value, ::std::ostream* os) {
|
||||
// A workarond for the bug in VC++ 7.1 that prevents us from instantiating
|
||||
// A workaround for the bug in VC++ 7.1 that prevents us from instantiating
|
||||
// UniversalPrinter with T directly.
|
||||
typedef T T1;
|
||||
UniversalPrinter<T1>::Print(value, os);
|
||||
|
||||
@@ -753,7 +753,7 @@ class ParameterizedTestSuiteRegistry {
|
||||
};
|
||||
|
||||
// Keep track of what type-parameterized test suite are defined and
|
||||
// where as well as which are intatiated. This allows susequently
|
||||
// where as well as which are intatiated. This allows subsequently
|
||||
// identifying suits that are defined but never used.
|
||||
class TypeParameterizedTestSuiteRegistry {
|
||||
public:
|
||||
@@ -764,7 +764,7 @@ class TypeParameterizedTestSuiteRegistry {
|
||||
// Add an instantiation of a suit.
|
||||
void RegisterInstantiation(const char* test_suite_name);
|
||||
|
||||
// For each suit repored as defined but not reported as instantiation,
|
||||
// For each suit reported as defined but not reported as instantiation,
|
||||
// emit a test that reports that fact (configurably, as an error).
|
||||
void CheckForInstantiations();
|
||||
|
||||
|
||||
+4
-2
@@ -108,9 +108,11 @@ subdir('www')
|
||||
subdir('www2'/'dir')
|
||||
subdir('www3'/'dir')
|
||||
|
||||
# GoogleTest 1.13.0 requires C++14
|
||||
# New GoogleTest versions require new C++ standards
|
||||
test_options = []
|
||||
if gtest_dep.version().version_compare('>=1.13.0')
|
||||
if gtest_dep.version().version_compare('>=1.17.0')
|
||||
test_options += 'cpp_std=c++17'
|
||||
elif gtest_dep.version().version_compare('>=1.13.0')
|
||||
test_options += 'cpp_std=c++14'
|
||||
endif
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM centos:7
|
||||
FROM alpine:latest
|
||||
|
||||
ARG auth="basic"
|
||||
ARG port="3128"
|
||||
|
||||
RUN yum install -y squid
|
||||
RUN apk update && apk add --no-cache squid
|
||||
|
||||
COPY ./${auth}_squid.conf /etc/squid/squid.conf
|
||||
COPY ./${auth}_passwd /etc/squid/passwd
|
||||
|
||||
@@ -27,7 +27,7 @@ acl Safe_ports port 591 # filemaker
|
||||
acl Safe_ports port 777 # multiling http
|
||||
acl CONNECT method CONNECT
|
||||
|
||||
auth_param basic program /usr/lib64/squid/basic_ncsa_auth /etc/squid/passwd
|
||||
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd
|
||||
auth_param basic realm proxy
|
||||
acl authenticated proxy_auth REQUIRED
|
||||
http_access allow authenticated
|
||||
|
||||
@@ -27,7 +27,7 @@ acl Safe_ports port 591 # filemaker
|
||||
acl Safe_ports port 777 # multiling http
|
||||
acl CONNECT method CONNECT
|
||||
|
||||
auth_param digest program /usr/lib64/squid/digest_file_auth /etc/squid/passwd
|
||||
auth_param digest program /usr/lib/squid/digest_file_auth /etc/squid/passwd
|
||||
auth_param digest realm proxy
|
||||
acl authenticated proxy_auth REQUIRED
|
||||
http_access allow authenticated
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
squid_basic:
|
||||
image: squid_basic
|
||||
|
||||
+5631
-391
File diff suppressed because it is too large
Load Diff
+32
-22
@@ -1,3 +1,4 @@
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <gtest/gtest.h>
|
||||
#include <httplib.h>
|
||||
@@ -5,6 +6,14 @@
|
||||
using namespace std;
|
||||
using namespace httplib;
|
||||
|
||||
std::string normalizeJson(const std::string &json) {
|
||||
std::string result;
|
||||
for (char c : json) {
|
||||
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { result += c; }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T> void ProxyTest(T &cli, bool basic) {
|
||||
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
||||
auto res = cli.Get("/httpbin/get");
|
||||
@@ -81,7 +90,7 @@ TEST(RedirectTest, YouTubeNoSSLBasic) {
|
||||
RedirectProxyText(cli, "/", true);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, DISABLED_YouTubeNoSSLDigest) {
|
||||
TEST(RedirectTest, YouTubeNoSSLDigest) {
|
||||
Client cli("youtube.com");
|
||||
RedirectProxyText(cli, "/", false);
|
||||
}
|
||||
@@ -92,6 +101,7 @@ TEST(RedirectTest, YouTubeSSLBasic) {
|
||||
}
|
||||
|
||||
TEST(RedirectTest, YouTubeSSLDigest) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
SSLClient cli("youtube.com");
|
||||
RedirectProxyText(cli, "/", false);
|
||||
}
|
||||
@@ -113,8 +123,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
auto res = cli.Get("/basic-auth/hello/world",
|
||||
{make_basic_authentication_header("hello", "world")});
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
normalizeJson(res->body));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -122,8 +132,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_basic_auth("hello", "world");
|
||||
auto res = cli.Get("/basic-auth/hello/world");
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
normalizeJson(res->body));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -143,13 +153,13 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
}
|
||||
|
||||
TEST(BaseAuthTest, NoSSL) {
|
||||
Client cli("httpbin.org");
|
||||
Client cli("httpcan.org");
|
||||
BaseAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
TEST(BaseAuthTest, SSL) {
|
||||
SSLClient cli("httpbin.org");
|
||||
SSLClient cli("httpcan.org");
|
||||
BaseAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
#endif
|
||||
@@ -172,15 +182,17 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
||||
"/digest-auth/auth/hello/world/MD5",
|
||||
"/digest-auth/auth/hello/world/SHA-256",
|
||||
"/digest-auth/auth/hello/world/SHA-512",
|
||||
"/digest-auth/auth-int/hello/world/MD5",
|
||||
};
|
||||
|
||||
cli.set_digest_auth("hello", "world");
|
||||
for (auto path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
std::string algo(path.substr(path.rfind('/') + 1));
|
||||
EXPECT_EQ(
|
||||
normalizeJson("{\"algorithm\":\"" + algo +
|
||||
"\",\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
normalizeJson(res->body));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -191,24 +203,22 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
||||
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
||||
}
|
||||
|
||||
// NOTE: Until httpbin.org fixes issue #46, the following test is commented
|
||||
// out. Please see https://httpbin.org/digest-auth/auth/hello/world
|
||||
// cli.set_digest_auth("bad", "world");
|
||||
// for (auto path : paths) {
|
||||
// auto res = cli.Get(path.c_str());
|
||||
// ASSERT_TRUE(res != nullptr);
|
||||
// EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
||||
// }
|
||||
cli.set_digest_auth("bad", "world");
|
||||
for (auto path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DigestAuthTest, SSL) {
|
||||
SSLClient cli("httpbin.org");
|
||||
SSLClient cli("httpcan.org");
|
||||
DigestAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
|
||||
TEST(DigestAuthTest, NoSSL) {
|
||||
Client cli("httpbin.org");
|
||||
Client cli("httpcan.org");
|
||||
DigestAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
#endif
|
||||
@@ -249,8 +259,8 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
|
||||
for (auto path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
EXPECT_EQ("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
normalizeJson(res->body));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user