Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71ba7e7b1b | ||
|
|
ebe7efa1cc | ||
|
|
22d90c29b4 | ||
|
|
b944f942ee | ||
|
|
550f728165 | ||
|
|
a4b2c61a65 | ||
|
|
5c0135fa5d | ||
|
|
2b5d1eea8d | ||
|
|
d274c0abe5 | ||
|
|
dda2e007a0 | ||
|
|
321a86d9f2 | ||
|
|
ada97046a2 | ||
|
|
6e73a63153 | ||
|
|
cdc223019a | ||
|
|
574f5ce93e | ||
|
|
2996cecee0 | ||
|
|
32bf5c9c09 | ||
|
|
735e5930eb | ||
|
|
748f47b377 | ||
|
|
4cb8ff9f90 | ||
|
|
985cd9f6a2 | ||
|
|
233f0fb1b8 | ||
|
|
03cf43ebaa | ||
|
|
3c4b96024f | ||
|
|
d74e4a7c9c | ||
|
|
bfa2f735f2 | ||
|
|
b6ab8435d7 | ||
|
|
39a64fb4e7 | ||
|
|
d7c14b6f3a | ||
|
|
1880693aef | ||
|
|
dd20342825 | ||
|
|
a268d65c4f | ||
|
|
b397c768e4 | ||
|
|
8e22a7676a | ||
|
|
8a7c536ad5 | ||
|
|
8aad481c69 | ||
|
|
5814e121df | ||
|
|
7adbccbaf7 | ||
|
|
eb10c22db1 | ||
|
|
708f860e3a | ||
|
|
eb30f15363 | ||
|
|
4941d5b56b | ||
|
|
9bbb4741b4 | ||
|
|
282f2feb77 | ||
|
|
60a1f00618 | ||
|
|
9104054ca5 | ||
|
|
d69f144a99 | ||
|
|
929dfbd348 | ||
|
|
3047183fd9 | ||
|
|
ef5e4044f1 | ||
|
|
3779800322 | ||
|
|
986a20fb7d | ||
|
|
8311e1105f | ||
|
|
ba6845925d | ||
|
|
343a0fc073 | ||
|
|
54f8a4d0f3 | ||
|
|
9c36aae4b7 | ||
|
|
b766025a83 | ||
|
|
9b5f76f833 | ||
|
|
d647f484a4 | ||
|
|
8794792baa | ||
|
|
b85768c1f3 | ||
|
|
e6d71bd702 | ||
|
|
258992a160 | ||
|
|
a7bc00e330 | ||
|
|
11a40584e9 | ||
|
|
3e86bdb4d8 | ||
|
|
c817d65695 |
69
.github/workflows/abidiff.yaml
vendored
Normal file
69
.github/workflows/abidiff.yaml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
# SPDX-FileCopyrightText: 2025 Andrea Pappacoda <andrea@pappacoda.it>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: abidiff
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: sh
|
||||
|
||||
jobs:
|
||||
abi:
|
||||
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
|
||||
container:
|
||||
image: debian:testing
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: apt -y --update install --no-install-recommends
|
||||
abigail-tools
|
||||
ca-certificates
|
||||
g++
|
||||
git
|
||||
libbrotli-dev
|
||||
libssl-dev
|
||||
meson
|
||||
pkg-config
|
||||
python3
|
||||
zlib1g-dev
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: current
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: previous
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout previous
|
||||
working-directory: previous
|
||||
run: |
|
||||
git switch master
|
||||
git describe --tags --abbrev=0 master | xargs git checkout
|
||||
|
||||
- name: Build current
|
||||
working-directory: current
|
||||
run: |
|
||||
meson setup --buildtype=debug -Dcpp-httplib_compile=true build
|
||||
ninja -C build
|
||||
|
||||
- name: Build previous
|
||||
working-directory: previous
|
||||
run: |
|
||||
meson setup --buildtype=debug -Dcpp-httplib_compile=true build
|
||||
ninja -C build
|
||||
|
||||
- name: Run abidiff
|
||||
run: abidiff
|
||||
--headers-dir1 previous/build
|
||||
--headers-dir2 current/build
|
||||
previous/build/libcpp-httplib.so
|
||||
current/build/libcpp-httplib.so
|
||||
6
.github/workflows/cifuzz.yaml
vendored
6
.github/workflows/cifuzz.yaml
vendored
@ -1,5 +1,11 @@
|
||||
name: CIFuzz
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
95
.github/workflows/test.yaml
vendored
95
.github/workflows/test.yaml
vendored
@ -1,10 +1,52 @@
|
||||
name: test
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gtest_filter:
|
||||
description: 'Google Test filter'
|
||||
test_linux:
|
||||
description: 'Test on Linux'
|
||||
type: boolean
|
||||
default: true
|
||||
test_macos:
|
||||
description: 'Test on MacOS'
|
||||
type: boolean
|
||||
default: true
|
||||
test_windows:
|
||||
description: 'Test on Windows'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '*' }}
|
||||
|
||||
jobs:
|
||||
style-check:
|
||||
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
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: run style check
|
||||
run: |
|
||||
clang-format --version
|
||||
cd test && make style_check
|
||||
|
||||
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
|
||||
@ -17,6 +59,11 @@ jobs:
|
||||
|
||||
macos:
|
||||
runs-on: macos-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_macos == 'true')
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
@ -27,6 +74,19 @@ jobs:
|
||||
|
||||
windows:
|
||||
runs-on: windows-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_windows == 'true')
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- with_ssl: false
|
||||
name: without SSL
|
||||
- with_ssl: true
|
||||
name: with SSL
|
||||
name: windows ${{ matrix.config.name }}
|
||||
steps:
|
||||
- name: Prepare Git for Checkout on Windows
|
||||
run: |
|
||||
@ -42,24 +102,25 @@ jobs:
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
- name: Setup msbuild on windows
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
- name: Install libraries
|
||||
run: |
|
||||
vcpkg install gtest curl zlib brotli
|
||||
choco install openssl
|
||||
|
||||
- name: Configure CMake with SSL
|
||||
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake -DHTTPLIB_TEST=ON -DHTTPLIB_REQUIRE_OPENSSL=ON -DHTTPLIB_REQUIRE_ZLIB=ON -DHTTPLIB_REQUIRE_BROTLI=ON
|
||||
- name: Build with with SSL
|
||||
run: cmake --build build --config Release
|
||||
- name: Run tests with SSL
|
||||
- name: Install vcpkg dependencies
|
||||
run: vcpkg install gtest curl zlib brotli
|
||||
- name: Install OpenSSL
|
||||
if: ${{ matrix.config.with_ssl }}
|
||||
run: choco install openssl
|
||||
- name: Configure CMake ${{ matrix.config.name }}
|
||||
run: >
|
||||
cmake -B build -S .
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake
|
||||
-DHTTPLIB_TEST=ON
|
||||
-DHTTPLIB_REQUIRE_ZLIB=ON
|
||||
-DHTTPLIB_REQUIRE_BROTLI=ON
|
||||
-DHTTPLIB_REQUIRE_OPENSSL=${{ matrix.config.with_ssl && 'ON' || 'OFF' }}
|
||||
- name: Build ${{ matrix.config.name }}
|
||||
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
|
||||
- name: Run tests ${{ matrix.config.name }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release
|
||||
|
||||
- name: Configure CMake without SSL
|
||||
run: cmake -B build-no-ssl -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake -DHTTPLIB_TEST=ON -DHTTPLIB_REQUIRE_OPENSSL=ON -DHTTPLIB_REQUIRE_ZLIB=ON -DHTTPLIB_REQUIRE_BROTLI=ON
|
||||
- name: Build without SSL
|
||||
run: cmake --build build-no-ssl --config Release
|
||||
- name: Run tests without SSL
|
||||
run: ctest --output-on-failure --test-dir build-no-ssl -C Release
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@ test/test.xcodeproj/*/xcuser*
|
||||
test/*.o
|
||||
test/*.pem
|
||||
test/*.srl
|
||||
test/_build_*
|
||||
work/
|
||||
benchmark/server*
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ FROM yhirose4dockerhub/ubuntu-builder AS builder
|
||||
WORKDIR /build
|
||||
COPY httplib.h .
|
||||
COPY docker/main.cc .
|
||||
RUN g++ -std=c++23 -static -o server -O2 -I. -DCPPHTTPLIB_USE_POLL main.cc && strip server
|
||||
RUN g++ -std=c++23 -static -o server -O2 -I. main.cc && strip server
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /build/server /server
|
||||
|
||||
44
README.md
44
README.md
@ -39,10 +39,10 @@ svr.listen("0.0.0.0", 8080);
|
||||
#include "path/to/httplib.h"
|
||||
|
||||
// HTTP
|
||||
httplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");
|
||||
httplib::Client cli("http://yhirose.github.io");
|
||||
|
||||
// HTTPS
|
||||
httplib::Client cli("https://cpp-httplib-server.yhirose.repl.co");
|
||||
httplib::Client cli("https://yhirose.github.io");
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
res->status;
|
||||
@ -125,6 +125,21 @@ int main(void)
|
||||
res.set_content(req.body, "text/plain");
|
||||
});
|
||||
|
||||
// If the handler takes time to finish, you can also poll the connection state
|
||||
svr.Get("/task", [&](const Request& req, Response& res) {
|
||||
const char * result = nullptr;
|
||||
process.run(); // for example, starting an external process
|
||||
while (result == nullptr) {
|
||||
sleep(1);
|
||||
if (req.is_connection_closed()) {
|
||||
process.kill(); // kill the process
|
||||
return;
|
||||
}
|
||||
result = process.stdout(); // != nullptr if the process finishes
|
||||
}
|
||||
res.set_content(result, "text/plain");
|
||||
});
|
||||
|
||||
svr.Get("/stop", [&](const Request& req, Response& res) {
|
||||
svr.stop();
|
||||
});
|
||||
@ -391,11 +406,11 @@ svr.Get("/chunked", [&](const Request& req, Response& res) {
|
||||
|
||||
```cpp
|
||||
svr.Get("/content", [&](const Request &req, Response &res) {
|
||||
res.set_file_content("./path/to/conent.html");
|
||||
res.set_file_content("./path/to/content.html");
|
||||
});
|
||||
|
||||
svr.Get("/content", [&](const Request &req, Response &res) {
|
||||
res.set_file_content("./path/to/conent", "text/html");
|
||||
res.set_file_content("./path/to/content", "text/html");
|
||||
});
|
||||
```
|
||||
|
||||
@ -447,7 +462,7 @@ Please see [Server example](https://github.com/yhirose/cpp-httplib/blob/master/e
|
||||
|
||||
### Default thread pool support
|
||||
|
||||
`ThreadPool` is used as a **default** task queue, and the default thread count is 8, or `std::thread::hardware_concurrency()`. You can change it with `CPPHTTPLIB_THREAD_POOL_COUNT`.
|
||||
`ThreadPool` is used as the **default** task queue, with a default thread count of 8 or `std::thread::hardware_concurrency() - 1`, whichever is greater. You can change it with `CPPHTTPLIB_THREAD_POOL_COUNT`.
|
||||
|
||||
If you want to set the thread count at runtime, there is no convenient way... But here is how.
|
||||
|
||||
@ -639,6 +654,9 @@ res = cli.Options("/resource/foo");
|
||||
cli.set_connection_timeout(0, 300000); // 300 milliseconds
|
||||
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-timeout` option
|
||||
svr.set_max_timeout(5000); // 5 seconds
|
||||
```
|
||||
|
||||
### Receive content with a content receiver
|
||||
@ -825,7 +843,7 @@ Please see https://github.com/google/brotli for more detail.
|
||||
|
||||
### Default `Accept-Encoding` value
|
||||
|
||||
The default `Acdcept-Encoding` value contains all possible compression types. So, the following two examples are same.
|
||||
The default `Accept-Encoding` value contains all possible compression types. So, the following two examples are same.
|
||||
|
||||
```c++
|
||||
res = cli.Get("/resource/foo");
|
||||
@ -854,11 +872,6 @@ res->body; // Compressed data
|
||||
|
||||
```
|
||||
|
||||
Use `poll` instead of `select`
|
||||
------------------------------
|
||||
|
||||
`select` system call is used as default since it's more widely supported. If you want to let cpp-httplib use `poll` instead, you can do so with `CPPHTTPLIB_USE_POLL`.
|
||||
|
||||
Unix Domain Socket Support
|
||||
--------------------------
|
||||
|
||||
@ -866,7 +879,7 @@ Unix Domain Socket support is available on Linux and macOS.
|
||||
|
||||
```c++
|
||||
// Server
|
||||
httplib::Server svr("./my-socket.sock");
|
||||
httplib::Server svr;
|
||||
svr.set_address_family(AF_UNIX).listen("./my-socket.sock", 80);
|
||||
|
||||
// Client
|
||||
@ -916,9 +929,6 @@ From Docker Hub
|
||||
|
||||
```bash
|
||||
> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server
|
||||
...
|
||||
|
||||
> docker run --init --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server
|
||||
Serving HTTP on 0.0.0.0 port 80 ...
|
||||
192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"
|
||||
192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."
|
||||
@ -951,12 +961,12 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
|
||||
> cpp-httplib officially supports only the latest Visual Studio. It might work with former versions of Visual Studio, but I can no longer verify it. Pull requests are always welcome for the older versions of Visual Studio unless they break the C++11 conformance.
|
||||
|
||||
> [!NOTE]
|
||||
> Windows 8 or lower, Visual Studio 2013 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.
|
||||
> Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
MIT license (© 2024 Yuji Hirose)
|
||||
MIT license (© 2025 Yuji Hirose)
|
||||
|
||||
Special Thanks To
|
||||
-----------------
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//
|
||||
// httplib.h
|
||||
//
|
||||
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//
|
||||
// main.cc
|
||||
//
|
||||
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
@ -41,7 +41,7 @@ std::string log(auto &req, auto &res) {
|
||||
auto http_referer = "-"; // TODO:
|
||||
auto http_user_agent = req.get_header_value("User-Agent", "-");
|
||||
|
||||
// NOTE: From NGINX defualt access log format
|
||||
// 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"';
|
||||
|
||||
@ -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 issue
|
||||
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client
|
||||
|
||||
server : server.cc ../httplib.h Makefile
|
||||
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//
|
||||
// server_and_client.cc
|
||||
//
|
||||
// Copyright (c) 2024 Yuji Hirose. All rights reserved.
|
||||
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
|
||||
// MIT License
|
||||
//
|
||||
|
||||
|
||||
@ -12,8 +12,7 @@ using namespace std;
|
||||
|
||||
class EventDispatcher {
|
||||
public:
|
||||
EventDispatcher() {
|
||||
}
|
||||
EventDispatcher() {}
|
||||
|
||||
void wait_event(DataSink *sink) {
|
||||
unique_lock<mutex> lk(m_);
|
||||
|
||||
@ -26,7 +26,7 @@ endif()
|
||||
|
||||
find_package(CURL REQUIRED)
|
||||
|
||||
add_executable(httplib-test test.cc)
|
||||
add_executable(httplib-test test.cc include_httplib.cc $<$<BOOL:${WIN32}>:include_windows_h.cc>)
|
||||
target_compile_options(httplib-test PRIVATE "$<$<CXX_COMPILER_ID:MSVC>:/utf-8;/bigobj>")
|
||||
target_link_libraries(httplib-test PRIVATE httplib GTest::gtest_main CURL::libcurl)
|
||||
gtest_discover_tests(httplib-test)
|
||||
|
||||
@ -28,6 +28,11 @@ TEST_ARGS = gtest/gtest-all.cc gtest/gtest_main.cc $(OPENSSL_SUPPORT) $(ZLIB_SUP
|
||||
# OSS-Fuzz will define its own value for LIB_FUZZING_ENGINE.
|
||||
LIB_FUZZING_ENGINE ?= standalone_fuzz_target_runner.o
|
||||
|
||||
CLANG_FORMAT = clang-format
|
||||
REALPATH = $(shell which grealpath 2>/dev/null || which realpath 2>/dev/null)
|
||||
STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \
|
||||
$(wildcard example/*.h example/*.cc fuzzing/*.h fuzzing/*.cc *.h *.cc ../httplib.h))
|
||||
|
||||
all : test test_split
|
||||
./test
|
||||
|
||||
@ -42,6 +47,31 @@ test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
|
||||
test_split : test.cc ../httplib.h httplib.cc Makefile cert.pem
|
||||
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS)
|
||||
|
||||
check_abi:
|
||||
@./check-shared-library-abi-compatibility.sh
|
||||
|
||||
.PHONY: style_check
|
||||
style_check: $(STYLE_CHECK_FILES)
|
||||
@for file in $(STYLE_CHECK_FILES); do \
|
||||
$(CLANG_FORMAT) $$file > $$file.formatted; \
|
||||
if ! diff -u $$file $$file.formatted; then \
|
||||
file2=$$($(REALPATH) --relative-to=.. $$file); \
|
||||
printf "\n%*s\n" 80 | tr ' ' '#'; \
|
||||
printf "##%*s##\n" 76; \
|
||||
printf "## %-70s ##\n" "$$file2 not properly formatted. Please run clang-format."; \
|
||||
printf "##%*s##\n" 76; \
|
||||
printf "%*s\n\n" 80 | tr ' ' '#'; \
|
||||
failed=1; \
|
||||
fi; \
|
||||
rm -f $$file.formatted; \
|
||||
done; \
|
||||
if [ -n "$$failed" ]; then \
|
||||
echo "Style check failed for one or more files. See above for details."; \
|
||||
false; \
|
||||
else \
|
||||
echo "All files are properly formatted."; \
|
||||
fi
|
||||
|
||||
test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS)
|
||||
|
||||
@ -66,5 +96,5 @@ cert.pem:
|
||||
./gen-certs.sh
|
||||
|
||||
clean:
|
||||
rm -f test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc
|
||||
rm -rf test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM
|
||||
|
||||
|
||||
@ -25,7 +25,9 @@ public:
|
||||
|
||||
bool is_readable() const override { return true; }
|
||||
|
||||
bool is_writable() const override { return true; }
|
||||
bool wait_readable() const override { return true; }
|
||||
|
||||
bool wait_writable() const override { return true; }
|
||||
|
||||
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
@ -39,6 +41,8 @@ public:
|
||||
|
||||
socket_t socket() const override { return 0; }
|
||||
|
||||
time_t duration() const override { return 0; };
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
size_t size_;
|
||||
|
||||
6
test/include_windows_h.cc
Normal file
6
test/include_windows_h.cc
Normal file
@ -0,0 +1,6 @@
|
||||
// Test if including windows.h conflicts with httplib.h
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#include <httplib.h>
|
||||
28
test/make-shared-library.sh
Executable file
28
test/make-shared-library.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 build_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_DIR=$1
|
||||
|
||||
# Make the build directory
|
||||
rm -rf $BUILD_DIR
|
||||
mkdir -p $BUILD_DIR/out
|
||||
|
||||
cd $BUILD_DIR
|
||||
|
||||
# Build the version
|
||||
git checkout $BUILD_DIR -q
|
||||
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_CXX_FLAGS="-g -Og" \
|
||||
-DBUILD_SHARED_LIBS=ON \
|
||||
-DHTTPLIB_COMPILE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=./out \
|
||||
../..
|
||||
|
||||
cmake --build . --target install
|
||||
cmake --build . --target clean
|
||||
|
||||
480
test/test.cc
480
test/test.cc
@ -156,27 +156,28 @@ TEST_F(UnixSocketTest, abstract) {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(SocketStream, is_writable_UNIX) {
|
||||
TEST(SocketStream, wait_writable_UNIX) {
|
||||
int fds[2];
|
||||
ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
|
||||
|
||||
const auto asSocketStream = [&](socket_t fd,
|
||||
std::function<bool(Stream &)> func) {
|
||||
return detail::process_client_socket(fd, 0, 0, 0, 0, func);
|
||||
return detail::process_client_socket(
|
||||
fd, 0, 0, 0, 0, 0, std::chrono::steady_clock::time_point::min(), func);
|
||||
};
|
||||
asSocketStream(fds[0], [&](Stream &s0) {
|
||||
EXPECT_EQ(s0.socket(), fds[0]);
|
||||
EXPECT_TRUE(s0.is_writable());
|
||||
EXPECT_TRUE(s0.wait_writable());
|
||||
|
||||
EXPECT_EQ(0, close(fds[1]));
|
||||
EXPECT_FALSE(s0.is_writable());
|
||||
EXPECT_FALSE(s0.wait_writable());
|
||||
|
||||
return true;
|
||||
});
|
||||
EXPECT_EQ(0, close(fds[0]));
|
||||
}
|
||||
|
||||
TEST(SocketStream, is_writable_INET) {
|
||||
TEST(SocketStream, wait_writable_INET) {
|
||||
sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
@ -206,11 +207,12 @@ TEST(SocketStream, is_writable_INET) {
|
||||
|
||||
const auto asSocketStream = [&](socket_t fd,
|
||||
std::function<bool(Stream &)> func) {
|
||||
return detail::process_client_socket(fd, 0, 0, 0, 0, func);
|
||||
return detail::process_client_socket(
|
||||
fd, 0, 0, 0, 0, 0, std::chrono::steady_clock::time_point::min(), func);
|
||||
};
|
||||
asSocketStream(disconnected_svr_sock, [&](Stream &ss) {
|
||||
EXPECT_EQ(ss.socket(), disconnected_svr_sock);
|
||||
EXPECT_FALSE(ss.is_writable());
|
||||
EXPECT_FALSE(ss.wait_writable());
|
||||
|
||||
return true;
|
||||
});
|
||||
@ -511,6 +513,15 @@ TEST(GetHeaderValueTest, RegularValueInt) {
|
||||
EXPECT_EQ(100ull, val);
|
||||
}
|
||||
|
||||
TEST(GetHeaderValueTest, RegularInvalidValueInt) {
|
||||
Headers headers = {{"Content-Length", "x"}};
|
||||
auto is_invalid_value = false;
|
||||
auto val = detail::get_header_value_u64(headers, "Content-Length", 0, 0,
|
||||
is_invalid_value);
|
||||
EXPECT_EQ(0ull, val);
|
||||
EXPECT_TRUE(is_invalid_value);
|
||||
}
|
||||
|
||||
TEST(GetHeaderValueTest, Range) {
|
||||
{
|
||||
Headers headers = {make_range_header({{1, -1}})};
|
||||
@ -1854,6 +1865,32 @@ TEST(PathUrlEncodeTest, PathUrlEncode) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PathUrlEncodeTest, IncludePercentEncodingLF) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [](const Request &req, Response &) {
|
||||
EXPECT_EQ("\x0A", req.get_param_value("something"));
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
{
|
||||
Client cli(HOST, PORT);
|
||||
cli.set_url_encode(false);
|
||||
|
||||
auto res = cli.Get("/?something=%0A");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BindServerTest, DISABLED_BindDualStack) {
|
||||
Server svr;
|
||||
|
||||
@ -2942,6 +2979,11 @@ protected:
|
||||
res.status = 401;
|
||||
res.set_header("WWW-Authenticate", "Basic realm=123456");
|
||||
})
|
||||
.Delete("/issue609",
|
||||
[](const httplib::Request &, httplib::Response &res,
|
||||
const httplib::ContentReader &) {
|
||||
res.set_content("ok", "text/plain");
|
||||
})
|
||||
#if defined(CPPHTTPLIB_ZLIB_SUPPORT) || defined(CPPHTTPLIB_BROTLI_SUPPORT)
|
||||
.Get("/compress",
|
||||
[&](const Request & /*req*/, Response &res) {
|
||||
@ -3036,6 +3078,16 @@ TEST_F(ServerTest, GetFileContent) {
|
||||
EXPECT_EQ("test.html", res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetFileContentWithRange) {
|
||||
auto res = cli_.Get("/file_content", {{make_range_header({{1, 3}})}});
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
|
||||
EXPECT_EQ("text/html", res->get_header_value("Content-Type"));
|
||||
EXPECT_EQ("bytes 1-3/9", res->get_header_value("Content-Range"));
|
||||
EXPECT_EQ(3, std::stoi(res->get_header_value("Content-Length")));
|
||||
EXPECT_EQ("est", res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetFileContentWithContentType) {
|
||||
auto res = cli_.Get("/file_content_with_content_type");
|
||||
ASSERT_TRUE(res);
|
||||
@ -3489,7 +3541,7 @@ TEST_F(ServerTest, LongRequest) {
|
||||
|
||||
TEST_F(ServerTest, TooLongRequest) {
|
||||
std::string request;
|
||||
for (size_t i = 0; i < 545; i++) {
|
||||
for (size_t i = 0; i < 546; i++) {
|
||||
request += "/TooLongRequest";
|
||||
}
|
||||
request += "_NG";
|
||||
@ -3500,6 +3552,19 @@ TEST_F(ServerTest, TooLongRequest) {
|
||||
EXPECT_EQ(StatusCode::UriTooLong_414, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, AlmostTooLongRequest) {
|
||||
// test for #2046 - URI length check shouldn't include other content on req
|
||||
// line URI is max URI length, minus 14 other chars in req line (GET, space,
|
||||
// leading /, space, HTTP/1.1)
|
||||
std::string request =
|
||||
"/" + string(CPPHTTPLIB_REQUEST_URI_MAX_LENGTH - 14, 'A');
|
||||
|
||||
auto res = cli_.Get(request.c_str());
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::NotFound_404, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, LongHeader) {
|
||||
Request req;
|
||||
req.method = "GET";
|
||||
@ -3794,12 +3859,14 @@ TEST_F(ServerTest, GetStreamedWithRangeError) {
|
||||
TEST_F(ServerTest, GetRangeWithMaxLongLength) {
|
||||
auto res = cli_.Get(
|
||||
"/with-range",
|
||||
{{"Range",
|
||||
"bytes=0-" + std::to_string(std::numeric_limits<long>::max())}});
|
||||
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
|
||||
EXPECT_EQ("0", res->get_header_value("Content-Length"));
|
||||
EXPECT_EQ(false, res->has_header("Content-Range"));
|
||||
EXPECT_EQ(0U, res->body.size());
|
||||
{{"Range", "bytes=0-" + std::to_string(std::numeric_limits<long>::max())},
|
||||
{"Accept-Encoding", ""}});
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
|
||||
EXPECT_EQ("7", res->get_header_value("Content-Length"));
|
||||
EXPECT_EQ(true, res->has_header("Content-Range"));
|
||||
EXPECT_EQ("bytes 0-6/7", res->get_header_value("Content-Range"));
|
||||
EXPECT_EQ(std::string("abcdefg"), res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetRangeWithZeroToInfinite) {
|
||||
@ -4001,6 +4068,13 @@ TEST_F(ServerTest, Issue1772) {
|
||||
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, Issue609) {
|
||||
auto res = cli_.Delete("/issue609");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ(std::string("ok"), res->body);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, GetStreamedChunked) {
|
||||
auto res = cli_.Get("/streamed-chunked");
|
||||
ASSERT_TRUE(res);
|
||||
@ -4845,7 +4919,8 @@ static bool send_request(time_t read_timeout_sec, const std::string &req,
|
||||
if (client_sock == INVALID_SOCKET) { return false; }
|
||||
|
||||
auto ret = detail::process_client_socket(
|
||||
client_sock, read_timeout_sec, 0, 0, 0, [&](Stream &strm) {
|
||||
client_sock, read_timeout_sec, 0, 0, 0, 0,
|
||||
std::chrono::steady_clock::time_point::min(), [&](Stream &strm) {
|
||||
if (req.size() !=
|
||||
static_cast<size_t>(strm.write(req.data(), req.size()))) {
|
||||
return false;
|
||||
@ -4889,8 +4964,10 @@ TEST(ServerRequestParsingTest, TrimWhitespaceFromHeaderValues) {
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
ASSERT_TRUE(send_request(5, req));
|
||||
EXPECT_EQ(header_value, "\v bar \x1B");
|
||||
std::string res;
|
||||
ASSERT_TRUE(send_request(5, req, &res));
|
||||
EXPECT_EQ(header_value, "");
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", res.substr(0, 24));
|
||||
}
|
||||
|
||||
// Sends a raw request and verifies that there isn't a crash or exception.
|
||||
@ -5048,6 +5125,14 @@ TEST(ServerRequestParsingTest, InvalidFieldValueContains_CR_LF_NUL) {
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
||||
}
|
||||
|
||||
TEST(ServerRequestParsingTest, InvalidFieldValueContains_LF) {
|
||||
std::string out;
|
||||
std::string request(
|
||||
"GET /header_field_value_check HTTP/1.1\r\nTest: [\n\n\n]\r\n\r\n", 55);
|
||||
test_raw_request(request, &out);
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
||||
}
|
||||
|
||||
TEST(ServerRequestParsingTest, EmptyFieldValue) {
|
||||
std::string out;
|
||||
|
||||
@ -6132,6 +6217,18 @@ TEST(SSLClientTest, WildcardHostNameMatch_Online) {
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST(SSLClientTest, Issue2004_Online) {
|
||||
Client client("https://google.com");
|
||||
client.set_follow_location(true);
|
||||
|
||||
auto res = client.Get("/");
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
|
||||
auto body = res->body;
|
||||
EXPECT_EQ(body.substr(0, 15), "<!doctype html>");
|
||||
}
|
||||
|
||||
#if 0
|
||||
TEST(SSLClientTest, SetInterfaceWithINET6) {
|
||||
auto cli = std::make_shared<httplib::Client>("https://httpbin.org");
|
||||
@ -7472,9 +7569,9 @@ TEST(MultipartFormDataTest, CloseDelimiterWithoutCRLF) {
|
||||
"text2"
|
||||
"\r\n------------";
|
||||
|
||||
std::string resonse;
|
||||
ASSERT_TRUE(send_request(1, req, &resonse));
|
||||
ASSERT_EQ("200", resonse.substr(9, 3));
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("200", response.substr(9, 3));
|
||||
}
|
||||
|
||||
TEST(MultipartFormDataTest, ContentLength) {
|
||||
@ -7519,11 +7616,10 @@ TEST(MultipartFormDataTest, ContentLength) {
|
||||
"text2"
|
||||
"\r\n------------\r\n";
|
||||
|
||||
std::string resonse;
|
||||
ASSERT_TRUE(send_request(1, req, &resonse));
|
||||
ASSERT_EQ("200", resonse.substr(9, 3));
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("200", response.substr(9, 3));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST(TaskQueueTest, IncreaseAtomicInteger) {
|
||||
@ -7901,6 +7997,114 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
|
||||
cli.Get("/test", {{"Test", "_\n\r_\n\r_"}});
|
||||
}
|
||||
|
||||
TEST(InvalidHeaderCharsTest, is_field_name) {
|
||||
EXPECT_TRUE(detail::fields::is_field_name("exampleToken"));
|
||||
EXPECT_TRUE(detail::fields::is_field_name("token123"));
|
||||
EXPECT_TRUE(detail::fields::is_field_name("!#$%&'*+-.^_`|~"));
|
||||
|
||||
EXPECT_FALSE(detail::fields::is_field_name("example token"));
|
||||
EXPECT_FALSE(detail::fields::is_field_name(" example_token"));
|
||||
EXPECT_FALSE(detail::fields::is_field_name("example_token "));
|
||||
EXPECT_FALSE(detail::fields::is_field_name("token@123"));
|
||||
EXPECT_FALSE(detail::fields::is_field_name(""));
|
||||
EXPECT_FALSE(detail::fields::is_field_name("example\rtoken"));
|
||||
EXPECT_FALSE(detail::fields::is_field_name("example\ntoken"));
|
||||
EXPECT_FALSE(detail::fields::is_field_name(std::string("\0", 1)));
|
||||
EXPECT_FALSE(detail::fields::is_field_name("example\ttoken"));
|
||||
}
|
||||
|
||||
TEST(InvalidHeaderCharsTest, is_field_value) {
|
||||
EXPECT_TRUE(detail::fields::is_field_value("exampleToken"));
|
||||
EXPECT_TRUE(detail::fields::is_field_value("token123"));
|
||||
EXPECT_TRUE(detail::fields::is_field_value("!#$%&'*+-.^_`|~"));
|
||||
|
||||
EXPECT_TRUE(detail::fields::is_field_value("example token"));
|
||||
EXPECT_FALSE(detail::fields::is_field_value(" example_token"));
|
||||
EXPECT_FALSE(detail::fields::is_field_value("example_token "));
|
||||
EXPECT_TRUE(detail::fields::is_field_value("token@123"));
|
||||
EXPECT_TRUE(detail::fields::is_field_value(""));
|
||||
EXPECT_FALSE(detail::fields::is_field_value("example\rtoken"));
|
||||
EXPECT_FALSE(detail::fields::is_field_value("example\ntoken"));
|
||||
EXPECT_FALSE(detail::fields::is_field_value(std::string("\0", 1)));
|
||||
EXPECT_TRUE(detail::fields::is_field_value("example\ttoken"));
|
||||
|
||||
EXPECT_TRUE(detail::fields::is_field_value("0"));
|
||||
}
|
||||
|
||||
TEST(InvalidHeaderCharsTest, OnServer) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/test_name", [&](const Request &req, Response &res) {
|
||||
std::string header = "Not Set";
|
||||
if (req.has_param("header")) { header = req.get_param_value("header"); }
|
||||
|
||||
res.set_header(header, "value");
|
||||
res.set_content("Page Content Page Content", "text/plain");
|
||||
});
|
||||
|
||||
svr.Get("/test_value", [&](const Request &req, Response &res) {
|
||||
std::string header = "Not Set";
|
||||
if (req.has_param("header")) { header = req.get_param_value("header"); }
|
||||
|
||||
res.set_header("X-Test", header);
|
||||
res.set_content("Page Content Page Content", "text/plain");
|
||||
});
|
||||
|
||||
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, PORT);
|
||||
{
|
||||
auto res = cli.Get(
|
||||
R"(/test_name?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ("Page Content Page Content", res->body);
|
||||
EXPECT_FALSE(res->has_header("HEADER_KEY"));
|
||||
}
|
||||
{
|
||||
auto res = cli.Get(
|
||||
R"(/test_value?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ("Page Content Page Content", res->body);
|
||||
EXPECT_FALSE(res->has_header("HEADER_KEY"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(InvalidHeaderValueTest, InvalidContentLength) {
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &, Response &) { handled = true; });
|
||||
|
||||
thread t = thread([&] { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
ASSERT_FALSE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Content-Length: x\r\n"
|
||||
"\r\n";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("HTTP/1.1 400 Bad Request",
|
||||
response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
TEST(Expect100ContinueTest, ServerClosesConnection) {
|
||||
static constexpr char reject[] = "Unauthorized";
|
||||
@ -8002,3 +8206,231 @@ TEST(Expect100ContinueTest, ServerClosesConnection) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename S, typename C>
|
||||
inline void max_timeout_test(S &svr, C &cli, time_t timeout, time_t threshold) {
|
||||
svr.Get("/stream", [&](const Request &, Response &res) {
|
||||
auto data = new std::string("01234567890123456789");
|
||||
|
||||
res.set_content_provider(
|
||||
data->size(), "text/plain",
|
||||
[&, data](size_t offset, size_t length, DataSink &sink) {
|
||||
const size_t DATA_CHUNK_SIZE = 4;
|
||||
const auto &d = *data;
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE));
|
||||
return true;
|
||||
},
|
||||
[data](bool success) {
|
||||
EXPECT_FALSE(success);
|
||||
delete data;
|
||||
});
|
||||
});
|
||||
|
||||
svr.Get("/stream_without_length", [&](const Request &, Response &res) {
|
||||
auto i = new size_t(0);
|
||||
|
||||
res.set_content_provider(
|
||||
"text/plain",
|
||||
[i](size_t, DataSink &sink) {
|
||||
if (*i < 5) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
sink.write("abcd", 4);
|
||||
(*i)++;
|
||||
} else {
|
||||
sink.done();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[i](bool success) {
|
||||
EXPECT_FALSE(success);
|
||||
delete i;
|
||||
});
|
||||
});
|
||||
|
||||
svr.Get("/chunked", [&](const Request &, Response &res) {
|
||||
auto i = new size_t(0);
|
||||
|
||||
res.set_chunked_content_provider(
|
||||
"text/plain",
|
||||
[i](size_t, DataSink &sink) {
|
||||
if (*i < 5) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
sink.os << "abcd";
|
||||
(*i)++;
|
||||
} else {
|
||||
sink.done();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[i](bool success) {
|
||||
EXPECT_FALSE(success);
|
||||
delete i;
|
||||
});
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
listen_thread.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
cli.set_max_timeout(std::chrono::milliseconds(timeout));
|
||||
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
auto res = cli.Get("/stream");
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(Error::Read, res.error());
|
||||
EXPECT_TRUE(timeout <= elapsed && elapsed < timeout + threshold)
|
||||
<< "Timeout exceeded by " << (elapsed - timeout) << "ms";
|
||||
}
|
||||
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
auto res = cli.Get("/stream_without_length");
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(Error::Read, res.error());
|
||||
EXPECT_TRUE(timeout <= elapsed && elapsed < timeout + threshold)
|
||||
<< "Timeout exceeded by " << (elapsed - timeout) << "ms";
|
||||
}
|
||||
|
||||
{
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
auto res = cli.Get("/chunked", [&](const char *data, size_t data_length) {
|
||||
EXPECT_EQ("abcd", string(data, data_length));
|
||||
return true;
|
||||
});
|
||||
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start)
|
||||
.count();
|
||||
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(Error::Read, res.error());
|
||||
EXPECT_TRUE(timeout <= elapsed && elapsed < timeout + threshold)
|
||||
<< "Timeout exceeded by " << (elapsed - timeout) << "ms";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MaxTimeoutTest, ContentStream) {
|
||||
time_t timeout = 2000;
|
||||
time_t threshold = 200;
|
||||
|
||||
Server svr;
|
||||
Client cli("localhost", PORT);
|
||||
max_timeout_test(svr, cli, timeout, threshold);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
TEST(MaxTimeoutTest, ContentStreamSSL) {
|
||||
time_t timeout = 2000;
|
||||
time_t threshold = 500; // SSL_shutdown is slow on some operating systems.
|
||||
|
||||
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
|
||||
SSLClient cli("localhost", PORT);
|
||||
cli.enable_server_certificate_verification(false);
|
||||
|
||||
max_timeout_test(svr, cli, timeout, threshold);
|
||||
}
|
||||
#endif
|
||||
|
||||
class EventDispatcher {
|
||||
public:
|
||||
EventDispatcher() {}
|
||||
|
||||
void wait_event(DataSink *sink) {
|
||||
unique_lock<mutex> lk(m_);
|
||||
int id = id_;
|
||||
cv_.wait(lk, [&] { return cid_ == id; });
|
||||
sink->write(message_.data(), message_.size());
|
||||
}
|
||||
|
||||
void send_event(const string &message) {
|
||||
lock_guard<mutex> lk(m_);
|
||||
cid_ = id_++;
|
||||
message_ = message;
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
mutex m_;
|
||||
condition_variable cv_;
|
||||
atomic_int id_{0};
|
||||
atomic_int cid_{-1};
|
||||
string message_;
|
||||
};
|
||||
|
||||
TEST(ClientInThreadTest, Issue2068) {
|
||||
EventDispatcher ed;
|
||||
|
||||
Server svr;
|
||||
svr.Get("/event1", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_chunked_content_provider("text/event-stream",
|
||||
[&](size_t /*offset*/, DataSink &sink) {
|
||||
ed.wait_event(&sink);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
auto listen_thread = std::thread([&svr]() { svr.listen(HOST, PORT); });
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
thread event_thread([&] {
|
||||
int id = 0;
|
||||
while (svr.is_running()) {
|
||||
this_thread::sleep_for(chrono::milliseconds(500));
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "data: " << id << "\n\n";
|
||||
ed.send_event(ss.str());
|
||||
id++;
|
||||
}
|
||||
});
|
||||
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
|
||||
listen_thread.join();
|
||||
event_thread.join();
|
||||
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
{
|
||||
auto client = detail::make_unique<Client>(HOST, PORT);
|
||||
client->set_read_timeout(std::chrono::minutes(10));
|
||||
|
||||
std::atomic<bool> stop{false};
|
||||
|
||||
std::thread t([&] {
|
||||
client->Get("/event1",
|
||||
[&](const char *, size_t) -> bool { return !stop; });
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
stop = true;
|
||||
client->stop();
|
||||
client.reset();
|
||||
|
||||
t.join();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@
|
||||
using namespace std;
|
||||
using namespace httplib;
|
||||
|
||||
template <typename T>
|
||||
void ProxyTest(T& cli, bool basic) {
|
||||
template <typename T> void ProxyTest(T &cli, bool basic) {
|
||||
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
||||
auto res = cli.Get("/httpbin/get");
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
@ -38,7 +37,7 @@ TEST(ProxyTest, SSLDigest) {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
template <typename T>
|
||||
void RedirectProxyText(T& cli, const char *path, bool basic) {
|
||||
void RedirectProxyText(T &cli, const char *path, bool basic) {
|
||||
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
||||
if (basic) {
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
@ -100,8 +99,7 @@ TEST(RedirectTest, YouTubeSSLDigest) {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
template <typename T>
|
||||
void BaseAuthTestFromHTTPWatch(T& cli) {
|
||||
template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
|
||||
@ -112,11 +110,11 @@ void BaseAuthTestFromHTTPWatch(T& cli) {
|
||||
}
|
||||
|
||||
{
|
||||
auto res =
|
||||
cli.Get("/basic-auth/hello/world",
|
||||
{make_basic_authentication_header("hello", "world")});
|
||||
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("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@ -124,7 +122,8 @@ 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("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@ -158,8 +157,7 @@ TEST(BaseAuthTest, SSL) {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
template <typename T>
|
||||
void DigestAuthTestFromHTTPWatch(T& cli) {
|
||||
template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_proxy("localhost", 3129);
|
||||
cli.set_proxy_digest_auth("hello", "world");
|
||||
|
||||
@ -181,7 +179,8 @@ void DigestAuthTestFromHTTPWatch(T& cli) {
|
||||
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);
|
||||
EXPECT_EQ("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@ -216,8 +215,7 @@ TEST(DigestAuthTest, NoSSL) {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
template <typename T>
|
||||
void KeepAliveTest(T& cli, bool basic) {
|
||||
template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
cli.set_proxy("localhost", basic ? 3128 : 3129);
|
||||
if (basic) {
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
@ -249,9 +247,10 @@ void KeepAliveTest(T& cli, bool basic) {
|
||||
"/httpbin/digest-auth/auth-int/hello/world/MD5",
|
||||
};
|
||||
|
||||
for (auto path: paths) {
|
||||
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("{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n",
|
||||
res->body);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user