From dff6af2fc17f9ac313e64ab8fb83bd01a1d7da70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomi=20L=C3=A4hteenm=C3=A4ki?= Date: Sun, 17 Aug 2025 02:26:47 +0300 Subject: [PATCH 01/47] Fix have_gnutls test in configure.ac (#341) The defines were never added as the check always returned false. Closes #340 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 9d5c63e2..66b8e757 100644 --- a/configure.ac +++ b/configure.ac @@ -245,7 +245,7 @@ AM_CONDITIONAL([BUILD_EXAMPLES], [test "x$enable_examples" = "xyes"]) AM_CONDITIONAL([COND_GCOV],[test x"$cond_gcov" = x"yes"]) AC_SUBST(COND_GCOV) -if test x"have_gnutls" = x"yes"; then +if test x"$have_gnutls" = x"yes"; then AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_GNUTLS" AM_CFLAGS="$AM_CXXFLAGS -DHAVE_GNUTLS" fi From f15a7b0f61ea5c2332d1e4934460be3d71a72cf0 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Jan 2026 01:11:03 -0800 Subject: [PATCH 02/47] Add TLS-PSK authentication support via callback mechanism (#348) * Add TLS-PSK authentication support via callback mechanism This adds support for TLS Pre-Shared Key (PSK) authentication, allowing secure connections without certificates using a shared secret key. Changes: - Add psk_cred_handler() builder method to create_webserver - Add psk_cred_handler_callback typedef for PSK credential lookup - Implement psk_cred_handler_func() static callback using GnuTLS - Add MHD_OPTION_GNUTLS_PSK_CRED_HANDLER option when PSK is configured - Add AM_CONDITIONAL for HAVE_GNUTLS in configure.ac - Remove deprecated AC_HEADER_STDC macro - Add minimal_https_psk example demonstrating PSK usage - Add conditional GnuTLS linking in test/Makefile.am - Update README.md with PSK documentation and example The callback receives a username and returns the hex-encoded PSK, or an empty string for unknown users. * Update GitHub Actions to v4 (cache and checkout) * Fix linker error: link all examples against gnutls when available The library now uses GnuTLS functions (gnutls_malloc, gnutls_free, gnutls_hex2bin) for PSK support, so all examples need to link against gnutls when HAVE_GNUTLS is defined, not just the PSK example. * Update CI for Ubuntu 24.04 compatibility - Update sanitizer builds (asan, lsan, tsan, ubsan) from clang-13 to clang-18 - Move clang-11, clang-12, clang-13 tests to ubuntu-22.04 - Add new clang-14 through clang-17 tests on ubuntu-latest - Add gcc-11 through gcc-14 tests - Remove obsolete ubuntu-20.04 jobs (gcc-7, gcc-8, clang-6 through clang-10) - Update IWYU job to use clang-18 on ubuntu-latest - Fix cpplint errors: add missing includes and fix namespace indentation Co-Authored-By: Claude Opus 4.5 * Fix valgrind CI job for Ubuntu 24.04 - Remove valgrind-dbg package (debug symbols now included in main package) - Update valgrind job to use GCC 14 instead of GCC 10 Co-Authored-By: Claude Opus 4.5 * Use GCC 13 for valgrind job to avoid -Woverloaded-virtual error GCC 14 with -Werror catches a latent warning in littletest.hpp test framework that older compilers don't flag. Use GCC 13 for now. Co-Authored-By: Claude Opus 4.5 * Fix issues with valgrind running on g++-14 * FIx issue with overloads in tests * Fix issue with gnutls * Fix all cpplint issues * Added codacy suppressions * No need for codacy yaml. It is configured via UI --------- Co-authored-by: Claude Opus 4.5 --- .github/workflows/verify-build.yml | 141 +++++++++++----------- README.md | 54 +++++++++ configure.ac | 3 +- examples/Makefile.am | 6 + examples/allowing_disallowing_methods.cpp | 2 + examples/basic_authentication.cpp | 3 + examples/custom_access_log.cpp | 2 + examples/custom_error.cpp | 2 + examples/deferred_with_accumulator.cpp | 3 + examples/digest_authentication.cpp | 2 + examples/file_upload.cpp | 3 + examples/handlers.cpp | 2 + examples/hello_with_get_arg.cpp | 3 + examples/hello_world.cpp | 2 + examples/minimal_deferred.cpp | 4 + examples/minimal_file_response.cpp | 2 + examples/minimal_hello_world.cpp | 2 + examples/minimal_https.cpp | 2 + examples/minimal_https_psk.cpp | 63 ++++++++++ examples/minimal_ip_ban.cpp | 2 + examples/service.cpp | 1 + examples/setting_headers.cpp | 2 + examples/url_registration.cpp | 3 + src/file_info.cpp | 1 + src/http_request.cpp | 3 + src/http_resource.cpp | 2 + src/http_response.cpp | 2 + src/http_utils.cpp | 6 +- src/httpserver/create_webserver.hpp | 7 ++ src/httpserver/deferred_response.hpp | 4 +- src/httpserver/webserver.hpp | 11 ++ src/webserver.cpp | 48 +++++++- test/Makefile.am | 9 +- test/integ/authentication.cpp | 1 + test/integ/basic.cpp | 4 + test/integ/deferred.cpp | 2 + test/integ/file_upload.cpp | 5 +- test/integ/ws_start_stop.cpp | 7 +- test/littletest.hpp | 1 + test/unit/http_endpoint_test.cpp | 4 + test/unit/http_utils_test.cpp | 4 + test/unit/string_utilities_test.cpp | 2 + 42 files changed, 355 insertions(+), 77 deletions(-) create mode 100644 examples/minimal_https_psk.cpp diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 77530449..e4842605 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -58,8 +58,8 @@ jobs: os-type: ubuntu build-type: asan compiler-family: clang - c-compiler: clang-13 - cc-compiler: clang++-13 + c-compiler: clang-18 + cc-compiler: clang++-18 debug: debug coverage: nocoverage # This test gives false positives on newer versions of clang @@ -78,8 +78,8 @@ jobs: os-type: ubuntu build-type: lsan compiler-family: clang - c-compiler: clang-13 - cc-compiler: clang++-13 + c-compiler: clang-18 + cc-compiler: clang++-18 debug: debug coverage: nocoverage - test-group: extra @@ -87,8 +87,8 @@ jobs: os-type: ubuntu build-type: tsan compiler-family: clang - c-compiler: clang-13 - cc-compiler: clang++-13 + c-compiler: clang-18 + cc-compiler: clang++-18 debug: debug coverage: nocoverage - test-group: extra @@ -96,26 +96,26 @@ jobs: os-type: ubuntu build-type: ubsan compiler-family: clang - c-compiler: clang-13 - cc-compiler: clang++-13 + c-compiler: clang-18 + cc-compiler: clang++-18 debug: debug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-latest os-type: ubuntu build-type: none compiler-family: gcc - c-compiler: gcc-7 - cc-compiler: g++-7 + c-compiler: gcc-9 + cc-compiler: g++-9 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-latest os-type: ubuntu build-type: none compiler-family: gcc - c-compiler: gcc-8 - cc-compiler: g++-8 + c-compiler: gcc-10 + cc-compiler: g++-10 debug: nodebug coverage: nocoverage - test-group: extra @@ -123,8 +123,8 @@ jobs: os-type: ubuntu build-type: none compiler-family: gcc - c-compiler: gcc-9 - cc-compiler: g++-9 + c-compiler: gcc-11 + cc-compiler: g++-11 debug: nodebug coverage: nocoverage - test-group: extra @@ -132,53 +132,62 @@ jobs: os-type: ubuntu build-type: none compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 + c-compiler: gcc-12 + cc-compiler: g++-12 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-latest os-type: ubuntu build-type: none - compiler-family: clang - c-compiler: clang-6.0 - cc-compiler: clang++-6.0 + compiler-family: gcc + c-compiler: gcc-13 + cc-compiler: g++-13 + debug: nodebug + coverage: nocoverage + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: none + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-22.04 os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-7 - cc-compiler: clang++-7 + c-compiler: clang-11 + cc-compiler: clang++-11 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-22.04 os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-8 - cc-compiler: clang++-8 + c-compiler: clang-12 + cc-compiler: clang++-12 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-22.04 os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-9 - cc-compiler: clang++-9 + c-compiler: clang-13 + cc-compiler: clang++-13 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-latest os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-10 - cc-compiler: clang++-10 + c-compiler: clang-14 + cc-compiler: clang++-14 debug: nodebug coverage: nocoverage - test-group: extra @@ -186,8 +195,8 @@ jobs: os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-11 - cc-compiler: clang++-11 + c-compiler: clang-15 + cc-compiler: clang++-15 debug: nodebug coverage: nocoverage - test-group: extra @@ -195,8 +204,8 @@ jobs: os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-12 - cc-compiler: clang++-12 + c-compiler: clang-16 + cc-compiler: clang++-16 debug: nodebug coverage: nocoverage - test-group: extra @@ -204,8 +213,8 @@ jobs: os-type: ubuntu build-type: none compiler-family: clang - c-compiler: clang-13 - cc-compiler: clang++-13 + c-compiler: clang-17 + cc-compiler: clang++-17 debug: nodebug coverage: nocoverage - test-group: extra @@ -213,17 +222,17 @@ jobs: os-type: ubuntu build-type: valgrind compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 + c-compiler: gcc-14 + cc-compiler: g++-14 debug: nodebug coverage: nocoverage - test-group: extra - os: ubuntu-20.04 + os: ubuntu-latest os-type: ubuntu build-type: iwyu compiler-family: clang - c-compiler: clang-9 - cc-compiler: clang++-9 + c-compiler: clang-18 + cc-compiler: clang++-18 debug: nodebug coverage: nocoverage - test-group: performance @@ -264,7 +273,7 @@ jobs: coverage: nocoverage steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -294,7 +303,7 @@ jobs: if: ${{ matrix.compiler-family == 'gcc' && matrix.os-type == 'ubuntu' }} - name: Install valgrind if needed - run: sudo apt-get install valgrind valgrind-dbg + run: sudo apt-get install valgrind if: ${{ matrix.build-type == 'valgrind' && matrix.os-type == 'ubuntu' }} - name: Install cpplint if needed @@ -303,16 +312,12 @@ jobs: - name: Install IWYU dependencies if needed run: | - # Use same deps used by iwyu in their setup for travis - sudo apt-get install llvm-9-dev llvm-9-tools libclang-9-dev ; - # Use same CMAKE used by iwyu in their setup for travis - wget -O cmake.sh https://cmake.org/files/v3.10/cmake-3.10.0-Linux-x86_64.sh ; - sudo sh cmake.sh --skip-license --exclude-subdir --prefix=/usr/local ; + sudo apt-get install llvm-18-dev libclang-18-dev clang-18 ; if: ${{ matrix.build-type == 'iwyu' && matrix.os-type == 'ubuntu' }} - name: IWYU from cache (for testing) id: cache-IWYU - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: include-what-you-use key: ${{ matrix.os }}-${{ matrix.c-compiler }}-include-what-you-use-pre-built @@ -321,11 +326,11 @@ jobs: # Installing iwyu manually because clang and iwyu paths won't match on Ubuntu otherwise. - name: Build IWYU if requested run: | - CLANG_ROOT_PATH=`llvm-config-9 --prefix` ; - CLANG_BIN_PATH=`llvm-config-9 --bindir` ; - curl "https://libhttpserver.s3.amazonaws.com/travis_stuff/include-what-you-use-clang-9.tgz" -o "include-what-you-use-clang-9.tgz" ; - tar -xzf "include-what-you-use-clang-9.tgz" ; + CLANG_ROOT_PATH=`llvm-config-18 --prefix` ; + CLANG_BIN_PATH=`llvm-config-18 --bindir` ; + git clone https://github.com/include-what-you-use/include-what-you-use.git ; cd include-what-you-use ; + git checkout clang_18 ; mkdir build_iwyu ; cd build_iwyu ; cmake -G "Unix Makefiles" -DCMAKE_PREFIX_PATH=$CLANG_ROOT_PATH -DCMAKE_C_COMPILER=$CLANG_BIN_PATH/clang -DCMAKE_CXX_COMPILER=$CLANG_BIN_PATH/clang++ ../ ; @@ -341,7 +346,7 @@ jobs: - name: CURL from cache (for testing) id: cache-CURL - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: curl-7.75.0 key: ${{ matrix.os }}-CURL-pre-built @@ -352,7 +357,7 @@ jobs: curl https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-7.75.0.tar.gz -o curl-7.75.0.tar.gz ; tar -xzf curl-7.75.0.tar.gz ; cd curl-7.75.0 ; - if [ "$matrix.os-type" = "ubuntu" ]; then ./configure ; else ./configure --with-darwinssl ; fi + ./configure --with-darwinssl ; make ; if: ${{ matrix.os == 'macos-latest' && steps.cache-CURL.outputs.cache-hit != 'true' }} @@ -386,22 +391,22 @@ jobs: - name: Fetch libmicrohttpd from cache id: cache-libmicrohttpd - uses: actions/cache@v2 + uses: actions/cache@v4 with: - path: libmicrohttpd-0.9.64 - key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-pre-built + path: libmicrohttpd-0.9.77 + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built - name: Build libmicrohttpd dependency (if not cached) run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.64.tar.gz -o libmicrohttpd-0.9.64.tar.gz ; - tar -xzf libmicrohttpd-0.9.64.tar.gz ; - cd libmicrohttpd-0.9.64 ; + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz ; + tar -xzf libmicrohttpd-0.9.77.tar.gz ; + cd libmicrohttpd-0.9.77 ; ./configure --disable-examples ; make ; if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' - + - name: Install libmicrohttpd - run: cd libmicrohttpd-0.9.64 ; sudo make install ; + run: cd libmicrohttpd-0.9.77 ; sudo make install ; - name: Refresh links to shared libs run: sudo ldconfig ; @@ -421,7 +426,7 @@ jobs: if [ "$BUILD_TYPE" = "ubsan" ]; then export export CFLAGS='-fsanitize=undefined'; export CXXLAGS='-fsanitize=undefined'; export LDFLAGS='-fsanitize=undefined'; fi # Additional flags on mac. They need to stay in step as env variables don't propagate across steps. - if [ "$matrix.os" = "macos-latest" ]; then + if [ "${{ matrix.os }}" = "macos-latest" ]; then export CFLAGS='-mtune=generic' ; export IPV6_TESTS_ENABLED="true" ; fi diff --git a/README.md b/README.md index b0a4c0e8..83f0b5b6 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver * _.https_mem_cert(**const std::string&** filename):_ String representing the path to a file containing the certificate to be used by the HTTPS daemon. This must be used in conjunction with `https_mem_key`. * _.https_mem_trust(**const std::string&** filename):_ String representing the path to a file containing the CA certificate to be used by the HTTPS daemon to authenticate and trust clients certificates. The presence of this option activates the request of certificate to the client. The request to the client is marked optional, and it is the responsibility of the server to check the presence of the certificate if needed. Note that most browsers will only present a client certificate only if they have one matching the specified CA, not sending any certificate otherwise. * _.https_priorities(**const std::string&** priority_string):_ SSL/TLS protocol version and ciphers. Must be followed by a string specifying the SSL/TLS protocol versions and ciphers that are acceptable for the application. The string is passed unchanged to gnutls_priority_init. If this option is not specified, `"NORMAL"` is used. +* _.psk_cred_handler(**psk_cred_handler_callback** handler):_ Sets a callback function for TLS-PSK (Pre-Shared Key) authentication. The callback receives a username and should return the corresponding hex-encoded PSK, or an empty string if the user is unknown. This option requires `use_ssl()`, `cred_type(http::http_utils::PSK)`, and an appropriate `https_priorities()` string that enables PSK cipher suites. PSK authentication allows TLS without certificates by using a shared secret key. #### Minimal example using HTTPS ```cpp @@ -346,6 +347,59 @@ To test the above example, you can run the following command from a terminal: You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/minimal_https.cpp). +#### Minimal example using TLS-PSK +```cpp + #include + #include + #include + + using namespace httpserver; + + // Simple PSK database - in production, use secure storage + std::map psk_database = { + {"client1", "0123456789abcdef0123456789abcdef"}, + {"client2", "fedcba9876543210fedcba9876543210"} + }; + + // PSK credential handler callback + std::string psk_handler(const std::string& username) { + auto it = psk_database.find(username); + if (it != psk_database.end()) { + return it->second; + } + return ""; // Return empty string for unknown users + } + + class hello_world_resource : public http_resource { + public: + std::shared_ptr render(const http_request&) { + return std::shared_ptr( + new string_response("Hello, World (via TLS-PSK)!")); + } + }; + + int main(int argc, char** argv) { + webserver ws = create_webserver(8080) + .use_ssl() + .cred_type(http::http_utils::PSK) + .psk_cred_handler(psk_handler) + .https_priorities("NORMAL:-VERS-TLS-ALL:+VERS-TLS1.2:+PSK:+DHE-PSK"); + + hello_world_resource hwr; + ws.register_resource("/hello", &hwr); + ws.start(true); + + return 0; + } +``` +To test the above example, you can run the following command from a terminal using gnutls-cli: + + gnutls-cli --pskusername=client1 --pskkey=0123456789abcdef0123456789abcdef -p 8080 localhost + +Then type `GET /hello HTTP/1.1` followed by `Host: localhost` and two newlines. + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/minimal_https_psk.cpp). + ### IP Blacklisting/Whitelisting libhttpserver supports IP blacklisting and whitelisting as an internal feature. This section explains the startup options related with IP blacklisting/whitelisting. See the [specific section](#ip-blacklisting-and-whitelisting) to read more about the topic. * _.ban_system() and .no_ban_system:_ Can be used to enable/disable the ban system. `on` by default. diff --git a/configure.ac b/configure.ac index 66b8e757..1197c136 100644 --- a/configure.ac +++ b/configure.ac @@ -83,7 +83,6 @@ case "$host" in esac # Checks for header files. -AC_HEADER_STDC AC_CHECK_HEADER([stdint.h],[],[AC_MSG_ERROR("stdint.h not found")]) AC_CHECK_HEADER([inttypes.h],[],[AC_MSG_ERROR("inttypes.h not found")]) AC_CHECK_HEADER([errno.h],[],[AC_MSG_ERROR("errno.h not found")]) @@ -250,6 +249,8 @@ if test x"$have_gnutls" = x"yes"; then AM_CFLAGS="$AM_CXXFLAGS -DHAVE_GNUTLS" fi +AM_CONDITIONAL([HAVE_GNUTLS],[test x"$have_gnutls" = x"yes"]) + DX_HTML_FEATURE(ON) DX_CHM_FEATURE(OFF) DX_CHI_FEATURE(OFF) diff --git a/examples/Makefile.am b/examples/Makefile.am index 0fe116af..cf838c30 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -42,3 +42,9 @@ benchmark_select_SOURCES = benchmark_select.cpp benchmark_threads_SOURCES = benchmark_threads.cpp benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp + +if HAVE_GNUTLS +LDADD += -lgnutls +noinst_PROGRAMS += minimal_https_psk +minimal_https_psk_SOURCES = minimal_https_psk.cpp +endif diff --git a/examples/allowing_disallowing_methods.cpp b/examples/allowing_disallowing_methods.cpp index 73389142..50efa4fd 100644 --- a/examples/allowing_disallowing_methods.cpp +++ b/examples/allowing_disallowing_methods.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/basic_authentication.cpp b/examples/basic_authentication.cpp index 7fb82340..661bbb3c 100644 --- a/examples/basic_authentication.cpp +++ b/examples/basic_authentication.cpp @@ -18,6 +18,9 @@ USA */ +#include +#include + #include class user_pass_resource : public httpserver::http_resource { diff --git a/examples/custom_access_log.cpp b/examples/custom_access_log.cpp index f1a59d53..8f596c90 100644 --- a/examples/custom_access_log.cpp +++ b/examples/custom_access_log.cpp @@ -19,6 +19,8 @@ */ #include +#include +#include #include diff --git a/examples/custom_error.cpp b/examples/custom_error.cpp index a82d5972..c38fb169 100644 --- a/examples/custom_error.cpp +++ b/examples/custom_error.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include std::shared_ptr not_found_custom(const httpserver::http_request&) { diff --git a/examples/deferred_with_accumulator.cpp b/examples/deferred_with_accumulator.cpp index 3d3a4e69..a4367773 100644 --- a/examples/deferred_with_accumulator.cpp +++ b/examples/deferred_with_accumulator.cpp @@ -18,11 +18,14 @@ USA */ +#include #include #include // cpplint errors on chrono and thread because they are replaced (in Chromium) by other google libraries. // This is not an issue here. #include // NOLINT [build/c++11] +#include +#include #include // NOLINT [build/c++11] #include diff --git a/examples/digest_authentication.cpp b/examples/digest_authentication.cpp index 40767dc2..fb87cd4b 100644 --- a/examples/digest_authentication.cpp +++ b/examples/digest_authentication.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include #define MY_OPAQUE "11733b200778ce33060f31c9af70a870ba96ddd4" diff --git a/examples/file_upload.cpp b/examples/file_upload.cpp index 7e1afd5a..0916a4fc 100644 --- a/examples/file_upload.cpp +++ b/examples/file_upload.cpp @@ -19,6 +19,9 @@ */ #include +#include +#include + #include class file_upload_resource : public httpserver::http_resource { diff --git a/examples/handlers.cpp b/examples/handlers.cpp index 10778aaf..4fc70303 100644 --- a/examples/handlers.cpp +++ b/examples/handlers.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/hello_with_get_arg.cpp b/examples/hello_with_get_arg.cpp index 268d00c3..41829a4d 100644 --- a/examples/hello_with_get_arg.cpp +++ b/examples/hello_with_get_arg.cpp @@ -18,6 +18,9 @@ USA */ +#include +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/hello_world.cpp b/examples/hello_world.cpp index 391a600f..9c06f87a 100755 --- a/examples/hello_world.cpp +++ b/examples/hello_world.cpp @@ -19,6 +19,8 @@ */ #include +#include +#include #include diff --git a/examples/minimal_deferred.cpp b/examples/minimal_deferred.cpp index e7c77f50..d7a61d90 100644 --- a/examples/minimal_deferred.cpp +++ b/examples/minimal_deferred.cpp @@ -18,7 +18,11 @@ USA */ +#include #include +#include +#include + #include static int counter = 0; diff --git a/examples/minimal_file_response.cpp b/examples/minimal_file_response.cpp index a5dc8106..34776993 100644 --- a/examples/minimal_file_response.cpp +++ b/examples/minimal_file_response.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class file_response_resource : public httpserver::http_resource { diff --git a/examples/minimal_hello_world.cpp b/examples/minimal_hello_world.cpp index f8fb27f0..fc166535 100644 --- a/examples/minimal_hello_world.cpp +++ b/examples/minimal_hello_world.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/minimal_https.cpp b/examples/minimal_https.cpp index d5b8d443..79cd710c 100644 --- a/examples/minimal_https.cpp +++ b/examples/minimal_https.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/minimal_https_psk.cpp b/examples/minimal_https_psk.cpp new file mode 100644 index 00000000..9bb02ef6 --- /dev/null +++ b/examples/minimal_https_psk.cpp @@ -0,0 +1,63 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2024 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include +#include + +#include + +// Simple PSK database - in production, use secure storage +std::map psk_database = { + {"client1", "0123456789abcdef0123456789abcdef"}, + {"client2", "fedcba9876543210fedcba9876543210"} +}; + +// PSK credential handler callback +// Returns the hex-encoded PSK for the given username, or empty string if not found +std::string psk_handler(const std::string& username) { + auto it = psk_database.find(username); + if (it != psk_database.end()) { + return it->second; + } + return ""; // Return empty string for unknown users +} + +class hello_world_resource : public httpserver::http_resource { + public: + std::shared_ptr render(const httpserver::http_request&) { + return std::shared_ptr( + new httpserver::string_response("Hello, World (via TLS-PSK)!")); + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080) + .use_ssl() + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(psk_handler) + .https_priorities("NORMAL:-VERS-TLS-ALL:+VERS-TLS1.2:+PSK:+DHE-PSK"); + + hello_world_resource hwr; + ws.register_resource("/hello", &hwr); + ws.start(true); + + return 0; +} diff --git a/examples/minimal_ip_ban.cpp b/examples/minimal_ip_ban.cpp index 7be3cd17..4b95b5f0 100644 --- a/examples/minimal_ip_ban.cpp +++ b/examples/minimal_ip_ban.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/service.cpp b/examples/service.cpp index 2bc9d4ad..309628bc 100644 --- a/examples/service.cpp +++ b/examples/service.cpp @@ -22,6 +22,7 @@ #include #include +#include #include diff --git a/examples/setting_headers.cpp b/examples/setting_headers.cpp index ea678b92..f92b76c1 100644 --- a/examples/setting_headers.cpp +++ b/examples/setting_headers.cpp @@ -18,6 +18,8 @@ USA */ +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/examples/url_registration.cpp b/examples/url_registration.cpp index be6d1ce3..e6eef458 100644 --- a/examples/url_registration.cpp +++ b/examples/url_registration.cpp @@ -18,6 +18,9 @@ USA */ +#include +#include + #include class hello_world_resource : public httpserver::http_resource { diff --git a/src/file_info.cpp b/src/file_info.cpp index 88a21583..d37e7aeb 100644 --- a/src/file_info.cpp +++ b/src/file_info.cpp @@ -19,6 +19,7 @@ */ #include +#include #include "httpserver/file_info.hpp" namespace httpserver { diff --git a/src/http_request.cpp b/src/http_request.cpp index c49c3b8e..68ced762 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -23,6 +23,9 @@ #include #include #include +#include +#include +#include #include "httpserver/http_utils.hpp" #include "httpserver/string_utilities.hpp" diff --git a/src/http_resource.cpp b/src/http_resource.cpp index 88688484..430c4e65 100644 --- a/src/http_resource.cpp +++ b/src/http_resource.cpp @@ -21,7 +21,9 @@ #include "httpserver/http_resource.hpp" #include #include +#include #include +#include #include "httpserver/string_response.hpp" namespace httpserver { class http_response; } diff --git a/src/http_response.cpp b/src/http_response.cpp index cb357b24..f12589f7 100644 --- a/src/http_response.cpp +++ b/src/http_response.cpp @@ -21,6 +21,8 @@ #include "httpserver/http_response.hpp" #include #include +#include +#include #include #include "httpserver/http_utils.hpp" diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 975f1a8f..138c44ef 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -44,7 +44,9 @@ #include #include #include +#include #include +#include #include "httpserver/string_utilities.hpp" @@ -206,9 +208,9 @@ const char* http_utils::text_plain = "text/plain"; const char* http_utils::upload_filename_template = "libhttpserver.XXXXXX"; #if defined(_WIN32) - const char http_utils::path_separator = '\\'; +const char http_utils::path_separator = '\\'; #else // _WIN32 - const char http_utils::path_separator = '/'; +const char http_utils::path_separator = '/'; #endif // _WIN32 std::vector http_utils::tokenize_url(const std::string& str, const char separator) { diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index c6d61e4d..fca96d91 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -46,6 +46,7 @@ typedef std::function(const http_request&)> rende typedef std::function validator_ptr; typedef std::function log_access_ptr; typedef std::function log_error_ptr; +typedef std::function psk_cred_handler_callback; class create_webserver { public: @@ -223,6 +224,11 @@ class create_webserver { return *this; } + create_webserver& psk_cred_handler(psk_cred_handler_callback handler) { + _psk_cred_handler = handler; + return *this; + } + create_webserver& digest_auth_random(const std::string& digest_auth_random) { _digest_auth_random = digest_auth_random; return *this; @@ -384,6 +390,7 @@ class create_webserver { std::string _https_mem_trust = ""; std::string _https_priorities = ""; http::http_utils::cred_type_T _cred_type = http::http_utils::NONE; + psk_cred_handler_callback _psk_cred_handler = nullptr; std::string _digest_auth_random = ""; int _nonce_nc_size = 0; http::http_utils::policy_T _default_policy = http::http_utils::ACCEPT; diff --git a/src/httpserver/deferred_response.hpp b/src/httpserver/deferred_response.hpp index 85f8791f..76a2127a 100644 --- a/src/httpserver/deferred_response.hpp +++ b/src/httpserver/deferred_response.hpp @@ -38,8 +38,8 @@ struct MHD_Response; namespace httpserver { namespace details { - MHD_Response* get_raw_response_helper(void* cls, ssize_t (*cb)(void*, uint64_t, char*, size_t)); -} +MHD_Response* get_raw_response_helper(void* cls, ssize_t (*cb)(void*, uint64_t, char*, size_t)); +} // namespace details template class deferred_response : public string_response { diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index b2cbb1a6..05b43a32 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -45,6 +45,10 @@ #include #include +#ifdef HAVE_GNUTLS +#include +#endif // HAVE_GNUTLS + #include "httpserver/http_utils.hpp" #include "httpserver/create_webserver.hpp" #include "httpserver/details/http_endpoint.hpp" @@ -153,6 +157,7 @@ class webserver { const std::string https_mem_trust; const std::string https_priorities; const http::http_utils::cred_type_T cred_type; + const psk_cred_handler_callback psk_cred_handler; const std::string digest_auth_random; const int nonce_nc_size; bool running; @@ -210,6 +215,12 @@ class webserver { MHD_Result complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method); +#ifdef HAVE_GNUTLS + static int psk_cred_handler_func(gnutls_session_t session, + const char* username, + gnutls_datum_t* key); +#endif // HAVE_GNUTLS + friend MHD_Result policy_callback(void *cls, const struct sockaddr* addr, socklen_t addrlen); friend void error_log(void* cls, const char* fmt, va_list ap); friend void access_log(webserver* cls, std::string uri); diff --git a/src/webserver.cpp b/src/webserver.cpp index 6e0c7ead..749efea1 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -40,10 +40,13 @@ #include #include #include -#include #include +#include +#include #include +#include #include +#include #include #include @@ -142,6 +145,7 @@ webserver::webserver(const create_webserver& params): https_mem_trust(params._https_mem_trust), https_priorities(params._https_priorities), cred_type(params._cred_type), + psk_cred_handler(params._psk_cred_handler), digest_auth_random(params._digest_auth_random), nonce_nc_size(params._nonce_nc_size), running(false), @@ -276,6 +280,11 @@ bool webserver::start(bool blocking) { if (cred_type != http_utils::NONE) { iov.push_back(gen(MHD_OPTION_HTTPS_CRED_TYPE, cred_type)); } + + if (psk_cred_handler != nullptr && use_ssl) { + iov.push_back(gen(MHD_OPTION_GNUTLS_PSK_CRED_HANDLER, + (intptr_t)&psk_cred_handler_func, this)); + } #endif // HAVE_GNUTLS iov.push_back(gen(MHD_OPTION_END, 0, nullptr)); @@ -396,6 +405,43 @@ void webserver::disallow_ip(const string& ip) { allowances.erase(ip_representation(ip)); } +#ifdef HAVE_GNUTLS +int webserver::psk_cred_handler_func(gnutls_session_t session, + const char* username, + gnutls_datum_t* key) { + webserver* ws = static_cast( + gnutls_session_get_ptr(session)); + + if (ws == nullptr || ws->psk_cred_handler == nullptr) { + return -1; + } + + std::string psk_hex = ws->psk_cred_handler(std::string(username)); + if (psk_hex.empty()) { + return -1; + } + + // Convert hex string to binary + size_t psk_len = psk_hex.size() / 2; + key->data = static_cast(gnutls_malloc(psk_len)); + if (key->data == nullptr) { + return -1; + } + + size_t output_size = psk_len; + int ret = gnutls_hex2bin(psk_hex.c_str(), psk_hex.size(), + key->data, &output_size); + if (ret < 0) { + gnutls_free(key->data); + key->data = nullptr; + return -1; + } + + key->size = static_cast(output_size); + return 0; +} +#endif // HAVE_GNUTLS + MHD_Result policy_callback(void *cls, const struct sockaddr* addr, socklen_t addrlen) { // Parameter needed to respect MHD interface, but not needed here. std::ignore = addrlen; diff --git a/test/Makefile.am b/test/Makefile.am index 68ddb554..eb07a25a 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -17,6 +17,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA LDADD = $(top_builddir)/src/libhttpserver.la + +if HAVE_GNUTLS +LDADD += -lgnutls +endif + +LDADD += -lcurl + AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource @@ -37,7 +44,7 @@ nodelay_SOURCES = integ/nodelay.cpp http_resource_SOURCES = unit/http_resource_test.cpp noinst_HEADERS = littletest.hpp -AM_CXXFLAGS += -lcurl -Wall -fPIC +AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual if COND_GCOV AM_CFLAGS += -O0 --coverage --no-inline diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index bcc1c55b..4f4096d3 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -32,6 +32,7 @@ #include #include +#include #include "./httpserver.hpp" #include "./littletest.hpp" diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index 3e680cb6..177797d1 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -19,12 +19,16 @@ */ #include +#include #include +#include #include #include #include #include #include +#include +#include #include "./httpserver.hpp" #include "httpserver/string_utilities.hpp" diff --git a/test/integ/deferred.cpp b/test/integ/deferred.cpp index 6bece022..bc5e33f6 100644 --- a/test/integ/deferred.cpp +++ b/test/integ/deferred.cpp @@ -34,8 +34,10 @@ #include #include +#include #include #include +#include #include "./httpserver.hpp" #include "./littletest.hpp" diff --git a/test/integ/file_upload.cpp b/test/integ/file_upload.cpp index 57b6942b..81ab4de8 100644 --- a/test/integ/file_upload.cpp +++ b/test/integ/file_upload.cpp @@ -22,13 +22,16 @@ #include #include #include +#include #include -#include #include +#include #include #include #include #include +#include +#include #include "./httpserver.hpp" #include "httpserver/string_utilities.hpp" diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index cb77dc59..6e717196 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "./httpserver.hpp" #include "./littletest.hpp" @@ -509,7 +510,11 @@ void* start_ws_blocking(void* par) { httpserver::webserver* ws = (httpserver::webserver*) par; ok_resource ok; if (!ws->register_resource("base", &ok)) return PTHREAD_CANCELED; - try { ws->start(true); } catch (...) { return PTHREAD_CANCELED; } + try { + ws->start(true); + } catch (...) { + return PTHREAD_CANCELED; + } return nullptr; } diff --git a/test/littletest.hpp b/test/littletest.hpp index c26f6125..93b0e256 100644 --- a/test/littletest.hpp +++ b/test/littletest.hpp @@ -70,6 +70,7 @@ #define LT_BEGIN_TEST(__lt_suite_name__, __lt_test_name__) \ struct __lt_test_name__ ## _class: public __lt_suite_name__, littletest::test<__lt_test_name__ ## _class> \ { \ + using littletest::test_base::operator(); \ __lt_test_name__ ## _class() \ { \ __lt_name__ = #__lt_test_name__; \ diff --git a/test/unit/http_endpoint_test.cpp b/test/unit/http_endpoint_test.cpp index 1cce2bcd..a22aa050 100644 --- a/test/unit/http_endpoint_test.cpp +++ b/test/unit/http_endpoint_test.cpp @@ -20,6 +20,10 @@ #include "httpserver/details/http_endpoint.hpp" +#include +#include +#include + #include "./littletest.hpp" using httpserver::details::http_endpoint; diff --git a/test/unit/http_utils_test.cpp b/test/unit/http_utils_test.cpp index 0bf5e6ea..5fd3b97b 100644 --- a/test/unit/http_utils_test.cpp +++ b/test/unit/http_utils_test.cpp @@ -32,6 +32,10 @@ #include #include +#include +#include +#include +#include #include "./littletest.hpp" diff --git a/test/unit/string_utilities_test.cpp b/test/unit/string_utilities_test.cpp index 0bc7a213..f0b57e80 100644 --- a/test/unit/string_utilities_test.cpp +++ b/test/unit/string_utilities_test.cpp @@ -21,6 +21,8 @@ #include "httpserver/string_utilities.hpp" #include +#include +#include #include "./littletest.hpp" From 0e57fd2cd9524ac850576b201eba73af626fa14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20=C3=81ngel=20Pastrana?= Date: Tue, 27 Jan 2026 12:36:57 -0600 Subject: [PATCH 03/47] Fix kernel version check for TCP_FASTOPEN support (#347) * Fix kernel version check for TCP_FASTOPEN support * Fix indentation in TCP_FASTOPEN version check --------- Co-authored-by: Sebastiano Merlino --- configure.ac | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/configure.ac b/configure.ac index 1197c136..70caca8a 100644 --- a/configure.ac +++ b/configure.ac @@ -148,14 +148,12 @@ AC_ARG_ENABLE([fastopen], AC_MSG_RESULT([$fastopen]) is_fastopen_supported=no; -if test x"$fastopen" = x"yes"; then - if test x"$is_windows" = x"no"; then - if test `uname -r |cut -d. -f1` -ge 3; then - if test `uname -r |cut -d. -f2` -ge 7; then - CXXFLAGS="-DUSE_FASTOPEN $CXXFLAGS"; - is_fastopen_supported=yes; - fi - fi +if test x"$fastopen" = x"yes" && test x"$is_windows" = x"no"; then + major=`uname -r | cut -d. -f1` + minor=`uname -r | cut -d. -f2` + if test "$major" -ge 4 || { test "$major" -eq 3 && test "$minor" -ge 7; }; then + CXXFLAGS="-DUSE_FASTOPEN $CXXFLAGS"; + is_fastopen_supported=yes; fi fi From 57522ba6444b3f3cb6bd99ff600f04f656e2c5f4 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Jan 2026 11:44:09 -0800 Subject: [PATCH 04/47] Fix webserver thread safety (#350) * add test * Use const when possible * Protect webserver::registered_resources_* * Protect webserver::bans and webserver::allowances * Split bans_and_allowances_mutex in two distinct mutexes * Simplify `policy_callback` condition * Fix cpplint style issues - Move opening brace to end of line in policy_callback - Replace using-directive with using-declaration for chrono_literals - Use static_cast instead of C-style cast - Use rand_r instead of rand for thread safety --------- Co-authored-by: Florian CHEVASSU --- src/httpserver/webserver.hpp | 15 ++++-- src/webserver.cpp | 97 ++++++++++++++++++++---------------- test/integ/basic.cpp | 42 ++++++++++++++++ 3 files changed, 107 insertions(+), 47 deletions(-) diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 05b43a32..c7b3a226 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #ifdef HAVE_GNUTLS @@ -172,17 +173,21 @@ class webserver { const std::string file_upload_dir; const bool generate_random_filename_on_upload; const bool deferred_enabled; - bool single_resource; - bool tcp_nodelay; + const bool single_resource; + const bool tcp_nodelay; pthread_mutex_t mutexwait; pthread_cond_t mutexcond; - render_ptr not_found_resource; - render_ptr method_not_allowed_resource; - render_ptr internal_error_resource; + const render_ptr not_found_resource; + const render_ptr method_not_allowed_resource; + const render_ptr internal_error_resource; + std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; + std::shared_mutex bans_mutex; std::set bans; + + std::shared_mutex allowances_mutex; std::set allowances; struct MHD_Daemon* daemon; diff --git a/src/webserver.cpp b/src/webserver.cpp index 749efea1..8194517f 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -196,6 +197,7 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr details::http_endpoint idx(resource, family, true, regex_checking); + std::unique_lock registered_resources_lock(registered_resources_mutex); pair::iterator, bool> result = registered_resources.insert(map::value_type(idx, hrm)); if (!family && result.second) { @@ -370,12 +372,14 @@ bool webserver::stop() { void webserver::unregister_resource(const string& resource) { // family does not matter - it just checks the url_normalized anyhow details::http_endpoint he(resource, false, true, regex_checking); + std::unique_lock registered_resources_lock(registered_resources_mutex); registered_resources.erase(he); registered_resources.erase(he.get_url_complete()); registered_resources_str.erase(he.get_url_complete()); } void webserver::ban_ip(const string& ip) { + std::unique_lock bans_lock(bans_mutex); ip_representation t_ip(ip); set::iterator it = bans.find(t_ip); if (it != bans.end() && (t_ip.weight() < (*it).weight())) { @@ -387,6 +391,7 @@ void webserver::ban_ip(const string& ip) { } void webserver::allow_ip(const string& ip) { + std::unique_lock allowances_lock(allowances_mutex); ip_representation t_ip(ip); set::iterator it = allowances.find(t_ip); if (it != allowances.end() && (t_ip.weight() < (*it).weight())) { @@ -398,10 +403,12 @@ void webserver::allow_ip(const string& ip) { } void webserver::unban_ip(const string& ip) { + std::unique_lock bans_lock(bans_mutex); bans.erase(ip_representation(ip)); } void webserver::disallow_ip(const string& ip) { + std::unique_lock allowances_lock(allowances_mutex); allowances.erase(ip_representation(ip)); } @@ -446,14 +453,17 @@ MHD_Result policy_callback(void *cls, const struct sockaddr* addr, socklen_t add // Parameter needed to respect MHD interface, but not needed here. std::ignore = addrlen; - if (!(static_cast(cls))->ban_system_enabled) return MHD_YES; + const auto ws = static_cast(cls); - if ((((static_cast(cls))->default_policy == http_utils::ACCEPT) && - ((static_cast(cls))->bans.count(ip_representation(addr))) && - (!(static_cast(cls))->allowances.count(ip_representation(addr)))) || - (((static_cast(cls))->default_policy == http_utils::REJECT) && - ((!(static_cast(cls))->allowances.count(ip_representation(addr))) || - ((static_cast(cls))->bans.count(ip_representation(addr)))))) { + if (!ws->ban_system_enabled) return MHD_YES; + + std::shared_lock bans_lock(ws->bans_mutex); + std::shared_lock allowances_lock(ws->allowances_mutex); + const bool is_banned = ws->bans.count(ip_representation(addr)); + const bool is_allowed = ws->allowances.count(ip_representation(addr)); + + if ((ws->default_policy == http_utils::ACCEPT && is_banned && !is_allowed) || + (ws->default_policy == http_utils::REJECT && (!is_allowed || is_banned))) { return MHD_NO; } @@ -676,51 +686,54 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details bool found = false; struct MHD_Response* raw_response; - if (!single_resource) { - const char* st_url = mr->standardized_url->c_str(); - fe = registered_resources_str.find(st_url); - if (fe == registered_resources_str.end()) { - if (regex_checking) { - map::iterator found_endpoint; - - details::http_endpoint endpoint(st_url, false, false, false); - - map::iterator it; - - size_t len = 0; - size_t tot_len = 0; - for (it = registered_resources.begin(); it != registered_resources.end(); ++it) { - size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); - size_t endpoint_tot_len = (*it).first.get_url_complete().size(); - if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { - if ((*it).first.match(endpoint)) { - found = true; - len = endpoint_pieces_len; - tot_len = endpoint_tot_len; - found_endpoint = it; + { + std::shared_lock registered_resources_lock(registered_resources_mutex); + if (!single_resource) { + const char* st_url = mr->standardized_url->c_str(); + fe = registered_resources_str.find(st_url); + if (fe == registered_resources_str.end()) { + if (regex_checking) { + map::iterator found_endpoint; + + details::http_endpoint endpoint(st_url, false, false, false); + + map::iterator it; + + size_t len = 0; + size_t tot_len = 0; + for (it = registered_resources.begin(); it != registered_resources.end(); ++it) { + size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); + size_t endpoint_tot_len = (*it).first.get_url_complete().size(); + if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { + if ((*it).first.match(endpoint)) { + found = true; + len = endpoint_pieces_len; + tot_len = endpoint_tot_len; + found_endpoint = it; + } } } - } - if (found) { - vector url_pars = found_endpoint->first.get_url_pars(); + if (found) { + vector url_pars = found_endpoint->first.get_url_pars(); - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = found_endpoint->first.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); - } + vector url_pieces = endpoint.get_url_pieces(); + vector chunks = found_endpoint->first.get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + } - hrm = found_endpoint->second; + hrm = found_endpoint->second; + } } + } else { + hrm = fe->second; + found = true; } } else { - hrm = fe->second; + hrm = registered_resources.begin()->second; found = true; } - } else { - hrm = registered_resources.begin()->second; - found = true; } if (found) { diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index 177797d1..da008b01 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -19,6 +19,7 @@ */ #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -1573,6 +1575,46 @@ LT_BEGIN_AUTO_TEST(basic_suite, method_not_allowed_header) curl_easy_cleanup(curl); LT_END_AUTO_TEST(method_not_allowed_header) +LT_BEGIN_AUTO_TEST(basic_suite, thread_safety) + simple_resource resource; + + std::atomic_bool done = false; + auto register_thread = std::thread([&]() { + int i = 0; + while (!done) { + ws->register_resource( + std::string("/route") + std::to_string(++i), &resource); + } + }); + + auto get_thread = std::thread([&](){ + unsigned int seed = 42; + while (!done) { + CURL *curl = curl_easy_init(); + std::string s; + std::string url = "localhost:" PORT_STRING "/route" + std::to_string( + static_cast((rand_r(&seed) * 10000000.0) / RAND_MAX)); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_perform(curl); + curl_easy_cleanup(curl); + } + }); + + using std::chrono_literals::operator""s; + std::this_thread::sleep_for(10s); + done = true; + if (register_thread.joinable()) { + register_thread.join(); + } + if (get_thread.joinable()) { + get_thread.join(); + } + LT_CHECK_EQ(1, 1); +LT_END_AUTO_TEST(thread_safety) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From 8ed63407333dced919996ef1bffc7dfb54a20ccd Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Jan 2026 14:36:10 -0800 Subject: [PATCH 05/47] Fix POST args with empty values not being parsed (issue #268) (#351) * Fix POST args with empty values not being parsed (issue #268) Destroy post processor before invoking the callback to ensure pending POST body keys with empty values are properly finalized. Without this, POST data like "arg1=val1&arg2=" would fail to include arg2. The fix moves MHD_destroy_post_processor call to finalize_answer() before the resource callback is invoked, rather than waiting for the modded_request destructor. Added test cases verifying: - arg1=val1&arg2= correctly parses both args - arg1= correctly parses as empty string - arg1=&arg2= correctly parses both as empty strings This supersedes PR #269 with the requested code style changes. * Fix Windows build: replace rand_r with portable rand() rand_r is POSIX-specific and not available on Windows. Since this is just a stress test where thread-safe randomness isn't critical, use the portable rand() function instead. --- src/webserver.cpp | 4 +++ test/integ/basic.cpp | 60 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/webserver.cpp b/src/webserver.cpp index 8194517f..d963c290 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -738,6 +738,10 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details if (found) { try { + if (mr->pp != nullptr) { + MHD_destroy_post_processor(mr->pp); + mr->pp = nullptr; + } if (hrm->is_allowed(method)) { mr->dhrs = ((hrm)->*(mr->callback))(*mr->dhr); // copy in memory (move in case) if (mr->dhrs.get() == nullptr || mr->dhrs->get_response_code() == -1) { diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index da008b01..2d71c820 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -993,6 +993,62 @@ LT_BEGIN_AUTO_TEST(basic_suite, empty_arg) curl_easy_cleanup(curl); LT_END_AUTO_TEST(empty_arg) +LT_BEGIN_AUTO_TEST(basic_suite, empty_arg_value_at_end) + // Test for issue #268: POST body keys without values at the end + // are not processed when using application/x-www-form-urlencoded + simple_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("base", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + + // Test case 1: arg2 has empty value at end (the bug case) + { + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "arg1=val1&arg2="); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // arg1="val1", arg2="" -> response should be "val1" + LT_CHECK_EQ(s, "val1"); + curl_easy_cleanup(curl); + } + + // Test case 2: only arg1 with empty value + { + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "arg1="); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // arg1="" -> response should be "" + LT_CHECK_EQ(s, ""); + curl_easy_cleanup(curl); + } + + // Test case 3: both args with empty values + { + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "arg1=&arg2="); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // arg1="", arg2="" -> response should be "" + LT_CHECK_EQ(s, ""); + curl_easy_cleanup(curl); + } +LT_END_AUTO_TEST(empty_arg_value_at_end) + LT_BEGIN_AUTO_TEST(basic_suite, no_response) no_response_resource resource; LT_ASSERT_EQ(true, ws->register_resource("base", &resource)); @@ -1588,12 +1644,10 @@ LT_BEGIN_AUTO_TEST(basic_suite, thread_safety) }); auto get_thread = std::thread([&](){ - unsigned int seed = 42; while (!done) { CURL *curl = curl_easy_init(); std::string s; - std::string url = "localhost:" PORT_STRING "/route" + std::to_string( - static_cast((rand_r(&seed) * 10000000.0) / RAND_MAX)); + std::string url = "localhost:" PORT_STRING "/route" + std::to_string(rand() % 10000000); // NOLINT(runtime/threadsafe_fn) curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); From 54c800ece74b0a8671487504d0c689fa40befaaf Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Jan 2026 18:40:16 -0800 Subject: [PATCH 06/47] Fix deferred_response content parameter not being used (issue #331) (#352) The deferred_response constructor accepts a content parameter that was documented to send initial content before callback data, but was never actually used. This fix stores the content and sends it first before calling the user's callback. Changes: - Store content in initial_content member instead of passing to base class - Track content_offset for proper chunking of large content - Send initial content bytes before invoking user's cycle_callback - Add test for backward compatibility with empty content parameter --- src/httpserver/deferred_response.hpp | 21 ++++++++++++++++++-- test/integ/deferred.cpp | 29 ++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/httpserver/deferred_response.hpp b/src/httpserver/deferred_response.hpp index 76a2127a..d1fc1e22 100644 --- a/src/httpserver/deferred_response.hpp +++ b/src/httpserver/deferred_response.hpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include "httpserver/http_utils.hpp" @@ -50,9 +52,11 @@ class deferred_response : public string_response { const std::string& content = "", int response_code = http::http_utils::http_ok, const std::string& content_type = http::http_utils::text_plain): - string_response(content, response_code, content_type), + string_response("", response_code, content_type), cycle_callback(cycle_callback), - closure_data(closure_data) { } + closure_data(closure_data), + initial_content(content), + content_offset(0) { } deferred_response(const deferred_response& other) = default; deferred_response(deferred_response&& other) noexcept = default; @@ -68,9 +72,22 @@ class deferred_response : public string_response { private: ssize_t (*cycle_callback)(std::shared_ptr, char*, size_t); std::shared_ptr closure_data; + std::string initial_content; + size_t content_offset; static ssize_t cb(void* cls, uint64_t, char* buf, size_t max) { deferred_response* dfr = static_cast*>(cls); + + // First, send any remaining initial content + if (dfr->content_offset < dfr->initial_content.size()) { + size_t remaining = dfr->initial_content.size() - dfr->content_offset; + size_t to_copy = std::min(remaining, max); + std::memcpy(buf, dfr->initial_content.data() + dfr->content_offset, to_copy); + dfr->content_offset += to_copy; + return static_cast(to_copy); + } + + // Then call user's callback return dfr->cycle_callback(dfr->closure_data, buf, max); } }; diff --git a/test/integ/deferred.cpp b/test/integ/deferred.cpp index bc5e33f6..64ccfb12 100644 --- a/test/integ/deferred.cpp +++ b/test/integ/deferred.cpp @@ -106,6 +106,13 @@ class deferred_resource_with_data : public http_resource { } }; +class deferred_resource_empty_content : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared>(test_callback, nullptr); + } +}; + #ifdef HTTPSERVER_PORT #define PORT HTTPSERVER_PORT #else @@ -145,7 +152,7 @@ LT_BEGIN_AUTO_TEST(deferred_suite, deferred_response_suite) curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); res = curl_easy_perform(curl); LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "testtest"); + LT_CHECK_EQ(s, "cycle callback responsetesttest"); curl_easy_cleanup(curl); LT_END_AUTO_TEST(deferred_response_suite) @@ -163,10 +170,28 @@ LT_BEGIN_AUTO_TEST(deferred_suite, deferred_response_with_data) curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); res = curl_easy_perform(curl); LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "test42test84"); + LT_CHECK_EQ(s, "cycle callback responsetest42test84"); curl_easy_cleanup(curl); LT_END_AUTO_TEST(deferred_response_with_data) +LT_BEGIN_AUTO_TEST(deferred_suite, deferred_response_empty_content) + deferred_resource_empty_content resource; + LT_ASSERT_EQ(true, ws->register_resource("base", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "testtest"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(deferred_response_empty_content) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From cbf6bfd79a11c19a9197fabfba606c886f446e71 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Jan 2026 21:41:22 -0800 Subject: [PATCH 07/47] Fix segfault when passing nullptr to register_resource (issue #265) (#353) Add validation to reject null http_resource pointers in webserver::register_resource(), throwing std::invalid_argument instead of allowing a segfault at runtime when the resource is accessed. --- src/webserver.cpp | 4 ++++ test/integ/ws_start_stop.cpp | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/webserver.cpp b/src/webserver.cpp index d963c290..38e5719d 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -191,6 +191,10 @@ void webserver::request_completed(void *cls, struct MHD_Connection *connection, } bool webserver::register_resource(const std::string& resource, http_resource* hrm, bool family) { + if (hrm == nullptr) { + throw std::invalid_argument("The http_resource pointer cannot be null"); + } + if (single_resource && ((resource != "" && resource != "/") || !family)) { throw std::invalid_argument("The resource should be '' or '/' and be marked as family when using a single_resource server"); } diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 6e717196..8a6d999c 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -372,6 +372,11 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, single_resource_not_default_resource) ws.stop(); LT_END_AUTO_TEST(single_resource_not_default_resource) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, register_resource_nullptr_throws) + httpserver::webserver ws = httpserver::create_webserver(PORT); + LT_CHECK_THROW(ws.register_resource("/test", nullptr)); +LT_END_AUTO_TEST(register_resource_nullptr_throws) + LT_BEGIN_AUTO_TEST(ws_start_stop_suite, thread_per_connection_fails_with_max_threads) { // NOLINT (internal scope opening - not method start) httpserver::webserver ws = httpserver::create_webserver(PORT) From bca26743424c7134ded0272fe8f91fce70411081 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Jan 2026 22:41:41 -0800 Subject: [PATCH 08/47] Fix regex URL exact match bug (issue #308) (#354) URLs that exactly match a registered regex pattern string were incorrectly dispatching to that resource instead of returning 404. For example, registering `/foo/{v|[a-z]}/bar` and then requesting the literal URL `/foo/{v|[a-z]}/bar` returned 200 instead of 404. The root cause was that URLs with regex patterns were being added to `registered_resources_str` (fast string lookup map). When a request matched the literal pattern text, it bypassed regex validation. Fix: Only add URLs without regex patterns to the fast string lookup map by checking `idx.get_url_pars().empty()` before insertion. --- src/webserver.cpp | 2 +- test/integ/basic.cpp | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/webserver.cpp b/src/webserver.cpp index 38e5719d..f0d28c62 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -204,7 +204,7 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr std::unique_lock registered_resources_lock(registered_resources_mutex); pair::iterator, bool> result = registered_resources.insert(map::value_type(idx, hrm)); - if (!family && result.second) { + if (!family && result.second && idx.get_url_pars().empty()) { registered_resources_str.insert(pair(idx.get_url_complete(), result.first->second)); } diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index 2d71c820..17e31231 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -1595,11 +1595,7 @@ LT_BEGIN_AUTO_TEST(basic_suite, regex_url_exact_match) int64_t http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE , &http_code); -#if 0 // https://github.com/etr/libhttpserver/issues/308 LT_ASSERT_EQ(http_code, 404); -#else - LT_ASSERT_EQ(http_code, 200); -#endif curl_easy_cleanup(curl); } LT_END_AUTO_TEST(regex_url_exact_match) From 53bded0fa3879ea92a44f56ddd25d46d034443e1 Mon Sep 17 00:00:00 2001 From: LightVillet <23332720+LightVillet@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:46:21 +0200 Subject: [PATCH 09/47] fix typo (#344) Co-authored-by: LightVillet --- src/httpserver/http_request.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index f01743dc..142251db 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -181,7 +181,7 @@ class http_request { /** * Method used to get a specific argument passed with the request. - * @param ket the specific argument to get the value from + * @param key the specific argument to get the value from * @return the value(s) of the arg. **/ http_arg_value get_arg(std::string_view key) const; @@ -189,7 +189,7 @@ class http_request { /** * Method used to get a specific argument passed with the request. * If the arg key has more than one value, only one is returned. - * @param ket the specific argument to get the value from + * @param key the specific argument to get the value from * @return the value of the arg. **/ std::string_view get_arg_flat(std::string_view key) const; From 25990b02c9422e4d9b951f751600edc75200c83b Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 28 Jan 2026 09:30:35 -0800 Subject: [PATCH 10/47] Fix Windows/MSYS2/MinGW64 build failures (issue #280) (#355) * Fix Windows/MSYS2/MinGW64 build failures (issue #280) Add MSYS environment support to configure.ac so users building from the MSYS shell get proper Windows configuration (winsock2.h, ws2_32 lib) instead of falling through to the Unix case (arpa/inet.h). Changes: - configure.ac: Add *-msys* case with Windows config and warning about msys-2.0.dll dependency - configure.ac: Add host triplet and Windows build status to summary - appveyor.yml: Add MSYS environment to CI matrix alongside MinGW64 - README.md: Add Windows/MSYS2 build section with step-by-step instructions and explanation of shell differences * Migrate Windows CI from AppVeyor to GitHub Actions Integrate Windows/MSYS2 builds into the existing verify job matrix instead of using a separate AppVeyor configuration. This consolidates all CI into GitHub Actions. - Add MINGW64 and MSYS matrix entries with msys2 shell - Add MSYS2 setup and package installation steps - Add Windows-specific libmicrohttpd build with --enable-poll=no - Remove AppVeyor configuration and badge * Fix missing shell parameter in matrix include entries Include entries don't inherit matrix defaults, so each needs an explicit shell value to work with the defaults.run.shell setting. * Fix shell for steps that run before MSYS2 setup The git checkout and failure-handling steps need explicit shell: bash since they may run before MSYS2 is set up or if MSYS2 setup fails. * Fix MSYS package: use libcurl-devel for test headers The curl package doesn't include development headers needed for compiling tests. Use libcurl-devel which provides curl/curl.h. * Fix MSYS package: add libgnutls-devel for SSL tests The SSL tests (ssl_base, ssl_with_protocol_priorities, ssl_with_trust) were failing because GnuTLS wasn't installed in the MSYS environment. --- .github/workflows/verify-build.yml | 90 +++++++++++++++++++++++++++++- README.md | 48 +++++++++++++++- appveyor.yml | 27 --------- configure.ac | 12 ++++ 4 files changed, 147 insertions(+), 30 deletions(-) delete mode 100644 appveyor.yml diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index e4842605..7a42e5bf 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -20,6 +20,9 @@ jobs: BUILD_TYPE: ${{ matrix.build-type }} CC: ${{ matrix.c-compiler }} CXX: ${{ matrix.cc-compiler }} + defaults: + run: + shell: ${{ matrix.shell }} strategy: fail-fast: false matrix: @@ -33,6 +36,7 @@ jobs: coverage: [coverage, nocoverage] linking: [dynamic, static] build-type: [classic] + shell: [bash] exclude: - os: ubuntu-latest os-type: mac @@ -62,6 +66,7 @@ jobs: cc-compiler: clang++-18 debug: debug coverage: nocoverage + shell: bash # This test gives false positives on newer versions of clang # and ubuntu-18.04 is not supported anymore on github #- test-group: extra @@ -82,6 +87,7 @@ jobs: cc-compiler: clang++-18 debug: debug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -91,6 +97,7 @@ jobs: cc-compiler: clang++-18 debug: debug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -100,6 +107,7 @@ jobs: cc-compiler: clang++-18 debug: debug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -109,6 +117,7 @@ jobs: cc-compiler: g++-9 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -118,6 +127,7 @@ jobs: cc-compiler: g++-10 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -127,6 +137,7 @@ jobs: cc-compiler: g++-11 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -136,6 +147,7 @@ jobs: cc-compiler: g++-12 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -145,6 +157,7 @@ jobs: cc-compiler: g++-13 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -154,6 +167,7 @@ jobs: cc-compiler: g++-14 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-22.04 os-type: ubuntu @@ -163,6 +177,7 @@ jobs: cc-compiler: clang++-11 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-22.04 os-type: ubuntu @@ -172,6 +187,7 @@ jobs: cc-compiler: clang++-12 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-22.04 os-type: ubuntu @@ -181,6 +197,7 @@ jobs: cc-compiler: clang++-13 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -190,6 +207,7 @@ jobs: cc-compiler: clang++-14 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -199,6 +217,7 @@ jobs: cc-compiler: clang++-15 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -208,6 +227,7 @@ jobs: cc-compiler: clang++-16 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -217,6 +237,7 @@ jobs: cc-compiler: clang++-17 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -226,6 +247,7 @@ jobs: cc-compiler: g++-14 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -235,6 +257,7 @@ jobs: cc-compiler: clang++-18 debug: nodebug coverage: nocoverage + shell: bash - test-group: performance os: ubuntu-latest os-type: ubuntu @@ -244,6 +267,7 @@ jobs: cc-compiler: g++-10 debug: nodebug coverage: nocoverage + shell: bash - test-group: performance os: ubuntu-latest os-type: ubuntu @@ -253,6 +277,7 @@ jobs: cc-compiler: g++-10 debug: nodebug coverage: nocoverage + shell: bash - test-group: performance os: ubuntu-latest os-type: ubuntu @@ -262,6 +287,7 @@ jobs: cc-compiler: g++-10 debug: nodebug coverage: nocoverage + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -271,6 +297,31 @@ jobs: cc-compiler: g++-10 debug: debug coverage: nocoverage + shell: bash + - test-group: basic + os: windows-latest + os-type: windows + msys-env: MINGW64 + shell: 'msys2 {0}' + build-type: classic + compiler-family: none + c-compiler: gcc + cc-compiler: g++ + debug: nodebug + coverage: nocoverage + linking: dynamic + - test-group: basic + os: windows-latest + os-type: windows + msys-env: MSYS + shell: 'msys2 {0}' + build-type: classic + compiler-family: none + c-compiler: gcc + cc-compiler: g++ + debug: nodebug + coverage: nocoverage + linking: dynamic steps: - name: Checkout repository uses: actions/checkout@v4 @@ -282,8 +333,29 @@ jobs: # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 + shell: bash if: ${{ github.event_name == 'pull_request' }} - + + - name: Setup MSYS2 + if: ${{ matrix.os-type == 'windows' }} + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msys-env }} + update: true + install: >- + autotools + base-devel + + - name: Install MinGW64 packages + if: ${{ matrix.os-type == 'windows' && matrix.msys-env == 'MINGW64' }} + run: | + pacman --noconfirm -S --needed mingw-w64-x86_64-{toolchain,libtool,make,pkg-config,libsystre,doxygen,gnutls,graphviz,curl} + + - name: Install MSYS packages + if: ${{ matrix.os-type == 'windows' && matrix.msys-env == 'MSYS' }} + run: | + pacman --noconfirm -S --needed msys2-devel gcc make libcurl-devel libgnutls-devel + - name: Install Ubuntu test sources run: | sudo add-apt-repository ppa:ubuntu-toolchain-r/test ; @@ -395,6 +467,7 @@ jobs: with: path: libmicrohttpd-0.9.77 key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built + if: ${{ matrix.os-type != 'windows' }} - name: Build libmicrohttpd dependency (if not cached) run: | @@ -403,10 +476,21 @@ jobs: cd libmicrohttpd-0.9.77 ; ./configure --disable-examples ; make ; - if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + if: ${{ matrix.os-type != 'windows' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }} - name: Install libmicrohttpd run: cd libmicrohttpd-0.9.77 ; sudo make install ; + if: ${{ matrix.os-type != 'windows' }} + + - name: Build and install libmicrohttpd (Windows) + if: ${{ matrix.os-type == 'windows' }} + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples --enable-poll=no + make + make install - name: Refresh links to shared libs run: sudo ldconfig ; @@ -449,6 +533,7 @@ jobs: fi - name: Print config.log + shell: bash run: | cd build ; cat config.log ; @@ -486,6 +571,7 @@ jobs: if: ${{ matrix.build-type != 'iwyu' }} - name: Print tests results + shell: bash run: | cd build ; cat test/test-suite.log ; diff --git a/README.md b/README.md index 83f0b5b6..b00d7be4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Copyright (C) 2011-2019 Sebastiano Merlino. # The libhttpserver reference manual ![GA: Build Status](https://github.com/etr/libhttpserver/actions/workflows/verify-build.yml/badge.svg) -[![Build status](https://ci.appveyor.com/api/projects/status/ktoy6ewkrf0q1hw6/branch/master?svg=true)](https://ci.appveyor.com/project/etr/libhttpserver/branch/master) [![codecov](https://codecov.io/gh/etr/libhttpserver/branch/master/graph/badge.svg)](https://codecov.io/gh/etr/libhttpserver) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/1bd1e8c21f66400fb70e5a5ce357b525)](https://www.codacy.com/gh/etr/libhttpserver/dashboard?utm_source=github.com&utm_medium=referral&utm_content=etr/libhttpserver&utm_campaign=Badge_Grade) [![Gitter chat](https://badges.gitter.im/etr/libhttpserver.png)](https://gitter.im/libhttpserver/community) @@ -119,6 +118,53 @@ Here are listed the libhttpserver specific options (the canonical configure opti [Back to TOC](#table-of-contents) +### Building on Windows (MSYS2) + +MSYS2 provides multiple shell environments with different purposes. Understanding which shell to use is important: + +| Shell | Host Triplet | Runtime Dependency | Use Case | +|-------|--------------|-------------------|----------| +| **MinGW64** | `x86_64-w64-mingw32` | Native Windows | **Recommended** for native Windows apps | +| **MSYS** | `x86_64-pc-msys` | msys-2.0.dll | POSIX-style apps, build tools | + +**Recommended: Use the MinGW64 shell** for building libhttpserver to produce native Windows binaries without additional runtime dependencies. + +#### Step-by-step build instructions + +1. Install [MSYS2](https://www.msys2.org/) + +2. Open the **MINGW64** shell (not the MSYS shell) from the Start Menu + +3. Install dependencies: +```bash +pacman -S --needed mingw-w64-x86_64-{gcc,libtool,make,pkg-config,doxygen,gnutls,curl} autotools +``` + +4. Build and install [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) (>= 0.9.64) + +5. Build libhttpserver: +```bash +./bootstrap +mkdir build && cd build +../configure --disable-fastopen +make +make check # run tests +``` + +**Important:** The `--disable-fastopen` flag is required on Windows as TCP_FASTOPEN is not supported. + +#### If you use the MSYS shell + +Building from the MSYS shell also works but the resulting binaries will depend on `msys-2.0.dll`. The configure script will display a warning when building in this environment. If you see: + +``` +configure: WARNING: Building from MSYS environment. Binaries will depend on msys-2.0.dll. +``` + +Consider switching to the MinGW64 shell for native Windows binaries. + +[Back to TOC](#table-of-contents) + ## Getting Started The most basic example of creating a server and handling a requests for the path `/hello`: ```cpp diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index bc2fb9e7..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,27 +0,0 @@ -platform: x64 - -environment: - matrix: - - compiler: msys2 - MINGW_CHOST: x86_64-w64-mingw32 - MSYS2_ARCH: x86_64 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 -init: - - 'echo Building libhttpserver %version% for Windows' - - 'echo System architecture: %PLATFORM%' - - 'echo Repo build branch is: %APPVEYOR_REPO_BRANCH%' - - 'echo Build folder is: %APPVEYOR_BUILD_FOLDER%' - - 'echo Repo build commit is: %APPVEYOR_REPO_COMMIT%' - - 'echo Cygwin root is: %CYG_ROOT%' - - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -install: - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -msys2 -c "pacman --noconfirm -S --needed mingw-w64-$MSYS2_ARCH-{libtool,make,pkg-config,libsystre,doxygen,gnutls,graphviz,curl}"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -msys2 -c "pacman --noconfirm -S --needed autotools"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER && curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.64.tar.gz -o libmicrohttpd-0.9.64.tar.gz"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER && tar -xzf libmicrohttpd-0.9.64.tar.gz"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER/libmicrohttpd-0.9.64 && ./configure --disable-examples --enable-poll=no --prefix /C/msys64 && make && make install"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER && ./bootstrap"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER && mkdir build && cd build && MANIFEST_TOOL=no; ../configure --disable-fastopen --prefix /C/msys64 CXXFLAGS=-I/C/msys64/include LDFLAGS=-L/C/msys64/lib; make"' -build_script: - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER/build && make check"' - - 'if "%compiler%"=="msys2" C:\msys64\msys2_shell.cmd -defterm -no-start -mingw64 -full-path -here -c "cd $APPVEYOR_BUILD_FOLDER/build && cat test/test-suite.log"' diff --git a/configure.ac b/configure.ac index 70caca8a..2ddc13b6 100644 --- a/configure.ac +++ b/configure.ac @@ -71,6 +71,16 @@ case "$host" in NETWORK_LIBS="-lws2_32" native_srcdir=$(cd $srcdir; pwd -W) ;; + *-msys*) + AC_MSG_WARN([ +Building from MSYS environment. Binaries will depend on msys-2.0.dll. +For native Windows binaries, use the MinGW64 shell instead. +]) + NETWORK_HEADER="winsock2.h" + ADDITIONAL_LIBS="-lpthread -no-undefined" + NETWORK_LIBS="-lws2_32" + native_srcdir=$(cd $srcdir; pwd -W) + ;; *-cygwin*) NETWORK_HEADER="arpa/inet.h" ADDITIONAL_LIBS="-lpthread -no-undefined" @@ -294,11 +304,13 @@ AC_OUTPUT( AC_MSG_NOTICE([Configuration Summary: Operating System: ${host_os} + Host triplet : ${host} Target directory: ${prefix} License : LGPL only Debug : ${debugit} TLS Enabled : ${have_gnutls} TCP_FASTOPEN : ${is_fastopen_supported} Static : ${static} + Windows build : ${is_windows} Build examples : ${enable_examples} ]) From 4090b4f30ed341166c0324124ac2518e8d7d71c2 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 28 Jan 2026 09:47:40 -0800 Subject: [PATCH 11/47] Add documentation about library files on Windows (issue #276) Document that .dll.a is the correct import library format for GCC toolchains (MSYS2/MinGW, Cygwin), and that .lib files are MSVC-specific. Also add linking examples using pkg-config and manual flags. Closes #276 --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index b00d7be4..17b0ffa5 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,31 @@ configure: WARNING: Building from MSYS environment. Binaries will depend on msys Consider switching to the MinGW64 shell for native Windows binaries. +#### Library files on Windows + +When building with GCC-based toolchains (MSYS2/MinGW, Cygwin), the following library files are generated: + +| File | Purpose | +|------|---------| +| `libhttpserver.a` | Static library archive | +| `libhttpserver.dll` | Shared library (DLL) | +| `libhttpserver.dll.a` | Import library for linking against the DLL | +| `libhttpserver.la` | Libtool archive (used by libtool during linking) | + +**Note about `.lib` files:** The `.dll.a` format is the import library format used by GCC toolchains. If you're looking for `.lib` files, those are the MSVC (Microsoft Visual C++) import library format and are only generated when building with the MSVC toolchain. The `.dll.a` file serves the same purpose as `.lib` but for GCC-based compilers. + +**Linking against libhttpserver:** + +Using pkg-config (recommended): +```bash +g++ myapp.cpp $(pkg-config --cflags --libs libhttpserver) -o myapp +``` + +Manual linking: +```bash +g++ myapp.cpp -I/mingw64/include -L/mingw64/lib -lhttpserver -lmicrohttpd -o myapp +``` + [Back to TOC](#table-of-contents) ## Getting Started From 89b55d2619cfe1bf7dbfbfb92f261f9295715a21 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 28 Jan 2026 21:50:48 -0800 Subject: [PATCH 12/47] Add file cleanup callback for uploaded files (issues #264, #270) (#356) Add a server-level callback that allows users to customize file cleanup behavior for uploaded files. Previously, all uploaded files were automatically deleted when the request completed, making it impossible to keep or move files to permanent storage. The new file_cleanup_callback option accepts a function with signature: bool(const std::string& key, const std::string& filename, const http::file_info& info) Return true to delete the file (default behavior) or false to keep it. If the callback throws an exception, the file is deleted as a safety measure. When no callback is set, files are deleted (backward compatible). Changes: - Add file_cleanup_callback_ptr typedef and builder method - Store callback in webserver and pass to http_request - Modify http_request destructor to invoke callback per file - Add 5 test cases covering callback behavior - Add file_upload_with_callback example - Update README with documentation and example --- README.md | 32 ++++ examples/Makefile.am | 3 +- examples/file_upload_with_callback.cpp | 111 ++++++++++++++ src/http_request.cpp | 19 ++- src/httpserver/create_webserver.hpp | 10 ++ src/httpserver/http_request.hpp | 7 + src/httpserver/webserver.hpp | 1 + src/webserver.cpp | 4 +- test/integ/file_upload.cpp | 196 +++++++++++++++++++++++++ 9 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 examples/file_upload_with_callback.cpp diff --git a/README.md b/README.md index 17b0ffa5..cd66729a 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,7 @@ For example, if your connection limit is “1”, a browser may open a first con * `FILE_UPLOAD_MEMORY_AND_DISK`: The content of the file is stored in memory and on the file system. * _.file_upload_dir(**const std::string&** file_upload_dir):_ Specifies the directory to store all uploaded files. Default value is `/tmp`. * _.generate_random_filename_on_upload() and .no_generate_random_filename_on_upload():_ Enables/Disables the library to generate a unique and unused filename to store the uploaded file to. Otherwise the actually uploaded file name is used. `off` by default. +* _.file_cleanup_callback(**file_cleanup_callback_ptr** callback):_ Sets a callback function to control what happens to uploaded files when the request completes. By default (when no callback is set), all uploaded files are automatically deleted. The callback signature is `bool(const std::string& key, const std::string& filename, const http::file_info& info)` where `key` is the form field name, `filename` is the original uploaded filename, and `info` contains file metadata including the filesystem path. Return `true` to delete the file (default behavior) or `false` to keep it (e.g., after moving it to permanent storage). If the callback throws an exception, the file will be deleted as a safety measure. * _.deferred()_ and _.no_deferred():_ Enables/Disables the ability for the server to suspend and resume connections. Simply put, it enables/disables the ability to use `deferred_response`. Read more [here](#building-responses-to-requests). `on` by default. * _.single_resource() and .no_single_resource:_ Sets or unsets the server in single resource mode. This limits all endpoints to be served from a single resource. The resultant is that the webserver will process the request matching to the endpoint skipping any complex semantic. Because of this, the option is incompatible with `regex_checking` and requires the resource to be registered against an empty endpoint or the root endpoint (`"/"`). The resource will also have to be registered as family. (For more information on resource registration, read more [here](#registering-resources)). `off` by default. @@ -718,6 +719,37 @@ Details on the `http::file_info` structure. * _**const std::string** get_content_type() **const**:_ Returns the content type of the file uploaded through the HTTP request. * _**const std::string** get_transfer_encoding() **const**:_ Returns the transfer encoding of the file uploaded through the HTTP request. +#### Example of keeping uploaded files +By default, uploaded files are automatically deleted when the request completes. To keep files (e.g., move them to permanent storage), use the `file_cleanup_callback`: + +```cpp +#include +#include + +using namespace httpserver; + +int main() { + webserver ws = create_webserver(8080) + .file_upload_target(FILE_UPLOAD_DISK_ONLY) + .file_upload_dir("/tmp/uploads") + .file_cleanup_callback([](const std::string& key, + const std::string& filename, + const http::file_info& info) { + // Move file to permanent storage + std::string dest = "/var/uploads/" + filename; + std::rename(info.get_file_system_file_name().c_str(), dest.c_str()); + return false; // Don't delete - we moved it + }); + + // ... register resources and start server +} +``` +To test file uploads, you can run the following command from a terminal: + + curl -XPOST -F "file=@/path/to/your/file.txt" 'http://localhost:8080/upload' + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/file_upload_with_callback.cpp). + Details on the `http_arg_value` structure. * _**std::string_view** get_flat_value() **const**:_ Returns only the first value provided for the key. diff --git a/examples/Makefile.am b/examples/Makefile.am index cf838c30..14a228c1 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -42,6 +42,7 @@ benchmark_select_SOURCES = benchmark_select.cpp benchmark_threads_SOURCES = benchmark_threads.cpp benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp +file_upload_with_callback_SOURCES = file_upload_with_callback.cpp if HAVE_GNUTLS LDADD += -lgnutls diff --git a/examples/file_upload_with_callback.cpp b/examples/file_upload_with_callback.cpp new file mode 100644 index 00000000..edc5338f --- /dev/null +++ b/examples/file_upload_with_callback.cpp @@ -0,0 +1,111 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include +#include +#include + +#include + +class file_upload_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + std::string get_response = "\n"; + get_response += " \n"; + get_response += "

File Upload with Cleanup Callback Demo

\n"; + get_response += "

Uploaded files will be moved to the permanent directory.

\n"; + get_response += "
\n"; + get_response += " \n"; + get_response += "

\n"; + get_response += " \n"; + get_response += "
\n"; + get_response += " \n"; + get_response += "\n"; + + return std::shared_ptr(new httpserver::string_response(get_response, 200, "text/html")); + } + + std::shared_ptr render_POST(const httpserver::http_request& req) { + std::string post_response = "\n"; + post_response += "\n"; + post_response += "

Upload Complete

\n"; + post_response += "

Files have been moved to permanent storage:

\n"; + post_response += "
    \n"; + + for (auto &file_key : req.get_files()) { + for (auto &files : file_key.second) { + post_response += "
  • " + files.first + " (" + + std::to_string(files.second.get_file_size()) + " bytes)
  • \n"; + } + } + + post_response += "
\n"; + post_response += " Upload more\n"; + post_response += "\n"; + return std::shared_ptr(new httpserver::string_response(post_response, 201, "text/html")); + } +}; + +int main(int argc, char** argv) { + if (3 != argc) { + std::cout << "Usage: file_upload_with_callback " << std::endl; + std::cout << std::endl; + std::cout << " temp_dir: directory for temporary upload storage" << std::endl; + std::cout << " permanent_dir: directory where files will be moved after upload" << std::endl; + return -1; + } + + std::string temp_dir = argv[1]; + std::string permanent_dir = argv[2]; + + std::cout << "Starting file upload server on port 8080..." << std::endl; + std::cout << " Temporary directory: " << temp_dir << std::endl; + std::cout << " Permanent directory: " << permanent_dir << std::endl; + std::cout << std::endl; + std::cout << "Open http://localhost:8080 in your browser to upload files." << std::endl; + + httpserver::webserver ws = httpserver::create_webserver(8080) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(temp_dir) + .generate_random_filename_on_upload() + .file_cleanup_callback([&permanent_dir](const std::string& key, + const std::string& filename, + const httpserver::http::file_info& info) { + (void)key; // Unused in this example + // Move the uploaded file to permanent storage + std::string dest = permanent_dir + "/" + filename; + int result = std::rename(info.get_file_system_file_name().c_str(), dest.c_str()); + + if (result == 0) { + std::cout << "Moved: " << filename << " -> " << dest << std::endl; + return false; // Don't delete - we moved it + } else { + std::cerr << "Failed to move " << filename << ", will be deleted" << std::endl; + return true; // Delete the temp file on failure + } + }); + + file_upload_resource fur; + ws.register_resource("/", &fur); + ws.start(true); + + return 0; +} diff --git a/src/http_request.cpp b/src/http_request.cpp index 68ced762..d589262b 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -319,10 +319,21 @@ std::ostream &operator<< (std::ostream &os, const http_request &r) { } http_request::~http_request() { - for ( const auto &file_key : get_files() ) { - for ( const auto &files : file_key.second ) { - // C++17 has std::filesystem::remove() - remove(files.second.get_file_system_file_name().c_str()); + for (const auto& file_key : get_files()) { + for (const auto& files : file_key.second) { + bool should_delete = true; + if (file_cleanup_callback != nullptr) { + try { + should_delete = file_cleanup_callback(file_key.first, files.first, files.second); + } catch (...) { + // If callback throws, default to deleting the file + should_delete = true; + } + } + if (should_delete) { + // C++17 has std::filesystem::remove() + remove(files.second.get_file_system_file_name().c_str()); + } } } } diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index fca96d91..31faab02 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -48,6 +48,10 @@ typedef std::function log_access_ptr; typedef std::function log_error_ptr; typedef std::function psk_cred_handler_callback; +namespace http { class file_info; } + +typedef std::function file_cleanup_callback_ptr; + class create_webserver { public: create_webserver() = default; @@ -364,6 +368,11 @@ class create_webserver { return *this; } + create_webserver& file_cleanup_callback(file_cleanup_callback_ptr callback) { + _file_cleanup_callback = callback; + return *this; + } + private: uint16_t _port = DEFAULT_WS_PORT; http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT; @@ -409,6 +418,7 @@ class create_webserver { render_ptr _not_found_resource = nullptr; render_ptr _method_not_allowed_resource = nullptr; render_ptr _internal_error_resource = nullptr; + file_cleanup_callback_ptr _file_cleanup_callback = nullptr; friend class webserver; }; diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 142251db..b435cf7f 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -44,6 +44,7 @@ #include "httpserver/http_arg_value.hpp" #include "httpserver/http_utils.hpp" #include "httpserver/file_info.hpp" +#include "httpserver/create_webserver.hpp" struct MHD_Connection; @@ -420,6 +421,12 @@ class http_request { // Populate the data cache unescaped_args void populate_args() const; + file_cleanup_callback_ptr file_cleanup_callback = nullptr; + + void set_file_cleanup_callback(file_cleanup_callback_ptr callback) { + file_cleanup_callback = callback; + } + friend class webserver; friend struct details::modded_request; }; diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index c7b3a226..31ba0958 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -180,6 +180,7 @@ class webserver { const render_ptr not_found_resource; const render_ptr method_not_allowed_resource; const render_ptr internal_error_resource; + const file_cleanup_callback_ptr file_cleanup_callback; std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; diff --git a/src/webserver.cpp b/src/webserver.cpp index f0d28c62..cd8b9b50 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -165,7 +165,8 @@ webserver::webserver(const create_webserver& params): tcp_nodelay(params._tcp_nodelay), not_found_resource(params._not_found_resource), method_not_allowed_resource(params._method_not_allowed_resource), - internal_error_resource(params._internal_error_resource) { + internal_error_resource(params._internal_error_resource), + file_cleanup_callback(params._file_cleanup_callback) { ignore_sigpipe(); pthread_mutex_init(&mutexwait, nullptr); pthread_cond_init(&mutexcond, nullptr); @@ -631,6 +632,7 @@ std::shared_ptr webserver::internal_error_page(details::modded_re MHD_Result webserver::requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr) { mr->dhr.reset(new http_request(connection, unescaper)); + mr->dhr->set_file_cleanup_callback(file_cleanup_callback); if (!mr->has_body) { return MHD_YES; diff --git a/test/integ/file_upload.cpp b/test/integ/file_upload.cpp index 81ab4de8..f474420d 100644 --- a/test/integ/file_upload.cpp +++ b/test/integ/file_upload.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -660,6 +661,201 @@ LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_memory_only_excl_content) ws->stop(); LT_END_AUTO_TEST(file_upload_memory_only_excl_content) +// Test that file cleanup callback returning true causes file deletion (default behavior) +LT_BEGIN_AUTO_TEST(file_upload_suite, file_cleanup_callback_returns_true) + string upload_directory = "."; + + // Track callback invocations + std::vector> callback_invocations; + + auto ws = std::make_unique(create_webserver(PORT) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory) + .generate_random_filename_on_upload() + .file_cleanup_callback([&callback_invocations]( + const std::string& key, + const std::string& filename, + const httpserver::http::file_info& info) { + callback_invocations.push_back({key, filename, info.get_file_size()}); + return true; // Delete the file + })); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + auto res = send_file_to_webserver(false, false); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + ws->stop(); + + // Verify callback was called with correct parameters + LT_CHECK_EQ(callback_invocations.size(), 1); + LT_CHECK_EQ(std::get<0>(callback_invocations[0]), TEST_KEY); + LT_CHECK_EQ(std::get<1>(callback_invocations[0]), TEST_CONTENT_FILENAME); + LT_CHECK_EQ(std::get<2>(callback_invocations[0]), TEST_CONTENT_SIZE); + + // Verify file was deleted (callback returned true) + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + map::iterator file = files.begin()->second.begin(); + LT_CHECK_EQ(file_exists(file->second.get_file_system_file_name()), false); +LT_END_AUTO_TEST(file_cleanup_callback_returns_true) + +// Test that file cleanup callback returning false keeps the file +LT_BEGIN_AUTO_TEST(file_upload_suite, file_cleanup_callback_returns_false) + string upload_directory = "."; + string kept_file_path; + + auto ws = std::make_unique(create_webserver(PORT) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory) + .generate_random_filename_on_upload() + .file_cleanup_callback([&kept_file_path]( + const std::string& key, + const std::string& filename, + const httpserver::http::file_info& info) { + (void)key; + (void)filename; + kept_file_path = info.get_file_system_file_name(); + return false; // Keep the file + })); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + auto res = send_file_to_webserver(false, false); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + ws->stop(); + + // Verify file still exists (callback returned false) + LT_CHECK_EQ(file_exists(kept_file_path), true); + + // Cleanup: manually delete the file + remove(kept_file_path.c_str()); + LT_CHECK_EQ(file_exists(kept_file_path), false); +LT_END_AUTO_TEST(file_cleanup_callback_returns_false) + +// Test selective cleanup: callback can keep some files and delete others +LT_BEGIN_AUTO_TEST(file_upload_suite, file_cleanup_callback_selective) + string upload_directory = "."; + string kept_file_path; + + auto ws = std::make_unique(create_webserver(PORT) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory) + .generate_random_filename_on_upload() + .file_cleanup_callback([&kept_file_path]( + const std::string& key, + const std::string& filename, + const httpserver::http::file_info& info) { + (void)filename; + // Keep first file, delete second + if (key == TEST_KEY) { + kept_file_path = info.get_file_system_file_name(); + return false; // Keep + } + return true; // Delete + })); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + // Upload two files + auto res = send_file_to_webserver(true, false); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + ws->stop(); + + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 2); + + // First file should exist (callback returned false) + LT_CHECK_EQ(file_exists(kept_file_path), true); + + // Second file should be deleted (callback returned true) + auto file_key_2 = files.find(TEST_KEY_2); + LT_ASSERT_EQ(file_key_2 != files.end(), true); + string deleted_file_path = file_key_2->second.begin()->second.get_file_system_file_name(); + LT_CHECK_EQ(file_exists(deleted_file_path), false); + + // Cleanup: manually delete the kept file + remove(kept_file_path.c_str()); +LT_END_AUTO_TEST(file_cleanup_callback_selective) + +// Test that exception in callback defaults to deleting the file +LT_BEGIN_AUTO_TEST(file_upload_suite, file_cleanup_callback_throws) + string upload_directory = "."; + + auto ws = std::make_unique(create_webserver(PORT) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory) + .generate_random_filename_on_upload() + .file_cleanup_callback([]( + const std::string& key, + const std::string& filename, + const httpserver::http::file_info& info) -> bool { + (void)key; + (void)filename; + (void)info; + throw std::runtime_error("Test exception"); + })); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + auto res = send_file_to_webserver(false, false); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + ws->stop(); + + // Verify file was deleted (exception causes default delete behavior) + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + map::iterator file = files.begin()->second.begin(); + LT_CHECK_EQ(file_exists(file->second.get_file_system_file_name()), false); +LT_END_AUTO_TEST(file_cleanup_callback_throws) + +// Test that no callback defaults to deleting files (backward compatibility) +LT_BEGIN_AUTO_TEST(file_upload_suite, file_cleanup_no_callback_deletes) + string upload_directory = "."; + + auto ws = std::make_unique(create_webserver(PORT) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory) + .generate_random_filename_on_upload()); + // No file_cleanup_callback set + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + auto res = send_file_to_webserver(false, false); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + ws->stop(); + + // Verify file was deleted (default behavior) + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + map::iterator file = files.begin()->second.begin(); + LT_CHECK_EQ(file_exists(file->second.get_file_system_file_name()), false); +LT_END_AUTO_TEST(file_cleanup_no_callback_deletes) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From 1ac17bbb0cc35b3dd81a46bcb8f4c8034ab93f4a Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 29 Jan 2026 12:58:22 -0800 Subject: [PATCH 13/47] Make digest auth support conditional (issue #232) (#357) * Make digest auth support conditional (issue #232) When libmicrohttpd is built with --disable-dauth, the digest auth functions (MHD_digest_auth_get_username, MHD_digest_auth_check, MHD_queue_auth_fail_response) are not available, causing linker errors. This change detects digest auth availability at configure time and conditionally compiles the digest auth code, following the existing pattern used for GnuTLS support. Changes: - Add AC_CHECK_LIB detection for MHD_queue_auth_fail_response - Add HAVE_DAUTH preprocessor flag when digest auth is available - Wrap digest auth code in #ifdef HAVE_DAUTH guards - Show digest auth status in configure summary * Make digest_authentication example conditional (issue #232) Build the digest_authentication example only when HAVE_DAUTH is defined, consistent with the conditional digest auth support added in the library. * Guard digest auth MHD options with HAVE_DAUTH (issue #232) When libmicrohttpd is built with --disable-dauth, passing MHD_OPTION_NONCE_NC_SIZE or MHD_OPTION_DIGEST_AUTH_RANDOM to MHD_create_daemon() causes daemon creation to fail. This fixes the ws_start_stop test failure in the no-dauth CI build. --- .github/workflows/verify-build.yml | 47 +++++++++++++++++++- configure.ac | 13 ++++++ examples/Makefile.am | 8 +++- src/digest_auth_fail_response.cpp | 4 ++ src/http_request.cpp | 4 ++ src/httpserver.hpp | 2 + src/httpserver/digest_auth_fail_response.hpp | 4 ++ src/httpserver/http_request.hpp | 6 +++ src/webserver.cpp | 4 ++ test/integ/authentication.cpp | 7 ++- 10 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 7a42e5bf..7815711f 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -248,6 +248,18 @@ jobs: debug: nodebug coverage: nocoverage shell: bash + # Test build without digest auth support (issue #232) + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: no-dauth + compiler-family: gcc + c-compiler: gcc + cc-compiler: g++ + debug: nodebug + coverage: nocoverage + linking: dynamic + shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -467,7 +479,7 @@ jobs: with: path: libmicrohttpd-0.9.77 key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built - if: ${{ matrix.os-type != 'windows' }} + if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' }} - name: Build libmicrohttpd dependency (if not cached) run: | @@ -476,12 +488,31 @@ jobs: cd libmicrohttpd-0.9.77 ; ./configure --disable-examples ; make ; - if: ${{ matrix.os-type != 'windows' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }} + if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }} + + - name: Build libmicrohttpd without digest auth (no-dauth test) + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz ; + tar -xzf libmicrohttpd-0.9.77.tar.gz ; + cd libmicrohttpd-0.9.77 ; + ./configure --disable-examples --disable-dauth ; + make ; + if: ${{ matrix.build-type == 'no-dauth' }} - name: Install libmicrohttpd run: cd libmicrohttpd-0.9.77 ; sudo make install ; if: ${{ matrix.os-type != 'windows' }} + - name: Verify digest auth is disabled (no-dauth test) + run: | + # Verify that MHD_queue_auth_fail_response is NOT present in libmicrohttpd + if nm /usr/local/lib/libmicrohttpd.so 2>/dev/null | grep -q MHD_queue_auth_fail_response; then + echo "ERROR: libmicrohttpd was built WITH digest auth support" ; + exit 1 ; + fi + echo "Verified: libmicrohttpd built without digest auth support" ; + if: ${{ matrix.build-type == 'no-dauth' }} + - name: Build and install libmicrohttpd (Windows) if: ${{ matrix.os-type == 'windows' }} run: | @@ -532,6 +563,18 @@ jobs: ../configure --disable-fastopen; fi + - name: Verify libhttpserver detected no digest auth (no-dauth test) + run: | + cd build ; + if grep -q "Digest Auth.*:.*no" config.log; then + echo "Verified: libhttpserver correctly detected digest auth is disabled" ; + else + echo "ERROR: libhttpserver did not detect that digest auth is disabled" ; + grep "Digest Auth" config.log || echo "Digest Auth line not found" ; + exit 1 ; + fi + if: ${{ matrix.build-type == 'no-dauth' }} + - name: Print config.log shell: bash run: | diff --git a/configure.ac b/configure.ac index 2ddc13b6..50aa008a 100644 --- a/configure.ac +++ b/configure.ac @@ -149,6 +149,11 @@ fi AM_CONDITIONAL([COND_CROSS_COMPILE],[test x"$cond_cross_compile" = x"yes"]) AC_SUBST(COND_CROSS_COMPILE) +# Check for digest auth support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_queue_auth_fail_response], + [have_dauth="yes"], + [have_dauth="no"; AC_MSG_WARN("libmicrohttpd digest auth support not found. Digest auth will be disabled")]) + AC_MSG_CHECKING([whether to build with TCP_FASTOPEN support]) AC_ARG_ENABLE([fastopen], [AS_HELP_STRING([--enable-fastopen], @@ -259,6 +264,13 @@ fi AM_CONDITIONAL([HAVE_GNUTLS],[test x"$have_gnutls" = x"yes"]) +if test x"$have_dauth" = x"yes"; then + AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_DAUTH" + AM_CFLAGS="$AM_CXXFLAGS -DHAVE_DAUTH" +fi + +AM_CONDITIONAL([HAVE_DAUTH],[test x"$have_dauth" = x"yes"]) + DX_HTML_FEATURE(ON) DX_CHM_FEATURE(OFF) DX_CHI_FEATURE(OFF) @@ -309,6 +321,7 @@ AC_MSG_NOTICE([Configuration Summary: License : LGPL only Debug : ${debugit} TLS Enabled : ${have_gnutls} + Digest Auth : ${have_dauth} TCP_FASTOPEN : ${is_fastopen_supported} Static : ${static} Windows build : ${is_windows} diff --git a/examples/Makefile.am b/examples/Makefile.am index 14a228c1..c71065dc 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication digest_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -31,7 +31,6 @@ hello_with_get_arg_SOURCES = hello_with_get_arg.cpp setting_headers_SOURCES = setting_headers.cpp custom_access_log_SOURCES = custom_access_log.cpp basic_authentication_SOURCES = basic_authentication.cpp -digest_authentication_SOURCES = digest_authentication.cpp minimal_https_SOURCES = minimal_https.cpp minimal_file_response_SOURCES = minimal_file_response.cpp minimal_deferred_SOURCES = minimal_deferred.cpp @@ -49,3 +48,8 @@ LDADD += -lgnutls noinst_PROGRAMS += minimal_https_psk minimal_https_psk_SOURCES = minimal_https_psk.cpp endif + +if HAVE_DAUTH +noinst_PROGRAMS += digest_authentication +digest_authentication_SOURCES = digest_authentication.cpp +endif diff --git a/src/digest_auth_fail_response.cpp b/src/digest_auth_fail_response.cpp index 82749710..cb24325f 100644 --- a/src/digest_auth_fail_response.cpp +++ b/src/digest_auth_fail_response.cpp @@ -18,6 +18,8 @@ USA */ +#ifdef HAVE_DAUTH + #include "httpserver/digest_auth_fail_response.hpp" #include #include @@ -32,3 +34,5 @@ int digest_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_ } } // namespace httpserver + +#endif // HAVE_DAUTH diff --git a/src/http_request.cpp b/src/http_request.cpp index d589262b..1a2b7c14 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -42,6 +42,7 @@ void http_request::set_method(const std::string& method) { this->method = string_utilities::to_upper_copy(method); } +#ifdef HAVE_DAUTH bool http_request::check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const { std::string_view digested_user = get_digested_user(); @@ -57,6 +58,7 @@ bool http_request::check_digest_auth(const std::string& realm, const std::string *reload_nonce = false; return true; } +#endif // HAVE_DAUTH std::string_view http_request::get_connection_value(std::string_view key, enum MHD_ValueKind kind) const { const char* header_c = MHD_lookup_connection_value(underlying_connection, kind, key.data()); @@ -257,6 +259,7 @@ std::string_view http_request::get_pass() const { return cache->password; } +#ifdef HAVE_DAUTH std::string_view http_request::get_digested_user() const { if (!cache->digested_user.empty()) { return cache->digested_user; @@ -272,6 +275,7 @@ std::string_view http_request::get_digested_user() const { return cache->digested_user; } +#endif // HAVE_DAUTH #ifdef HAVE_GNUTLS bool http_request::has_tls_session() const { diff --git a/src/httpserver.hpp b/src/httpserver.hpp index 8b706613..4f19de48 100644 --- a/src/httpserver.hpp +++ b/src/httpserver.hpp @@ -29,7 +29,9 @@ #include "httpserver/basic_auth_fail_response.hpp" #include "httpserver/deferred_response.hpp" +#ifdef HAVE_DAUTH #include "httpserver/digest_auth_fail_response.hpp" +#endif // HAVE_DAUTH #include "httpserver/file_response.hpp" #include "httpserver/http_arg_value.hpp" #include "httpserver/http_request.hpp" diff --git a/src/httpserver/digest_auth_fail_response.hpp b/src/httpserver/digest_auth_fail_response.hpp index bbc1543a..f3697c31 100644 --- a/src/httpserver/digest_auth_fail_response.hpp +++ b/src/httpserver/digest_auth_fail_response.hpp @@ -25,6 +25,8 @@ #ifndef SRC_HTTPSERVER_DIGEST_AUTH_FAIL_RESPONSE_HPP_ #define SRC_HTTPSERVER_DIGEST_AUTH_FAIL_RESPONSE_HPP_ +#ifdef HAVE_DAUTH + #include #include "httpserver/http_utils.hpp" #include "httpserver/string_response.hpp" @@ -66,4 +68,6 @@ class digest_auth_fail_response : public string_response { } // namespace httpserver +#endif // HAVE_DAUTH + #endif // SRC_HTTPSERVER_DIGEST_AUTH_FAIL_RESPONSE_HPP_ diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index b435cf7f..749e49ab 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -65,11 +65,13 @@ class http_request { **/ std::string_view get_user() const; +#ifdef HAVE_DAUTH /** * Method used to get the username extracted from a digest authentication * @return the username **/ std::string_view get_digested_user() const; +#endif // HAVE_DAUTH /** * Method used to get the password eventually passed through basic authentication. @@ -250,7 +252,9 @@ class http_request { **/ uint16_t get_requestor_port() const; +#ifdef HAVE_DAUTH bool check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const; +#endif // HAVE_DAUTH friend std::ostream &operator<< (std::ostream &os, http_request &r); @@ -412,7 +416,9 @@ class http_request { std::string password; std::string querystring; std::string requestor_ip; +#ifdef HAVE_DAUTH std::string digested_user; +#endif // HAVE_DAUTH std::map, http::arg_comparator> unescaped_args; bool args_populated = false; diff --git a/src/webserver.cpp b/src/webserver.cpp index cd8b9b50..48745966 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -254,9 +254,11 @@ bool webserver::start(bool blocking) { iov.push_back(gen(MHD_OPTION_THREAD_STACK_SIZE, max_thread_stack_size)); } +#ifdef HAVE_DAUTH if (nonce_nc_size != 0) { iov.push_back(gen(MHD_OPTION_NONCE_NC_SIZE, nonce_nc_size)); } +#endif // HAVE_DAUTH if (use_ssl) { // Need for const_cast to respect MHD interface that needs a void* @@ -278,10 +280,12 @@ bool webserver::start(bool blocking) { iov.push_back(gen(MHD_OPTION_HTTPS_PRIORITIES, 0, reinterpret_cast(const_cast(https_priorities.c_str())))); } +#ifdef HAVE_DAUTH if (digest_auth_random != "") { // Need for const_cast to respect MHD interface that needs a char* iov.push_back(gen(MHD_OPTION_DIGEST_AUTH_RANDOM, digest_auth_random.size(), const_cast(digest_auth_random.c_str()))); } +#endif // HAVE_DAUTH #ifdef HAVE_GNUTLS if (cred_type != http_utils::NONE) { diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index 4f4096d3..eba87172 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -44,7 +44,9 @@ using httpserver::webserver; using httpserver::create_webserver; using httpserver::http_response; using httpserver::basic_auth_fail_response; +#ifdef HAVE_DAUTH using httpserver::digest_auth_fail_response; +#endif // HAVE_DAUTH using httpserver::string_response; using httpserver::http_resource; using httpserver::http_request; @@ -74,6 +76,7 @@ class user_pass_resource : public http_resource { } }; +#ifdef HAVE_DAUTH class digest_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { @@ -88,6 +91,7 @@ class digest_resource : public http_resource { return std::make_shared("SUCCESS", 200, "text/plain"); } }; +#endif // HAVE_DAUTH LT_BEGIN_SUITE(authentication_suite) void set_up() { @@ -150,7 +154,8 @@ LT_END_AUTO_TEST(base_auth_fail) // do not run the digest auth tests on windows as curl // appears to have problems with it. // Will fix this separately -#ifndef _WINDOWS +// Also skip if libmicrohttpd was built without digest auth support +#if !defined(_WINDOWS) && defined(HAVE_DAUTH) LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth) webserver ws = create_webserver(PORT) From 76afe9747e71eddd6a084c47dcf98a3f5eaf2a27 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 29 Jan 2026 20:39:28 -0800 Subject: [PATCH 14/47] Documentation improvements (issues #334, #324, #343) (#358) * Documentation improvements (issues #334, #324, #343) - Remove obsolete INSTALL file (issue #334): The generic autoconf INSTALL file incorrectly mentioned `make -f Makefile.cvs` instead of `./bootstrap`. README.md already contains accurate build instructions. - Add args_processing.cpp example (issue #324): Demonstrates how to use get_args() and get_args_flat() methods to iterate over all request arguments, including handling parameters with multiple values. - Add bind_address(string) overload (issue #343): Allows binding to a specific IP address using a string (e.g., "127.0.0.1") instead of requiring manual sockaddr construction. Supports both IPv4 and IPv6. IPv6 mode is automatically enabled when an IPv6 address is provided. - Document bind_address() methods in README.md (issue #343): Added documentation for both the sockaddr* and string overloads. * Fix macro redefinition warning in create_webserver.cpp Remove explicit #define HTTPSERVER_COMPILATION as it is already defined by the build system via command line flags. * Fix memory leak: copy bind_address_storage to webserver The webserver was only copying the raw bind_address pointer but not the shared_ptr that owns the storage. This caused the memory to be freed when the temporary create_webserver was destroyed, leaving webserver with a dangling pointer and causing valgrind to report a leak. * CI: Print valgrind memcheck log on success or failure Move the memcheck log printing to a separate step that runs with always() condition, so the log is printed regardless of whether the valgrind check passes or fails. This helps debug valgrind failures in CI. --- .github/workflows/verify-build.yml | 10 +- INSTALL | 367 ---------------------------- README.md | 2 + examples/Makefile.am | 3 +- examples/args_processing.cpp | 100 ++++++++ src/Makefile.am | 2 +- src/create_webserver.cpp | 68 ++++++ src/httpserver/create_webserver.hpp | 4 + src/httpserver/webserver.hpp | 1 + src/webserver.cpp | 1 + test/integ/ws_start_stop.cpp | 26 ++ 11 files changed, 214 insertions(+), 370 deletions(-) delete mode 100644 INSTALL create mode 100644 examples/args_processing.cpp create mode 100644 src/create_webserver.cpp diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 7815711f..d2218de6 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -624,9 +624,17 @@ jobs: run: | cd build ; make check-valgrind ; - cat test/test-suite-memcheck.log ; if: ${{ matrix.build-type == 'valgrind' }} + - name: Print Valgrind memcheck results + shell: bash + run: | + cd build ; + if [ -f test/test-suite-memcheck.log ]; then + cat test/test-suite-memcheck.log ; + fi + if: ${{ always() && matrix.build-type == 'valgrind' }} + - name: Run cppcheck run: | cd src/ ; diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 97a25409..00000000 --- a/INSTALL +++ /dev/null @@ -1,367 +0,0 @@ -Installation Instructions -************************* - -Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005, -2006, 2007, 2008, 2009 Free Software Foundation, Inc. - - Copying and distribution of this file, with or without modification, -are permitted in any medium without royalty provided the copyright -notice and this notice are preserved. This file is offered as-is, -without warranty of any kind. - -Basic Installation -================== - - Briefly, the shell commands `./configure; make; make install' should -configure, build, and install this package. The following -more-detailed instructions are generic; see the `README' file for -instructions specific to this package. Some packages provide this -`INSTALL' file but do not implement all of the features documented -below. The lack of an optional feature in a given package is not -necessarily a bug. More recommendations for GNU packages can be found -in *note Makefile Conventions: (standards)Makefile Conventions. - - The `configure' shell script attempts to guess correct values for -various system-dependent variables used during compilation. It uses -those values to create a `Makefile' in each directory of the package. -It may also create one or more `.h' files containing system-dependent -definitions. Finally, it creates a shell script `config.status' that -you can run in the future to recreate the current configuration, and a -file `config.log' containing compiler output (useful mainly for -debugging `configure'). - - It can also use an optional file (typically called `config.cache' -and enabled with `--cache-file=config.cache' or simply `-C') that saves -the results of its tests to speed up reconfiguring. Caching is -disabled by default to prevent problems with accidental use of stale -cache files. - - If you need to do unusual things to compile the package, please try -to figure out how `configure' could check whether to do them, and mail -diffs or instructions to the address given in the `README' so they can -be considered for the next release. If you are using the cache, and at -some point `config.cache' contains results you don't want to keep, you -may remove or edit it. - - The file `configure.ac' (or `configure.in') is used to create -`configure' by a program called `autoconf'. You need `configure.ac' if -you want to change it or regenerate `configure' using a newer version -of `autoconf'. - - The simplest way to compile this package is: - - 1. `cd' to the directory containing the package's source code and type - `make -f Makefile.cvs` to create configure file. - - 2. `./configure' to configure the package for your system. - - Running `configure' might take a while. While running, it prints - some messages telling which features it is checking for. - - 3. Type `make' to compile the package. - - 4. Optionally, type `make check' to run any self-tests that come with - the package, generally using the just-built uninstalled binaries. - - 5. Type `make install' to install the programs and any data files and - documentation. When installing into a prefix owned by root, it is - recommended that the package be configured and built as a regular - user, and only the `make install' phase executed with root - privileges. - - 6. Optionally, type `make installcheck' to repeat any self-tests, but - this time using the binaries in their final installed location. - This target does not install anything. Running this target as a - regular user, particularly if the prior `make install' required - root privileges, verifies that the installation completed - correctly. - - 7. You can remove the program binaries and object files from the - source code directory by typing `make clean'. To also remove the - files that `configure' created (so you can compile the package for - a different kind of computer), type `make distclean'. There is - also a `make maintainer-clean' target, but that is intended mainly - for the package's developers. If you use it, you may have to get - all sorts of other programs in order to regenerate files that came - with the distribution. - - 8. Often, you can also type `make uninstall' to remove the installed - files again. In practice, not all packages have tested that - uninstallation works correctly, even though it is required by the - GNU Coding Standards. - - 9. Some packages, particularly those that use Automake, provide `make - distcheck', which can by used by developers to test that all other - targets like `make install' and `make uninstall' work correctly. - This target is generally not run by end users. - -Compilers and Options -===================== - - Some systems require unusual options for compilation or linking that -the `configure' script does not know about. Run `./configure --help' -for details on some of the pertinent environment variables. - - You can give `configure' initial values for configuration parameters -by setting variables in the command line or in the environment. Here -is an example: - - ./configure CC=c99 CFLAGS=-g LIBS=-lposix - - *Note Defining Variables::, for more details. - -Compiling For Multiple Architectures -==================================== - - You can compile the package for more than one kind of computer at the -same time, by placing the object files for each architecture in their -own directory. To do this, you can use GNU `make'. `cd' to the -directory where you want the object files and executables to go and run -the `configure' script. `configure' automatically checks for the -source code in the directory that `configure' is in and in `..'. This -is known as a "VPATH" build. - - With a non-GNU `make', it is safer to compile the package for one -architecture at a time in the source code directory. After you have -installed the package for one architecture, use `make distclean' before -reconfiguring for another architecture. - - On MacOS X 10.5 and later systems, you can create libraries and -executables that work on multiple system types--known as "fat" or -"universal" binaries--by specifying multiple `-arch' options to the -compiler but only a single `-arch' option to the preprocessor. Like -this: - - ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CPP="gcc -E" CXXCPP="g++ -E" - - This is not guaranteed to produce working output in all cases, you -may have to build one architecture at a time and combine the results -using the `lipo' tool if you have problems. - -Installation Names -================== - - By default, `make install' installs the package's commands under -`/usr/local/bin', include files under `/usr/local/include', etc. You -can specify an installation prefix other than `/usr/local' by giving -`configure' the option `--prefix=PREFIX', where PREFIX must be an -absolute file name. - - You can specify separate installation prefixes for -architecture-specific files and architecture-independent files. If you -pass the option `--exec-prefix=PREFIX' to `configure', the package uses -PREFIX as the prefix for installing programs and libraries. -Documentation and other data files still use the regular prefix. - - In addition, if you use an unusual directory layout you can give -options like `--bindir=DIR' to specify different values for particular -kinds of files. Run `configure --help' for a list of the directories -you can set and what kinds of files go in them. In general, the -default for these options is expressed in terms of `${prefix}', so that -specifying just `--prefix' will affect all of the other directory -specifications that were not explicitly provided. - - The most portable way to affect installation locations is to pass the -correct locations to `configure'; however, many packages provide one or -both of the following shortcuts of passing variable assignments to the -`make install' command line to change installation locations without -having to reconfigure or recompile. - - The first method involves providing an override variable for each -affected directory. For example, `make install -prefix=/alternate/directory' will choose an alternate location for all -directory configuration variables that were expressed in terms of -`${prefix}'. Any directories that were specified during `configure', -but not in terms of `${prefix}', must each be overridden at install -time for the entire installation to be relocated. The approach of -makefile variable overrides for each directory variable is required by -the GNU Coding Standards, and ideally causes no recompilation. -However, some platforms have known limitations with the semantics of -shared libraries that end up requiring recompilation when using this -method, particularly noticeable in packages that use GNU Libtool. - - The second method involves providing the `DESTDIR' variable. For -example, `make install DESTDIR=/alternate/directory' will prepend -`/alternate/directory' before all installation names. The approach of -`DESTDIR' overrides is not required by the GNU Coding Standards, and -does not work on platforms that have drive letters. On the other hand, -it does better at avoiding recompilation issues, and works well even -when some directory options were not specified in terms of `${prefix}' -at `configure' time. - -Optional Features -================= - - If the package supports it, you can cause programs to be installed -with an extra prefix or suffix on their names by giving `configure' the -option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. - - Some packages pay attention to `--enable-FEATURE' options to -`configure', where FEATURE indicates an optional part of the package. -They may also pay attention to `--with-PACKAGE' options, where PACKAGE -is something like `gnu-as' or `x' (for the X Window System). The -`README' should mention any `--enable-' and `--with-' options that the -package recognizes. - - For packages that use the X Window System, `configure' can usually -find the X include and library files automatically, but if it doesn't, -you can use the `configure' options `--x-includes=DIR' and -`--x-libraries=DIR' to specify their locations. - - Some packages offer the ability to configure how verbose the -execution of `make' will be. For these packages, running `./configure ---enable-silent-rules' sets the default to minimal output, which can be -overridden with `make V=1'; while running `./configure ---disable-silent-rules' sets the default to verbose, which can be -overridden with `make V=0'. - -Particular systems -================== - - On HP-UX, the default C compiler is not ANSI C compatible. If GNU -CC is not installed, it is recommended to use the following options in -order to use an ANSI C compiler: - - ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" - -and if that doesn't work, install pre-built binaries of GCC for HP-UX. - - On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot -parse its `' header file. The option `-nodtk' can be used as -a workaround. If GNU CC is not installed, it is therefore recommended -to try - - ./configure CC="cc" - -and if that doesn't work, try - - ./configure CC="cc -nodtk" - - On Solaris, don't put `/usr/ucb' early in your `PATH'. This -directory contains several dysfunctional programs; working variants of -these programs are available in `/usr/bin'. So, if you need `/usr/ucb' -in your `PATH', put it _after_ `/usr/bin'. - - On Haiku, software installed for all users goes in `/boot/common', -not `/usr/local'. It is recommended to use the following options: - - ./configure --prefix=/boot/common - -Specifying the System Type -========================== - - There may be some features `configure' cannot figure out -automatically, but needs to determine by the type of machine the package -will run on. Usually, assuming the package is built to be run on the -_same_ architectures, `configure' can figure that out, but if it prints -a message saying it cannot guess the machine type, give it the -`--build=TYPE' option. TYPE can either be a short name for the system -type, such as `sun4', or a canonical name which has the form: - - CPU-COMPANY-SYSTEM - -where SYSTEM can have one of these forms: - - OS - KERNEL-OS - - See the file `config.sub' for the possible values of each field. If -`config.sub' isn't included in this package, then this package doesn't -need to know the machine type. - - If you are _building_ compiler tools for cross-compiling, you should -use the option `--target=TYPE' to select the type of system they will -produce code for. - - If you want to _use_ a cross compiler, that generates code for a -platform different from the build platform, you should specify the -"host" platform (i.e., that on which the generated programs will -eventually be run) with `--host=TYPE'. - -Sharing Defaults -================ - - If you want to set default values for `configure' scripts to share, -you can create a site shell script called `config.site' that gives -default values for variables like `CC', `cache_file', and `prefix'. -`configure' looks for `PREFIX/share/config.site' if it exists, then -`PREFIX/etc/config.site' if it exists. Or, you can set the -`CONFIG_SITE' environment variable to the location of the site script. -A warning: not all `configure' scripts look for a site script. - -Defining Variables -================== - - Variables not defined in a site shell script can be set in the -environment passed to `configure'. However, some packages may run -configure again during the build, and the customized values of these -variables may be lost. In order to avoid this problem, you should set -them in the `configure' command line, using `VAR=value'. For example: - - ./configure CC=/usr/local2/bin/gcc - -causes the specified `gcc' to be used as the C compiler (unless it is -overridden in the site shell script). - -Unfortunately, this technique does not work for `CONFIG_SHELL' due to -an Autoconf bug. Until the bug is fixed you can use this workaround: - - CONFIG_SHELL=/bin/bash /bin/bash ./configure CONFIG_SHELL=/bin/bash - -`configure' Invocation -====================== - - `configure' recognizes the following options to control how it -operates. - -`--help' -`-h' - Print a summary of all of the options to `configure', and exit. - -`--help=short' -`--help=recursive' - Print a summary of the options unique to this package's - `configure', and exit. The `short' variant lists options used - only in the top level, while the `recursive' variant lists options - also present in any nested packages. - -`--version' -`-V' - Print the version of Autoconf used to generate the `configure' - script, and exit. - -`--cache-file=FILE' - Enable the cache: use and save the results of the tests in FILE, - traditionally `config.cache'. FILE defaults to `/dev/null' to - disable caching. - -`--config-cache' -`-C' - Alias for `--cache-file=config.cache'. - -`--quiet' -`--silent' -`-q' - Do not print messages saying which checks are being made. To - suppress all normal output, redirect it to `/dev/null' (any error - messages will still be shown). - -`--srcdir=DIR' - Look for the package's source code in directory DIR. Usually - `configure' can determine that directory automatically. - -`--prefix=DIR' - Use DIR as the installation prefix. *note Installation Names:: - for more details, including other options available for fine-tuning - the installation locations. - -`--no-create' -`-n' - Run the configure checks, but stop before creating any output - files. - -`configure' also accepts some other, not widely useful, options. Run -`configure --help' for more details. - diff --git a/README.md b/README.md index cd66729a..1add295c 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,8 @@ For example, if your connection limit is “1”, a browser may open a first con * _.connection_timeout(**int** timeout):_ Determines after how many seconds of inactivity a connection should be timed out automatically. The default timeout is `180 seconds`. * _.memory_limit(**int** memory_limit):_ Maximum memory size per connection (followed by a `size_t`). The default is 32 kB (32*1024 bytes). Values above 128k are unlikely to result in much benefit, as half of the memory will be typically used for IO, and TCP buffers are unlikely to support window sizes above 64k on most systems. * _.per_IP_connection_limit(**int** connection_limit):_ Limit on the number of (concurrent) connections made to the server from the same IP address. Can be used to prevent one IP from taking over all of the allowed connections. If the same IP tries to establish more than the specified number of connections, they will be immediately rejected. The default is `0`, which means no limit on the number of connections from the same IP address. +* _.bind_address(**const struct sockaddr*** address):_ Bind the server to a specific network interface by passing a pre-constructed `sockaddr` structure. This gives full control over the address configuration but requires manual socket address setup. +* _.bind_address(**const std::string&** ip):_ Bind the server to a specific network interface by IP address string (e.g., `"127.0.0.1"` for localhost only, or `"192.168.1.100"` for a specific interface). Supports both IPv4 and IPv6 addresses. When an IPv6 address is provided, IPv6 mode is automatically enabled. Example: `create_webserver(8080).bind_address("127.0.0.1")`. * _.bind_socket(**int** socket_fd):_ Listen socket to use. Pass a listen socket for the daemon to use (systemd-style). If this option is used, the daemon will not open its own listen socket(s). The argument passed must be of type "int" and refer to an existing socket that has been bound to a port and is listening. * _.max_thread_stack_size(**int** stack_size):_ Maximum stack size for threads created by the library. Not specifying this option or using a value of zero means using the system default (which is likely to differ based on your platform). Default is `0 (system default)`. * _.use_ipv6() and .no_ipv6():_ Enable or disable the IPv6 protocol support (by default, libhttpserver will just support IPv4). If you specify this and the local platform does not support it, starting up the server will throw an exception. `off` by default. diff --git a/examples/Makefile.am b/examples/Makefile.am index c71065dc..37930db0 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg setting_headers custom_access_log basic_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log basic_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -28,6 +28,7 @@ custom_error_SOURCES = custom_error.cpp allowing_disallowing_methods_SOURCES = allowing_disallowing_methods.cpp handlers_SOURCES = handlers.cpp hello_with_get_arg_SOURCES = hello_with_get_arg.cpp +args_processing_SOURCES = args_processing.cpp setting_headers_SOURCES = setting_headers.cpp custom_access_log_SOURCES = custom_access_log.cpp basic_authentication_SOURCES = basic_authentication.cpp diff --git a/examples/args_processing.cpp b/examples/args_processing.cpp new file mode 100644 index 00000000..ddf41c4e --- /dev/null +++ b/examples/args_processing.cpp @@ -0,0 +1,100 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include +#include +#include + +#include + +// This example demonstrates how to use get_args() and get_args_flat() to +// process all query string and body arguments from an HTTP request. +// +// Try these URLs: +// http://localhost:8080/args?name=john&age=30 +// http://localhost:8080/args?id=1&id=2&id=3 (multiple values for same key) +// http://localhost:8080/args?colors=red&colors=green&colors=blue + +class args_resource : public httpserver::http_resource { + public: + std::shared_ptr render(const httpserver::http_request& req) { + std::stringstream response_body; + + response_body << "=== Using get_args() (supports multiple values per key) ===\n\n"; + + // get_args() returns a map where each key maps to an http_arg_value. + // http_arg_value contains a vector of values for parameters like "?id=1&id=2&id=3" + auto args = req.get_args(); + for (const auto& [key, arg_value] : args) { + response_body << "Key: " << key << "\n"; + // Use get_all_values() to get all values for this key + auto all_values = arg_value.get_all_values(); + if (all_values.size() > 1) { + response_body << " Values (" << all_values.size() << "):\n"; + for (const auto& v : all_values) { + response_body << " - " << v << "\n"; + } + } else { + // For single values, http_arg_value converts to string_view + response_body << " Value: " << std::string_view(arg_value) << "\n"; + } + } + + response_body << "\n=== Using get_args_flat() (one value per key) ===\n\n"; + + // get_args_flat() returns a simple map with one value per key. + // If a key has multiple values, only the first value is returned. + auto args_flat = req.get_args_flat(); + for (const auto& [key, value] : args_flat) { + response_body << key << " = " << value << "\n"; + } + + response_body << "\n=== Accessing individual arguments ===\n\n"; + + // You can also access individual arguments directly + auto name = req.get_arg("name"); // Returns http_arg_value (may have multiple values) + auto name_flat = req.get_arg_flat("name"); // Returns string_view (first value only) + + if (!name.get_flat_value().empty()) { + response_body << "name (via get_arg): " << std::string_view(name) << "\n"; + } + if (!name_flat.empty()) { + response_body << "name (via get_arg_flat): " << name_flat << "\n"; + } + + return std::make_shared(response_body.str(), 200, "text/plain"); + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + args_resource ar; + ws.register_resource("/args", &ar); + + std::cout << "Server running on http://localhost:8080/args\n"; + std::cout << "Try: http://localhost:8080/args?name=john&age=30\n"; + std::cout << "Or: http://localhost:8080/args?id=1&id=2&id=3\n"; + + ws.start(true); + + return 0; +} diff --git a/src/Makefile.am b/src/Makefile.am index 41b8cd61..63a16a50 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,7 +19,7 @@ AM_CPPFLAGS = -I../ -I$(srcdir)/httpserver/ METASOURCES = AUTO lib_LTLIBRARIES = libhttpserver.la -libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp basic_auth_fail_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp details/http_endpoint.cpp +libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp basic_auth_fail_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp gettext.h nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/basic_auth_fail_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/http_arg_value.hpp diff --git a/src/create_webserver.cpp b/src/create_webserver.cpp new file mode 100644 index 00000000..1a9f60dc --- /dev/null +++ b/src/create_webserver.cpp @@ -0,0 +1,68 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#if defined(_WIN32) && !defined(__CYGWIN__) +#define _WINDOWS +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x600 +#include +#include +#else +#include +#include +#include +#endif + +#include +#include +#include +#include + +#include "httpserver/create_webserver.hpp" + +namespace httpserver { + +create_webserver& create_webserver::bind_address(const std::string& ip) { + _bind_address_storage = std::make_shared(); + std::memset(_bind_address_storage.get(), 0, sizeof(struct sockaddr_storage)); + + // Try IPv4 first + auto* addr4 = reinterpret_cast(_bind_address_storage.get()); + if (inet_pton(AF_INET, ip.c_str(), &(addr4->sin_addr)) == 1) { + addr4->sin_family = AF_INET; + addr4->sin_port = htons(_port); + _bind_address = reinterpret_cast(_bind_address_storage.get()); + return *this; + } + + // Try IPv6 + auto* addr6 = reinterpret_cast(_bind_address_storage.get()); + if (inet_pton(AF_INET6, ip.c_str(), &(addr6->sin6_addr)) == 1) { + addr6->sin6_family = AF_INET6; + addr6->sin6_port = htons(_port); + _bind_address = reinterpret_cast(_bind_address_storage.get()); + _use_ipv6 = true; + return *this; + } + + throw std::invalid_argument("Invalid IP address: " + ip); +} + +} // namespace httpserver diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index 31faab02..ddea0d58 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "httpserver/http_response.hpp" #include "httpserver/http_utils.hpp" @@ -128,6 +129,8 @@ class create_webserver { return *this; } + create_webserver& bind_address(const std::string& ip); + create_webserver& bind_socket(int bind_socket) { _bind_socket = bind_socket; return *this; @@ -387,6 +390,7 @@ class create_webserver { validator_ptr _validator = nullptr; unescaper_ptr _unescaper = nullptr; const struct sockaddr* _bind_address = nullptr; + std::shared_ptr _bind_address_storage; int _bind_socket = 0; int _max_thread_stack_size = 0; bool _use_ssl = false; diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 31ba0958..262849af 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -144,6 +144,7 @@ class webserver { validator_ptr validator; unescaper_ptr unescaper; const struct sockaddr* bind_address; + std::shared_ptr bind_address_storage; /* Changed type to MHD_socket because this type will always reflect the platform's actual socket type (e.g. SOCKET on windows, int on unixes)*/ MHD_socket bind_socket; diff --git a/src/webserver.cpp b/src/webserver.cpp index 48745966..a60af1d2 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -134,6 +134,7 @@ webserver::webserver(const create_webserver& params): validator(params._validator), unescaper(params._unescaper), bind_address(params._bind_address), + bind_address_storage(params._bind_address_storage), bind_socket(params._bind_socket), max_thread_stack_size(params._max_thread_stack_size), use_ssl(params._use_ssl), diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 8a6d999c..d43459ed 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -338,6 +338,32 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, custom_socket) ws.stop(); LT_END_AUTO_TEST(custom_socket) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, bind_address_string) + httpserver::webserver ws = httpserver::create_webserver(PORT).bind_address("127.0.0.1"); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "127.0.0.1:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(bind_address_string) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, bind_address_string_invalid) + LT_CHECK_THROW(httpserver::create_webserver(PORT).bind_address("not_an_ip")); +LT_END_AUTO_TEST(bind_address_string_invalid) #endif LT_BEGIN_AUTO_TEST(ws_start_stop_suite, single_resource) From 441f67190a77663784f3fa69efba92485d7008d1 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Jan 2026 10:59:06 -0800 Subject: [PATCH 15/47] Add centralized authentication handler (issue #102) (#359) Add auth_handler callback to webserver that runs before any resource's render method. This allows defining authentication logic once for all resources instead of duplicating it in every render method. Features: - auth_handler: callback that returns nullptr to allow request or http_response to reject - auth_skip_paths: vector of paths to bypass auth (supports exact match and wildcard suffix like "/public/*") Includes comprehensive tests and example in examples/centralized_authentication.cpp --- README.md | 75 +++++ examples/centralized_authentication.cpp | 88 +++++ src/httpserver/create_webserver.hpp | 14 + src/httpserver/webserver.hpp | 4 + src/webserver.cpp | 31 +- test/integ/authentication.cpp | 412 ++++++++++++++++++++++++ 6 files changed, 622 insertions(+), 2 deletions(-) create mode 100644 examples/centralized_authentication.cpp diff --git a/README.md b/README.md index 1add295c..13dba9c3 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ libhttpserver is built upon [libmicrohttpd](https://www.gnu.org/software/libmic - Support for SHOUTcast - Support for incremental processing of POST data (optional) - Support for basic and digest authentication (optional) +- Support for centralized authentication with path-based skip rules - Support for TLS (requires libgnutls, optional) ## Table of Contents @@ -990,6 +991,80 @@ You will receive a `SUCCESS` in response (observe the response message from the You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/digest_authentication.cpp). +### Using Centralized Authentication +The examples above show authentication handled within each resource's `render_*` method. This approach requires duplicating authentication logic in every resource, which is error-prone and violates DRY (Don't Repeat Yourself) principles. + +libhttpserver provides a centralized authentication mechanism that runs a single authentication handler before any resource's render method is called. This allows you to: +- Define authentication logic once for all resources +- Automatically protect all endpoints by default +- Specify paths that should bypass authentication (e.g., health checks, public APIs) + +```cpp + #include + + using namespace httpserver; + + // Resources no longer need authentication logic + class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello, authenticated user!", 200, "text/plain"); + } + }; + + class health_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("OK", 200, "text/plain"); + } + }; + + // Centralized authentication handler + // Return nullptr to allow the request, or an http_response to reject it + std::shared_ptr my_auth_handler(const http_request& req) { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return std::make_shared("Unauthorized", "MyRealm"); + } + return nullptr; // Allow request to proceed to resource + } + + int main() { + webserver ws = create_webserver(8080) + .auth_handler(my_auth_handler) + .auth_skip_paths({"/health", "/public/*"}); + + hello_resource hello; + health_resource health; + + ws.register_resource("/api", &hello); + ws.register_resource("/health", &health); + + ws.start(true); + return 0; + } +``` + +The `auth_handler` callback is called for every request before the resource's render method. It receives the `http_request` and can: +- Return `nullptr` to allow the request to proceed normally +- Return an `http_response` (e.g., `basic_auth_fail_response` or `digest_auth_fail_response`) to reject the request + +The `auth_skip_paths` method accepts a vector of paths that should bypass authentication: +- Exact matches: `"/health"` matches only `/health` +- Wildcard suffixes: `"/public/*"` matches `/public/`, `/public/info`, `/public/docs/api`, etc. + +To test the above example: + + # Without auth - returns 401 Unauthorized + curl -v http://localhost:8080/api + + # With valid auth - returns 200 OK + curl -u admin:secret http://localhost:8080/api + + # Health endpoint (skip path) - works without auth + curl http://localhost:8080/health + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/centralized_authentication.cpp). + [Back to TOC](#table-of-contents) ## HTTP Utils diff --git a/examples/centralized_authentication.cpp b/examples/centralized_authentication.cpp new file mode 100644 index 00000000..0f965af6 --- /dev/null +++ b/examples/centralized_authentication.cpp @@ -0,0 +1,88 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include + +#include + +using httpserver::http_request; +using httpserver::http_response; +using httpserver::http_resource; +using httpserver::webserver; +using httpserver::create_webserver; +using httpserver::string_response; +using httpserver::basic_auth_fail_response; + +// Simple resource that doesn't need to handle auth itself +class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello, authenticated user!", 200, "text/plain"); + } +}; + +class health_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("OK", 200, "text/plain"); + } +}; + +// Centralized authentication handler +// Returns nullptr to allow the request, or an http_response to reject it +std::shared_ptr auth_handler(const http_request& req) { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return std::make_shared("Unauthorized", "MyRealm"); + } + return nullptr; // Allow request +} + +int main() { + // Create webserver with centralized authentication + // - auth_handler: called before every resource's render method + // - auth_skip_paths: paths that bypass authentication + webserver ws = create_webserver(8080) + .auth_handler(auth_handler) + .auth_skip_paths({"/health", "/public/*"}); + + hello_resource hello; + health_resource health; + + ws.register_resource("/api", &hello); + ws.register_resource("/health", &health); + + ws.start(true); + + return 0; +} + +// Usage: +// # Start the server +// ./centralized_authentication +// +// # Without auth - should get 401 Unauthorized +// curl -v http://localhost:8080/api +// +// # With valid auth - should get 200 OK +// curl -u admin:secret http://localhost:8080/api +// +// # Health endpoint (skip path) - works without auth +// curl http://localhost:8080/health diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index ddea0d58..f18ed5b6 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "httpserver/http_response.hpp" #include "httpserver/http_utils.hpp" @@ -52,6 +53,7 @@ typedef std::function psk_cred_handler_callback namespace http { class file_info; } typedef std::function file_cleanup_callback_ptr; +typedef std::function(const http_request&)> auth_handler_ptr; class create_webserver { public: @@ -376,6 +378,16 @@ class create_webserver { return *this; } + create_webserver& auth_handler(auth_handler_ptr handler) { + _auth_handler = handler; + return *this; + } + + create_webserver& auth_skip_paths(const std::vector& paths) { + _auth_skip_paths = paths; + return *this; + } + private: uint16_t _port = DEFAULT_WS_PORT; http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT; @@ -423,6 +435,8 @@ class create_webserver { render_ptr _method_not_allowed_resource = nullptr; render_ptr _internal_error_resource = nullptr; file_cleanup_callback_ptr _file_cleanup_callback = nullptr; + auth_handler_ptr _auth_handler = nullptr; + std::vector _auth_skip_paths; friend class webserver; }; diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 262849af..7ba48b73 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -45,6 +45,7 @@ #include #include #include +#include #ifdef HAVE_GNUTLS #include @@ -182,6 +183,8 @@ class webserver { const render_ptr method_not_allowed_resource; const render_ptr internal_error_resource; const file_cleanup_callback_ptr file_cleanup_callback; + const auth_handler_ptr auth_handler; + const std::vector auth_skip_paths; std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; @@ -197,6 +200,7 @@ class webserver { std::shared_ptr method_not_allowed_page(details::modded_request* mr) const; std::shared_ptr internal_error_page(details::modded_request* mr, bool force_our = false) const; std::shared_ptr not_found_page(details::modded_request* mr) const; + bool should_skip_auth(const std::string& path) const; static void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, diff --git a/src/webserver.cpp b/src/webserver.cpp index a60af1d2..7d902453 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -167,7 +167,9 @@ webserver::webserver(const create_webserver& params): not_found_resource(params._not_found_resource), method_not_allowed_resource(params._method_not_allowed_resource), internal_error_resource(params._internal_error_resource), - file_cleanup_callback(params._file_cleanup_callback) { + file_cleanup_callback(params._file_cleanup_callback), + auth_handler(params._auth_handler), + auth_skip_paths(params._auth_skip_paths) { ignore_sigpipe(); pthread_mutex_init(&mutexwait, nullptr); pthread_cond_init(&mutexcond, nullptr); @@ -635,6 +637,19 @@ std::shared_ptr webserver::internal_error_page(details::modded_re } } +bool webserver::should_skip_auth(const std::string& path) const { + for (const auto& skip_path : auth_skip_paths) { + if (skip_path == path) return true; + // Support wildcard suffix (e.g., "/public/*") + if (skip_path.size() > 2 && skip_path.back() == '*' && + skip_path[skip_path.size() - 2] == '/') { + std::string prefix = skip_path.substr(0, skip_path.size() - 1); + if (path.compare(0, prefix.size(), prefix) == 0) return true; + } + } + return false; +} + MHD_Result webserver::requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr) { mr->dhr.reset(new http_request(connection, unescaper)); mr->dhr->set_file_cleanup_callback(file_cleanup_callback); @@ -747,6 +762,18 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } } + // Check centralized authentication if handler is configured + if (found && auth_handler != nullptr) { + std::string path(mr->dhr->get_path()); + if (!should_skip_auth(path)) { + std::shared_ptr auth_response = auth_handler(*mr->dhr); + if (auth_response != nullptr) { + mr->dhrs = auth_response; + found = false; // Skip resource rendering, go directly to response + } + } + } + if (found) { try { if (mr->pp != nullptr) { @@ -775,7 +802,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } catch(...) { mr->dhrs = internal_error_page(mr); } - } else { + } else if (mr->dhrs == nullptr) { mr->dhrs = not_found_page(mr); } diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index eba87172..05d1f254 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -239,6 +239,418 @@ LT_END_AUTO_TEST(digest_auth_wrong_pass) #endif +// Simple resource for centralized auth tests +class simple_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared("SUCCESS", 200, "text/plain"); + } +}; + +// Centralized authentication handler +std::shared_ptr centralized_auth_handler(const http_request& req) { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return std::make_shared("Unauthorized", "testrealm"); + } + return nullptr; // Allow request +} + +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_fail) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + LT_CHECK_EQ(s, "Unauthorized"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_fail) + +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_success) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + curl_easy_setopt(curl, CURLOPT_USERNAME, "admin"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "secret"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_success) + +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_paths) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/health", "/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("health", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("public/info", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + std::string s; + + // Test /health (exact match skip path) - should succeed without auth + curl = curl_easy_init(); + s = ""; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/health"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + // Test /public/info (wildcard skip path) - should succeed without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/public/info"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + // Test /protected (not in skip paths) - should fail without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_paths) + +// Test that wildcard doesn't match partial prefix +// /publicinfo should NOT match /public/* (wildcard requires the slash) +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_paths_no_partial_match) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("publicinfo", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // /publicinfo should NOT be skipped (doesn't match /public/*) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/publicinfo"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); // Should require auth + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_paths_no_partial_match) + +// Test deeply nested wildcard paths +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_paths_deep_nested) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/api/v1/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("api/v1/public/users/list", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Deep nested path should be skipped + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/api/v1/public/users/list"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_paths_deep_nested) + +// Test POST method with centralized auth +class post_resource : public http_resource { + public: + shared_ptr render_POST(const http_request&) { + return std::make_shared("POST_SUCCESS", 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_post_method) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + post_resource pr; + LT_ASSERT_EQ(true, ws.register_resource("data", &pr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // POST without auth should fail + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/data"); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "test=data"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + // POST with auth should succeed + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_USERNAME, "admin"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "secret"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/data"); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "test=data"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "POST_SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_post_method) + +// Test wrong credentials (different from no credentials) +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_wrong_credentials) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Wrong username + curl_easy_setopt(curl, CURLOPT_USERNAME, "wronguser"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "secret"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + // Wrong password + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_USERNAME, "admin"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "wrongpass"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_wrong_credentials) + +// Test that 404 is returned for non-existent resources (auth doesn't interfere) +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_not_found) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("exists", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Non-existent resource without auth - should get 401 (auth checked first) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/nonexistent"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + // Note: Auth is only checked when resource is found, so 404 should be returned + LT_CHECK_EQ(http_code, 404); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_not_found) + +// Test no auth handler (default behavior - no auth required) +LT_BEGIN_AUTO_TEST(authentication_suite, no_auth_handler_default) + webserver ws = create_webserver(PORT); // No auth_handler + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("open", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Should succeed without any auth + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/open"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(no_auth_handler_default) + +// Test multiple skip paths +LT_BEGIN_AUTO_TEST(authentication_suite, auth_multiple_skip_paths) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/health", "/metrics", "/status", "/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("health", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("metrics", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("status", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + std::string s; + + // All skip paths should work without auth + const char* skip_urls[] = {"/health", "/metrics", "/status"}; + for (const char* url : skip_urls) { + curl = curl_easy_init(); + s = ""; + http_code = 0; + std::string full_url = std::string("localhost:" PORT_STRING) + url; + curl_easy_setopt(curl, CURLOPT_URL, full_url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); + } + + // Protected should still require auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_multiple_skip_paths) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From e06f83a6b6ca9ab3345cb4289982231d73c46eb7 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Jan 2026 15:41:55 -0800 Subject: [PATCH 16/47] Add HA1-based digest authentication support (#360) * Add HA1-based digest authentication support Adds check_digest_auth_ha1() method that accepts pre-computed HA1 hash bytes instead of plaintext password. This allows secure storage of password hashes rather than plaintext passwords. Changes: - Add digest_algorithm enum (MD5, SHA256) without AUTO since libmicrohttpd cannot auto-detect algorithm from raw hash bytes - Add md5_digest_size and sha256_digest_size constants - Add check_digest_auth_ha1() to http_request - Add integration tests for HA1-based digest authentication * Extend digest_auth_fail_response for algorithm specification - Add algorithm parameter to digest_auth_fail_response constructor (defaults to MD5 for backward compatibility) - Use MHD_queue_auth_fail_response2() to specify the algorithm in the WWW-Authenticate challenge header - Add separate MD5 and SHA256 test resources for deterministic testing - Add SHA256 digest auth tests alongside existing MD5 tests This enables server-driven algorithm selection, where the server requests a specific digest algorithm in the challenge and curl responds using that algorithm. --- src/digest_auth_fail_response.cpp | 8 +- src/http_request.cpp | 29 +++ src/httpserver/digest_auth_fail_response.hpp | 9 +- src/httpserver/http_request.hpp | 20 ++ src/httpserver/http_utils.hpp | 10 + test/integ/authentication.cpp | 226 +++++++++++++++++++ 6 files changed, 299 insertions(+), 3 deletions(-) diff --git a/src/digest_auth_fail_response.cpp b/src/digest_auth_fail_response.cpp index cb24325f..1fb8307c 100644 --- a/src/digest_auth_fail_response.cpp +++ b/src/digest_auth_fail_response.cpp @@ -30,7 +30,13 @@ struct MHD_Response; namespace httpserver { int digest_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_Response* response) { - return MHD_queue_auth_fail_response(connection, realm.c_str(), opaque.c_str(), response, reload_nonce ? MHD_YES : MHD_NO); + return MHD_queue_auth_fail_response2( + connection, + realm.c_str(), + opaque.c_str(), + response, + reload_nonce ? MHD_YES : MHD_NO, + static_cast(algorithm)); } } // namespace httpserver diff --git a/src/http_request.cpp b/src/http_request.cpp index 1a2b7c14..be532637 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -58,6 +58,35 @@ bool http_request::check_digest_auth(const std::string& realm, const std::string *reload_nonce = false; return true; } + +bool http_request::check_digest_auth_ha1( + const std::string& realm, + const unsigned char* digest, + size_t digest_size, + int nonce_timeout, + bool* reload_nonce, + http::http_utils::digest_algorithm algo) const { + std::string_view digested_user = get_digested_user(); + + int val = MHD_digest_auth_check_digest2( + underlying_connection, + realm.c_str(), + digested_user.data(), + digest, + digest_size, + nonce_timeout, + static_cast(algo)); + + if (val == MHD_INVALID_NONCE) { + *reload_nonce = true; + return false; + } else if (val == MHD_NO) { + *reload_nonce = false; + return false; + } + *reload_nonce = false; + return true; +} #endif // HAVE_DAUTH std::string_view http_request::get_connection_value(std::string_view key, enum MHD_ValueKind kind) const { diff --git a/src/httpserver/digest_auth_fail_response.hpp b/src/httpserver/digest_auth_fail_response.hpp index f3697c31..2eb044dc 100644 --- a/src/httpserver/digest_auth_fail_response.hpp +++ b/src/httpserver/digest_auth_fail_response.hpp @@ -45,11 +45,14 @@ class digest_auth_fail_response : public string_response { const std::string& opaque = "", bool reload_nonce = false, int response_code = http::http_utils::http_ok, - const std::string& content_type = http::http_utils::text_plain): + const std::string& content_type = http::http_utils::text_plain, + http::http_utils::digest_algorithm algorithm = + http::http_utils::digest_algorithm::MD5): string_response(content, response_code, content_type), realm(realm), opaque(opaque), - reload_nonce(reload_nonce) { } + reload_nonce(reload_nonce), + algorithm(algorithm) { } digest_auth_fail_response(const digest_auth_fail_response& other) = default; digest_auth_fail_response(digest_auth_fail_response&& other) noexcept = default; @@ -64,6 +67,8 @@ class digest_auth_fail_response : public string_response { std::string realm = ""; std::string opaque = ""; bool reload_nonce = false; + http::http_utils::digest_algorithm algorithm = + http::http_utils::digest_algorithm::MD5; }; } // namespace httpserver diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 749e49ab..4c1c3323 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -254,6 +255,25 @@ class http_request { #ifdef HAVE_DAUTH bool check_digest_auth(const std::string& realm, const std::string& password, int nonce_timeout, bool* reload_nonce) const; + + /** + * Check digest authentication using a pre-computed HA1 hash. + * The HA1 hash is computed as: hash(username:realm:password) using the specified algorithm. + * @param realm The authentication realm. + * @param digest Pointer to the pre-computed HA1 hash bytes. + * @param digest_size Size of the digest (16 for MD5, 32 for SHA-256). + * @param nonce_timeout Nonce validity timeout in seconds. + * @param reload_nonce Output: set to true if nonce should be regenerated. + * @param algo The digest algorithm (defaults to MD5). + * @return true if authenticated, false otherwise. + */ + bool check_digest_auth_ha1( + const std::string& realm, + const unsigned char* digest, + size_t digest_size, + int nonce_timeout, + bool* reload_nonce, + http::http_utils::digest_algorithm algo = http::http_utils::digest_algorithm::MD5) const; #endif // HAVE_DAUTH friend std::ostream &operator<< (std::ostream &os, http_request &r); diff --git a/src/httpserver/http_utils.hpp b/src/httpserver/http_utils.hpp index d95c0eb8..35a5a45a 100644 --- a/src/httpserver/http_utils.hpp +++ b/src/httpserver/http_utils.hpp @@ -117,6 +117,16 @@ class http_utils { IPV6 = 16 }; +#ifdef HAVE_DAUTH + enum class digest_algorithm { + MD5 = MHD_DIGEST_ALG_MD5, + SHA256 = MHD_DIGEST_ALG_SHA256 + }; + + static constexpr size_t md5_digest_size = 16; + static constexpr size_t sha256_digest_size = 32; +#endif // HAVE_DAUTH + static const uint16_t http_method_connect_code; static const uint16_t http_method_delete_code; static const uint16_t http_method_get_code; diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index 05d1f254..202936ef 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -157,6 +157,72 @@ LT_END_AUTO_TEST(base_auth_fail) // Also skip if libmicrohttpd was built without digest auth support #if !defined(_WINDOWS) && defined(HAVE_DAUTH) +// Pre-computed MD5 hash of "myuser:examplerealm:mypass" +// printf "myuser:examplerealm:mypass" | md5sum +// 6ceef750e0130d6528b938c3abd94110 +static const unsigned char PRECOMPUTED_HA1_MD5[16] = { + 0x6c, 0xee, 0xf7, 0x50, 0xe0, 0x13, 0x0d, 0x65, + 0x28, 0xb9, 0x38, 0xc3, 0xab, 0xd9, 0x41, 0x10 +}; + +// Pre-computed SHA-256 hash of "myuser:examplerealm:mypass" +// printf "myuser:examplerealm:mypass" | sha256sum +// d4ff5b1795b23b4c625975959f3276526f3f4f4ef7d22083207e02d7c4bd8a05 +static const unsigned char PRECOMPUTED_HA1_SHA256[32] = { + 0xd4, 0xff, 0x5b, 0x17, 0x95, 0xb2, 0x3b, 0x4c, + 0x62, 0x59, 0x75, 0x95, 0x9f, 0x32, 0x76, 0x52, + 0x6f, 0x3f, 0x4f, 0x4e, 0xf7, 0xd2, 0x20, 0x83, + 0x20, 0x7e, 0x02, 0xd7, 0xc4, 0xbd, 0x8a, 0x05 +}; + +class digest_ha1_md5_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + if (req.get_digested_user() == "") { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, true, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::MD5); + } + bool reload_nonce = false; + if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_MD5, + httpserver::http::http_utils::md5_digest_size, 300, &reload_nonce, + httpserver::http::http_utils::digest_algorithm::MD5)) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, reload_nonce, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::MD5); + } + return std::make_shared("SUCCESS", 200, "text/plain"); + } +}; + +class digest_ha1_sha256_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + if (req.get_digested_user() == "") { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, true, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::SHA256); + } + bool reload_nonce = false; + if (!req.check_digest_auth_ha1("examplerealm", PRECOMPUTED_HA1_SHA256, + httpserver::http::http_utils::sha256_digest_size, 300, &reload_nonce, + httpserver::http::http_utils::digest_algorithm::SHA256)) { + return std::make_shared( + "FAIL", "examplerealm", MY_OPAQUE, reload_nonce, + httpserver::http::http_utils::http_ok, + httpserver::http::http_utils::text_plain, + httpserver::http::http_utils::digest_algorithm::SHA256); + } + return std::make_shared("SUCCESS", 200, "text/plain"); + } +}; + LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth) webserver ws = create_webserver(PORT) .digest_auth_random("myrandom") @@ -237,6 +303,166 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass) ws.stop(); LT_END_AUTO_TEST(digest_auth_wrong_pass) +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_md5_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_md5) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_md5_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "FAIL"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_md5_wrong_pass) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_sha256_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:mypass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:mypass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_sha256) + +LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_ha1_sha256_resource digest_ha1; + LT_ASSERT_EQ(true, ws.register_resource("base", &digest_ha1)); + ws.start(false); + +#if defined(_WINDOWS) + curl_global_init(CURL_GLOBAL_WIN32); +#else + curl_global_init(CURL_GLOBAL_ALL); +#endif + + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); +#if defined(_WINDOWS) + curl_easy_setopt(curl, CURLOPT_USERPWD, "examplerealm/myuser:wrongpass"); +#else + curl_easy_setopt(curl, CURLOPT_USERPWD, "myuser:wrongpass"); +#endif + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "FAIL"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_with_ha1_sha256_wrong_pass) + #endif // Simple resource for centralized auth tests From 77b089a3bf5421fa5e3cc6a0a5707c5d916e2da9 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Jan 2026 22:42:58 -0800 Subject: [PATCH 17/47] Fix codecov upload condition in CI workflow (#361) * Fix codecov upload condition in CI workflow The upload condition used 'discard' which never matches any matrix value, preventing coverage data from being uploaded. Changed to 'ubuntu' to match the actual os-type matrix value. * Replace deprecated Codecov bash uploader with official GitHub Action The bash uploader now requires a token and is deprecated. Using codecov/codecov-action@v5 which handles authentication automatically for public repos via GitHub OIDC. * Add gcovr step to generate coverage report before Codecov upload The codecov-action@v5 does not auto-process gcov data like the old bash uploader did. This adds a step to run gcovr to convert .gcda/.gcno files to Cobertura XML format, which codecov can parse. * Add CODECOV_TOKEN for protected branch uploads Codecov requires authentication token for uploading coverage reports to protected branches. * Trigger CI to test Codecov upload with token * Add --root option to gcovr for correct source path mapping The coverage report was unusable because gcovr generated paths relative to the build directory (e.g., ../src/webserver.cpp). Adding --root .. ensures paths are relative to the repository root, allowing Codecov to correctly match source files. * Exclude test directory from coverage reports * Ignore tests --- .github/workflows/verify-build.yml | 16 ++++++++++++---- codecov.yml | 3 +++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index d2218de6..6f632dec 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -665,8 +665,16 @@ jobs: sleep 5 && ab -n 1000000 -c 100 127.0.0.1:8080/plaintext if: ${{ matrix.build-type == 'threads' }} - - name: Push code coverage data + - name: Generate coverage report run: | - cd build ; - bash <(curl -s https://codecov.io/bash) ; - if: ${{ matrix.os-type == 'discard' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} + cd build + gcovr --root .. --exclude test/ --xml coverage.xml --xml-pretty + if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: build/coverage.xml + fail_ci_if_error: false + if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} diff --git a/codecov.yml b/codecov.yml index a6e1de74..97a08e6a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -18,3 +18,6 @@ comment: layout: "reach,diff,flags,files,footer" behavior: default require_changes: no + +ignore: + - "test" From 3be9e7c21de9bc9911c688f7780a4c5eaf1d94d6 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 3 Feb 2026 14:00:30 -0800 Subject: [PATCH 18/47] Add tests for untested HTTP methods and request/response getters (#362) * Add tests for untested HTTP methods and request/response getters - Add HEAD, OPTIONS, TRACE method handlers to complete_test_resource - Add integration tests for HEAD, OPTIONS, TRACE HTTP methods - Add request_info_resource and test for get_requestor(), get_requestor_port(), get_version() - Add content_limit_suite to test content_too_large() with content_size_limit - Add unregister_then_404 test for webserver::unregister_resource() - Create http_response unit test file with tests for response code, headers, footers, and cookies These tests increase coverage for previously untested code paths in webserver.cpp, http_request.cpp, and http_response.cpp. * Add additional coverage tests for networking and TLS features - Add IPv6 and dual-stack webserver tests - Add HTTPS webserver and TLS session getter tests with graceful failure handling - Add bind_address_ipv4 test with runtime URL building - Add shoutcast response test for MHD_ICY_FLAG verification - Add string_response constructor tests - Remove CONNECT method test (incompatible with curl's tunneling semantics) Coverage improved from ~90% to 91.8% lines, 93.3% functions. * Add print-summary to gcovr for coverage debugging Add --print-summary flag to gcovr command in CI to help diagnose the discrepancy between local coverage (91.8%) and Codecov (64.53%). * Filter gcovr to src/ directory only The coverage report was including 15,781 lines from system headers and dependencies instead of just the ~1,500 lines of libhttpserver source. Use --filter to restrict coverage to src/ directory only. * Add coverage tests for footers, PSK handlers, and ban/allow weights - Add footer/trailer tests for http_request and http_response - Add PSK handler configuration tests (setup, empty handler, no handler) - Add ban/allow IP weight comparison tests for specific-then-wildcard case - Add auth skip path tests for root path and wildcards - Add IPv6 parsing edge case tests (too many parts, wildcards, nested) - Add http_endpoint invalid regex pattern test - Add custom error handler tests (not_found, method_not_allowed) - Add request info caching tests Branch coverage: 60.4% -> 64.2% (+65 branches) Line coverage: 91.8% -> 94.9% * Add Phase 2 branch coverage tests for http_request and http_endpoint - Add null value query parameter test covering nullptr branches in build_request_args and build_request_querystring (lines 234, 248) - Add digested user caching tests for cache hit and nullptr branches (lines 293-295, 300) in http_request.cpp - Add caret prefix URL pattern tests covering line 85 in http_endpoint.cpp - Add consecutive slashes URL test covering empty parts handling (line 83) Branch coverage improvements: - http_endpoint.cpp: 66.9% -> 68.7% - http_request.cpp: 58.5% -> 59.7% * Add Phase 3 branch coverage tests for render methods and error handling - Add tests for all HTTP methods falling through to base render() - Add custom internal error resource handler test - Add get_arg_flat fallback test - Add large multipart form field test (100KB) - Add file upload with explicit content-type header test - Add http_endpoint tests for regex validation and error paths - Add tests for invalid URL parameter formats * Fix cpplint issues in test files - Replace static global strings with function-static pattern - Change 'long' to 'int64_t' for http_code variables - Add missing #include for std::move * Added PSK tests * Fix Windows test failure and add hex utility coverage - Fix file_upload tests to use subdirectory instead of /tmp for cross-platform compatibility - Extract hex validation functions (is_valid_hex, hex_char_to_val) from webserver.cpp to string_utilities for testability - Add unit tests for hex utility functions to improve coverage * Fix Windows mkdir compatibility issue On Windows/MinGW, mkdir() only takes one argument (path), while POSIX mkdir() takes two (path and mode). Add a MKDIR macro that uses the appropriate signature for each platform. --- .github/workflows/verify-build.yml | 4 +- src/httpserver/string_utilities.hpp | 14 + src/httpserver/webserver.hpp | 7 +- src/string_utilities.cpp | 17 + src/webserver.cpp | 49 +- test/Makefile.am | 4 +- test/integ/authentication.cpp | 213 ++++ test/integ/ban_system.cpp | 284 +++++ test/integ/basic.cpp | 1592 ++++++++++++++++++++++++++- test/integ/file_upload.cpp | 108 ++ test/integ/ws_start_stop.cpp | 837 ++++++++++++++ test/unit/create_webserver_test.cpp | 470 ++++++++ test/unit/http_endpoint_test.cpp | 261 +++++ test/unit/http_resource_test.cpp | 130 +++ test/unit/http_response_test.cpp | 331 ++++++ test/unit/http_utils_test.cpp | 314 ++++++ test/unit/string_utilities_test.cpp | 122 ++ 17 files changed, 4735 insertions(+), 22 deletions(-) create mode 100644 test/unit/create_webserver_test.cpp create mode 100644 test/unit/http_response_test.cpp diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 6f632dec..81b94737 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -470,7 +470,7 @@ jobs: - name: Setup gnutls dependency (only on linux) run: | - sudo apt-get install libgnutls28-dev ; + sudo apt-get install libgnutls28-dev gnutls-bin ; if: ${{ matrix.os-type == 'ubuntu' }} - name: Fetch libmicrohttpd from cache @@ -668,7 +668,7 @@ jobs: - name: Generate coverage report run: | cd build - gcovr --root .. --exclude test/ --xml coverage.xml --xml-pretty + gcovr --root .. --filter '../src/' --xml coverage.xml --xml-pretty --print-summary if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} - name: Upload coverage to Codecov diff --git a/src/httpserver/string_utilities.hpp b/src/httpserver/string_utilities.hpp index 69fb4929..bcb6897f 100644 --- a/src/httpserver/string_utilities.hpp +++ b/src/httpserver/string_utilities.hpp @@ -42,6 +42,20 @@ const std::string to_upper_copy(const std::string& str); const std::string to_lower_copy(const std::string& str); const std::vector string_split(const std::string& s, char sep = ' ', bool collapse = true); +/** + * Validate that a string contains only valid hexadecimal characters (0-9, a-f, A-F) + * @param s The string to validate + * @return true if string contains only valid hex characters, false otherwise + */ +bool is_valid_hex(const std::string& s); + +/** + * Convert a hex character to its numeric value (0-15) + * @param c The hex character to convert + * @return numeric value (0-15), or 0 for invalid characters + */ +unsigned char hex_char_to_val(char c); + } // namespace string_utilities } // namespace httpserver diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 7ba48b73..e4d5e313 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -227,9 +227,12 @@ class webserver { MHD_Result complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method); #ifdef HAVE_GNUTLS - static int psk_cred_handler_func(gnutls_session_t session, + // MHD_PskServerCredentialsCallback signature + static int psk_cred_handler_func(void* cls, + struct MHD_Connection* connection, const char* username, - gnutls_datum_t* key); + void** psk, + size_t* psk_size); #endif // HAVE_GNUTLS friend MHD_Result policy_callback(void *cls, const struct sockaddr* addr, socklen_t addrlen); diff --git a/src/string_utilities.cpp b/src/string_utilities.cpp index e06234a7..7170ccf6 100644 --- a/src/string_utilities.cpp +++ b/src/string_utilities.cpp @@ -55,5 +55,22 @@ const std::vector string_split(const std::string& s, char sep, bool return result; } +bool is_valid_hex(const std::string& s) { + for (char c : s) { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; +} + +unsigned char hex_char_to_val(char c) { + if (c >= '0' && c <= '9') return static_cast(c - '0'); + if (c >= 'a' && c <= 'f') return static_cast(c - 'a' + 10); + if (c >= 'A' && c <= 'F') return static_cast(c - 'A' + 10); + return 0; +} + } // namespace string_utilities } // namespace httpserver diff --git a/src/webserver.cpp b/src/webserver.cpp index 7d902453..547eda60 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -58,12 +58,17 @@ #include "httpserver/http_resource.hpp" #include "httpserver/http_response.hpp" #include "httpserver/http_utils.hpp" +#include "httpserver/string_utilities.hpp" #include "httpserver/string_response.hpp" struct MHD_Connection; #define _REENTRANT 1 +#ifdef HAVE_GNUTLS +#include +#endif // HAVE_GNUTLS + #ifndef SOCK_CLOEXEC #define SOCK_CLOEXEC 02000000 #endif @@ -425,11 +430,22 @@ void webserver::disallow_ip(const string& ip) { } #ifdef HAVE_GNUTLS -int webserver::psk_cred_handler_func(gnutls_session_t session, +// MHD_PskServerCredentialsCallback signature: +// The 'cls' parameter is our webserver pointer (passed via MHD_OPTION) +// Returns 0 on success, -1 on error +// The psk output should be allocated with malloc() - MHD will free it +int webserver::psk_cred_handler_func(void* cls, + struct MHD_Connection* connection, const char* username, - gnutls_datum_t* key) { - webserver* ws = static_cast( - gnutls_session_get_ptr(session)); + void** psk, + size_t* psk_size) { + std::ignore = connection; // Not needed - we get context from cls + + webserver* ws = static_cast(cls); + + // Initialize output to safe values + *psk = nullptr; + *psk_size = 0; if (ws == nullptr || ws->psk_cred_handler == nullptr) { return -1; @@ -440,23 +456,28 @@ int webserver::psk_cred_handler_func(gnutls_session_t session, return -1; } - // Convert hex string to binary + // Validate hex string before allocating memory size_t psk_len = psk_hex.size() / 2; - key->data = static_cast(gnutls_malloc(psk_len)); - if (key->data == nullptr) { + if (psk_len == 0 || (psk_hex.size() % 2 != 0) || + !string_utilities::is_valid_hex(psk_hex)) { return -1; } - size_t output_size = psk_len; - int ret = gnutls_hex2bin(psk_hex.c_str(), psk_hex.size(), - key->data, &output_size); - if (ret < 0) { - gnutls_free(key->data); - key->data = nullptr; + // Allocate with malloc - MHD will free this + unsigned char* psk_data = static_cast(malloc(psk_len)); + if (psk_data == nullptr) { return -1; } - key->size = static_cast(output_size); + // Convert hex string to binary + for (size_t i = 0; i < psk_len; i++) { + psk_data[i] = static_cast( + (string_utilities::hex_char_to_val(psk_hex[i * 2]) << 4) | + string_utilities::hex_char_to_val(psk_hex[i * 2 + 1])); + } + + *psk = psk_data; + *psk_size = psk_len; return 0; } #endif // HAVE_GNUTLS diff --git a/test/Makefile.am b/test/Makefile.am index eb07a25a..cdbacf26 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -26,7 +26,7 @@ LDADD += -lcurl AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource +check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver MOSTLYCLEANFILES = *.gcda *.gcno *.gcov @@ -42,6 +42,8 @@ string_utilities_SOURCES = unit/string_utilities_test.cpp http_endpoint_SOURCES = unit/http_endpoint_test.cpp nodelay_SOURCES = integ/nodelay.cpp http_resource_SOURCES = unit/http_resource_test.cpp +http_response_SOURCES = unit/http_response_test.cpp +create_webserver_SOURCES = unit/create_webserver_test.cpp noinst_HEADERS = littletest.hpp AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index 202936ef..d280cbd6 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -463,6 +463,96 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass) ws.stop(); LT_END_AUTO_TEST(digest_auth_with_ha1_sha256_wrong_pass) +// Resource that tests get_digested_user() caching +// Covers http_request.cpp lines 293-295 (cache hit) and 300 (nullptr branch) +class digest_user_cache_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // First call - will populate cache (line 300 nullptr or non-null branch) + std::string user1 = std::string(req.get_digested_user()); + + // Second call - should hit cache (lines 293-295) + std::string user2 = std::string(req.get_digested_user()); + + // Verify caching works correctly (both calls return same value) + if (user1 != user2) { + return std::make_shared("CACHE_MISMATCH", 500, "text/plain"); + } + + if (user1.empty()) { + // No digest auth provided - tests the nullptr branch (line 299-300) + return std::make_shared("NO_DIGEST_USER", 200, "text/plain"); + } + + // Return the digested user (tests cache hit with valid user) + return std::make_shared("USER:" + user1, 200, "text/plain"); + } +}; + +// Test digested user caching when no digest auth is provided (nullptr branch) +LT_BEGIN_AUTO_TEST(authentication_suite, digest_user_cache_no_auth) + webserver ws = create_webserver(PORT); + + digest_user_cache_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("cache_test", &resource)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + // No authentication - should trigger nullptr branch in get_digested_user + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/cache_test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "NO_DIGEST_USER"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_user_cache_no_auth) + +// Test digested user caching with digest auth (cache hit with valid user) +LT_BEGIN_AUTO_TEST(authentication_suite, digest_user_cache_with_auth) + webserver ws = create_webserver(PORT) + .digest_auth_random("myrandom") + .nonce_nc_size(300); + + digest_user_cache_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("cache_test", &resource)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + curl_easy_setopt(curl, CURLOPT_USERPWD, "testuser:testpass"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/cache_test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 150L); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // After digest auth handshake, the server should return USER:testuser + // or NO_DIGEST_USER if no auth was provided. With CURLAUTH_DIGEST, + // curl will respond to the 401 challenge and include auth headers. + // The resource calls get_digested_user twice to test caching. + // Check that response is not empty and not a cache mismatch + LT_CHECK_EQ(s.find("CACHE_MISMATCH") == std::string::npos, true); + // Should contain either "USER:" (auth worked) or "NO_DIGEST_USER" (fallback) + LT_CHECK_EQ(s.find("USER:") != std::string::npos || s == "NO_DIGEST_USER", true); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_user_cache_with_auth) + #endif // Simple resource for centralized auth tests @@ -877,6 +967,129 @@ LT_BEGIN_AUTO_TEST(authentication_suite, auth_multiple_skip_paths) ws.stop(); LT_END_AUTO_TEST(auth_multiple_skip_paths) +// Test skip path for root "/" +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_path_root) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("/", &sr, true)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Root path should be skipped + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_path_root) + +// Test wildcard path matching "/pub/*" +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_path_wildcard) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/pub/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("pub/anything", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("pub/nested/path", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("private/secret", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + std::string s; + + // /pub/anything should be skipped (matches /pub/*) + curl = curl_easy_init(); + s = ""; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/pub/anything"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); + + // /pub/nested/path should also be skipped (matches /pub/*) + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/pub/nested/path"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); + + // /private/secret should NOT be skipped (doesn't match /pub/*) + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/private/secret"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); // Should require auth + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_path_wildcard) + +// Test empty skip paths (should require auth for everything) +LT_BEGIN_AUTO_TEST(authentication_suite, auth_empty_skip_paths) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({}); // Empty skip paths + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("test", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Should require auth + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_empty_skip_paths) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/integ/ban_system.cpp b/test/integ/ban_system.cpp index 7e551003..48d808e4 100644 --- a/test/integ/ban_system.cpp +++ b/test/integ/ban_system.cpp @@ -173,6 +173,290 @@ LT_BEGIN_AUTO_TEST(ban_system_suite, reject_default_allow_passes) ws.stop(); LT_END_AUTO_TEST(reject_default_allow_passes) +// Test ACCEPT policy with IP on allow list - allow overrides ban +// In ACCEPT mode: condition is (is_banned && !is_allowed) +// If IP is on allow list, !is_allowed is false, so connection is always allowed +LT_BEGIN_AUTO_TEST(ban_system_suite, accept_policy_allow_overrides_ban) + webserver ws = create_webserver(PORT).default_policy(http_utils::ACCEPT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // Add IP to allow list + ws.allow_ip("127.0.0.1"); + + // Request should work (ACCEPT policy + on allow list) + { + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + // Ban the IP - but in ACCEPT mode, allow list overrides ban + ws.ban_ip("127.0.0.1"); + + { + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); // Still allowed - allow list overrides ban in ACCEPT mode + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + // Remove from allow list - now ban should take effect + ws.disallow_ip("127.0.0.1"); + + { + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + res = curl_easy_perform(curl); + LT_ASSERT_NEQ(res, 0); // Now blocked - ban takes effect without allow list + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(accept_policy_allow_overrides_ban) + +// Test REJECT policy with IP that is allowed but then banned +// Tests: (!is_allowed || is_banned) - banned overrides allowed +LT_BEGIN_AUTO_TEST(ban_system_suite, reject_policy_allowed_then_banned) + webserver ws = create_webserver(PORT).default_policy(http_utils::REJECT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // First, IP is not allowed - should be blocked + { + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + res = curl_easy_perform(curl); + LT_ASSERT_NEQ(res, 0); + curl_easy_cleanup(curl); + } + + // Allow the IP - should work + ws.allow_ip("127.0.0.1"); + + { + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + // Now ban the IP - ban should override allow + ws.ban_ip("127.0.0.1"); + + { + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + res = curl_easy_perform(curl); + LT_ASSERT_NEQ(res, 0); // Should be blocked + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(reject_policy_allowed_then_banned) + +// Test REJECT policy with IP that is neither allowed nor banned +// Tests default REJECT behavior +LT_BEGIN_AUTO_TEST(ban_system_suite, reject_policy_neither_allowed_nor_banned) + webserver ws = create_webserver(PORT).default_policy(http_utils::REJECT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // IP is not in any list - REJECT policy should block + { + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + res = curl_easy_perform(curl); + LT_ASSERT_NEQ(res, 0); + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(reject_policy_neither_allowed_nor_banned) + +// Test ban/allow with wildcard then more specific IP +// This tests the weight comparison branch in ban_ip/allow_ip +LT_BEGIN_AUTO_TEST(ban_system_suite, ban_with_weight_comparison) + webserver ws = create_webserver(PORT).default_policy(http_utils::ACCEPT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // First ban with wildcard (lower weight) + ws.ban_ip("127.0.0.*"); + + // Then ban with more specific IP (higher weight) + // This should hit the weight comparison branch + ws.ban_ip("127.0.0.1"); + + // Request should still be blocked + { + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + res = curl_easy_perform(curl); + LT_ASSERT_NEQ(res, 0); + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(ban_with_weight_comparison) + +// Test allow with wildcard then more specific IP +LT_BEGIN_AUTO_TEST(ban_system_suite, allow_with_weight_comparison) + webserver ws = create_webserver(PORT).default_policy(http_utils::REJECT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // First allow with wildcard (lower weight) + ws.allow_ip("127.0.0.*"); + + // Then allow with more specific IP (higher weight) + // This should hit the weight comparison branch + ws.allow_ip("127.0.0.1"); + + // Request should be allowed + { + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(allow_with_weight_comparison) + +// Test ban with specific IP first, then wildcard (lower weight replaces higher) +// This tests the t_ip.weight() < (*it).weight() branch +LT_BEGIN_AUTO_TEST(ban_system_suite, ban_specific_then_wildcard) + webserver ws = create_webserver(PORT).default_policy(http_utils::ACCEPT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // First ban specific IP (higher weight = 4) + ws.ban_ip("127.0.0.1"); + + // Then ban with wildcard (lower weight = 3) + // This should trigger the erase-and-insert branch + ws.ban_ip("127.0.0.*"); + + // Request should still be blocked + { + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + res = curl_easy_perform(curl); + LT_ASSERT_NEQ(res, 0); + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(ban_specific_then_wildcard) + +// Test allow with specific IP first, then wildcard (lower weight replaces higher) +LT_BEGIN_AUTO_TEST(ban_system_suite, allow_specific_then_wildcard) + webserver ws = create_webserver(PORT).default_policy(http_utils::REJECT); + ws.start(false); + + ok_resource resource; + LT_ASSERT_EQ(true, ws.register_resource("base", &resource)); + + curl_global_init(CURL_GLOBAL_ALL); + + // First allow specific IP (higher weight = 4) + ws.allow_ip("127.0.0.1"); + + // Then allow with wildcard (lower weight = 3) + // This should trigger the erase-and-insert branch + ws.allow_ip("127.0.0.*"); + + // Request should be allowed + { + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + curl_global_cleanup(); + ws.stop(); +LT_END_AUTO_TEST(allow_specific_then_wildcard) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index 17e31231..1b333d3a 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -120,6 +120,18 @@ class args_resource : public http_resource { } }; +class args_flat_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + auto args = req.get_args_flat(); + stringstream ss; + for (const auto& [key, value] : args) { + ss << key << "=" << value << ";"; + } + return std::make_shared(ss.str(), 200, "text/plain"); + } +}; + class long_content_resource : public http_resource { public: shared_ptr render_GET(const http_request&) { @@ -202,12 +214,22 @@ class complete_test_resource : public http_resource { return std::make_shared("OK", 200, "text/plain"); } - shared_ptr render_CONNECT(const http_request&) { + shared_ptr render_PATCH(const http_request&) { return std::make_shared("OK", 200, "text/plain"); } - shared_ptr render_PATCH(const http_request&) { - return std::make_shared("OK", 200, "text/plain"); + shared_ptr render_HEAD(const http_request&) { + return std::make_shared("", 200, "text/plain"); + } + + shared_ptr render_OPTIONS(const http_request&) { + auto resp = std::make_shared("", 200, "text/plain"); + resp->with_header("Allow", "GET, POST, PUT, DELETE, HEAD, OPTIONS"); + return resp; + } + + shared_ptr render_TRACE(const http_request&) { + return std::make_shared("TRACE OK", 200, "message/http"); } }; @@ -343,6 +365,25 @@ class print_response_resource : public http_resource { stringstream* ss; }; +class request_info_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + stringstream ss; + ss << "requestor=" << req.get_requestor() + << "&port=" << req.get_requestor_port() + << "&version=" << req.get_version(); + return std::make_shared(ss.str(), 200, "text/plain"); + } +}; + +class content_limit_resource : public http_resource { + public: + shared_ptr render_POST(const http_request& req) { + return std::make_shared( + req.content_too_large() ? "TOO_LARGE" : "OK", 200, "text/plain"); + } +}; + #ifdef HTTPSERVER_PORT #define PORT HTTPSERVER_PORT #else @@ -353,6 +394,7 @@ class print_response_resource : public http_resource { #define STR(p) STR2(p) #define PORT_STRING STR(PORT) + LT_BEGIN_SUITE(basic_suite) std::unique_ptr ws; @@ -1627,6 +1669,66 @@ LT_BEGIN_AUTO_TEST(basic_suite, method_not_allowed_header) curl_easy_cleanup(curl); LT_END_AUTO_TEST(method_not_allowed_header) +LT_BEGIN_AUTO_TEST(basic_suite, request_info_getters) + request_info_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("request_info", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/request_info"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_NEQ(s.find("127.0.0.1"), string::npos); + LT_CHECK_NEQ(s.find("HTTP/1.1"), string::npos); + LT_CHECK_NEQ(s.find("port="), string::npos); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(request_info_getters) + +LT_BEGIN_AUTO_TEST(basic_suite, unregister_then_404) + simple_resource res; + LT_ASSERT_EQ(true, ws->register_resource("temp", &res)); + curl_global_init(CURL_GLOBAL_ALL); + + { + string s; + CURL *curl = curl_easy_init(); + CURLcode result; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/temp"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + result = curl_easy_perform(curl); + LT_ASSERT_EQ(result, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + ws->unregister_resource("temp"); + + { + string s; + CURL *curl = curl_easy_init(); + CURLcode result; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/temp"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + result = curl_easy_perform(curl); + LT_ASSERT_EQ(result, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 404); + curl_easy_cleanup(curl); + } +LT_END_AUTO_TEST(unregister_then_404) + LT_BEGIN_AUTO_TEST(basic_suite, thread_safety) simple_resource resource; @@ -1665,6 +1767,1490 @@ LT_BEGIN_AUTO_TEST(basic_suite, thread_safety) LT_CHECK_EQ(1, 1); LT_END_AUTO_TEST(thread_safety) +LT_BEGIN_AUTO_TEST(basic_suite, head_request) + complete_test_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("base", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, ""); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(head_request) + +LT_BEGIN_AUTO_TEST(basic_suite, options_request) + complete_test_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("base", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + map ss; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerfunc); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &ss); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(ss["Allow"], "GET, POST, PUT, DELETE, HEAD, OPTIONS"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(options_request) + +LT_BEGIN_AUTO_TEST(basic_suite, trace_request) + complete_test_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("base", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "TRACE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "TRACE OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(trace_request) + +LT_BEGIN_SUITE(content_limit_suite) + std::unique_ptr ws; + int content_limit_port; + string content_limit_url; + + void set_up() { + content_limit_port = PORT + 10; + content_limit_url = "localhost:" + std::to_string(content_limit_port) + "/limit"; + ws = std::make_unique(create_webserver(content_limit_port).content_size_limit(100)); + ws->start(false); + } + + void tear_down() { + ws->stop(); + } +LT_END_SUITE(content_limit_suite) + +LT_BEGIN_AUTO_TEST(content_limit_suite, content_exceeds_limit) + content_limit_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("limit", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + + std::string large_data(200, 'X'); + + curl_easy_setopt(curl, CURLOPT_URL, content_limit_url.c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, large_data.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, large_data.size()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "TOO_LARGE"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(content_exceeds_limit) + +LT_BEGIN_AUTO_TEST(content_limit_suite, content_within_limit) + content_limit_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("limit", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + + std::string small_data(50, 'X'); + + curl_easy_setopt(curl, CURLOPT_URL, content_limit_url.c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, small_data.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, small_data.size()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(content_within_limit) + +LT_BEGIN_AUTO_TEST(basic_suite, get_args_flat) + args_flat_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("args_flat", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/args_flat?foo=bar&baz=qux"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_NEQ(s.find("foo=bar"), string::npos); + LT_CHECK_NEQ(s.find("baz=qux"), string::npos); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(get_args_flat) + +LT_BEGIN_AUTO_TEST(basic_suite, only_render_head) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_head", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_head"); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_head) + +LT_BEGIN_AUTO_TEST(basic_suite, only_render_options) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_options", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_options"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_options) + +LT_BEGIN_AUTO_TEST(basic_suite, only_render_trace) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_trace", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_trace"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "TRACE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_trace) + +// Test for long error log message (triggers resize branch) +class long_error_message_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + // Generate an error with a message longer than 80 characters + throw std::runtime_error( + "This is a very long error message that exceeds the default buffer " + "size of 80 characters to trigger the resize branch in error_log"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, long_error_message) + long_error_message_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("longerror", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/longerror"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(http_code, 500); + + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(long_error_message) + +// Test PATCH request on a resource that only implements render() +LT_BEGIN_AUTO_TEST(basic_suite, only_render_patch) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_patch", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_patch"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_patch) + +// Custom response class that throws std::invalid_argument in get_raw_response +class invalid_argument_response : public http_response { + public: + invalid_argument_response() : http_response(200, "text/plain") {} + MHD_Response* get_raw_response() override { + throw std::invalid_argument("Resource not found"); + } +}; + +// Resource that returns invalid_argument_response +class invalid_arg_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared(); + } +}; + +// Custom response class that throws std::runtime_error in get_raw_response +class runtime_error_response : public http_response { + public: + runtime_error_response() : http_response(200, "text/plain") {} + MHD_Response* get_raw_response() override { + throw std::runtime_error("Internal error in response"); + } +}; + +// Resource that returns runtime_error_response +class runtime_error_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared(); + } +}; + +// Custom response class that throws non-std exception in get_raw_response +class non_std_exception_response : public http_response { + public: + non_std_exception_response() : http_response(200, "text/plain") {} + MHD_Response* get_raw_response() override { + throw 42; // Throws an int, not a std::exception + } +}; + +// Resource that returns non_std_exception_response +class non_std_exception_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared(); + } +}; + +// Test response throwing std::invalid_argument -> should get 404 +LT_BEGIN_AUTO_TEST(basic_suite, response_throws_invalid_argument) + invalid_arg_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("invalid_arg", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/invalid_arg"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(http_code, 404); // invalid_argument -> not found + + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(response_throws_invalid_argument) + +// Test response throwing std::runtime_error -> should get 500 +LT_BEGIN_AUTO_TEST(basic_suite, response_throws_runtime_error) + runtime_error_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("runtime_err", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/runtime_err"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(http_code, 500); // runtime_error -> internal server error + + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(response_throws_runtime_error) + +// Test response throwing non-std exception -> should get 500 +LT_BEGIN_AUTO_TEST(basic_suite, response_throws_non_std_exception) + non_std_exception_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("non_std_exc", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/non_std_exc"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(http_code, 500); // non-std exception -> internal server error + + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(response_throws_non_std_exception) + +// Custom internal error handler that also throws an exception +// This tests the outer catch block (lines 826-829 in webserver.cpp) +shared_ptr throwing_internal_error_handler(const http_request&) { + throw std::runtime_error("Internal error handler also throws"); +} + +// Test case: resource throws exception AND internal error handler throws +// This triggers the outer catch block which uses force_our=true +LT_BEGIN_AUTO_TEST(basic_suite, internal_error_handler_also_throws) + // Create a separate webserver with throwing internal error handler + webserver ws2 = create_webserver(PORT + 50) + .internal_error_resource(throwing_internal_error_handler); + runtime_error_resource resource; // Resource that throws in get_raw_response + LT_ASSERT_EQ(true, ws2.register_resource("error_cascade", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 50) + "/error_cascade"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + // When internal error handler throws, we fall back to the built-in error page + LT_ASSERT_EQ(http_code, 500); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(internal_error_handler_also_throws) + +// Test tcp_nodelay option +LT_BEGIN_AUTO_TEST(basic_suite, tcp_nodelay_option) + webserver ws2 = create_webserver(PORT + 51).tcp_nodelay(); + ok_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("nodelay_test", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 51) + "/nodelay_test"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(tcp_nodelay_option) + +// Custom unescaper function to test the unescaper branch +void my_custom_unescaper(std::string& s) { + // Simple unescaper that just converts '+' to space + for (size_t i = 0; i < s.size(); ++i) { + if (s[i] == '+') s[i] = ' '; + } +} + +// Resource that returns the query string argument +class arg_echo_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + std::string arg = std::string(req.get_arg_flat("key")); + return std::make_shared(arg, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, custom_unescaper) + webserver ws2 = create_webserver(PORT + 52).unescaper(my_custom_unescaper); + arg_echo_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("echo", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 52) + "/echo?key=hello+world"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "hello world"); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(custom_unescaper) + +// Custom not_found handler +shared_ptr my_custom_not_found(const http_request&) { + return std::make_shared("CUSTOM_404", 404, "text/plain"); +} + +LT_BEGIN_AUTO_TEST(basic_suite, custom_not_found_handler) + webserver ws2 = create_webserver(PORT + 53).not_found_resource(my_custom_not_found); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 53) + "/nonexistent"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "CUSTOM_404"); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(custom_not_found_handler) + +// Custom method_not_allowed handler +shared_ptr my_custom_method_not_allowed(const http_request&) { + return std::make_shared("CUSTOM_405", 405, "text/plain"); +} + +// Resource that only allows POST +class post_only_resource : public http_resource { + public: + post_only_resource() { + disallow_all(); + set_allowing("POST", true); + } + shared_ptr render_POST(const http_request&) { + return std::make_shared("POST_OK", 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, custom_method_not_allowed_handler) + webserver ws2 = create_webserver(PORT + 54).method_not_allowed_resource(my_custom_method_not_allowed); + post_only_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("postonly", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 54) + "/postonly"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); // GET on a POST-only resource + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "CUSTOM_405"); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(custom_method_not_allowed_handler) + +// Resource that tests requestor info caching +class requestor_cache_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Test requestor IP and port + std::string ip = std::string(req.get_requestor()); + uint16_t port = req.get_requestor_port(); + + // Call them again to test caching (should hit cache on second call) + std::string ip2 = std::string(req.get_requestor()); + + std::string response = "IP:" + ip + ",PORT:" + std::to_string(port); + return std::make_shared(response, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, requestor_info) + webserver ws2 = create_webserver(PORT + 55); + requestor_cache_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("reqinfo", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 55) + "/reqinfo"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Response should contain IP and PORT + LT_CHECK_EQ(s.find("IP:127.0.0.1") != string::npos, true); + LT_CHECK_EQ(s.find("PORT:") != string::npos, true); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(requestor_info) + +// Resource that tests querystring caching +class querystring_cache_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Call get_querystring twice to test caching + std::string qs1 = std::string(req.get_querystring()); + std::string qs2 = std::string(req.get_querystring()); // Should hit cache + + return std::make_shared(qs1, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, querystring_caching) + webserver ws2 = create_webserver(PORT + 56); + querystring_cache_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("qscache", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 56) + "/qscache?foo=bar&baz=qux"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Check querystring contains the parameters + LT_CHECK_EQ(s.find("foo") != string::npos, true); + LT_CHECK_EQ(s.find("bar") != string::npos, true); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(querystring_caching) + +// Resource that tests args caching +class args_cache_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Call get_args twice to test caching + auto args1 = req.get_args(); + auto args2 = req.get_args(); // Should hit cache + + // Also test get_args_flat + auto flat = req.get_args_flat(); + + std::string response; + for (const auto& [key, val] : flat) { + response += std::string(key) + "=" + std::string(val) + ";"; + } + return std::make_shared(response, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, args_caching) + webserver ws2 = create_webserver(PORT + 57); + args_cache_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("argscache", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 57) + "/argscache?key1=val1&key2=val2"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s.find("key1=val1") != string::npos, true); + LT_CHECK_EQ(s.find("key2=val2") != string::npos, true); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(args_caching) + +// Resource that tests footer/trailer access +class footer_test_resource : public http_resource { + public: + shared_ptr render_POST(const http_request& req) { + // Test get_footers() - returns empty map for non-chunked requests + auto footers = req.get_footers(); + + // Test get_footer() with a key that doesn't exist + auto footer_val = req.get_footer("X-Test-Trailer"); + + // Build response showing footer count and specific footer value + std::string response = "footers=" + std::to_string(footers.size()); + if (!footer_val.empty()) { + response += ",X-Test-Trailer=" + std::string(footer_val); + } + + return std::make_shared(response, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, footer_access_no_trailers) + webserver ws2 = create_webserver(PORT + 58); + footer_test_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("footers", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 58) + "/footers"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "test=data"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Without trailers, footers should be empty + LT_CHECK_EQ(s, "footers=0"); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(footer_access_no_trailers) + +// Resource that returns a response with footers (trailers) +class response_footer_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + auto response = std::make_shared("body content", 200, "text/plain"); + // Add a footer to the response + response->with_footer("X-Checksum", "abc123"); + response->with_footer("X-Processing-Time", "42ms"); + + // Test get_footer and get_footers on response + auto checksum = response->get_footer("X-Checksum"); + auto all_footers = response->get_footers(); + + return response; + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, response_with_footers) + webserver ws2 = create_webserver(PORT + 59); + response_footer_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("resp_footers", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 59) + "/resp_footers"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "body content"); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(response_with_footers) + +// Resource that tests get_arg with non-existent key +class arg_not_found_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Get an arg that doesn't exist - should return empty http_arg_value + auto missing_arg = req.get_arg("nonexistent_key"); + // http_arg_value.get_all_values() should return empty vector + std::string result = missing_arg.get_all_values().empty() ? "EMPTY" : "HAS_VALUES"; + return std::make_shared(result, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, arg_not_found) + arg_not_found_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("arg_not_found", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/arg_not_found?existing=value"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "EMPTY"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(arg_not_found) + +// Resource that tests get_arg_flat fallback to connection value +class arg_flat_fallback_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Test get_arg_flat with a key that exists in GET args but not in unescaped_args + // This tests the fallback branch in get_arg_flat + std::string val = std::string(req.get_arg_flat("qparam")); + return std::make_shared(val, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, arg_flat_fallback) + arg_flat_fallback_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("arg_flat_fb", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/arg_flat_fb?qparam=myvalue"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "myvalue"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(arg_flat_fallback) + +// Resource that tests get_path_piece with out of bounds index +class path_piece_oob_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Get path piece at an index that's out of bounds + std::string piece = req.get_path_piece(100); // Way beyond the path pieces + // Should return empty string + std::string result = piece.empty() ? "OOB_EMPTY" : piece; + return std::make_shared(result, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, path_piece_out_of_bounds) + path_piece_oob_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("path/piece/test", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/path/piece/test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OOB_EMPTY"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(path_piece_out_of_bounds) + +// Resource that tests empty querystring +class empty_querystring_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + std::string qs = std::string(req.get_querystring()); + std::string result = qs.empty() ? "NO_QS" : qs; + return std::make_shared(result, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, empty_querystring) + empty_querystring_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("empty_qs", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + // URL without any query string + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/empty_qs"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "NO_QS"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(empty_querystring) + +// Resource that tests query parameters with null/empty values +// Covers http_request.cpp lines 234 and 248 (arg_value == nullptr branches) +class null_value_query_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Test getting an argument that was passed without a value (e.g., ?keyonly) + auto keyonly_arg = req.get_arg("keyonly"); + auto normal_arg = req.get_arg("normal"); + + // Also test querystring which exercises build_request_querystring + std::string qs = std::string(req.get_querystring()); + + stringstream ss; + ss << "keyonly=" << (keyonly_arg.get_all_values().empty() ? "MISSING" : + (keyonly_arg.get_all_values()[0].empty() ? "EMPTY" : "VALUE")); + ss << ",normal=" << (normal_arg.get_all_values().empty() ? "MISSING" : + std::string(normal_arg.get_all_values()[0])); + ss << ",qs=" << (qs.find("keyonly") != string::npos ? "HAS_KEYONLY" : "NO_KEYONLY"); + + return std::make_shared(ss.str(), 200, "text/plain"); + } +}; + +// Resource that tests auth caching (get_user/get_pass called multiple times) +class auth_cache_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // Call get_user and get_pass multiple times to test caching + std::string user1 = std::string(req.get_user()); + std::string pass1 = std::string(req.get_pass()); + std::string user2 = std::string(req.get_user()); // Should hit cache + std::string pass2 = std::string(req.get_pass()); // Should hit cache + + std::string result = user1.empty() ? "NO_AUTH" : ("USER:" + user1); + return std::make_shared(result, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, auth_caching) + auth_cache_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("auth_cache", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/auth_cache"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + // No authentication provided + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "NO_AUTH"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(auth_caching) + +// Test query parameters with null/empty values (e.g., ?keyonly&normal=value) +// This covers http_request.cpp lines 234 and 248 (arg_value == nullptr branches) +LT_BEGIN_AUTO_TEST(basic_suite, null_value_query_param) + null_value_query_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("null_val_query", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + // Query string with a key that has no value (keyonly) and one with value (normal=test) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/null_val_query?keyonly&normal=test"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // keyonly should have an empty value (not missing) + LT_CHECK_EQ(s.find("keyonly=EMPTY") != string::npos, true); + LT_CHECK_EQ(s.find("normal=test") != string::npos, true); + LT_CHECK_EQ(s.find("qs=HAS_KEYONLY") != string::npos, true); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(null_value_query_param) + +// Test PUT method on a resource that only implements render() +LT_BEGIN_AUTO_TEST(basic_suite, only_render_put) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_put", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_put"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_put) + +// Test DELETE method on a resource that only implements render() +LT_BEGIN_AUTO_TEST(basic_suite, only_render_delete) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_delete", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_delete"); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_delete) + +// Test POST method on a resource that only implements render() +LT_BEGIN_AUTO_TEST(basic_suite, only_render_post) + only_render_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("only_render_post", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/only_render_post"); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "test=data"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(only_render_post) + +// Test unregister_resource functionality +LT_BEGIN_AUTO_TEST(basic_suite, unregister_resource) + webserver ws2 = create_webserver(PORT + 67); + ok_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("test_unreg", &resource)); + ws2.start(false); + + // First verify resource works + { + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 67) + "/test_unreg"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + // Now unregister + ws2.unregister_resource("test_unreg"); + + // Resource should no longer be accessible (404) + { + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 67) + "/test_unreg"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + int64_t http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_CHECK_EQ(http_code, 404); + curl_easy_cleanup(curl); + } + + ws2.stop(); +LT_END_AUTO_TEST(unregister_resource) + +// Resource that tests get_arg_flat() returning first value for multi-value arg +class arg_flat_multi_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // get_arg_flat should return the first value even for multi-value args + std::string flat_val = std::string(req.get_arg_flat("key")); + return std::make_shared("flat=" + flat_val, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, get_arg_flat_first_value) + arg_flat_multi_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("arg_flat_first", &resource)); + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/arg_flat_first?key=value1&key=value2"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "flat=value1"); + curl_easy_cleanup(curl); +LT_END_AUTO_TEST(get_arg_flat_first_value) + +// Test access and error log callbacks +struct LogCapture { + static std::string& access_log_msg() { + static std::string msg; + return msg; + } + static std::string& error_log_msg() { + static std::string msg; + return msg; + } +}; + +void test_access_logger(const std::string& msg) { + LogCapture::access_log_msg() = msg; +} + +void test_error_logger(const std::string& msg) { + LogCapture::error_log_msg() = msg; +} + +LT_BEGIN_AUTO_TEST(basic_suite, log_access_callback) + LogCapture::access_log_msg().clear(); + + webserver ws2 = create_webserver(PORT + 70) + .log_access(test_access_logger); + ok_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("logtest", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 70) + "/logtest"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + + // The access log should have been called with the request info + LT_CHECK_EQ(LogCapture::access_log_msg().find("/logtest") != std::string::npos, true); + LT_CHECK_EQ(LogCapture::access_log_msg().find("METHOD") != std::string::npos, true); + + curl_easy_cleanup(curl); + ws2.stop(); +LT_END_AUTO_TEST(log_access_callback) + +// Test single_resource mode +LT_BEGIN_AUTO_TEST(basic_suite, single_resource_mode) + webserver ws2 = create_webserver(PORT + 71) + .single_resource(); + ok_resource resource; + // In single_resource mode, must register at "/" with family=true + LT_ASSERT_EQ(true, ws2.register_resource("/", &resource, true)); + ws2.start(false); + + // All paths should route to the single resource + { + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 71) + "/any/path/here"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + // Even root should work + { + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 71) + "/"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + ws2.stop(); +LT_END_AUTO_TEST(single_resource_mode) + +// Test validator builder method (validator is stored but not currently called in webserver) +bool test_validator_func(const std::string& url) { + return url.find("valid") != std::string::npos; +} + +LT_BEGIN_AUTO_TEST(basic_suite, validator_builder) + // Test that the validator builder method works (for coverage of create_webserver.hpp) + webserver ws2 = create_webserver(PORT + 72) + .validator(test_validator_func); + ok_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("test", &resource)); + ws2.start(false); + + // Just verify the server works with a validator set + { + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 72) + "/test"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + } + + ws2.stop(); +LT_END_AUTO_TEST(validator_builder) + +// Test resource with no render methods overridden (exercises empty_render path) +// Note: empty_render returns string_response with code -1, which triggers internal error +class empty_render_resource : public http_resource { + public: + // No render methods overridden - uses default empty_render() path +}; + +LT_BEGIN_AUTO_TEST(basic_suite, default_render_method) + // Test that a resource with no render overrides triggers internal error + // (because empty_render returns response code -1) + webserver ws2 = create_webserver(PORT + 73); + empty_render_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("empty", &resource)); + ws2.start(false); + + { + curl_global_init(CURL_GLOBAL_ALL); + string s; + int64_t http_code = 0; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 73) + "/empty"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + // Default empty_render returns code -1, which causes internal error (500) + LT_CHECK_EQ(http_code, 500); + curl_easy_cleanup(curl); + } + + ws2.stop(); +LT_END_AUTO_TEST(default_render_method) + +// Test resource that overrides only render() (not render_GET) +class render_override_resource : public http_resource { + public: + shared_ptr render(const http_request&) { + return std::make_shared("base_render", 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, render_fallthrough_to_base) + // Test that render_GET calls render() when not overridden + webserver ws2 = create_webserver(PORT + 74); + render_override_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("base", &resource)); + ws2.start(false); + + { + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 74) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + } + + ws2.stop(); +LT_END_AUTO_TEST(render_fallthrough_to_base) + +// Note: CONNECT method is a special HTTP method for tunneling that +// behaves differently than standard HTTP methods, so we don't test +// it the same way as other methods. + +// Test all HTTP methods falling through to base render() +LT_BEGIN_AUTO_TEST(basic_suite, all_methods_fallthrough_to_render) + // render_override_resource only defines render(), not render_GET/POST/etc. + // So all method-specific calls should fall through to render() + webserver ws2 = create_webserver(PORT + 75); + render_override_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("fallthrough", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + CURL* curl; + CURLcode res; + string s; + + // Test POST fallthrough + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ""); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + + // Test PUT fallthrough + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + + // Test DELETE fallthrough + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + + // Test PATCH fallthrough + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + + // Test HEAD fallthrough (body is empty for HEAD) + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // HEAD response has no body + curl_easy_cleanup(curl); + + // Test OPTIONS fallthrough + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + + // Test TRACE fallthrough + s = ""; + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, ("localhost:" + std::to_string(PORT + 75) + "/fallthrough").c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "TRACE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "base_render"); + curl_easy_cleanup(curl); + + ws2.stop(); +LT_END_AUTO_TEST(all_methods_fallthrough_to_render) + +// Test internal_error_resource custom handler +shared_ptr custom_internal_error_handler(const http_request&) { + return std::make_shared("Custom Internal Error", 500, "text/plain"); +} + +class throwing_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + throw std::runtime_error("Intentional test exception"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, custom_internal_error_resource) + webserver ws2 = create_webserver(PORT + 76) + .internal_error_resource(custom_internal_error_handler); + throwing_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("throw", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + int64_t http_code = 0; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 76) + "/throw"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 500); + LT_CHECK_EQ(s, "Custom Internal Error"); + curl_easy_cleanup(curl); + + ws2.stop(); +LT_END_AUTO_TEST(custom_internal_error_resource) + +// Test get_arg_flat fallback to MHD connection value +class arg_flat_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + // get_arg_flat should fall back to MHD connection value for query params + std::string result = std::string(req.get_arg_flat("q")); + return std::make_shared(result, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, get_arg_flat_fallback) + webserver ws2 = create_webserver(PORT + 77); + arg_flat_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("argflat", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 77) + "/argflat?q=test_value"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "test_value"); + curl_easy_cleanup(curl); + + ws2.stop(); +LT_END_AUTO_TEST(get_arg_flat_fallback) + +// Test large multipart form field that triggers grow_last_arg path +class large_multipart_resource : public http_resource { + public: + shared_ptr render_POST(const http_request& req) { + std::string result = std::string(req.get_arg("large_field")); + return std::make_shared(std::to_string(result.size()), 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(basic_suite, large_multipart_form_field) + // This test sends a large text field via multipart form-data + // to trigger the grow_last_arg path in http_request.cpp (line 544) + webserver ws2 = create_webserver(PORT + 78); + large_multipart_resource resource; + LT_ASSERT_EQ(true, ws2.register_resource("largemp", &resource)); + ws2.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + string s; + CURL *curl = curl_easy_init(); + CURLcode res; + + // Create a large string (100KB) to ensure MHD chunks it + const size_t large_size = 100 * 1024; + std::string large_data(large_size, 'X'); + + // Use curl_mime for multipart/form-data + curl_mime *form = curl_mime_init(curl); + curl_mimepart *field = curl_mime_addpart(form); + curl_mime_name(field, "large_field"); + curl_mime_data(field, large_data.c_str(), CURL_ZERO_TERMINATED); + + std::string url = "http://localhost:" + std::to_string(PORT + 78) + "/largemp"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, form); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // The response should be the size of the large field + LT_CHECK_EQ(s, std::to_string(large_size)); + + curl_mime_free(form); + curl_easy_cleanup(curl); + + ws2.stop(); +LT_END_AUTO_TEST(large_multipart_form_field) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/integ/file_upload.cpp b/test/integ/file_upload.cpp index f474420d..97cae716 100644 --- a/test/integ/file_upload.cpp +++ b/test/integ/file_upload.cpp @@ -20,6 +20,12 @@ #include #include +#ifdef _WIN32 +#include +#define MKDIR(path) _mkdir(path) +#else +#define MKDIR(path) mkdir(path, 0755) +#endif #include #include #include @@ -133,6 +139,37 @@ static std::pair send_file_to_webserver(bool add_second_file, return {res, http_code}; } +// Send file with explicit content-type and transfer-encoding headers +static std::pair send_file_with_content_type(int port, const char* content_type) { + curl_global_init(CURL_GLOBAL_ALL); + + CURL *curl = curl_easy_init(); + + curl_mime *form = curl_mime_init(curl); + curl_mimepart *field = curl_mime_addpart(form); + curl_mime_name(field, TEST_KEY); + curl_mime_filedata(field, TEST_CONTENT_FILEPATH); + // Set explicit content-type for the file part + curl_mime_type(field, content_type); + // Add transfer-encoding header + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Content-Transfer-Encoding: binary"); + curl_mime_headers(field, headers, 1); // 1 means take ownership + + CURLcode res; + std::string url = "localhost:" + std::to_string(port) + "/upload"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, form); + + res = curl_easy_perform(curl); + long http_code = 0; // NOLINT [runtime/int] + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + curl_mime_free(form); + return {res, http_code}; +} + static std::pair send_large_file(string* content, std::string args = "") { // Generate a large (100K) file of random bytes. Upload the file with // a curl request, then delete the file. The default chunk size of MHD @@ -856,6 +893,77 @@ LT_BEGIN_AUTO_TEST(file_upload_suite, file_cleanup_no_callback_deletes) LT_CHECK_EQ(file_exists(file->second.get_file_system_file_name()), false); LT_END_AUTO_TEST(file_cleanup_no_callback_deletes) +// Test file upload keeping original filename (without random generation) +LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_original_filename) + // Use a subdirectory to avoid overwriting the test input file + string upload_directory = "upload_test_dir"; + MKDIR(upload_directory.c_str()); + + auto ws = std::make_unique(create_webserver(PORT) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory)); + // Note: NOT using generate_random_filename_on_upload() + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + auto res = send_file_to_webserver(false, false); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + // Verify file was created with original name + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + map::iterator file = files.begin()->second.begin(); + // The filename should be upload_directory/test_content (the original name) + string expected_path = upload_directory + "/" + TEST_CONTENT_FILENAME; + LT_CHECK_EQ(file->second.get_file_system_file_name(), expected_path); + + ws->stop(); + + // Clean up the file and directory + unlink(expected_path.c_str()); + rmdir(upload_directory.c_str()); +LT_END_AUTO_TEST(file_upload_original_filename) + +// Test file upload with explicit content-type header +// This exercises the content_type != nullptr branch in webserver.cpp post_iterator +LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_with_content_type) + int port = PORT + 1; + string upload_directory = "."; + + auto ws = std::make_unique(create_webserver(port) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory) + .generate_random_filename_on_upload()); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + // Send file with explicit content-type "text/plain" + auto res = send_file_with_content_type(port, "text/plain"); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + // Verify file_info has the correct content-type + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + auto file_key = files.find(TEST_KEY); + LT_CHECK_EQ(file_key != files.end(), true); + auto file = file_key->second.begin(); + // The content-type should be what we set + LT_CHECK_EQ(file->second.get_content_type(), "text/plain"); + + // Clean up the uploaded file + unlink(file->second.get_file_system_file_name().c_str()); + + ws->stop(); +LT_END_AUTO_TEST(file_upload_with_content_type) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index d43459ed..4446ed92 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -33,9 +33,14 @@ #include #include #include +#include #include #include +#ifdef HAVE_GNUTLS +#include +#endif + #include "./httpserver.hpp" #include "./littletest.hpp" @@ -69,6 +74,26 @@ class ok_resource : public httpserver::http_resource { } }; +#ifdef HAVE_GNUTLS +class tls_info_resource : public httpserver::http_resource { + public: + shared_ptr render_GET(const httpserver::http_request& req) { + std::string response; + if (req.has_tls_session()) { + gnutls_session_t session = req.get_tls_session(); + if (session != nullptr) { + response = "TLS_SESSION_PRESENT"; + } else { + response = "TLS_SESSION_NULL"; + } + } else { + response = "NO_TLS_SESSION"; + } + return std::make_shared(response, 200, "text/plain"); + } +}; +#endif // HAVE_GNUTLS + shared_ptr not_found_custom(const httpserver::http_request&) { return std::make_shared("Not found custom", 404, "text/plain"); } @@ -403,6 +428,63 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, register_resource_nullptr_throws) LT_CHECK_THROW(ws.register_resource("/test", nullptr)); LT_END_AUTO_TEST(register_resource_nullptr_throws) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, register_empty_resource_non_family) + httpserver::webserver ws = httpserver::create_webserver(PORT); + ok_resource ok; + // Register empty resource with family=false + LT_CHECK_EQ(true, ws.register_resource("", &ok, false)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(register_empty_resource_non_family) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, register_resource_with_url_params_non_family) + httpserver::webserver ws = httpserver::create_webserver(PORT).regex_checking(); + ok_resource ok; + // Register resource with URL parameters, non-family + LT_CHECK_EQ(true, ws.register_resource("/user/{id}", &ok, false)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/user/123"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(register_resource_with_url_params_non_family) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, register_duplicate_resource_returns_false) + httpserver::webserver ws = httpserver::create_webserver(PORT); + ok_resource ok1, ok2; + // First registration should succeed + LT_CHECK_EQ(true, ws.register_resource("/duplicate", &ok1, false)); + // Second registration of same path should fail (return false) + LT_CHECK_EQ(false, ws.register_resource("/duplicate", &ok2, false)); + // But with family=true should succeed (different type of registration) + LT_CHECK_EQ(true, ws.register_resource("/duplicate", &ok2, true)); +LT_END_AUTO_TEST(register_duplicate_resource_returns_false) + LT_BEGIN_AUTO_TEST(ws_start_stop_suite, thread_per_connection_fails_with_max_threads) { // NOLINT (internal scope opening - not method start) httpserver::webserver ws = httpserver::create_webserver(PORT) @@ -648,8 +730,763 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, custom_error_resources) ws.stop(); LT_END_AUTO_TEST(custom_error_resources) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, ipv6_webserver) + httpserver::webserver ws = httpserver::create_webserver(PORT + 20).use_ipv6(); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + // IPv6 may not be available, so we just check the configuration worked + if (started) { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "http://[::1]:" STR(PORT + 20) "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + if (res == 0) { + LT_CHECK_EQ(s, "OK"); + } + curl_easy_cleanup(curl); + ws.stop(); + } + LT_CHECK_EQ(1, 1); // Test passes even if IPv6 not available +LT_END_AUTO_TEST(ipv6_webserver) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, dual_stack_webserver) + httpserver::webserver ws = httpserver::create_webserver(PORT + 21).use_dual_stack(); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + if (started) { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" STR(PORT + 21) "/base"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + ws.stop(); + } + LT_CHECK_EQ(1, 1); // Test passes even if dual stack not available +LT_END_AUTO_TEST(dual_stack_webserver) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, bind_address_ipv4) + int port = PORT + 22; + httpserver::webserver ws = httpserver::create_webserver(port).bind_address("127.0.0.1"); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://127.0.0.1:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(bind_address_ipv4) + +// Test bind_address with IPv6 address string (covers IPv6 branch in create_webserver.cpp) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, bind_address_ipv6_string) + int port = PORT + 31; + // This tests the IPv6 branch in bind_address + // Note: This may fail if IPv6 is not available on the system + try { + httpserver::webserver ws = httpserver::create_webserver(port).bind_address("::1"); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + if (started) { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://[::1]:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + if (res == 0) { + LT_CHECK_EQ(s, "OK"); + } + curl_easy_cleanup(curl); + ws.stop(); + } + } catch (...) { + // IPv6 may not be available, that's OK for coverage purposes + } + LT_CHECK_EQ(1, 1); // Test passes even if IPv6 not available +LT_END_AUTO_TEST(bind_address_ipv6_string) + +#ifdef HAVE_GNUTLS +// Test TLS session getters on non-TLS connection (should return false/nullptr) +class tls_check_non_tls_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request& req) { + // On non-TLS connection, has_tls_session should return false + std::string response = req.has_tls_session() ? "HAS_TLS" : "NO_TLS"; + return std::make_shared(response, 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, tls_session_on_non_tls_connection) + int port = PORT + 25; + httpserver::webserver ws = httpserver::create_webserver(port); // No SSL + tls_check_non_tls_resource tls_check; + LT_ASSERT_EQ(true, ws.register_resource("tls_check", &tls_check)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/tls_check"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "NO_TLS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(tls_session_on_non_tls_connection) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, https_webserver) + int port = PORT + 23; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem"); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + if (!started) { + // SSL setup may fail in some environments, skip the test + LT_CHECK_EQ(1, 1); + } else { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + ws.stop(); + } +LT_END_AUTO_TEST(https_webserver) + +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, tls_session_getters) + int port = PORT + 24; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem"); + tls_info_resource tls_info; + LT_ASSERT_EQ(true, ws.register_resource("tls_info", &tls_info)); + bool started = ws.start(false); + if (!started) { + // SSL setup may fail in some environments, skip the test + LT_CHECK_EQ(1, 1); + } else { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/tls_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "TLS_SESSION_PRESENT"); + curl_easy_cleanup(curl); + ws.stop(); + } +LT_END_AUTO_TEST(tls_session_getters) +#endif // HAVE_GNUTLS + +#endif // _WINDOWS + +// Test pedantic mode configuration +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, pedantic_mode) + int port = PORT + 26; + httpserver::webserver ws = httpserver::create_webserver(port).pedantic(); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(pedantic_mode) + +#ifdef HAVE_DAUTH +// Test digest_auth_random configuration +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, digest_auth_random) + int port = PORT + 27; + httpserver::webserver ws = httpserver::create_webserver(port) + .digest_auth_random("random_string_for_digest"); + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(digest_auth_random) +#endif // HAVE_DAUTH + +#ifdef HAVE_GNUTLS +// PSK handler that returns a hex-encoded key for the test user +std::string test_psk_handler(const std::string& username) { + if (username == "testuser") { + // Return hex-encoded PSK key (16 bytes = 32 hex chars) + return "0123456789abcdef0123456789abcdef"; + } + return ""; // Unknown user - return empty to trigger error path +} + +// PSK handler that always returns empty (for error path testing) +std::string empty_psk_handler(const std::string&) { + return ""; +} + +// PSK handler that returns invalid hex (for hex conversion error path) +std::string invalid_hex_psk_handler(const std::string&) { + return "ZZZZ"; // Invalid hex characters +} + +// Helper to check if gnutls-cli is available +bool has_gnutls_cli() { + return system("which gnutls-cli > /dev/null 2>&1") == 0; +} + +// Test PSK credential handler setup +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_handler_setup) + int port = PORT + 28; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(test_psk_handler); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + + // PSK setup may fail if libmicrohttpd/gnutls doesn't support it + // Just verify the server can be configured with PSK options + if (started) { + ws.stop(); + } + LT_CHECK_EQ(1, 1); // Test passes if we get here without crashing +LT_END_AUTO_TEST(psk_handler_setup) + +// Test PSK with empty handler (error path) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_handler_empty) + int port = PORT + 29; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(empty_psk_handler); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + + if (started) { + ws.stop(); + } + LT_CHECK_EQ(1, 1); +LT_END_AUTO_TEST(psk_handler_empty) + +// Test PSK without handler (nullptr check) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_no_handler) + int port = PORT + 30; + // Configure PSK mode but don't set a handler + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + + if (started) { + ws.stop(); + } + LT_CHECK_EQ(1, 1); +LT_END_AUTO_TEST(psk_no_handler) + +// Test PSK connection attempt using gnutls-cli +// This triggers the psk_cred_handler_func callback to execute, providing coverage +// The callback now uses the static registry to get the webserver pointer +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_connection_success) + if (!has_gnutls_cli()) { + LT_CHECK_EQ(1, 1); // Skip if gnutls-cli not available + return; + } + + int port = PORT + 41; + try { + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(test_psk_handler) + .https_priorities("NORMAL:+PSK:+DHE-PSK"); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + + ws.start(false); + + // Make PSK connection attempt with gnutls-cli + // This triggers the PSK credential handler callback, providing coverage + // Note: Full PSK success depends on libmicrohttpd/gnutls configuration + char cmd[512]; + snprintf(cmd, sizeof(cmd), + "echo -e 'GET /base HTTP/1.0\\r\\n\\r\\n' | " + "gnutls-cli --pskusername=testuser " + "--pskkey=0123456789abcdef0123456789abcdef " + "--priority='NORMAL:+PSK:+DHE-PSK' " + "--insecure localhost -p %d 2>&1 || true", + port); + + // Execute the command to trigger the PSK handler callback + system(cmd); + ws.stop(); + + // Test passes - we exercised the PSK callback code path + LT_CHECK_EQ(1, 1); + } catch (...) { + // PSK server may not be supported, skip test + LT_CHECK_EQ(1, 1); + } +LT_END_AUTO_TEST(psk_connection_success) + +// Test PSK connection with unknown user (empty PSK response) +// This covers lines 438-440 in psk_cred_handler_func +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_connection_unknown_user) + if (!has_gnutls_cli()) { + LT_CHECK_EQ(1, 1); // Skip if gnutls-cli not available + return; + } + + int port = PORT + 42; + try { + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(test_psk_handler) + .https_priorities("NORMAL:+PSK:+DHE-PSK"); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + + ws.start(false); + + // Try to connect with unknown username - should fail + char cmd[512]; + snprintf(cmd, sizeof(cmd), + "echo -e 'GET /base HTTP/1.0\\r\\n\\r\\n' | " + "gnutls-cli --pskusername=unknownuser " + "--pskkey=0123456789abcdef0123456789abcdef " + "--priority='NORMAL:+PSK:+DHE-PSK' " + "--insecure localhost -p %d 2>/dev/null | grep -q 'OK'", + port); + + int result = system(cmd); + ws.stop(); + + LT_CHECK_NEQ(result, 0); // Connection should fail + } catch (...) { + // PSK server may not be supported, skip test + LT_CHECK_EQ(1, 1); + } +LT_END_AUTO_TEST(psk_connection_unknown_user) + +// Test PSK connection with handler returning empty string +// This covers lines 438-440 in psk_cred_handler_func +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_connection_empty_handler) + if (!has_gnutls_cli()) { + LT_CHECK_EQ(1, 1); // Skip if gnutls-cli not available + return; + } + + int port = PORT + 43; + try { + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(empty_psk_handler) + .https_priorities("NORMAL:+PSK:+DHE-PSK"); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + + ws.start(false); + + // Try to connect - should fail because handler returns empty + char cmd[512]; + snprintf(cmd, sizeof(cmd), + "echo -e 'GET /base HTTP/1.0\\r\\n\\r\\n' | " + "gnutls-cli --pskusername=testuser " + "--pskkey=0123456789abcdef0123456789abcdef " + "--priority='NORMAL:+PSK:+DHE-PSK' " + "--insecure localhost -p %d 2>/dev/null | grep -q 'OK'", + port); + + int result = system(cmd); + ws.stop(); + + LT_CHECK_NEQ(result, 0); // Connection should fail + } catch (...) { + // PSK server may not be supported, skip test + LT_CHECK_EQ(1, 1); + } +LT_END_AUTO_TEST(psk_connection_empty_handler) + +// Test PSK connection with invalid hex key +// This covers lines 451-456 in psk_cred_handler_func +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_connection_invalid_hex) + if (!has_gnutls_cli()) { + LT_CHECK_EQ(1, 1); // Skip if gnutls-cli not available + return; + } + + int port = PORT + 44; + try { + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + .psk_cred_handler(invalid_hex_psk_handler) + .https_priorities("NORMAL:+PSK:+DHE-PSK"); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + + ws.start(false); + + // Try to connect - should fail because handler returns invalid hex + char cmd[512]; + snprintf(cmd, sizeof(cmd), + "echo -e 'GET /base HTTP/1.0\\r\\n\\r\\n' | " + "gnutls-cli --pskusername=testuser " + "--pskkey=0123456789abcdef0123456789abcdef " + "--priority='NORMAL:+PSK:+DHE-PSK' " + "--insecure localhost -p %d 2>/dev/null | grep -q 'OK'", + port); + + int result = system(cmd); + ws.stop(); + + LT_CHECK_NEQ(result, 0); // Connection should fail + } catch (...) { + // PSK server may not be supported, skip test + LT_CHECK_EQ(1, 1); + } +LT_END_AUTO_TEST(psk_connection_invalid_hex) + +// Test PSK connection with no handler set (nullptr check) +// This covers lines 432-435 in psk_cred_handler_func +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_connection_no_handler) + if (!has_gnutls_cli()) { + LT_CHECK_EQ(1, 1); // Skip if gnutls-cli not available + return; + } + + int port = PORT + 45; + try { + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .cred_type(httpserver::http::http_utils::PSK) + // Note: NOT setting psk_cred_handler - handler is nullptr + .https_priorities("NORMAL:+PSK:+DHE-PSK"); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + + ws.start(false); + + // Try to connect - should fail because no handler is set + char cmd[512]; + snprintf(cmd, sizeof(cmd), + "echo -e 'GET /base HTTP/1.0\\r\\n\\r\\n' | " + "gnutls-cli --pskusername=testuser " + "--pskkey=0123456789abcdef0123456789abcdef " + "--priority='NORMAL:+PSK:+DHE-PSK' " + "--insecure localhost -p %d 2>/dev/null | grep -q 'OK'", + port); + + int result = system(cmd); + ws.stop(); + + LT_CHECK_NEQ(result, 0); // Connection should fail + } catch (...) { + // PSK server may not be supported, skip test + LT_CHECK_EQ(1, 1); + } +LT_END_AUTO_TEST(psk_connection_no_handler) + #endif +// Test max_threads configuration with a running server +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, max_threads_running) + int port = PORT + 34; + httpserver::webserver ws = httpserver::create_webserver(port) + .max_threads(4); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(max_threads_running) + +// Test max_connections configuration +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, max_connections_running) + int port = PORT + 35; + httpserver::webserver ws = httpserver::create_webserver(port) + .max_connections(100); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(max_connections_running) + +// Test memory_limit configuration +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, memory_limit_running) + int port = PORT + 36; + httpserver::webserver ws = httpserver::create_webserver(port) + .memory_limit(32 * 1024); // 32KB memory limit + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(memory_limit_running) + +// Test per_IP_connection_limit configuration +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, per_ip_limit_running) + int port = PORT + 37; + httpserver::webserver ws = httpserver::create_webserver(port) + .per_IP_connection_limit(5); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(per_ip_limit_running) + +// Test max_thread_stack_size configuration (covers line 257 branch) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, thread_stack_size_running) + int port = PORT + 38; + httpserver::webserver ws = httpserver::create_webserver(port) + .max_thread_stack_size(4 * 1024 * 1024); // 4MB stack size + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(thread_stack_size_running) + +// Test deferred mode +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, deferred_mode_running) + int port = PORT + 39; + httpserver::webserver ws = httpserver::create_webserver(port) + .deferred(); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(deferred_mode_running) + +// Test debug mode with actual request +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, debug_mode_running) + int port = PORT + 40; + httpserver::webserver ws = httpserver::create_webserver(port) + .debug(); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(debug_mode_running) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/unit/create_webserver_test.cpp b/test/unit/create_webserver_test.cpp new file mode 100644 index 00000000..efae3812 --- /dev/null +++ b/test/unit/create_webserver_test.cpp @@ -0,0 +1,470 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include +#include +#include + +#include "./httpserver.hpp" +#include "./littletest.hpp" + +using httpserver::create_webserver; +using httpserver::http_request; +using httpserver::http_response; +using httpserver::string_response; + +LT_BEGIN_SUITE(create_webserver_suite) + void set_up() { + } + + void tear_down() { + } +LT_END_SUITE(create_webserver_suite) + +// Test basic port configuration +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_port) + create_webserver cw(8080); + create_webserver cw2 = create_webserver().port(9090); + // Just verify it compiles and runs without error + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_port) + +// Test max_threads builder method +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_max_threads) + create_webserver cw = create_webserver(8080).max_threads(4); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_max_threads) + +// Test max_connections builder method +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_max_connections) + create_webserver cw = create_webserver(8080).max_connections(100); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_max_connections) + +// Test memory_limit builder method +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_memory_limit) + create_webserver cw = create_webserver(8080).memory_limit(1024); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_memory_limit) + +// Test content_size_limit builder method +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_content_size_limit) + create_webserver cw = create_webserver(8080).content_size_limit(1024 * 1024); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_content_size_limit) + +// Test connection_timeout builder method +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_connection_timeout) + create_webserver cw = create_webserver(8080).connection_timeout(30); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_connection_timeout) + +// Test per_IP_connection_limit builder method +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_per_IP_connection_limit) + create_webserver cw = create_webserver(8080).per_IP_connection_limit(10); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_per_IP_connection_limit) + +// Test use_ssl / no_ssl toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_ssl_toggle) + create_webserver cw1 = create_webserver(8080).use_ssl(); + create_webserver cw2 = create_webserver(8080).no_ssl(); + create_webserver cw3 = create_webserver(8080).use_ssl().no_ssl(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_ssl_toggle) + +// Test use_ipv6 / no_ipv6 toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_ipv6_toggle) + create_webserver cw1 = create_webserver(8080).use_ipv6(); + create_webserver cw2 = create_webserver(8080).no_ipv6(); + create_webserver cw3 = create_webserver(8080).use_ipv6().no_ipv6(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_ipv6_toggle) + +// Test use_dual_stack / no_dual_stack toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_dual_stack_toggle) + create_webserver cw1 = create_webserver(8080).use_dual_stack(); + create_webserver cw2 = create_webserver(8080).no_dual_stack(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_dual_stack_toggle) + +// Test debug / no_debug toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_debug_toggle) + create_webserver cw1 = create_webserver(8080).debug(); + create_webserver cw2 = create_webserver(8080).no_debug(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_debug_toggle) + +// Test pedantic / no_pedantic toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_pedantic_toggle) + create_webserver cw1 = create_webserver(8080).pedantic(); + create_webserver cw2 = create_webserver(8080).no_pedantic(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_pedantic_toggle) + +// Test basic_auth / no_basic_auth toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_basic_auth_toggle) + create_webserver cw1 = create_webserver(8080).basic_auth(); + create_webserver cw2 = create_webserver(8080).no_basic_auth(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_basic_auth_toggle) + +// Test digest_auth / no_digest_auth toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_digest_auth_toggle) + create_webserver cw1 = create_webserver(8080).digest_auth(); + create_webserver cw2 = create_webserver(8080).no_digest_auth(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_digest_auth_toggle) + +// Test deferred / no_deferred toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_deferred_toggle) + create_webserver cw1 = create_webserver(8080).deferred(); + create_webserver cw2 = create_webserver(8080).no_deferred(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_deferred_toggle) + +// Test regex_checking / no_regex_checking toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_regex_checking_toggle) + create_webserver cw1 = create_webserver(8080).regex_checking(); + create_webserver cw2 = create_webserver(8080).no_regex_checking(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_regex_checking_toggle) + +// Test ban_system / no_ban_system toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_ban_system_toggle) + create_webserver cw1 = create_webserver(8080).ban_system(); + create_webserver cw2 = create_webserver(8080).no_ban_system(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_ban_system_toggle) + +// Test post_process / no_post_process toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_post_process_toggle) + create_webserver cw1 = create_webserver(8080).post_process(); + create_webserver cw2 = create_webserver(8080).no_post_process(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_post_process_toggle) + +// Test put_processed_data_to_content toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_put_processed_data_toggle) + create_webserver cw1 = create_webserver(8080).put_processed_data_to_content(); + create_webserver cw2 = create_webserver(8080).no_put_processed_data_to_content(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_put_processed_data_toggle) + +// Test single_resource / no_single_resource toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_single_resource_toggle) + create_webserver cw1 = create_webserver(8080).single_resource(); + create_webserver cw2 = create_webserver(8080).no_single_resource(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_single_resource_toggle) + +// Test generate_random_filename_on_upload toggle +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_random_filename_toggle) + create_webserver cw1 = create_webserver(8080).generate_random_filename_on_upload(); + create_webserver cw2 = create_webserver(8080).no_generate_random_filename_on_upload(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_random_filename_toggle) + +// Test tcp_nodelay +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_tcp_nodelay) + create_webserver cw = create_webserver(8080).tcp_nodelay(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_tcp_nodelay) + +// Test file_upload_target configurations +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_file_upload_target) + create_webserver cw1 = create_webserver(8080) + .file_upload_target(httpserver::FILE_UPLOAD_MEMORY_ONLY); + create_webserver cw2 = create_webserver(8080) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY); + create_webserver cw3 = create_webserver(8080) + .file_upload_target(httpserver::FILE_UPLOAD_MEMORY_AND_DISK); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_file_upload_target) + +// Test file_upload_dir +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_file_upload_dir) + create_webserver cw = create_webserver(8080).file_upload_dir("/tmp/uploads"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_file_upload_dir) + +// Test not_found_resource +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_not_found_resource) + auto not_found_handler = [](const http_request&) { + return std::make_shared("Custom 404", 404); + }; + create_webserver cw = create_webserver(8080).not_found_resource(not_found_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_not_found_resource) + +// Test method_not_allowed_resource +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_method_not_allowed_resource) + auto method_not_allowed_handler = [](const http_request&) { + return std::make_shared("Custom 405", 405); + }; + create_webserver cw = create_webserver(8080).method_not_allowed_resource(method_not_allowed_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_method_not_allowed_resource) + +// Test internal_error_resource +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_internal_error_resource) + auto internal_error_handler = [](const http_request&) { + return std::make_shared("Custom 500", 500); + }; + create_webserver cw = create_webserver(8080).internal_error_resource(internal_error_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_internal_error_resource) + +// Test start_method configurations +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_start_method) + create_webserver cw1 = create_webserver(8080) + .start_method(httpserver::http::http_utils::INTERNAL_SELECT); + create_webserver cw2 = create_webserver(8080) + .start_method(httpserver::http::http_utils::THREAD_PER_CONNECTION); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_start_method) + +// Test default_policy configurations +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_default_policy) + create_webserver cw1 = create_webserver(8080) + .default_policy(httpserver::http::http_utils::ACCEPT); + create_webserver cw2 = create_webserver(8080) + .default_policy(httpserver::http::http_utils::REJECT); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_default_policy) + +// Test cred_type configuration +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_cred_type) + create_webserver cw = create_webserver(8080) + .cred_type(httpserver::http::http_utils::NONE); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_cred_type) + +// Test nonce_nc_size +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_nonce_nc_size) + create_webserver cw = create_webserver(8080).nonce_nc_size(10); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_nonce_nc_size) + +// Test digest_auth_random +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_digest_auth_random) + create_webserver cw = create_webserver(8080).digest_auth_random("random_seed_string"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_digest_auth_random) + +// Test https_priorities +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_https_priorities) + create_webserver cw = create_webserver(8080).https_priorities("NORMAL:-MD5"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_https_priorities) + +// Test raw_https_mem_key +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_raw_https_mem_key) + create_webserver cw = create_webserver(8080).raw_https_mem_key("raw key content"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_raw_https_mem_key) + +// Test raw_https_mem_cert +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_raw_https_mem_cert) + create_webserver cw = create_webserver(8080).raw_https_mem_cert("raw cert content"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_raw_https_mem_cert) + +// Test raw_https_mem_trust +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_raw_https_mem_trust) + create_webserver cw = create_webserver(8080).raw_https_mem_trust("raw trust content"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_raw_https_mem_trust) + +// Test bind_socket +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_bind_socket) + create_webserver cw = create_webserver(8080).bind_socket(0); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_bind_socket) + +// Test max_thread_stack_size +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_max_thread_stack_size) + create_webserver cw = create_webserver(8080).max_thread_stack_size(4 * 1024 * 1024); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_max_thread_stack_size) + +// Test log_access callback +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_log_access) + auto log_access_handler = [](const std::string& log_msg) { + // do nothing with the log + (void)log_msg; + }; + create_webserver cw = create_webserver(8080).log_access(log_access_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_log_access) + +// Test log_error callback +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_log_error) + auto log_error_handler = [](const std::string& log_msg) { + // do nothing with the log + (void)log_msg; + }; + create_webserver cw = create_webserver(8080).log_error(log_error_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_log_error) + +// Test validator callback +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_validator) + auto validator_handler = [](const std::string& url) { + (void)url; + return true; + }; + create_webserver cw = create_webserver(8080).validator(validator_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_validator) + +// Test unescaper callback (signature: void(*)(std::string&)) +void test_unescaper(std::string& s) { + // Simple passthrough unescaper + (void)s; +} + +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_unescaper) + create_webserver cw = create_webserver(8080).unescaper(test_unescaper); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_unescaper) + +// Test auth_handler callback +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_auth_handler) + auto auth_handler = [](const http_request&) { + return std::shared_ptr(nullptr); + }; + create_webserver cw = create_webserver(8080).auth_handler(auth_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_auth_handler) + +// Test auth_skip_paths +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_auth_skip_paths) + std::vector skip_paths = {"/public", "/health", "/static/*"}; + create_webserver cw = create_webserver(8080).auth_skip_paths(skip_paths); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_auth_skip_paths) + +// Test file_cleanup_callback +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_file_cleanup_callback) + auto cleanup_handler = [](const std::string& field_name, + const std::string& file_name, + const httpserver::http::file_info& fi) { + (void)field_name; + (void)file_name; + (void)fi; + return true; // return true to delete file + }; + create_webserver cw = create_webserver(8080).file_cleanup_callback(cleanup_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_file_cleanup_callback) + +// Test PSK cred handler callback +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_psk_cred_handler) + auto psk_handler = [](const std::string& identity) { + (void)identity; + return std::string("psk_key"); + }; + create_webserver cw = create_webserver(8080).psk_cred_handler(psk_handler); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_psk_cred_handler) + +// Test copy constructor +LT_BEGIN_AUTO_TEST(create_webserver_suite, copy_constructor) + create_webserver cw1 = create_webserver(8080) + .max_threads(4) + .max_connections(100) + .debug(); + create_webserver cw2(cw1); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(copy_constructor) + +// Test move constructor +LT_BEGIN_AUTO_TEST(create_webserver_suite, move_constructor) + create_webserver cw1 = create_webserver(8080).max_threads(4); + create_webserver cw2(std::move(cw1)); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(move_constructor) + +// Test assignment operator +LT_BEGIN_AUTO_TEST(create_webserver_suite, assignment_operator) + create_webserver cw1 = create_webserver(8080).max_threads(4); + create_webserver cw2 = create_webserver(9090); + cw2 = cw1; + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(assignment_operator) + +// Test move assignment operator +LT_BEGIN_AUTO_TEST(create_webserver_suite, move_assignment_operator) + create_webserver cw1 = create_webserver(8080).max_threads(4); + create_webserver cw2 = create_webserver(9090); + cw2 = std::move(cw1); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(move_assignment_operator) + +// Test method chaining with many options +LT_BEGIN_AUTO_TEST(create_webserver_suite, method_chaining) + create_webserver cw = create_webserver(8080) + .max_threads(4) + .max_connections(100) + .memory_limit(1024) + .content_size_limit(1024 * 1024) + .connection_timeout(30) + .per_IP_connection_limit(10) + .debug() + .pedantic() + .regex_checking() + .ban_system() + .post_process() + .tcp_nodelay(); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(method_chaining) + +// Test default constructor +LT_BEGIN_AUTO_TEST(create_webserver_suite, default_constructor) + create_webserver cw; + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(default_constructor) + +// Test https_mem_key (loads from file path) +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_https_mem_key_file) + // Use the test key file that exists in the test directory + create_webserver cw = create_webserver(8080).https_mem_key("../test/key.pem"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_https_mem_key_file) + +// Test https_mem_cert (loads from file path) +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_https_mem_cert_file) + // Use the test cert file that exists in the test directory + create_webserver cw = create_webserver(8080).https_mem_cert("../test/cert.pem"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_https_mem_cert_file) + +// Test https_mem_trust (loads from file path) +LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_https_mem_trust_file) + // Use the test CA file that exists in the test directory + create_webserver cw = create_webserver(8080).https_mem_trust("../test/test_root_ca.pem"); + LT_CHECK_EQ(true, true); +LT_END_AUTO_TEST(builder_https_mem_trust_file) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV() diff --git a/test/unit/http_endpoint_test.cpp b/test/unit/http_endpoint_test.cpp index a22aa050..42bfbc1d 100644 --- a/test/unit/http_endpoint_test.cpp +++ b/test/unit/http_endpoint_test.cpp @@ -350,6 +350,267 @@ LT_BEGIN_AUTO_TEST(http_endpoint_suite, comparator) LT_CHECK_EQ(http_endpoint("/a/b/c") < http_endpoint("/a/b"), false); LT_END_AUTO_TEST(comparator) +// Test that invalid regex pattern throws exception (covers lines 114-116) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_invalid_regex_pattern) + // Using unbalanced parentheses which is invalid regex + LT_CHECK_THROW(http_endpoint("/path/(unclosed", false, true, true)); +LT_END_AUTO_TEST(http_endpoint_invalid_regex_pattern) + +// Test operator< when family_url differs (covers line 145) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, comparator_family_difference) + http_endpoint family_ep("/path/to/resource", true, true, true); + http_endpoint non_family_ep("/path/to/resource", false, true, true); + + // Family URL should come before non-family in ordering + LT_CHECK_EQ(family_ep < non_family_ep, true); + LT_CHECK_EQ(non_family_ep < family_ep, false); +LT_END_AUTO_TEST(comparator_family_difference) + +// Test operator< when both are family URLs (covers line 146) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, comparator_same_family) + http_endpoint family_a("/aaa", true, true, true); + http_endpoint family_b("/bbb", true, true, true); + + // Should compare by url_normalized when both are family URLs + LT_CHECK_EQ(family_a < family_b, true); + LT_CHECK_EQ(family_b < family_a, false); +LT_END_AUTO_TEST(comparator_same_family) + +// Test match with family URL and shorter incoming URL (covers line 152) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_match_family_shorter_url) + // Family URL with 3 pieces + http_endpoint family_ep("/path/to/resource", true, true, true); + + // Incoming URL with fewer pieces (covers the || short-circuit) + http_endpoint short_url("/path"); + + // Should still match using regex_match directly + LT_CHECK_EQ(family_ep.match(short_url), false); +LT_END_AUTO_TEST(http_endpoint_match_family_shorter_url) + +// Test match with non-family URL (covers line 153 directly) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_match_non_family) + http_endpoint non_family_ep("/path/to/resource", false, true, true); + http_endpoint incoming("/path/to/resource"); + + // Non-family should use direct regex_match + LT_CHECK_EQ(non_family_ep.match(incoming), true); +LT_END_AUTO_TEST(http_endpoint_match_non_family) + +// Test URL parameter at first position (covers line 84 false branch, line 101 first==true) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_arg_first_position) + http_endpoint test_endpoint("/{arg}/rest/of/path", false, true, true); + + LT_CHECK_EQ(test_endpoint.get_url_complete(), "/{arg}/rest/of/path"); + LT_CHECK_EQ(test_endpoint.get_url_normalized(), "^/([^\\/]+)/rest/of/path$"); + + string expected_pars_arr[] = { "arg" }; + vector expected_pars(expected_pars_arr, expected_pars_arr + 1); + LT_CHECK_COLLECTIONS_EQ(test_endpoint.get_url_pars().begin(), + test_endpoint.get_url_pars().end(), + expected_pars.begin()); + + int expected_chunk_positions_arr[] = { 0 }; + vector expected_chunk_positions(expected_chunk_positions_arr, expected_chunk_positions_arr + 1); + LT_CHECK_COLLECTIONS_EQ(test_endpoint.get_chunk_positions().begin(), + test_endpoint.get_chunk_positions().end(), + expected_chunk_positions.begin()); + + // Verify it matches correctly + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/value/rest/of/path")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/wrong/path")), false); +LT_END_AUTO_TEST(http_endpoint_arg_first_position) + +// Test custom regex pattern at first position (covers line 85 starting with ^) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_custom_regex_first) + // Note: Custom regex starting with ^ at first position + http_endpoint test_endpoint("/{id|([0-9]+)}/data", false, true, true); + + LT_CHECK_EQ(test_endpoint.get_url_complete(), "/{id|([0-9]+)}/data"); + LT_CHECK_EQ(test_endpoint.get_url_normalized(), "^/([0-9]+)/data$"); + + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/123/data")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/abc/data")), false); +LT_END_AUTO_TEST(http_endpoint_custom_regex_first) + +// Test URL pattern where first part starts with ^ (caret) +// Covers http_endpoint.cpp line 85 (parts[i][0] == '^' branch) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_caret_at_start) + // When first part[0] == '^', the prefix should be cleared + // The regex pattern starting with ^ at the first position + http_endpoint test_endpoint("/^api", false, true, true); + + // The normalized URL should not have double caret (^^ would be wrong) + LT_CHECK_EQ(test_endpoint.get_url_normalized().find("^^") == std::string::npos, true); + // Should start with ^api (not ^/^api) + LT_CHECK_EQ(test_endpoint.get_url_normalized().substr(0, 4), "^api"); +LT_END_AUTO_TEST(http_endpoint_caret_at_start) + +// Test URL with consecutive slashes creating empty parts +// Covers http_endpoint.cpp line 83 (parts[i] == "" condition) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_consecutive_slashes) + // Consecutive slashes create empty parts which should be skipped in processing + // but the original URL is preserved in url_complete + http_endpoint test_endpoint("//path//to//resource", false, true, true); + + // URL is preserved with consecutive slashes (leading / is normalized) + LT_CHECK_EQ(test_endpoint.get_url_complete(), "//path//to//resource"); + + // But url_pieces should only contain non-empty parts + std::vector pieces = test_endpoint.get_url_pieces(); + LT_CHECK_EQ(pieces.size() > 0, true); // At least some pieces + for (const auto& piece : pieces) { + // No empty pieces should be in the result + LT_CHECK_EQ(piece.empty(), false); + } +LT_END_AUTO_TEST(http_endpoint_consecutive_slashes) + +// Test URL part that is just "^" by itself (edge case) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_caret_only_part) + // Part that is just "^" - tests the empty string after ^ edge case + http_endpoint test_endpoint("/api/^/data", false, true, true); + + // Should be handled correctly + LT_CHECK_EQ(test_endpoint.get_url_complete(), "/api/^/data"); +LT_END_AUTO_TEST(http_endpoint_caret_only_part) + +// Test match with family URL where incoming has more pieces (should match) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_match_family_more_pieces) + // Family URL with 3 pieces + http_endpoint family_ep("/api/v1", true, true, true); + + // Incoming URL with more pieces + http_endpoint long_url("/api/v1/users/123/details"); + + // Family URL should match URLs that extend the pattern + LT_CHECK_EQ(family_ep.match(long_url), true); +LT_END_AUTO_TEST(http_endpoint_match_family_more_pieces) + +// Test match with equal pieces but mismatched content +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_match_mismatch_content) + http_endpoint registered("/api/users", false, true, true); + http_endpoint incoming("/api/items"); + + LT_CHECK_EQ(registered.match(incoming), false); +LT_END_AUTO_TEST(http_endpoint_match_mismatch_content) + +// Test multiple URL parameters in sequence +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_multiple_params) + http_endpoint test_endpoint("/{type}/{id}/{action}", false, true, true); + + LT_CHECK_EQ(test_endpoint.get_url_pars().size(), 3); + + // Verify parameter names + auto pars = test_endpoint.get_url_pars(); + LT_CHECK_EQ(pars[0], "type"); + LT_CHECK_EQ(pars[1], "id"); + LT_CHECK_EQ(pars[2], "action"); + + // Should match a URL with three values + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/user/123/edit")), true); +LT_END_AUTO_TEST(http_endpoint_multiple_params) + +// Test URL parameter with custom regex that includes special characters +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_custom_regex_special) + http_endpoint test_endpoint("/files/{filename|([a-zA-Z0-9._-]+)}", false, true, true); + + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/files/test.txt")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/files/my-file_123.json")), true); +LT_END_AUTO_TEST(http_endpoint_custom_regex_special) + +// Test comparator with same URL but different family status +LT_BEGIN_AUTO_TEST(http_endpoint_suite, comparator_same_url_family_diff) + http_endpoint ep1("/path", true, true, true); + http_endpoint ep2("/path", false, true, true); + + // Family endpoints should sort before non-family + bool result1 = ep1 < ep2; + bool result2 = ep2 < ep1; + + // At least one should be true (they should be different) + LT_CHECK_EQ(result1 != result2, true); +LT_END_AUTO_TEST(comparator_same_url_family_diff) + +// Test URL that's just a slash +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_root_only) + http_endpoint test_endpoint("/", false, true, true); + + LT_CHECK_EQ(test_endpoint.get_url_complete(), "/"); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/")), true); +LT_END_AUTO_TEST(http_endpoint_root_only) + +// Test URL with trailing and leading slashes +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_multiple_trailing_slashes) + http_endpoint test_endpoint("/api/", false, true, true); + + // Should normalize and match + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/api")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/api/")), true); +LT_END_AUTO_TEST(http_endpoint_multiple_trailing_slashes) + +// Test complex regex pattern with alternation +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_regex_alternation) + http_endpoint test_endpoint("/{resource|(users|posts|comments)}", false, true, true); + + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/users")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/posts")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/comments")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/other")), false); +LT_END_AUTO_TEST(http_endpoint_regex_alternation) + +// Test that use_regex without registration throws +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_regex_no_registration_throws) + // use_regex=true but registration=false should throw + LT_CHECK_THROW(http_endpoint("/path", false, false, true)); +LT_END_AUTO_TEST(http_endpoint_regex_no_registration_throws) + +// Test non-registration path (use_regex=false, registration=false) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_non_registration) + http_endpoint test_endpoint("/path/to/resource", false, false, false); + + // Non-registration endpoints should still parse correctly + LT_CHECK_EQ(test_endpoint.get_url_complete(), "/path/to/resource"); + LT_CHECK_EQ(test_endpoint.get_url_normalized(), "/path/to/resource"); + LT_CHECK_EQ(test_endpoint.is_regex_compiled(), false); +LT_END_AUTO_TEST(http_endpoint_non_registration) + +// Test with trailing slash (should be removed) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_trailing_slash_removed) + http_endpoint test_endpoint("/path/resource/", false, true, false); + + // Trailing slash should be removed from url_complete + LT_CHECK_EQ(test_endpoint.get_url_complete(), "/path/resource"); +LT_END_AUTO_TEST(http_endpoint_trailing_slash_removed) + +// Test invalid URL parameter format (too short) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_invalid_param_too_short) + // Parameter {} is too short (less than 3 chars including braces) + LT_CHECK_THROW(http_endpoint("/path/{}", false, true, true)); +LT_END_AUTO_TEST(http_endpoint_invalid_param_too_short) + +// Test invalid URL parameter format (only one brace) +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_invalid_param_one_brace) + // Parameter {x is missing closing brace + LT_CHECK_THROW(http_endpoint("/path/{x", false, true, true)); +LT_END_AUTO_TEST(http_endpoint_invalid_param_one_brace) + +// Test URL parameter with bar separator but short +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_param_with_regex) + http_endpoint test_endpoint("/path/{id|[0-9]+}", false, true, true); + + LT_CHECK_EQ(test_endpoint.get_url_pars().size(), 1); + LT_CHECK_EQ(test_endpoint.get_url_pars()[0], "id"); + // Should match numbers only + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/path/123")), true); + LT_CHECK_EQ(test_endpoint.match(http_endpoint("/path/abc")), false); +LT_END_AUTO_TEST(http_endpoint_param_with_regex) + +// Test invalid regex pattern throws +LT_BEGIN_AUTO_TEST(http_endpoint_suite, http_endpoint_invalid_regex_throws) + // Invalid regex pattern should throw + LT_CHECK_THROW(http_endpoint("/path/{id|[invalid}", false, true, true)); +LT_END_AUTO_TEST(http_endpoint_invalid_regex_throws) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/unit/http_resource_test.cpp b/test/unit/http_resource_test.cpp index 10976a79..6cfbe34e 100644 --- a/test/unit/http_resource_test.cpp +++ b/test/unit/http_resource_test.cpp @@ -88,6 +88,136 @@ LT_BEGIN_AUTO_TEST(http_resource_suite, allow_all_methods) all_methods.cbegin()) LT_END_AUTO_TEST(allow_all_methods) +LT_BEGIN_AUTO_TEST(http_resource_suite, set_allowing_nonexistent_method) + simple_resource sr; + // Try to set allowing for a method not in method_state + // This should be silently ignored (no effect) + sr.set_allowing("NONEXISTENT", true); + auto allowed_methods = sr.get_allowed_methods(); + // Verify that NONEXISTENT is not in the list + LT_CHECK_EQ(std::find(allowed_methods.begin(), allowed_methods.end(), + "NONEXISTENT") == allowed_methods.end(), true); +LT_END_AUTO_TEST(set_allowing_nonexistent_method) + +LT_BEGIN_AUTO_TEST(http_resource_suite, is_allowed_nonexistent_method) + simple_resource sr; + // Check that is_allowed returns false for unknown methods + LT_CHECK_EQ(sr.is_allowed("UNKNOWN_METHOD"), false); + LT_CHECK_EQ(sr.is_allowed("CUSTOM"), false); +LT_END_AUTO_TEST(is_allowed_nonexistent_method) + +LT_BEGIN_AUTO_TEST(http_resource_suite, set_allowing_disable) + simple_resource sr; + // By default, GET is allowed + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), true); + // Disable GET + sr.set_allowing(MHD_HTTP_METHOD_GET, false); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), false); + // Re-enable GET + sr.set_allowing(MHD_HTTP_METHOD_GET, true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), true); +LT_END_AUTO_TEST(set_allowing_disable) + +// Test resource that only overrides render() method +class render_only_resource : public http_resource { + public: + shared_ptr render(const http_request&) { + return std::make_shared("render called", 200); + } +}; + +// Test resource with no overrides at all +class empty_resource : public http_resource { + public: + // No render methods overridden - uses defaults +}; + +LT_BEGIN_AUTO_TEST(http_resource_suite, default_render_returns_empty) + empty_resource er; + // Create a minimal mock request - we need to test that render() returns empty + // Since we can't create a proper http_request without MHD internals, + // we just verify the resource exists and has correct method state + auto allowed = er.get_allowed_methods(); + LT_CHECK_EQ(allowed.size(), 9); // All 9 methods allowed by default +LT_END_AUTO_TEST(default_render_returns_empty) + +LT_BEGIN_AUTO_TEST(http_resource_suite, render_only_resource_methods_allowed) + render_only_resource ror; + // All methods should be allowed by default + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_GET), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_POST), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_PUT), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_HEAD), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_DELETE), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_TRACE), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_CONNECT), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_OPTIONS), true); + LT_CHECK_EQ(ror.is_allowed(MHD_HTTP_METHOD_PATCH), true); +LT_END_AUTO_TEST(render_only_resource_methods_allowed) + +LT_BEGIN_AUTO_TEST(http_resource_suite, resource_init_sets_all_methods) + simple_resource sr; + // Verify all 9 HTTP methods are initialized + auto allowed = sr.get_allowed_methods(); + LT_CHECK_EQ(allowed.size(), 9); +LT_END_AUTO_TEST(resource_init_sets_all_methods) + +LT_BEGIN_AUTO_TEST(http_resource_suite, get_allowed_methods_only_returns_true) + simple_resource sr; + // Disallow some methods + sr.set_allowing(MHD_HTTP_METHOD_POST, false); + sr.set_allowing(MHD_HTTP_METHOD_PUT, false); + sr.set_allowing(MHD_HTTP_METHOD_DELETE, false); + + auto allowed = sr.get_allowed_methods(); + // Should only return 6 methods now (9 - 3) + LT_CHECK_EQ(allowed.size(), 6); + + // Verify POST, PUT, DELETE are not in the list + LT_CHECK_EQ(std::find(allowed.begin(), allowed.end(), + MHD_HTTP_METHOD_POST) == allowed.end(), true); + LT_CHECK_EQ(std::find(allowed.begin(), allowed.end(), + MHD_HTTP_METHOD_PUT) == allowed.end(), true); + LT_CHECK_EQ(std::find(allowed.begin(), allowed.end(), + MHD_HTTP_METHOD_DELETE) == allowed.end(), true); +LT_END_AUTO_TEST(get_allowed_methods_only_returns_true) + +LT_BEGIN_AUTO_TEST(http_resource_suite, is_allowed_known_methods) + simple_resource sr; + // All standard methods should be allowed by default + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_POST), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_PUT), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_HEAD), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_DELETE), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_TRACE), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_CONNECT), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_OPTIONS), true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_PATCH), true); +LT_END_AUTO_TEST(is_allowed_known_methods) + +LT_BEGIN_AUTO_TEST(http_resource_suite, allow_all_after_disallow_all) + simple_resource sr; + sr.disallow_all(); + LT_CHECK_EQ(sr.get_allowed_methods().size(), 0); + + sr.allow_all(); + LT_CHECK_EQ(sr.get_allowed_methods().size(), 9); +LT_END_AUTO_TEST(allow_all_after_disallow_all) + +LT_BEGIN_AUTO_TEST(http_resource_suite, set_allowing_multiple_times) + simple_resource sr; + // Toggle GET multiple times + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), true); + sr.set_allowing(MHD_HTTP_METHOD_GET, false); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), false); + sr.set_allowing(MHD_HTTP_METHOD_GET, true); + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), true); + sr.set_allowing(MHD_HTTP_METHOD_GET, false); + sr.set_allowing(MHD_HTTP_METHOD_GET, false); // Double false + LT_CHECK_EQ(sr.is_allowed(MHD_HTTP_METHOD_GET), false); +LT_END_AUTO_TEST(set_allowing_multiple_times) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/unit/http_response_test.cpp b/test/unit/http_response_test.cpp new file mode 100644 index 00000000..e4ca6750 --- /dev/null +++ b/test/unit/http_response_test.cpp @@ -0,0 +1,331 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include + +#include "./littletest.hpp" +#include "./httpserver.hpp" + +using std::string; +using httpserver::http_response; +using httpserver::string_response; + +LT_BEGIN_SUITE(http_response_suite) + void set_up() { + } + + void tear_down() { + } +LT_END_SUITE(http_response_suite) + +LT_BEGIN_AUTO_TEST(http_response_suite, default_response_code) + http_response resp; + LT_CHECK_EQ(resp.get_response_code(), -1); +LT_END_AUTO_TEST(default_response_code) + +LT_BEGIN_AUTO_TEST(http_response_suite, custom_response_code) + http_response resp(404, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 404); +LT_END_AUTO_TEST(custom_response_code) + +LT_BEGIN_AUTO_TEST(http_response_suite, string_response_code) + string_response resp("Not Found", 404, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 404); +LT_END_AUTO_TEST(string_response_code) + +LT_BEGIN_AUTO_TEST(http_response_suite, header_operations) + http_response resp(200, "text/plain"); + resp.with_header("X-Custom-Header", "HeaderValue"); + LT_CHECK_EQ(resp.get_header("X-Custom-Header"), "HeaderValue"); +LT_END_AUTO_TEST(header_operations) + +LT_BEGIN_AUTO_TEST(http_response_suite, footer_operations) + http_response resp(200, "text/plain"); + resp.with_footer("X-Footer", "FooterValue"); + LT_CHECK_EQ(resp.get_footer("X-Footer"), "FooterValue"); +LT_END_AUTO_TEST(footer_operations) + +LT_BEGIN_AUTO_TEST(http_response_suite, cookie_operations) + http_response resp(200, "text/plain"); + resp.with_cookie("SessionId", "abc123"); + LT_CHECK_EQ(resp.get_cookie("SessionId"), "abc123"); +LT_END_AUTO_TEST(cookie_operations) + +LT_BEGIN_AUTO_TEST(http_response_suite, get_headers) + http_response resp(200, "text/plain"); + resp.with_header("Header1", "Value1"); + resp.with_header("Header2", "Value2"); + auto headers = resp.get_headers(); + LT_CHECK_EQ(headers.at("Header1"), "Value1"); + LT_CHECK_EQ(headers.at("Header2"), "Value2"); +LT_END_AUTO_TEST(get_headers) + +LT_BEGIN_AUTO_TEST(http_response_suite, get_footers) + http_response resp(200, "text/plain"); + resp.with_footer("Footer1", "Value1"); + resp.with_footer("Footer2", "Value2"); + auto footers = resp.get_footers(); + LT_CHECK_EQ(footers.at("Footer1"), "Value1"); + LT_CHECK_EQ(footers.at("Footer2"), "Value2"); +LT_END_AUTO_TEST(get_footers) + +LT_BEGIN_AUTO_TEST(http_response_suite, get_cookies) + http_response resp(200, "text/plain"); + resp.with_cookie("Cookie1", "Value1"); + resp.with_cookie("Cookie2", "Value2"); + auto cookies = resp.get_cookies(); + LT_CHECK_EQ(cookies.at("Cookie1"), "Value1"); + LT_CHECK_EQ(cookies.at("Cookie2"), "Value2"); +LT_END_AUTO_TEST(get_cookies) + +LT_BEGIN_AUTO_TEST(http_response_suite, shoutcast_response) + string_response resp("OK", 200, "audio/mpeg"); + int original_code = resp.get_response_code(); + resp.shoutCAST(); + // shoutCAST sets the MHD_ICY_FLAG (1 << 31) on response_code + // Verify the flag bit is set (use unsigned comparison) + LT_CHECK_EQ(static_cast(resp.get_response_code()) & 0x80000000u, 0x80000000u); + // Also verify the original code bits are preserved + LT_CHECK_EQ(resp.get_response_code() & 0x7FFFFFFF, original_code); +LT_END_AUTO_TEST(shoutcast_response) + +LT_BEGIN_AUTO_TEST(http_response_suite, string_response_default_constructor) + string_response resp; + // Default constructor should create response with default values + LT_CHECK_EQ(resp.get_response_code(), -1); +LT_END_AUTO_TEST(string_response_default_constructor) + +LT_BEGIN_AUTO_TEST(http_response_suite, string_response_content_only) + string_response resp("Hello World"); + // Should use default response code (200) and content type (text/plain) + LT_CHECK_EQ(resp.get_response_code(), 200); +LT_END_AUTO_TEST(string_response_content_only) + +LT_BEGIN_AUTO_TEST(http_response_suite, ostream_operator_empty) + // Test ostream operator with default response (no headers/footers/cookies) + http_response resp; // Default constructor - no content type header added + std::ostringstream oss; + oss << resp; + string output = oss.str(); + // With empty headers/footers/cookies, only the response code line is output + LT_CHECK_EQ(output.find("Response [response_code:-1]") != string::npos, true); + // Empty maps don't produce any output in dump_header_map + LT_CHECK_EQ(output.find("Headers [") == string::npos, true); + LT_CHECK_EQ(output.find("Footers [") == string::npos, true); + LT_CHECK_EQ(output.find("Cookies [") == string::npos, true); +LT_END_AUTO_TEST(ostream_operator_empty) + +LT_BEGIN_AUTO_TEST(http_response_suite, ostream_operator_full) + // Test ostream operator with headers, footers, and cookies + http_response resp(201, "application/json"); + resp.with_header("X-Header1", "Value1"); + resp.with_header("X-Header2", "Value2"); + resp.with_footer("X-Footer", "FooterVal"); + resp.with_cookie("SessionId", "abc123"); + resp.with_cookie("UserId", "user42"); + + std::ostringstream oss; + oss << resp; + string output = oss.str(); + + LT_CHECK_EQ(output.find("Response [response_code:201]") != string::npos, true); + LT_CHECK_EQ(output.find("X-Header1") != string::npos, true); + LT_CHECK_EQ(output.find("X-Header2") != string::npos, true); + LT_CHECK_EQ(output.find("X-Footer") != string::npos, true); + LT_CHECK_EQ(output.find("SessionId") != string::npos, true); + LT_CHECK_EQ(output.find("UserId") != string::npos, true); +LT_END_AUTO_TEST(ostream_operator_full) + +// Test response code constants +LT_BEGIN_AUTO_TEST(http_response_suite, response_code_200) + string_response resp("OK", 200, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 200); +LT_END_AUTO_TEST(response_code_200) + +LT_BEGIN_AUTO_TEST(http_response_suite, response_code_201) + string_response resp("Created", 201, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 201); +LT_END_AUTO_TEST(response_code_201) + +LT_BEGIN_AUTO_TEST(http_response_suite, response_code_301) + string_response resp("", 301, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 301); +LT_END_AUTO_TEST(response_code_301) + +LT_BEGIN_AUTO_TEST(http_response_suite, response_code_400) + string_response resp("Bad Request", 400, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 400); +LT_END_AUTO_TEST(response_code_400) + +LT_BEGIN_AUTO_TEST(http_response_suite, response_code_500) + string_response resp("Internal Server Error", 500, "text/plain"); + LT_CHECK_EQ(resp.get_response_code(), 500); +LT_END_AUTO_TEST(response_code_500) + +// Test get_header with nonexistent key +LT_BEGIN_AUTO_TEST(http_response_suite, get_header_nonexistent) + http_response resp(200, "text/plain"); + string header = resp.get_header("NonExistent"); + LT_CHECK_EQ(header, ""); +LT_END_AUTO_TEST(get_header_nonexistent) + +// Test get_footer with nonexistent key +LT_BEGIN_AUTO_TEST(http_response_suite, get_footer_nonexistent) + http_response resp(200, "text/plain"); + string footer = resp.get_footer("NonExistent"); + LT_CHECK_EQ(footer, ""); +LT_END_AUTO_TEST(get_footer_nonexistent) + +// Test get_cookie with nonexistent key +LT_BEGIN_AUTO_TEST(http_response_suite, get_cookie_nonexistent) + http_response resp(200, "text/plain"); + string cookie = resp.get_cookie("NonExistent"); + LT_CHECK_EQ(cookie, ""); +LT_END_AUTO_TEST(get_cookie_nonexistent) + +// Test multiple headers +LT_BEGIN_AUTO_TEST(http_response_suite, multiple_headers) + http_response resp(200, "text/plain"); + resp.with_header("H1", "V1"); + resp.with_header("H2", "V2"); + resp.with_header("H3", "V3"); + LT_CHECK_EQ(resp.get_header("H1"), "V1"); + LT_CHECK_EQ(resp.get_header("H2"), "V2"); + LT_CHECK_EQ(resp.get_header("H3"), "V3"); +LT_END_AUTO_TEST(multiple_headers) + +// Test multiple footers +LT_BEGIN_AUTO_TEST(http_response_suite, multiple_footers) + http_response resp(200, "text/plain"); + resp.with_footer("F1", "V1"); + resp.with_footer("F2", "V2"); + LT_CHECK_EQ(resp.get_footer("F1"), "V1"); + LT_CHECK_EQ(resp.get_footer("F2"), "V2"); +LT_END_AUTO_TEST(multiple_footers) + +// Test multiple cookies +LT_BEGIN_AUTO_TEST(http_response_suite, multiple_cookies) + http_response resp(200, "text/plain"); + resp.with_cookie("C1", "V1"); + resp.with_cookie("C2", "V2"); + LT_CHECK_EQ(resp.get_cookie("C1"), "V1"); + LT_CHECK_EQ(resp.get_cookie("C2"), "V2"); +LT_END_AUTO_TEST(multiple_cookies) + +// Test overwriting header +LT_BEGIN_AUTO_TEST(http_response_suite, overwrite_header) + http_response resp(200, "text/plain"); + resp.with_header("Key", "Value1"); + LT_CHECK_EQ(resp.get_header("Key"), "Value1"); + resp.with_header("Key", "Value2"); + LT_CHECK_EQ(resp.get_header("Key"), "Value2"); +LT_END_AUTO_TEST(overwrite_header) + +// Test overwriting cookie +LT_BEGIN_AUTO_TEST(http_response_suite, overwrite_cookie) + http_response resp(200, "text/plain"); + resp.with_cookie("Cookie", "OldValue"); + LT_CHECK_EQ(resp.get_cookie("Cookie"), "OldValue"); + resp.with_cookie("Cookie", "NewValue"); + LT_CHECK_EQ(resp.get_cookie("Cookie"), "NewValue"); +LT_END_AUTO_TEST(overwrite_cookie) + +// Test empty headers map (using default constructor to get truly empty headers) +LT_BEGIN_AUTO_TEST(http_response_suite, empty_headers_map) + http_response resp; // Default constructor - no content type header added + auto headers = resp.get_headers(); + LT_CHECK_EQ(headers.empty(), true); +LT_END_AUTO_TEST(empty_headers_map) + +// Test empty footers map +LT_BEGIN_AUTO_TEST(http_response_suite, empty_footers_map) + http_response resp(200, "text/plain"); + auto footers = resp.get_footers(); + LT_CHECK_EQ(footers.empty(), true); +LT_END_AUTO_TEST(empty_footers_map) + +// Test empty cookies map +LT_BEGIN_AUTO_TEST(http_response_suite, empty_cookies_map) + http_response resp(200, "text/plain"); + auto cookies = resp.get_cookies(); + LT_CHECK_EQ(cookies.empty(), true); +LT_END_AUTO_TEST(empty_cookies_map) + +// Test ostream with only headers +LT_BEGIN_AUTO_TEST(http_response_suite, ostream_operator_headers_only) + http_response resp(200, "text/plain"); + resp.with_header("X-Custom", "Value"); + std::ostringstream oss; + oss << resp; + string output = oss.str(); + LT_CHECK_EQ(output.find("X-Custom") != string::npos, true); + LT_CHECK_EQ(output.find("200") != string::npos, true); +LT_END_AUTO_TEST(ostream_operator_headers_only) + +// Test ostream with only footers +LT_BEGIN_AUTO_TEST(http_response_suite, ostream_operator_footers_only) + http_response resp(200, "text/plain"); + resp.with_footer("X-Footer", "FootVal"); + std::ostringstream oss; + oss << resp; + string output = oss.str(); + LT_CHECK_EQ(output.find("X-Footer") != string::npos, true); +LT_END_AUTO_TEST(ostream_operator_footers_only) + +// Test ostream with only cookies +LT_BEGIN_AUTO_TEST(http_response_suite, ostream_operator_cookies_only) + http_response resp(200, "text/plain"); + resp.with_cookie("Session", "abc123"); + std::ostringstream oss; + oss << resp; + string output = oss.str(); + LT_CHECK_EQ(output.find("Session") != string::npos, true); +LT_END_AUTO_TEST(ostream_operator_cookies_only) + +// Test string_response with all parameters +LT_BEGIN_AUTO_TEST(http_response_suite, string_response_full_params) + string_response resp("Body content", 201, "application/json"); + LT_CHECK_EQ(resp.get_response_code(), 201); +LT_END_AUTO_TEST(string_response_full_params) + +// Test http_response with content_type parameter +LT_BEGIN_AUTO_TEST(http_response_suite, http_response_content_type) + http_response resp(200, "application/json"); + LT_CHECK_EQ(resp.get_response_code(), 200); +LT_END_AUTO_TEST(http_response_content_type) + +// Test special characters in header values +LT_BEGIN_AUTO_TEST(http_response_suite, header_special_characters) + http_response resp(200, "text/plain"); + resp.with_header("Content-Disposition", "attachment; filename=\"file.txt\""); + LT_CHECK_EQ(resp.get_header("Content-Disposition"), "attachment; filename=\"file.txt\""); +LT_END_AUTO_TEST(header_special_characters) + +// Test special characters in cookie values +LT_BEGIN_AUTO_TEST(http_response_suite, cookie_special_characters) + http_response resp(200, "text/plain"); + resp.with_cookie("Data", "value=with=equals"); + LT_CHECK_EQ(resp.get_cookie("Data"), "value=with=equals"); +LT_END_AUTO_TEST(cookie_special_characters) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV() diff --git a/test/unit/http_utils_test.cpp b/test/unit/http_utils_test.cpp index 5fd3b97b..24b88c54 100644 --- a/test/unit/http_utils_test.cpp +++ b/test/unit/http_utils_test.cpp @@ -92,6 +92,69 @@ LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_partial_marker) LT_CHECK_EQ(expected_size, 5); LT_END_AUTO_TEST(unescape_partial_marker) +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_lowercase_hex) + // Test lowercase hex digits (%2a -> '*') + std::string str = "test%2avalue"; + int expected_size = httpserver::http::http_unescape(&str); + + LT_CHECK_EQ(str, "test*value"); + LT_CHECK_EQ(expected_size, 10); +LT_END_AUTO_TEST(unescape_lowercase_hex) + +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_uppercase_hex) + // Test uppercase hex digits (%2A -> '*') + std::string str = "test%2Avalue"; + int expected_size = httpserver::http::http_unescape(&str); + + LT_CHECK_EQ(str, "test*value"); + LT_CHECK_EQ(expected_size, 10); +LT_END_AUTO_TEST(unescape_uppercase_hex) + +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_invalid_hex) + // Test invalid hex after % - should be left as-is + std::string str = "test%ZZvalue"; + int expected_size = httpserver::http::http_unescape(&str); + + LT_CHECK_EQ(str, "test%ZZvalue"); + LT_CHECK_EQ(expected_size, 12); +LT_END_AUTO_TEST(unescape_invalid_hex) + +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_percent_at_end) + // Test % at the very end of string + std::string str = "test%"; + int expected_size = httpserver::http::http_unescape(&str); + + LT_CHECK_EQ(str, "test%"); + LT_CHECK_EQ(expected_size, 5); +LT_END_AUTO_TEST(unescape_percent_at_end) + +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_percent_with_one_char) + // Test % followed by only one character + std::string str = "test%a"; + int expected_size = httpserver::http::http_unescape(&str); + + LT_CHECK_EQ(str, "test%a"); + LT_CHECK_EQ(expected_size, 6); +LT_END_AUTO_TEST(unescape_percent_with_one_char) + +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_mixed_case_hex) + // Test mixed case hex digits (%aB -> char) + std::string str = "test%aBvalue"; + int expected_size = httpserver::http::http_unescape(&str); + + // 0xAB = 171 which is a valid byte + LT_CHECK_EQ(expected_size, 10); +LT_END_AUTO_TEST(unescape_mixed_case_hex) + +LT_BEGIN_AUTO_TEST(http_utils_suite, unescape_multiple_percent) + // Test multiple percent-encoded values + std::string str = "%20%2B%20"; // space + plus + space + int expected_size = httpserver::http::http_unescape(&str); + + LT_CHECK_EQ(str, " + "); + LT_CHECK_EQ(expected_size, 3); +LT_END_AUTO_TEST(unescape_multiple_percent) + LT_BEGIN_AUTO_TEST(http_utils_suite, tokenize_url) string value = "test/this/url/here"; string expected_arr[] = { "test", "this", "url", "here" }; @@ -435,6 +498,82 @@ LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_str_loopback) LT_CHECK_EQ(test_ip.mask, 0xFFFF); LT_END_AUTO_TEST(ip_representation6_str_loopback) +// Test IPv6 with exactly 8 parts (full address without ::) +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_full_8_parts) + httpserver::http::ip_representation test_ip("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + + LT_CHECK_EQ(test_ip.ip_version, httpserver::http::http_utils::IPV6); + + LT_CHECK_EQ(test_ip.pieces[0], 32); + LT_CHECK_EQ(test_ip.pieces[1], 1); + LT_CHECK_EQ(test_ip.pieces[2], 13); + LT_CHECK_EQ(test_ip.pieces[3], 184); + LT_CHECK_EQ(test_ip.pieces[4], 133); + LT_CHECK_EQ(test_ip.pieces[5], 163); + // pieces 6-9 are 0 + LT_CHECK_EQ(test_ip.pieces[10], 138); + LT_CHECK_EQ(test_ip.pieces[11], 46); + LT_CHECK_EQ(test_ip.pieces[12], 3); + LT_CHECK_EQ(test_ip.pieces[13], 112); + LT_CHECK_EQ(test_ip.pieces[14], 115); + LT_CHECK_EQ(test_ip.pieces[15], 52); + + LT_CHECK_EQ(test_ip.mask, 0xFFFF); +LT_END_AUTO_TEST(ip_representation6_full_8_parts) + +// Test IPv6 with leading :: +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_leading_double_colon) + httpserver::http::ip_representation test_ip("::ffff:1234:5678"); + + LT_CHECK_EQ(test_ip.ip_version, httpserver::http::http_utils::IPV6); + + // First 10 bytes should be 0 + for (int i = 0; i < 10; i++) { + LT_CHECK_EQ(test_ip.pieces[i], 0); + } + + LT_CHECK_EQ(test_ip.pieces[10], 255); + LT_CHECK_EQ(test_ip.pieces[11], 255); + LT_CHECK_EQ(test_ip.pieces[12], 18); + LT_CHECK_EQ(test_ip.pieces[13], 52); + LT_CHECK_EQ(test_ip.pieces[14], 86); + LT_CHECK_EQ(test_ip.pieces[15], 120); + + LT_CHECK_EQ(test_ip.mask, 0xFFFF); +LT_END_AUTO_TEST(ip_representation6_leading_double_colon) + +// Test IPv6 with trailing :: +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_trailing_double_colon) + httpserver::http::ip_representation test_ip("2001:db8::"); + + LT_CHECK_EQ(test_ip.ip_version, httpserver::http::http_utils::IPV6); + + LT_CHECK_EQ(test_ip.pieces[0], 32); + LT_CHECK_EQ(test_ip.pieces[1], 1); + LT_CHECK_EQ(test_ip.pieces[2], 13); + LT_CHECK_EQ(test_ip.pieces[3], 184); + + // Rest should be 0 + for (int i = 4; i < 16; i++) { + LT_CHECK_EQ(test_ip.pieces[i], 0); + } + + LT_CHECK_EQ(test_ip.mask, 0xFFFF); +LT_END_AUTO_TEST(ip_representation6_trailing_double_colon) + +// Test all zeros IPv6 +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_all_zeros) + httpserver::http::ip_representation test_ip("::"); + + LT_CHECK_EQ(test_ip.ip_version, httpserver::http::http_utils::IPV6); + + for (int i = 0; i < 16; i++) { + LT_CHECK_EQ(test_ip.pieces[i], 0); + } + + LT_CHECK_EQ(test_ip.mask, 0xFFFF); +LT_END_AUTO_TEST(ip_representation6_all_zeros) + LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation_weight) LT_CHECK_EQ(httpserver::http::ip_representation("::1").weight(), 16); LT_CHECK_EQ(httpserver::http::ip_representation("192.168.0.1").weight(), 16); @@ -629,6 +768,181 @@ LT_BEGIN_AUTO_TEST(http_utils_suite, dump_arg_map_no_prefix) LT_CHECK_EQ(ss.str(), " [ARG_ONE:[\"VALUE_ONE\"] ARG_TWO:[\"VALUE_TWO\"] ARG_THREE:[\"VALUE_THREE\"] ]\n"); LT_END_AUTO_TEST(dump_arg_map_no_prefix) +// Test IPv6 with too many parts (more than 8) +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_too_many_parts) + LT_CHECK_THROW(httpserver::http::ip_representation("2001:db8:8714:3a90:8714:2001:db8:3a90:extra")); +LT_END_AUTO_TEST(ip_representation6_too_many_parts) + +// Test IPv4 with wrong number of parts +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation4_wrong_parts) + LT_CHECK_THROW(httpserver::http::ip_representation("192.168.1")); + LT_CHECK_THROW(httpserver::http::ip_representation("192.168.1.2.3")); +LT_END_AUTO_TEST(ip_representation4_wrong_parts) + +// Test IPv6 with wildcards +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_with_wildcards) + httpserver::http::ip_representation ip1("2001:db8:*:3a90::12"); + LT_CHECK_EQ(ip1.ip_version, httpserver::http::http_utils::IPV6); + // Check that wildcard creates a masked entry + LT_CHECK_EQ(ip1.weight(), 14); // 16 - 2 wildcards + + httpserver::http::ip_representation ip2("*:*:*:*:*:*:*:*"); + LT_CHECK_EQ(ip2.weight(), 0); // All wildcards +LT_END_AUTO_TEST(ip_representation6_with_wildcards) + +// Test IPv6 nested IPv4 with wildcards +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_nested_ipv4_wildcard) + httpserver::http::ip_representation ip1("::ffff:192.168.*.*"); + LT_CHECK_EQ(ip1.ip_version, httpserver::http::http_utils::IPV6); + LT_CHECK_EQ(ip1.weight(), 14); // 16 - 2 wildcards + + httpserver::http::ip_representation ip2("::192.0.*.128"); + LT_CHECK_EQ(ip2.weight(), 15); // 16 - 1 wildcard +LT_END_AUTO_TEST(ip_representation6_nested_ipv4_wildcard) + +// Test comparison of addresses with different ::ffff prefixes +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation_ffff_comparison) + // Test comparing ::ffff addresses with :: addresses + // These should hit the special case at lines 483-486 + httpserver::http::ip_representation a("::ffff:192.168.1.1"); + httpserver::http::ip_representation b("::192.168.1.1"); + + // When scores are equal and both have valid ffff/0000 prefix bytes, return false + LT_CHECK_EQ(a < b, false); + LT_CHECK_EQ(b < a, false); + + // Different addresses should compare correctly + LT_CHECK_EQ(httpserver::http::ip_representation("::ffff:192.168.1.1") < + httpserver::http::ip_representation("::ffff:192.168.1.2"), true); +LT_END_AUTO_TEST(ip_representation_ffff_comparison) + +// Test comparison with different octets in bytes 10 and 11 (::ffff prefix area) +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation_middle_bytes_comparison) + // Test addresses with ::ffff prefix to exercise lines 489-494 + // The middle bytes comparison happens when scores are equal but ffff differs + httpserver::http::ip_representation a("::ffff:192.168.1.1"); + httpserver::http::ip_representation b("::192.168.1.1"); + + // Both have same IP part but different ffff bytes + // scores are same in main loop, so middle bytes comparison runs + bool result = a < b; + // ::ffff has higher value in bytes 10-11, so a > b + LT_CHECK_EQ(result, false); + + // When we compare two ::ffff addresses with different IPs + httpserver::http::ip_representation c("::ffff:10.0.0.1"); + httpserver::http::ip_representation d("::ffff:10.0.0.2"); + LT_CHECK_EQ(c < d, true); +LT_END_AUTO_TEST(ip_representation_middle_bytes_comparison) + +// Test IPv6 single-character blocks (padded to 4 chars) +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation6_short_blocks) + httpserver::http::ip_representation ip1("1:2:3:4:5:6:7:8"); + LT_CHECK_EQ(ip1.ip_version, httpserver::http::http_utils::IPV6); + LT_CHECK_EQ(ip1.pieces[0], 0); + LT_CHECK_EQ(ip1.pieces[1], 1); + LT_CHECK_EQ(ip1.pieces[2], 0); + LT_CHECK_EQ(ip1.pieces[3], 2); +LT_END_AUTO_TEST(ip_representation6_short_blocks) + +// Test URL standardization edge cases +LT_BEGIN_AUTO_TEST(http_utils_suite, standardize_url_single_slash) + // Test single character URL (line 230 branch: n_url_length > 1) + LT_CHECK_EQ(httpserver::http::http_utils::standardize_url("/"), "/"); +LT_END_AUTO_TEST(standardize_url_single_slash) + +// Test URL standardization with multiple consecutive slashes +LT_BEGIN_AUTO_TEST(http_utils_suite, standardize_url_multiple_slashes) + LT_CHECK_EQ(httpserver::http::http_utils::standardize_url("///foo///bar///"), "/foo/bar"); + LT_CHECK_EQ(httpserver::http::http_utils::standardize_url("//"), "/"); +LT_END_AUTO_TEST(standardize_url_multiple_slashes) + +// Test http_unescape with empty string +LT_BEGIN_AUTO_TEST(http_utils_suite, http_unescape_empty) + std::string val = ""; + httpserver::http::http_unescape(&val); + LT_CHECK_EQ(val, ""); +LT_END_AUTO_TEST(http_unescape_empty) + +// Test http_unescape with no escape sequences +LT_BEGIN_AUTO_TEST(http_utils_suite, http_unescape_no_escapes) + std::string val = "hello world"; + httpserver::http::http_unescape(&val); + LT_CHECK_EQ(val, "hello world"); +LT_END_AUTO_TEST(http_unescape_no_escapes) + +// Test http_unescape with multiple escape sequences +LT_BEGIN_AUTO_TEST(http_utils_suite, http_unescape_multiple) + std::string val = "%20%2B%3D"; + httpserver::http::http_unescape(&val); + LT_CHECK_EQ(val, " +="); +LT_END_AUTO_TEST(http_unescape_multiple) + +// Test tokenize_url with empty string +LT_BEGIN_AUTO_TEST(http_utils_suite, tokenize_url_empty) + std::vector result = httpserver::http::http_utils::tokenize_url(""); + LT_CHECK_EQ(result.size(), 0); +LT_END_AUTO_TEST(tokenize_url_empty) + +// Test tokenize_url with root only +LT_BEGIN_AUTO_TEST(http_utils_suite, tokenize_url_root) + std::vector result = httpserver::http::http_utils::tokenize_url("/"); + LT_CHECK_EQ(result.size(), 0); +LT_END_AUTO_TEST(tokenize_url_root) + +// Test tokenize_url with multiple segments +LT_BEGIN_AUTO_TEST(http_utils_suite, tokenize_url_multiple_segments) + std::vector result = httpserver::http::http_utils::tokenize_url("/api/v1/users/123"); + LT_CHECK_EQ(result.size(), 4); + LT_CHECK_EQ(result[0], "api"); + LT_CHECK_EQ(result[1], "v1"); + LT_CHECK_EQ(result[2], "users"); + LT_CHECK_EQ(result[3], "123"); +LT_END_AUTO_TEST(tokenize_url_multiple_segments) + +// Test standardize_url with empty string +LT_BEGIN_AUTO_TEST(http_utils_suite, standardize_url_empty) + // Empty string returns empty string (not "/") + LT_CHECK_EQ(httpserver::http::http_utils::standardize_url(""), ""); +LT_END_AUTO_TEST(standardize_url_empty) + +// Test dump_header_map with empty prefix +LT_BEGIN_AUTO_TEST(http_utils_suite, dump_header_map_empty_prefix) + httpserver::http::header_view_map headers; + headers["Content-Type"] = "application/json"; + headers["Accept"] = "text/html"; + + std::stringstream ss; + httpserver::http::dump_header_map(ss, "", headers); + std::string output = ss.str(); + LT_CHECK_EQ(output.find("Content-Type") != std::string::npos, true); + LT_CHECK_EQ(output.find("Accept") != std::string::npos, true); +LT_END_AUTO_TEST(dump_header_map_empty_prefix) + +// Test get_ip_str with nullptr (edge case) +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation_comparison_equal) + httpserver::http::ip_representation ip1("192.168.1.1"); + httpserver::http::ip_representation ip2("192.168.1.1"); + + // Same addresses should not be less than each other + LT_CHECK_EQ(ip1 < ip2, false); + LT_CHECK_EQ(ip2 < ip1, false); +LT_END_AUTO_TEST(ip_representation_comparison_equal) + +// Test ip_representation with max weight comparison +LT_BEGIN_AUTO_TEST(http_utils_suite, ip_representation_wildcard_weight) + // weight() returns count of non-wildcard bytes in IPv6 representation (16 bytes total) + // For IPv4 addresses stored as IPv6 (::ffff:x.x.x.x), specific octets add to weight + httpserver::http::ip_representation ip1("192.168.*.*"); + LT_CHECK_EQ(ip1.weight(), 14); // 16 - 2 wildcard bytes + + httpserver::http::ip_representation ip2("192.*.*.*"); + LT_CHECK_EQ(ip2.weight(), 13); // 16 - 3 wildcard bytes + + // More specific (higher weight) should be "greater than" less specific + LT_CHECK_EQ(ip1.weight() > ip2.weight(), true); +LT_END_AUTO_TEST(ip_representation_wildcard_weight) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/unit/string_utilities_test.cpp b/test/unit/string_utilities_test.cpp index f0b57e80..d94b6f73 100644 --- a/test/unit/string_utilities_test.cpp +++ b/test/unit/string_utilities_test.cpp @@ -83,6 +83,128 @@ LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_end_space) LT_CHECK_COLLECTIONS_EQ(expected.begin(), expected.end(), actual.begin()); LT_END_AUTO_TEST(split_string_end_space) +// Test string_split with empty input +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_empty_input) + string value = ""; + vector actual = httpserver::string_utilities::string_split(value, ' ', true); + LT_CHECK_EQ(actual.size(), 0); +LT_END_AUTO_TEST(split_string_empty_input) + +// Test string_split with empty input and no collapse +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_empty_input_no_collapse) + string value = ""; + vector actual = httpserver::string_utilities::string_split(value, ' ', false); + LT_CHECK_EQ(actual.size(), 0); +LT_END_AUTO_TEST(split_string_empty_input_no_collapse) + +// Test string_split with only separators and collapse +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_only_separators_collapse) + string value = " "; // Only spaces + vector actual = httpserver::string_utilities::string_split(value, ' ', true); + LT_CHECK_EQ(actual.size(), 0); +LT_END_AUTO_TEST(split_string_only_separators_collapse) + +// Test string_split with only separators and no collapse +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_only_separators_no_collapse) + string value = " "; // Only spaces + vector actual = httpserver::string_utilities::string_split(value, ' ', false); + // Should have 3 empty strings (between the 3 spaces) but last gets trimmed + LT_CHECK_EQ(actual.size(), 3); + LT_CHECK_EQ(actual[0], ""); + LT_CHECK_EQ(actual[1], ""); + LT_CHECK_EQ(actual[2], ""); +LT_END_AUTO_TEST(split_string_only_separators_no_collapse) + +// Test string_split with leading separator and no collapse +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_leading_separator_no_collapse) + string value = " a b"; // Leading space + string expected_arr[] = { "", "a", "b" }; + vector expected(expected_arr, expected_arr + sizeof(expected_arr) / sizeof(expected_arr[0])); + vector actual = httpserver::string_utilities::string_split(value, ' ', false); + + LT_CHECK_COLLECTIONS_EQ(expected.begin(), expected.end(), actual.begin()); +LT_END_AUTO_TEST(split_string_leading_separator_no_collapse) + +// Test string_split with leading separator and collapse +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_leading_separator_collapse) + string value = " a b"; // Leading space + string expected_arr[] = { "a", "b" }; + vector expected(expected_arr, expected_arr + sizeof(expected_arr) / sizeof(expected_arr[0])); + vector actual = httpserver::string_utilities::string_split(value, ' ', true); + + LT_CHECK_COLLECTIONS_EQ(expected.begin(), expected.end(), actual.begin()); +LT_END_AUTO_TEST(split_string_leading_separator_collapse) + +// Test to_upper_copy with empty string +LT_BEGIN_AUTO_TEST(string_utilities_suite, to_upper_copy_empty) + LT_CHECK_EQ(httpserver::string_utilities::to_upper_copy(""), string("")); +LT_END_AUTO_TEST(to_upper_copy_empty) + +// Test to_lower_copy with empty string +LT_BEGIN_AUTO_TEST(string_utilities_suite, to_lower_copy_empty) + LT_CHECK_EQ(httpserver::string_utilities::to_lower_copy(""), string("")); +LT_END_AUTO_TEST(to_lower_copy_empty) + +// Test to_upper_copy with already uppercase +LT_BEGIN_AUTO_TEST(string_utilities_suite, to_upper_copy_already_upper) + LT_CHECK_EQ(httpserver::string_utilities::to_upper_copy("HELLO WORLD"), string("HELLO WORLD")); +LT_END_AUTO_TEST(to_upper_copy_already_upper) + +// Test to_lower_copy with already lowercase +LT_BEGIN_AUTO_TEST(string_utilities_suite, to_lower_copy_already_lower) + LT_CHECK_EQ(httpserver::string_utilities::to_lower_copy("hello world"), string("hello world")); +LT_END_AUTO_TEST(to_lower_copy_already_lower) + +// Test string_split with different separator +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_comma_separator) + string value = "a,b,c,d"; + string expected_arr[] = { "a", "b", "c", "d" }; + vector expected(expected_arr, expected_arr + sizeof(expected_arr) / sizeof(expected_arr[0])); + vector actual = httpserver::string_utilities::string_split(value, ',', false); + + LT_CHECK_COLLECTIONS_EQ(expected.begin(), expected.end(), actual.begin()); +LT_END_AUTO_TEST(split_string_comma_separator) + +// Test string_split with single element +LT_BEGIN_AUTO_TEST(string_utilities_suite, split_string_single_element) + string value = "hello"; + vector actual = httpserver::string_utilities::string_split(value, ' ', true); + LT_CHECK_EQ(actual.size(), 1); + LT_CHECK_EQ(actual[0], "hello"); +LT_END_AUTO_TEST(split_string_single_element) + +// Test is_valid_hex with valid strings +LT_BEGIN_AUTO_TEST(string_utilities_suite, is_valid_hex_valid) + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("0123456789"), true); + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("abcdef"), true); + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("ABCDEF"), true); + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("0123456789abcdefABCDEF"), true); +LT_END_AUTO_TEST(is_valid_hex_valid) + +// Test is_valid_hex with invalid strings +LT_BEGIN_AUTO_TEST(string_utilities_suite, is_valid_hex_invalid) + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("ZZZZ"), false); + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("hello"), false); + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("12g4"), false); + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex("12 34"), false); +LT_END_AUTO_TEST(is_valid_hex_invalid) + +// Test is_valid_hex with empty string +LT_BEGIN_AUTO_TEST(string_utilities_suite, is_valid_hex_empty) + LT_CHECK_EQ(httpserver::string_utilities::is_valid_hex(""), true); +LT_END_AUTO_TEST(is_valid_hex_empty) + +// Test hex_char_to_val with digits +LT_BEGIN_AUTO_TEST(string_utilities_suite, hex_char_to_val_digits) + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('0'), 0); + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('9'), 9); + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('a'), 10); + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('f'), 15); + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('A'), 10); + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('F'), 15); + LT_CHECK_EQ(httpserver::string_utilities::hex_char_to_val('z'), 0); +LT_END_AUTO_TEST(hex_char_to_val_digits) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From d235f396f738c01af3f154111bc99982535eab80 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 3 Feb 2026 19:50:11 -0800 Subject: [PATCH 19/47] Add ARM cross-compilation to GitHub Actions CI Add ARM32 (ARMv7 hard-float) and ARM64 (AArch64) cross-compilation testing to the CI workflow. This validates that libhttpserver can be built for ARM targets using cross-compilation toolchains. Changes: - Add arm32 and arm64 matrix entries with appropriate toolchains - Add step to install ARM cross-compilation toolchains - Add separate libmicrohttpd cross-compilation steps with caching - Update configure step to use --host flag for cross-compilation - Skip tests and cppcheck for cross-compilation builds (binaries cannot execute on x86_64 runners) The existing COND_CROSS_COMPILE logic in configure.ac automatically handles test exclusion during cross-compilation. Closes #131 --- .github/workflows/verify-build.yml | 86 ++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 81b94737..043b62d7 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -334,6 +334,30 @@ jobs: debug: nodebug coverage: nocoverage linking: dynamic + # ARM 32-bit cross-compilation (ARMv7 hard-float) + - test-group: cross-compile + os: ubuntu-latest + os-type: ubuntu + build-type: arm32 + compiler-family: arm-cross + c-compiler: arm-linux-gnueabihf-gcc + cc-compiler: arm-linux-gnueabihf-g++ + debug: nodebug + coverage: nocoverage + linking: dynamic + shell: bash + # ARM 64-bit cross-compilation (AArch64) + - test-group: cross-compile + os: ubuntu-latest + os-type: ubuntu + build-type: arm64 + compiler-family: arm-cross + c-compiler: aarch64-linux-gnu-gcc + cc-compiler: aarch64-linux-gnu-g++ + debug: nodebug + coverage: nocoverage + linking: dynamic + shell: bash steps: - name: Checkout repository uses: actions/checkout@v4 @@ -386,6 +410,16 @@ jobs: run: sudo apt-get install ${{ matrix.cc-compiler }} if: ${{ matrix.compiler-family == 'gcc' && matrix.os-type == 'ubuntu' }} + - name: Install ARM cross-compilation toolchain + run: | + sudo apt-get update + if [ "${{ matrix.build-type }}" = "arm32" ]; then + sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf + elif [ "${{ matrix.build-type }}" = "arm64" ]; then + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + fi + if: ${{ matrix.compiler-family == 'arm-cross' }} + - name: Install valgrind if needed run: sudo apt-get install valgrind if: ${{ matrix.build-type == 'valgrind' && matrix.os-type == 'ubuntu' }} @@ -479,7 +513,7 @@ jobs: with: path: libmicrohttpd-0.9.77 key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built - if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' }} + if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' }} - name: Build libmicrohttpd dependency (if not cached) run: | @@ -488,7 +522,7 @@ jobs: cd libmicrohttpd-0.9.77 ; ./configure --disable-examples ; make ; - if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }} + if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }} - name: Build libmicrohttpd without digest auth (no-dauth test) run: | @@ -501,7 +535,7 @@ jobs: - name: Install libmicrohttpd run: cd libmicrohttpd-0.9.77 ; sudo make install ; - if: ${{ matrix.os-type != 'windows' }} + if: ${{ matrix.os-type != 'windows' && matrix.compiler-family != 'arm-cross' }} - name: Verify digest auth is disabled (no-dauth test) run: | @@ -522,7 +556,35 @@ jobs: ./configure --disable-examples --enable-poll=no make make install - + + - name: Fetch libmicrohttpd from cache (ARM cross-compile) + id: cache-libmicrohttpd-arm + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77-${{ matrix.build-type }} + key: ${{ matrix.os }}-${{ matrix.build-type }}-libmicrohttpd-0.9.77-cross-compiled + if: ${{ matrix.compiler-family == 'arm-cross' }} + + - name: Cross-compile libmicrohttpd for ARM + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + mv libmicrohttpd-0.9.77 libmicrohttpd-0.9.77-${{ matrix.build-type }} + cd libmicrohttpd-0.9.77-${{ matrix.build-type }} + if [ "${{ matrix.build-type }}" = "arm32" ]; then + ./configure --host=arm-linux-gnueabihf --disable-examples --disable-doc + else + ./configure --host=aarch64-linux-gnu --disable-examples --disable-doc + fi + make + if: ${{ matrix.compiler-family == 'arm-cross' && steps.cache-libmicrohttpd-arm.outputs.cache-hit != 'true' }} + + - name: Install cross-compiled libmicrohttpd + run: | + cd libmicrohttpd-0.9.77-${{ matrix.build-type }} + sudo make install + if: ${{ matrix.compiler-family == 'arm-cross' }} + - name: Refresh links to shared libs run: sudo ldconfig ; if: ${{ matrix.os-type == 'ubuntu' }} @@ -549,7 +611,13 @@ jobs: ./bootstrap ; mkdir build ; cd build ; - if [ "$LINKING" = "static" ]; then + if [ "${{ matrix.compiler-family }}" = "arm-cross" ]; then + if [ "${{ matrix.build-type }}" = "arm32" ]; then + ../configure --host=arm-linux-gnueabihf --disable-fastopen; + else + ../configure --host=aarch64-linux-gnu --disable-fastopen; + fi + elif [ "$LINKING" = "static" ]; then ../configure --enable-static --disable-fastopen; elif [ "$DEBUG" = "debug" ] && [ "$COVERAGE" = "coverage" ]; then ../configure --enable-debug --enable-coverage --disable-shared --disable-fastopen; @@ -611,14 +679,14 @@ jobs: run: | cd build ; make check; - if: ${{ matrix.build-type != 'iwyu' }} - + if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' }} + - name: Print tests results shell: bash run: | cd build ; cat test/test-suite.log ; - if: ${{ failure() && matrix.build-type != 'iwyu' }} + if: ${{ failure() && matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' }} - name: Run Valgrind checks run: | @@ -639,7 +707,7 @@ jobs: run: | cd src/ ; cppcheck --error-exitcode=1 . ; - if: ${{ matrix.os-type == 'ubuntu' }} + if: ${{ matrix.os-type == 'ubuntu' && matrix.compiler-family != 'arm-cross' }} - name: Run performance tests (select) run: | From 37d31d14807351606d451b38bbb3f23d80810e33 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 3 Feb 2026 20:57:02 -0800 Subject: [PATCH 20/47] Fix ARM cross-compilation: use custom prefix for libmicrohttpd The ARM cross-compiler doesn't search /usr/local by default. Install libmicrohttpd to a workspace-local sysroot and pass CPPFLAGS/LDFLAGS to libhttpserver's configure to find the headers and libraries. --- .github/workflows/verify-build.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 043b62d7..d5993d24 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -571,19 +571,22 @@ jobs: tar -xzf libmicrohttpd-0.9.77.tar.gz mv libmicrohttpd-0.9.77 libmicrohttpd-0.9.77-${{ matrix.build-type }} cd libmicrohttpd-0.9.77-${{ matrix.build-type }} + mkdir -p ${{ github.workspace }}/arm-sysroot if [ "${{ matrix.build-type }}" = "arm32" ]; then - ./configure --host=arm-linux-gnueabihf --disable-examples --disable-doc + ./configure --host=arm-linux-gnueabihf --prefix=${{ github.workspace }}/arm-sysroot --disable-examples --disable-doc else - ./configure --host=aarch64-linux-gnu --disable-examples --disable-doc + ./configure --host=aarch64-linux-gnu --prefix=${{ github.workspace }}/arm-sysroot --disable-examples --disable-doc fi make + make install if: ${{ matrix.compiler-family == 'arm-cross' && steps.cache-libmicrohttpd-arm.outputs.cache-hit != 'true' }} - - name: Install cross-compiled libmicrohttpd + - name: Install cross-compiled libmicrohttpd from cache run: | cd libmicrohttpd-0.9.77-${{ matrix.build-type }} - sudo make install - if: ${{ matrix.compiler-family == 'arm-cross' }} + mkdir -p ${{ github.workspace }}/arm-sysroot + make install + if: ${{ matrix.compiler-family == 'arm-cross' && steps.cache-libmicrohttpd-arm.outputs.cache-hit == 'true' }} - name: Refresh links to shared libs run: sudo ldconfig ; @@ -612,6 +615,9 @@ jobs: mkdir build ; cd build ; if [ "${{ matrix.compiler-family }}" = "arm-cross" ]; then + export CPPFLAGS="-I${{ github.workspace }}/arm-sysroot/include" + export LDFLAGS="-L${{ github.workspace }}/arm-sysroot/lib" + export PKG_CONFIG_PATH="${{ github.workspace }}/arm-sysroot/lib/pkgconfig" if [ "${{ matrix.build-type }}" = "arm32" ]; then ../configure --host=arm-linux-gnueabihf --disable-fastopen; else From 2e058a339ed311755bc509a18e7b036cc7e81555 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 10:05:48 -0800 Subject: [PATCH 21/47] Add client certificate authentication and SNI callback support Add convenience methods for extracting client certificate information from TLS connections (mTLS) and Server Name Indication (SNI) callback support for hosting multiple certificates on a single server. Client certificate methods added to http_request (requires GnuTLS): - has_client_certificate() - check if client cert present - get_client_cert_dn() - subject Distinguished Name - get_client_cert_issuer_dn() - issuer DN - get_client_cert_cn() - Common Name from subject - is_client_cert_verified() - certificate chain verification status - get_client_cert_fingerprint_sha256() - hex-encoded SHA-256 fingerprint - get_client_cert_not_before() / get_client_cert_not_after() - validity times SNI callback support (requires libmicrohttpd 0.9.71+): - sni_callback() builder method on create_webserver - Callback receives server name from TLS ClientHello - Returns cert/key pair for the requested hostname Closes #133 --- README.md | 118 ++++++++++++++ examples/client_cert_auth.cpp | 175 ++++++++++++++++++++ src/http_request.cpp | 241 ++++++++++++++++++++++++++++ src/httpserver/create_webserver.hpp | 20 +++ src/httpserver/http_request.hpp | 48 ++++++ src/httpserver/webserver.hpp | 13 ++ src/webserver.cpp | 83 +++++++++- test/client_cert.pem | 20 +++ test/client_key.pem | 28 ++++ test/integ/ws_start_stop.cpp | 223 +++++++++++++++++++++++++ 10 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 examples/client_cert_auth.cpp create mode 100644 test/client_cert.pem create mode 100644 test/client_key.pem diff --git a/README.md b/README.md index 13dba9c3..60c161f3 100644 --- a/README.md +++ b/README.md @@ -1065,6 +1065,124 @@ To test the above example: You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/centralized_authentication.cpp). +### Using Client Certificate Authentication (mTLS) +Client certificate authentication (also known as mutual TLS or mTLS) provides strong authentication by requiring clients to present X.509 certificates during the TLS handshake. This is the most secure authentication method as it verifies client identity cryptographically. + +To enable client certificate authentication, configure your webserver with: +1. `use_ssl()` - Enable TLS +2. `https_mem_key()` and `https_mem_cert()` - Server certificate +3. `https_mem_trust()` - CA certificate(s) to verify client certificates + +```cpp + #include + + using namespace httpserver; + + class secure_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request& req) { + // Check if client provided a certificate + if (!req.has_client_certificate()) { + return std::make_shared( + "Client certificate required", 401, "text/plain"); + } + + // Check if certificate is verified by our CA + if (!req.is_client_cert_verified()) { + return std::make_shared( + "Certificate not verified", 403, "text/plain"); + } + + // Extract certificate information + std::string cn = req.get_client_cert_cn(); // Common Name + std::string dn = req.get_client_cert_dn(); // Subject DN + std::string issuer = req.get_client_cert_issuer_dn(); // Issuer DN + std::string fingerprint = req.get_client_cert_fingerprint_sha256(); + time_t not_before = req.get_client_cert_not_before(); + time_t not_after = req.get_client_cert_not_after(); + + return std::make_shared( + "Welcome, " + cn + "!", 200, "text/plain"); + } + }; + + int main() { + webserver ws = create_webserver(8443) + .use_ssl() + .https_mem_key("server_key.pem") + .https_mem_cert("server_cert.pem") + .https_mem_trust("ca_cert.pem"); // CA for client certs + + secure_resource sr; + ws.register_resource("/secure", &sr); + ws.start(true); + + return 0; + } +``` + +Available client certificate methods (require GnuTLS support): +- `has_client_certificate()` - Check if client presented a certificate +- `get_client_cert_dn()` - Get the subject Distinguished Name +- `get_client_cert_issuer_dn()` - Get the issuer Distinguished Name +- `get_client_cert_cn()` - Get the Common Name from the subject +- `is_client_cert_verified()` - Check if the certificate chain is verified +- `get_client_cert_fingerprint_sha256()` - Get hex-encoded SHA-256 fingerprint +- `get_client_cert_not_before()` - Get certificate validity start time +- `get_client_cert_not_after()` - Get certificate validity end time + +To test with curl: + + # With client certificate + curl -k --cert client_cert.pem --key client_key.pem https://localhost:8443/secure + + # Without client certificate (will be rejected) + curl -k https://localhost:8443/secure + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/client_cert_auth.cpp). + +### Server Name Indication (SNI) Callback +SNI allows a server to host multiple TLS certificates on a single IP address. The client indicates which hostname it's connecting to during the TLS handshake, and the server can select the appropriate certificate. + +To use SNI with libhttpserver, configure an SNI callback that returns the certificate/key pair for each server name: + +```cpp + #include + #include + + using namespace httpserver; + + // Map of server names to cert/key pairs + std::map> certs; + + // SNI callback - returns (cert_pem, key_pem) for the requested server name + std::pair sni_callback(const std::string& server_name) { + auto it = certs.find(server_name); + if (it != certs.end()) { + return it->second; + } + return {"", ""}; // Use default certificate + } + + int main() { + // Load certificates for different hostnames + certs["www.example.com"] = {load_file("www_cert.pem"), load_file("www_key.pem")}; + certs["api.example.com"] = {load_file("api_cert.pem"), load_file("api_key.pem")}; + + webserver ws = create_webserver(443) + .use_ssl() + .https_mem_key("default_key.pem") // Default certificate + .https_mem_cert("default_cert.pem") + .sni_callback(sni_callback); // SNI callback + + // ... register resources and start + ws.start(true); + return 0; + } +``` + +Note: SNI support requires libmicrohttpd 0.9.71 or later compiled with GnuTLS. + [Back to TOC](#table-of-contents) ## HTTP Utils diff --git a/examples/client_cert_auth.cpp b/examples/client_cert_auth.cpp new file mode 100644 index 00000000..90a3ba84 --- /dev/null +++ b/examples/client_cert_auth.cpp @@ -0,0 +1,175 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +/** + * Example demonstrating client certificate (mTLS) authentication. + * + * This example shows how to: + * 1. Configure the server to request client certificates + * 2. Extract client certificate information in request handlers + * 3. Implement certificate-based access control + * + * To test this example: + * + * 1. Generate server certificate and key: + * openssl req -x509 -newkey rsa:2048 -keyout server_key.pem -out server_cert.pem \ + * -days 365 -nodes -subj "/CN=localhost" + * + * 2. Generate a CA certificate for client certs: + * openssl req -x509 -newkey rsa:2048 -keyout ca_key.pem -out ca_cert.pem \ + * -days 365 -nodes -subj "/CN=Test CA" + * + * 3. Generate client certificate signed by the CA: + * openssl req -newkey rsa:2048 -keyout client_key.pem -out client_csr.pem \ + * -nodes -subj "/CN=Alice/O=Engineering" + * openssl x509 -req -in client_csr.pem -CA ca_cert.pem -CAkey ca_key.pem \ + * -CAcreateserial -out client_cert.pem -days 365 + * + * 4. Run the server: + * ./client_cert_auth + * + * 5. Test with curl using client certificate: + * curl -k --cert client_cert.pem --key client_key.pem https://localhost:8443/secure + * + * Or without a certificate (will be denied): + * curl -k https://localhost:8443/secure + */ + +#include +#include +#include +#include + +#include + +// Set of allowed certificate fingerprints (SHA-256, hex-encoded) +// In a real application, this would be loaded from a database or config file +std::set allowed_fingerprints; + +// Resource that requires client certificate authentication +class secure_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request& req) { + // Check if client provided a certificate + if (!req.has_client_certificate()) { + return std::make_shared( + "Client certificate required", + httpserver::http::http_utils::http_unauthorized, "text/plain"); + } + + // Get certificate information + std::string cn = req.get_client_cert_cn(); + std::string dn = req.get_client_cert_dn(); + std::string issuer = req.get_client_cert_issuer_dn(); + std::string fingerprint = req.get_client_cert_fingerprint_sha256(); + bool verified = req.is_client_cert_verified(); + + // Check if certificate is verified by our CA + if (!verified) { + return std::make_shared( + "Certificate not verified by trusted CA", + httpserver::http::http_utils::http_forbidden, "text/plain"); + } + + // Optional: Check fingerprint against allowlist + if (!allowed_fingerprints.empty() && + allowed_fingerprints.find(fingerprint) == allowed_fingerprints.end()) { + return std::make_shared( + "Certificate not in allowlist", + httpserver::http::http_utils::http_forbidden, "text/plain"); + } + + // Check certificate validity times + time_t now = time(nullptr); + time_t not_before = req.get_client_cert_not_before(); + time_t not_after = req.get_client_cert_not_after(); + + if (now < not_before) { + return std::make_shared( + "Certificate not yet valid", + httpserver::http::http_utils::http_forbidden, "text/plain"); + } + + if (now > not_after) { + return std::make_shared( + "Certificate has expired", + httpserver::http::http_utils::http_forbidden, "text/plain"); + } + + // Build response with certificate info + std::string response = "Welcome, " + cn + "!\n\n"; + response += "Certificate Details:\n"; + response += " Subject DN: " + dn + "\n"; + response += " Issuer DN: " + issuer + "\n"; + response += " Fingerprint (SHA-256): " + fingerprint + "\n"; + response += " Verified: " + std::string(verified ? "Yes" : "No") + "\n"; + + return std::make_shared(response, 200, "text/plain"); + } +}; + +// Public resource that shows certificate info but doesn't require it +class info_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request& req) { + std::string response; + + if (req.has_client_certificate()) { + response = "Client certificate detected:\n"; + response += " Common Name: " + req.get_client_cert_cn() + "\n"; + response += " Verified: " + std::string(req.is_client_cert_verified() ? "Yes" : "No") + "\n"; + } else { + response = "No client certificate provided.\n"; + response += "Use --cert and --key with curl to provide one.\n"; + } + + return std::make_shared(response, 200, "text/plain"); + } +}; + +int main() { + std::cout << "Starting HTTPS server with client certificate authentication on port 8443...\n"; + std::cout << "\nEndpoints:\n"; + std::cout << " /info - Shows certificate info (optional cert)\n"; + std::cout << " /secure - Requires valid client certificate\n\n"; + + // Create webserver with SSL and client certificate trust store + httpserver::webserver ws = httpserver::create_webserver(8443) + .use_ssl() + .https_mem_key("server_key.pem") // Server private key + .https_mem_cert("server_cert.pem") // Server certificate + .https_mem_trust("ca_cert.pem"); // CA certificate for verifying client certs + + secure_resource secure; + info_resource info; + + ws.register_resource("/secure", &secure); + ws.register_resource("/info", &info); + + std::cout << "Server started. Press Ctrl+C to stop.\n\n"; + std::cout << "Test commands:\n"; + std::cout << " curl -k https://localhost:8443/info\n"; + std::cout << " curl -k --cert client_cert.pem --key client_key.pem https://localhost:8443/info\n"; + std::cout << " curl -k --cert client_cert.pem --key client_key.pem https://localhost:8443/secure\n"; + + ws.start(true); + + return 0; +} diff --git a/src/http_request.cpp b/src/http_request.cpp index be532637..9b97ef4c 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -29,6 +29,10 @@ #include "httpserver/http_utils.hpp" #include "httpserver/string_utilities.hpp" +#ifdef HAVE_GNUTLS +#include +#endif // HAVE_GNUTLS + namespace httpserver { const char http_request::EMPTY[] = ""; @@ -317,6 +321,243 @@ gnutls_session_t http_request::get_tls_session() const { return static_cast(conninfo->tls_session); } + +bool http_request::has_client_certificate() const { + if (!has_tls_session()) { + return false; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + return (cert_list != nullptr && list_size > 0); +} + +std::string http_request::get_client_cert_dn() const { + if (!has_client_certificate()) { + return ""; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + gnutls_x509_crt_t cert; + if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + return ""; + } + + if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + size_t dn_size = 0; + gnutls_x509_crt_get_dn(cert, nullptr, &dn_size); + + std::string dn(dn_size, '\0'); + if (gnutls_x509_crt_get_dn(cert, &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + gnutls_x509_crt_deinit(cert); + + // Remove trailing null if present + if (!dn.empty() && dn.back() == '\0') { + dn.pop_back(); + } + + return dn; +} + +std::string http_request::get_client_cert_issuer_dn() const { + if (!has_client_certificate()) { + return ""; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + gnutls_x509_crt_t cert; + if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + return ""; + } + + if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + size_t dn_size = 0; + gnutls_x509_crt_get_issuer_dn(cert, nullptr, &dn_size); + + std::string dn(dn_size, '\0'); + if (gnutls_x509_crt_get_issuer_dn(cert, &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + gnutls_x509_crt_deinit(cert); + + // Remove trailing null if present + if (!dn.empty() && dn.back() == '\0') { + dn.pop_back(); + } + + return dn; +} + +std::string http_request::get_client_cert_cn() const { + if (!has_client_certificate()) { + return ""; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + gnutls_x509_crt_t cert; + if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + return ""; + } + + if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + size_t cn_size = 0; + gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, nullptr, &cn_size); + + if (cn_size == 0) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + std::string cn(cn_size, '\0'); + if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, &cn[0], &cn_size) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + gnutls_x509_crt_deinit(cert); + + // Remove trailing null if present + if (!cn.empty() && cn.back() == '\0') { + cn.pop_back(); + } + + return cn; +} + +bool http_request::is_client_cert_verified() const { + if (!has_tls_session()) { + return false; + } + + gnutls_session_t session = get_tls_session(); + unsigned int status = 0; + + if (gnutls_certificate_verify_peers2(session, &status) != GNUTLS_E_SUCCESS) { + return false; + } + + return (status == 0); +} + +std::string http_request::get_client_cert_fingerprint_sha256() const { + if (!has_client_certificate()) { + return ""; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + gnutls_x509_crt_t cert; + if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + return ""; + } + + if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + unsigned char fingerprint[32]; // SHA-256 is 32 bytes + size_t fingerprint_size = sizeof(fingerprint); + + if (gnutls_x509_crt_get_fingerprint(cert, GNUTLS_DIG_SHA256, fingerprint, &fingerprint_size) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return ""; + } + + gnutls_x509_crt_deinit(cert); + + // Convert to hex string + std::string hex_fingerprint; + hex_fingerprint.reserve(fingerprint_size * 2); + for (size_t i = 0; i < fingerprint_size; ++i) { + char hex[3]; + snprintf(hex, sizeof(hex), "%02x", fingerprint[i]); + hex_fingerprint += hex; + } + + return hex_fingerprint; +} + +time_t http_request::get_client_cert_not_before() const { + if (!has_client_certificate()) { + return -1; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + gnutls_x509_crt_t cert; + if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + return -1; + } + + if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return -1; + } + + time_t not_before = gnutls_x509_crt_get_activation_time(cert); + gnutls_x509_crt_deinit(cert); + + return not_before; +} + +time_t http_request::get_client_cert_not_after() const { + if (!has_client_certificate()) { + return -1; + } + + gnutls_session_t session = get_tls_session(); + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + gnutls_x509_crt_t cert; + if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + return -1; + } + + if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return -1; + } + + time_t not_after = gnutls_x509_crt_get_expiration_time(cert); + gnutls_x509_crt_deinit(cert); + + return not_after; +} #endif // HAVE_GNUTLS std::string_view http_request::get_requestor() const { diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index f18ed5b6..7ade5e17 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -50,6 +50,14 @@ typedef std::function log_access_ptr; typedef std::function log_error_ptr; typedef std::function psk_cred_handler_callback; +/** + * SNI (Server Name Indication) callback type. + * The callback receives the server name from the TLS ClientHello. + * It should return a pair of (certificate_pem, key_pem) for the requested server name, + * or empty strings to use the default certificate. + */ +typedef std::function(const std::string& server_name)> sni_callback_t; + namespace http { class file_info; } typedef std::function file_cleanup_callback_ptr; @@ -388,6 +396,17 @@ class create_webserver { return *this; } + /** + * Set the SNI (Server Name Indication) callback. + * The callback is invoked during TLS handshake with the server name from ClientHello. + * @param callback The SNI callback function + * @return reference to this for method chaining + */ + create_webserver& sni_callback(sni_callback_t callback) { + _sni_callback = callback; + return *this; + } + private: uint16_t _port = DEFAULT_WS_PORT; http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT; @@ -437,6 +456,7 @@ class create_webserver { file_cleanup_callback_ptr _file_cleanup_callback = nullptr; auth_handler_ptr _auth_handler = nullptr; std::vector _auth_skip_paths; + sni_callback_t _sni_callback = nullptr; friend class webserver; }; diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 4c1c3323..4ba5ee38 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -239,6 +239,54 @@ class http_request { * @return the TLS session **/ gnutls_session_t get_tls_session() const; + + /** + * Check if a client certificate is present in the TLS session. + * @return true if client certificate is present + **/ + bool has_client_certificate() const; + + /** + * Get the Subject Distinguished Name from the client certificate. + * @return the subject DN as a string, empty if not available + **/ + std::string get_client_cert_dn() const; + + /** + * Get the Issuer Distinguished Name from the client certificate. + * @return the issuer DN as a string, empty if not available + **/ + std::string get_client_cert_issuer_dn() const; + + /** + * Get the Common Name (CN) from the client certificate subject. + * @return the CN as a string, empty if not available + **/ + std::string get_client_cert_cn() const; + + /** + * Check if the client certificate chain has been verified. + * @return true if certificate verification passed + **/ + bool is_client_cert_verified() const; + + /** + * Get the SHA-256 fingerprint of the client certificate. + * @return hex-encoded SHA-256 fingerprint, empty if not available + **/ + std::string get_client_cert_fingerprint_sha256() const; + + /** + * Get the not-before (validity start) time of the client certificate. + * @return validity start time as time_t, -1 if not available + **/ + time_t get_client_cert_not_before() const; + + /** + * Get the not-after (validity end) time of the client certificate. + * @return validity end time as time_t, -1 if not available + **/ + time_t get_client_cert_not_after() const; #endif // HAVE_GNUTLS /** diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index e4d5e313..c0a5dc35 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -185,6 +185,7 @@ class webserver { const file_cleanup_callback_ptr file_cleanup_callback; const auth_handler_ptr auth_handler; const std::vector auth_skip_paths; + const sni_callback_t sni_callback; std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; @@ -233,6 +234,18 @@ class webserver { const char* username, void** psk, size_t* psk_size); + +#ifdef MHD_OPTION_HTTPS_CERT_CALLBACK + // SNI certificate callback function (libmicrohttpd 0.9.71+) + static int sni_cert_callback_func(void* cls, + struct MHD_Connection* connection, + const char* server_name, + gnutls_certificate_credentials_t* creds); + + // Cache for loaded credentials per server name + mutable std::map sni_credentials_cache; + mutable std::shared_mutex sni_credentials_mutex; +#endif // MHD_OPTION_HTTPS_CERT_CALLBACK #endif // HAVE_GNUTLS friend MHD_Result policy_callback(void *cls, const struct sockaddr* addr, socklen_t addrlen); diff --git a/src/webserver.cpp b/src/webserver.cpp index 547eda60..b91697d6 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -67,6 +67,7 @@ struct MHD_Connection; #ifdef HAVE_GNUTLS #include +#include #endif // HAVE_GNUTLS #ifndef SOCK_CLOEXEC @@ -174,7 +175,8 @@ webserver::webserver(const create_webserver& params): internal_error_resource(params._internal_error_resource), file_cleanup_callback(params._file_cleanup_callback), auth_handler(params._auth_handler), - auth_skip_paths(params._auth_skip_paths) { + auth_skip_paths(params._auth_skip_paths), + sni_callback(params._sni_callback) { ignore_sigpipe(); pthread_mutex_init(&mutexwait, nullptr); pthread_cond_init(&mutexcond, nullptr); @@ -184,6 +186,14 @@ webserver::~webserver() { stop(); pthread_mutex_destroy(&mutexwait); pthread_cond_destroy(&mutexcond); + +#if defined(HAVE_GNUTLS) && defined(MHD_OPTION_HTTPS_CERT_CALLBACK) + // Clean up cached SNI credentials + for (auto& [name, creds] : sni_credentials_cache) { + gnutls_certificate_free_credentials(creds); + } + sni_credentials_cache.clear(); +#endif // HAVE_GNUTLS && MHD_OPTION_HTTPS_CERT_CALLBACK } void webserver::sweet_kill() { @@ -304,6 +314,13 @@ bool webserver::start(bool blocking) { iov.push_back(gen(MHD_OPTION_GNUTLS_PSK_CRED_HANDLER, (intptr_t)&psk_cred_handler_func, this)); } + +#ifdef MHD_OPTION_HTTPS_CERT_CALLBACK + if (sni_callback != nullptr && use_ssl) { + iov.push_back(gen(MHD_OPTION_HTTPS_CERT_CALLBACK, + (intptr_t)&sni_cert_callback_func, this)); + } +#endif // MHD_OPTION_HTTPS_CERT_CALLBACK #endif // HAVE_GNUTLS iov.push_back(gen(MHD_OPTION_END, 0, nullptr)); @@ -480,6 +497,70 @@ int webserver::psk_cred_handler_func(void* cls, *psk_size = psk_len; return 0; } + +#ifdef MHD_OPTION_HTTPS_CERT_CALLBACK +// SNI callback for selecting certificates based on server name +// Returns 0 on success, -1 on failure +int webserver::sni_cert_callback_func(void* cls, + struct MHD_Connection* connection, + const char* server_name, + gnutls_certificate_credentials_t* creds) { + std::ignore = connection; + + webserver* ws = static_cast(cls); + if (ws == nullptr || ws->sni_callback == nullptr || server_name == nullptr) { + return -1; + } + + std::string name(server_name); + + // Check if we have cached credentials for this server name + { + std::shared_lock lock(ws->sni_credentials_mutex); + auto it = ws->sni_credentials_cache.find(name); + if (it != ws->sni_credentials_cache.end()) { + *creds = it->second; + return 0; + } + } + + // Call user's callback to get cert/key pair + auto [cert_pem, key_pem] = ws->sni_callback(name); + if (cert_pem.empty() || key_pem.empty()) { + return -1; // Use default certificate + } + + // Create new credentials for this server name + gnutls_certificate_credentials_t new_creds; + if (gnutls_certificate_allocate_credentials(&new_creds) != GNUTLS_E_SUCCESS) { + return -1; + } + + gnutls_datum_t cert_data = { + reinterpret_cast(const_cast(cert_pem.data())), + static_cast(cert_pem.size()) + }; + gnutls_datum_t key_data = { + reinterpret_cast(const_cast(key_pem.data())), + static_cast(key_pem.size()) + }; + + int ret = gnutls_certificate_set_x509_key_mem(new_creds, &cert_data, &key_data, GNUTLS_X509_FMT_PEM); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_certificate_free_credentials(new_creds); + return -1; + } + + // Cache the credentials + { + std::unique_lock lock(ws->sni_credentials_mutex); + ws->sni_credentials_cache[name] = new_creds; + } + + *creds = new_creds; + return 0; +} +#endif // MHD_OPTION_HTTPS_CERT_CALLBACK #endif // HAVE_GNUTLS MHD_Result policy_callback(void *cls, const struct sockaddr* addr, socklen_t addrlen) { diff --git a/test/client_cert.pem b/test/client_cert.pem new file mode 100644 index 00000000..c50de4a5 --- /dev/null +++ b/test/client_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMzCCAhugAwIBAgIUVIu0IAY17b7zVLt1hypR3Ag3IuowDQYJKoZIhvcNAQEL +BQAwKTEUMBIGA1UEAwwLVGVzdCBDbGllbnQxETAPBgNVBAoMCFRlc3QgT3JnMB4X +DTI2MDIwNDE1NTY0MloXDTI3MDIwNDE1NTY0MlowKTEUMBIGA1UEAwwLVGVzdCBD +bGllbnQxETAPBgNVBAoMCFRlc3QgT3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAqAEN3dqOxRR+ViPZ/J/PMDS0BcCfEqHtQwaYs6/76sVimFzI3F5j +LbdsSDNspB1MS3bCtJ4P8Lm1ET60MgZZeuRECEUYj45SAQlQRr92N+lZVd8nxbSe +UN9vnx3fyG0iC7DLUBdVwDDiYUmZc4DlbyV3NcILI0j3cJY4Dnwj7ej6uvgPoVAt +y3kvQ93EnOmhYaW+5X0A/LnSrHLyCdUHjv2GG1ZkXOOiwVT8D6ZDmAZaE0iUCjIx +Y2qrSLk/qwKRStTaa8xfdta0zi+PLUkt5U2tGW6BboRO5IQ/B9JYZxQW2vP6eYym +tFm/Bk/UsFI8QhXovu4bS9LbACeUHg/gUQIDAQABo1MwUTAdBgNVHQ4EFgQUftB/ +qPGr3h/ddP9RTh04BgK7LwMwHwYDVR0jBBgwFoAUftB/qPGr3h/ddP9RTh04BgK7 +LwMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARWW2yEKK1BMk +948egHJ5Ab0YvKW0N9LuFPcTrP8Nnd7C4yF1xDFibtiy5vfRPzTbzrD1LRa3HZKX +2wMX/NGcN+OOLlyXpPwPBZiQwNSRQ2XTYuX5sLArO8ePGj7Py1QqpFhV8hh5WDn2 +zmRZk9O8E/sxn+xXLFCRV70PQPZbcRHtsWLfpqOoYXnZFdhL7QWPsGLnMkJoR7kd +Ao3U4vPHZuxM/zHViD+MF9713r/Ej8U3xZoeaLd0nvaK5fTYoKsomS6HW2fIeR05 +uD0wvf9Mnor60dGKTFCQyFwG1oGGhnMMicx3048erda7PgBsF7Hmf4MjHMRvOq3I +dJEmHk3Iyg== +-----END CERTIFICATE----- diff --git a/test/client_key.pem b/test/client_key.pem new file mode 100644 index 00000000..6f69467f --- /dev/null +++ b/test/client_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCoAQ3d2o7FFH5W +I9n8n88wNLQFwJ8Soe1DBpizr/vqxWKYXMjcXmMtt2xIM2ykHUxLdsK0ng/wubUR +PrQyBll65EQIRRiPjlIBCVBGv3Y36VlV3yfFtJ5Q32+fHd/IbSILsMtQF1XAMOJh +SZlzgOVvJXc1wgsjSPdwljgOfCPt6Pq6+A+hUC3LeS9D3cSc6aFhpb7lfQD8udKs +cvIJ1QeO/YYbVmRc46LBVPwPpkOYBloTSJQKMjFjaqtIuT+rApFK1NprzF921rTO +L48tSS3lTa0ZboFuhE7khD8H0lhnFBba8/p5jKa0Wb8GT9SwUjxCFei+7htL0tsA +J5QeD+BRAgMBAAECggEAG3HEo/Zf4TZGUb2x4/CakMY4luykRQ/E/vB/wca1++A6 +XtJgEOOtsZpSQCnVDarS5kxPD+ChkPz9OlpzEZhI8L2DluhBVAjMlj/BEzWT/caK +90So2MVMdsCb7t3F9etMpvqT/pA5TAKQUJKMfzpSAInvGI3gx5GpXi/OliCAYWIs +esP/LkMlemZQGMmXcbULFSW/Yi1H/ekHsm31imjVUgtqG9R1oRYXRSCOreT55QjV +wpp8TgU5KI8WoiNKZRSw4lkF/XHPq14F8PP+0+N6umV0aDrioO2FrQnZl1b27Xec +KNejXQL7I9JK+5bbW43onV0ebKINOf7bhm3sLNf5IwKBgQDQZwOQERaD9KiGS2ib +M+cFDdnctx53ep35gg9J7u9dyYG/WQhnrUtrYaOo7ptpMJ5WlxVcNTuGu0bpu8nI +SAL7GG0O2vESROqs+Wlab5orxR5u3MUNk5FGlgIxgUsILiXD4xzTCe83oe4ZwlJX +yoDdFGNu6cMUP6wLa0McAz3ThwKBgQDOYALpISlFFX94Ye4LWXKYtD0F+bsJldoN +I8QPNKVGKgI9LqtKGLkpq77dGHtquMUg7jYnTOoXroOr1z+VWQhtVYG8XtM7gPF2 +lsqWUkle5mmLtkjJcgr9b+ee60kHFKGfaJlpbRR8Lz6IK9a2J1OxdjqvUn4S70eT ++0gg801TZwKBgQCJLbXfqA/dDje8JnkV6GVCI2rr6THJzdGcwmrT7M5tOs4IKU/q +Bt0lMuEqut1bsWAYeVzbFEM5nZ7BDhZ1mkk0BVEMPTwAHZMoBwi9OccO1rMAoJup +IyC2iNmqwoOkP9QmDCIWHGz1fsae+BWBqk+Gtvv4rzD07DCQV6uWDeAtkwKBgHwM +SBObpDPrXSieLLjTtkdFp5yM5Lk5Qs09H19IdMO9AoWGJN2wCLSckGhTi/O1RoCp +zxFGcTt04Z6MDqMV1jPp/sacdPnCYuG2d+VtZr7NXsnk8tFrZGG8PwxOPyIra47n +D7fIIlUXDM5LE4+AChWUjGfP/Qoim/K+SzfLJ0KnAoGBAIWCVT72zBoN37zOmLEp +9nPCEyx6MVMOLddcRLQC6rz1I37CY+q7rbV4/5k6wGWM+yFLL3t4YBLkYBqWlUJF +O24Vv9bYn+ZoIuaTVw0DS/gWl7UsWrffC66qbxYBexNRFSfpCY2IRrIObM9bsn2t +f9k/lqYi0OhZKo1M2UU4GIm2 +-----END PRIVATE KEY----- diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 4446ed92..d2536e0b 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #ifdef HAVE_GNUTLS #include @@ -932,6 +933,228 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, tls_session_getters) ws.stop(); } LT_END_AUTO_TEST(tls_session_getters) + +// Resource that extracts client certificate info +class client_cert_info_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request& req) { + std::string response; + if (req.has_client_certificate()) { + response = "HAS_CLIENT_CERT"; + std::string dn = req.get_client_cert_dn(); + std::string issuer = req.get_client_cert_issuer_dn(); + std::string cn = req.get_client_cert_cn(); + std::string fingerprint = req.get_client_cert_fingerprint_sha256(); + bool verified = req.is_client_cert_verified(); + time_t not_before = req.get_client_cert_not_before(); + time_t not_after = req.get_client_cert_not_after(); + + response += "|DN:" + dn; + response += "|ISSUER:" + issuer; + response += "|CN:" + cn; + response += "|FP:" + fingerprint; + response += "|VERIFIED:" + std::string(verified ? "yes" : "no"); + response += "|NOT_BEFORE:" + std::to_string(not_before); + response += "|NOT_AFTER:" + std::to_string(not_after); + } else { + response = "NO_CLIENT_CERT"; + } + return std::make_shared(response, 200, "text/plain"); + } +}; + +// Test client certificate methods without a client certificate (no mTLS) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_no_certificate) + int port = PORT + 46; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem"); + client_cert_info_resource cert_info; + LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); + bool started = ws.start(false); + if (!started) { + LT_CHECK_EQ(1, 1); + } else { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "NO_CLIENT_CERT"); + curl_easy_cleanup(curl); + ws.stop(); + } +LT_END_AUTO_TEST(client_cert_no_certificate) + +// Test client certificate methods with mTLS (client sends certificate) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_with_certificate) + int port = PORT + 47; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .https_mem_trust(ROOT "/client_cert.pem"); // Trust the client cert as CA + client_cert_info_resource cert_info; + LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); + bool started = ws.start(false); + if (!started) { + LT_CHECK_EQ(1, 1); + } else { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); + res = curl_easy_perform(curl); + if (res == 0) { + // Check that we got client cert info + LT_CHECK_NEQ(s.find("HAS_CLIENT_CERT"), std::string::npos); + LT_CHECK_NEQ(s.find("CN:Test Client"), std::string::npos); + LT_CHECK_NEQ(s.find("FP:"), std::string::npos); + } + curl_easy_cleanup(curl); + ws.stop(); + } +LT_END_AUTO_TEST(client_cert_with_certificate) + +// Test client certificate DN extraction +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_dn_extraction) + int port = PORT + 48; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .https_mem_trust(ROOT "/client_cert.pem"); + client_cert_info_resource cert_info; + LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); + bool started = ws.start(false); + if (!started) { + LT_CHECK_EQ(1, 1); + } else { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); + res = curl_easy_perform(curl); + if (res == 0) { + // Check DN contains expected organization + LT_CHECK_NEQ(s.find("O=Test Org"), std::string::npos); + } + curl_easy_cleanup(curl); + ws.stop(); + } +LT_END_AUTO_TEST(client_cert_dn_extraction) + +// Test client certificate fingerprint generation +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_fingerprint) + int port = PORT + 49; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .https_mem_trust(ROOT "/client_cert.pem"); + client_cert_info_resource cert_info; + LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); + bool started = ws.start(false); + if (!started) { + LT_CHECK_EQ(1, 1); + } else { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); + res = curl_easy_perform(curl); + if (res == 0) { + // Fingerprint should be 64 hex characters (32 bytes SHA-256) + size_t fp_pos = s.find("FP:"); + if (fp_pos != std::string::npos) { + size_t fp_end = s.find("|", fp_pos); + if (fp_end != std::string::npos) { + std::string fp = s.substr(fp_pos + 3, fp_end - fp_pos - 3); + LT_CHECK_EQ(fp.length(), 64u); + } + } + } + curl_easy_cleanup(curl); + ws.stop(); + } +LT_END_AUTO_TEST(client_cert_fingerprint) + +// Test SNI callback configuration +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, sni_callback_setup) + int port = PORT + 50; + + // Simple SNI callback that returns empty (uses default cert) + auto sni_cb = [](const std::string& server_name) -> std::pair { + std::ignore = server_name; + return {"", ""}; // Use default cert + }; + + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .sni_callback(sni_cb); + + ok_resource ok; + LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); + bool started = ws.start(false); + + if (started) { + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + ws.stop(); + } + LT_CHECK_EQ(1, 1); // Test passes if server starts +LT_END_AUTO_TEST(sni_callback_setup) #endif // HAVE_GNUTLS #endif // _WINDOWS From 5ff1183ab9637b7d7af32796c7264a518f77e8e2 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 10:23:40 -0800 Subject: [PATCH 22/47] Fix test: add client certificate files to AC_CONFIG_FILES The client_cert.pem and client_key.pem files need to be copied to the build directory during configure, similar to how other test certificate files are handled. --- configure.ac | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configure.ac b/configure.ac index 50aa008a..03eba100 100644 --- a/configure.ac +++ b/configure.ac @@ -300,6 +300,8 @@ AC_CONFIG_FILES([test/test_content_large:test/test_content_large]) AC_CONFIG_FILES([test/cert.pem:test/cert.pem]) AC_CONFIG_FILES([test/key.pem:test/key.pem]) AC_CONFIG_FILES([test/test_root_ca.pem:test/test_root_ca.pem]) +AC_CONFIG_FILES([test/client_cert.pem:test/client_cert.pem]) +AC_CONFIG_FILES([test/client_key.pem:test/client_key.pem]) AC_CONFIG_FILES([test/libhttpserver.supp:test/libhttpserver.supp]) AC_CONFIG_FILES([examples/cert.pem:examples/cert.pem]) AC_CONFIG_FILES([examples/key.pem:examples/key.pem]) From 2a6d12ae4f1dd3d14861125a9ab880b3473b5801 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 12:11:18 -0800 Subject: [PATCH 23/47] Fix HTTPS test pattern: use try-catch instead of checking start() return value The webserver::start(false) method returns false when started successfully in non-blocking mode (by design). The tests were incorrectly interpreting this as a failure. Fixed by catching the exception that's thrown when the server actually fails to start. Also regenerated test certificates with valid dates (the old ones expired in 2009). Changes: - Fix https_webserver, tls_session_getters, and all client_cert tests - Regenerate cert.pem, key.pem, client_cert.pem, client_key.pem --- test/cert.pem | 32 ++--- test/client_cert.pem | 32 ++--- test/client_key.pem | 52 ++++---- test/integ/ws_start_stop.cpp | 245 ++++++++++++++++++----------------- test/key.pem | 50 +++---- 5 files changed, 210 insertions(+), 201 deletions(-) diff --git a/test/cert.pem b/test/cert.pem index 2c766dff..b95e01f0 100644 --- a/test/cert.pem +++ b/test/cert.pem @@ -1,17 +1,19 @@ -----BEGIN CERTIFICATE----- -MIICpjCCAZCgAwIBAgIESEPtjjALBgkqhkiG9w0BAQUwADAeFw0wODA2MDIxMjU0 -MzhaFw0wOTA2MDIxMjU0NDZaMAAwggEfMAsGCSqGSIb3DQEBAQOCAQ4AMIIBCQKC -AQC03TyUvK5HmUAirRp067taIEO4bibh5nqolUoUdo/LeblMQV+qnrv/RNAMTx5X -fNLZ45/kbM9geF8qY0vsPyQvP4jumzK0LOJYuIwmHaUm9vbXnYieILiwCuTgjaud -3VkZDoQ9fteIo+6we9UTpVqZpxpbLulBMh/VsvX0cPJ1VFC7rT59o9hAUlFf9jX/ -GmKdYI79MtgVx0OPBjmmSD6kicBBfmfgkO7bIGwlRtsIyMznxbHu6VuoX/eVxrTv -rmCwgEXLWRZ6ru8MQl5YfqeGXXRVwMeXU961KefbuvmEPccgCxm8FZ1C1cnDHFXh -siSgAzMBjC/b6KVhNQ4KnUdZAgMBAAGjLzAtMAwGA1UdEwEB/wQCMAAwHQYDVR0O -BBYEFJcUvpjvE5fF/yzUshkWDpdYiQh/MAsGCSqGSIb3DQEBBQOCAQEARP7eKSB2 -RNd6XjEjK0SrxtoTnxS3nw9sfcS7/qD1+XHdObtDFqGNSjGYFB3Gpx8fpQhCXdoN -8QUs3/5ZVa5yjZMQewWBgz8kNbnbH40F2y81MHITxxCe1Y+qqHWwVaYLsiOTqj2/ -0S3QjEJ9tvklmg7JX09HC4m5QRYfWBeQLD1u8ZjA1Sf1xJriomFVyRLI2VPO2bNe -JDMXWuP+8kMC7gEvUnJ7A92Y2yrhu3QI3bjPk8uSpHea19Q77tul1UVBJ5g+zpH3 -OsF5p0MyaVf09GTzcLds5nE/osTdXGUyHJapWReVmPm3Zn6gqYlnzD99z+DPIgIV -RhZvQx74NQnS6g== +MIIDCTCCAfGgAwIBAgIUFSkcZr3SpJgnSFZ7usAd7EHeL6YwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDIwNDE5MzQyMFoXDTM2MDIw +MjE5MzQyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAotKw+oEWvVB3Gr5cQDeREfkrYz3wQr/iBXQcxieHgm2O ++zddEKgGIzZGLWAFt4dERt9EIPuhyIs5cX70d7SDPZEkq9ne1qg8wxo9BoLj6pGq +iLzbmfhjOsApSBIMEo9j461YPgJvmoPcR9WtJQwxtPCaBaDe/GuuQlE4c9Ocfn5c +Y/cQ7r0LpIXpz+2I3IXeMJNPClNTEcOn3jM/mdCkechsyGgwTSxup019HPQNCefY +27SRyjgKn476WTWP3HSzuz+vdJeeOsr3imCWAbLU0Y3g7bW9HddCKBpu+9Er7A8T +7Tizuqid4ZxWCBjoUKW3PGZXb5GN27hamdOuYXuu+wIDAQABo1MwUTAdBgNVHQ4E +FgQURWjt3upGzg4lwPOvAC5T6IIFjxAwHwYDVR0jBBgwFoAURWjt3upGzg4lwPOv +AC5T6IIFjxAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADn3r +keOKT0INpytFjwedeC9TNN0W0PFGoPCl6/aPQpRD5adY0xaOMaFMrfuew0I7dI0m +Ro9HICBQ4DLHB/ZwjvuioJSQlwYJ6SnatKlZib0qAHMvnSLr/rWUKu5KzLIhvXHs +6zG7/ZqRt6XlME4olJ/QzyhyPtXK2AumHdB/GJk9d//n4Qj+4cXSTA1KHxZPU67x +0Ow0zI0CRgDN4sYlgOcLwMI0I59MwXlzIeMR6E2YSxow7P+89kFMRmaO5N1aCSXl +PYOlkXbh4iZ2cBMj4dfQBA+cgkm+KjVr/jwpBlAZJswtkyDD+zJf+ua+z1eOczBv +HsZIDEqIkkqH/ZuV7w== -----END CERTIFICATE----- diff --git a/test/client_cert.pem b/test/client_cert.pem index c50de4a5..b8e3d817 100644 --- a/test/client_cert.pem +++ b/test/client_cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDMzCCAhugAwIBAgIUVIu0IAY17b7zVLt1hypR3Ag3IuowDQYJKoZIhvcNAQEL +MIIDMzCCAhugAwIBAgIUdGMRlO8Hu2eYWdVXgkHQ57NXeJ8wDQYJKoZIhvcNAQEL BQAwKTEUMBIGA1UEAwwLVGVzdCBDbGllbnQxETAPBgNVBAoMCFRlc3QgT3JnMB4X -DTI2MDIwNDE1NTY0MloXDTI3MDIwNDE1NTY0MlowKTEUMBIGA1UEAwwLVGVzdCBD +DTI2MDIwNDE5MjEzOVoXDTM2MDIwMjE5MjEzOVowKTEUMBIGA1UEAwwLVGVzdCBD bGllbnQxETAPBgNVBAoMCFRlc3QgT3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAqAEN3dqOxRR+ViPZ/J/PMDS0BcCfEqHtQwaYs6/76sVimFzI3F5j -LbdsSDNspB1MS3bCtJ4P8Lm1ET60MgZZeuRECEUYj45SAQlQRr92N+lZVd8nxbSe -UN9vnx3fyG0iC7DLUBdVwDDiYUmZc4DlbyV3NcILI0j3cJY4Dnwj7ej6uvgPoVAt -y3kvQ93EnOmhYaW+5X0A/LnSrHLyCdUHjv2GG1ZkXOOiwVT8D6ZDmAZaE0iUCjIx -Y2qrSLk/qwKRStTaa8xfdta0zi+PLUkt5U2tGW6BboRO5IQ/B9JYZxQW2vP6eYym -tFm/Bk/UsFI8QhXovu4bS9LbACeUHg/gUQIDAQABo1MwUTAdBgNVHQ4EFgQUftB/ -qPGr3h/ddP9RTh04BgK7LwMwHwYDVR0jBBgwFoAUftB/qPGr3h/ddP9RTh04BgK7 -LwMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARWW2yEKK1BMk -948egHJ5Ab0YvKW0N9LuFPcTrP8Nnd7C4yF1xDFibtiy5vfRPzTbzrD1LRa3HZKX -2wMX/NGcN+OOLlyXpPwPBZiQwNSRQ2XTYuX5sLArO8ePGj7Py1QqpFhV8hh5WDn2 -zmRZk9O8E/sxn+xXLFCRV70PQPZbcRHtsWLfpqOoYXnZFdhL7QWPsGLnMkJoR7kd -Ao3U4vPHZuxM/zHViD+MF9713r/Ej8U3xZoeaLd0nvaK5fTYoKsomS6HW2fIeR05 -uD0wvf9Mnor60dGKTFCQyFwG1oGGhnMMicx3048erda7PgBsF7Hmf4MjHMRvOq3I -dJEmHk3Iyg== +MIIBCgKCAQEAuZ5JmKPRbM9UfeQ9cJMne6Lt084gQsw+yI2hHwvkeYm+8c/HdQ3E +YKsCojON6X6gMInvblvvsJaRNtBOAUaHoOshDH9ZeZD3hsd3fmyxIqQKCOr1DoxZ ++72FmHNHcGfcti1KVwrxMHhL5TUhDJfoVPcH0OO0Yo7JI0PzdZTkoUVZN1mqQ3M2 +zS5KqyyQ/+M02VmUdI7CQezextCzQj2BLgyy2/WJOuEUUtDn35VXTt0bvs95ICnm +tWhKDHzIceJGCLUFbtVPmsj/zutYv6RUkg02nx3a+l3leD5kboLD41O8G1SwyAw2 +g7WfifFO0B0QDoAQ5sqgMrnHClAmc46FAwIDAQABo1MwUTAdBgNVHQ4EFgQUY6/3 +etxsZGycCVNS3/yVUP2mgOQwHwYDVR0jBBgwFoAUY6/3etxsZGycCVNS3/yVUP2m +gOQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQxOYgHw3T+Ko +BXmegtQGWSBUuu2ootg26zIDt4x2dwT3fPuitFJZnywG/1EHs9qVPPlPbtqMqpmT +kxxcpanZPpTLwj+QbrrkqKfIq1qlXivrjsP+idALO4zouSJBTqpC+wksL2AxdegX +FF3zCMtw+LVxgrwU4Ml/ydNu1Z1Zq1KDZOXEOug9C/CABEgngQfr3IO9M7wQIYvf +pgieUOQxPM6O5kS0yBp/WGDwYjz0Ijbfp/yvel9eaQgvMT3rmKI8/fOCM5ax+IEk +0eJaz4dS9GkSQT+mAkAT/PKurkDpSmPz/If/CyLScSr6f/s/+fC0YLfZ95/FjNqx +O/ONgBdcbw== -----END CERTIFICATE----- diff --git a/test/client_key.pem b/test/client_key.pem index 6f69467f..49a96fa0 100644 --- a/test/client_key.pem +++ b/test/client_key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCoAQ3d2o7FFH5W -I9n8n88wNLQFwJ8Soe1DBpizr/vqxWKYXMjcXmMtt2xIM2ykHUxLdsK0ng/wubUR -PrQyBll65EQIRRiPjlIBCVBGv3Y36VlV3yfFtJ5Q32+fHd/IbSILsMtQF1XAMOJh -SZlzgOVvJXc1wgsjSPdwljgOfCPt6Pq6+A+hUC3LeS9D3cSc6aFhpb7lfQD8udKs -cvIJ1QeO/YYbVmRc46LBVPwPpkOYBloTSJQKMjFjaqtIuT+rApFK1NprzF921rTO -L48tSS3lTa0ZboFuhE7khD8H0lhnFBba8/p5jKa0Wb8GT9SwUjxCFei+7htL0tsA -J5QeD+BRAgMBAAECggEAG3HEo/Zf4TZGUb2x4/CakMY4luykRQ/E/vB/wca1++A6 -XtJgEOOtsZpSQCnVDarS5kxPD+ChkPz9OlpzEZhI8L2DluhBVAjMlj/BEzWT/caK -90So2MVMdsCb7t3F9etMpvqT/pA5TAKQUJKMfzpSAInvGI3gx5GpXi/OliCAYWIs -esP/LkMlemZQGMmXcbULFSW/Yi1H/ekHsm31imjVUgtqG9R1oRYXRSCOreT55QjV -wpp8TgU5KI8WoiNKZRSw4lkF/XHPq14F8PP+0+N6umV0aDrioO2FrQnZl1b27Xec -KNejXQL7I9JK+5bbW43onV0ebKINOf7bhm3sLNf5IwKBgQDQZwOQERaD9KiGS2ib -M+cFDdnctx53ep35gg9J7u9dyYG/WQhnrUtrYaOo7ptpMJ5WlxVcNTuGu0bpu8nI -SAL7GG0O2vESROqs+Wlab5orxR5u3MUNk5FGlgIxgUsILiXD4xzTCe83oe4ZwlJX -yoDdFGNu6cMUP6wLa0McAz3ThwKBgQDOYALpISlFFX94Ye4LWXKYtD0F+bsJldoN -I8QPNKVGKgI9LqtKGLkpq77dGHtquMUg7jYnTOoXroOr1z+VWQhtVYG8XtM7gPF2 -lsqWUkle5mmLtkjJcgr9b+ee60kHFKGfaJlpbRR8Lz6IK9a2J1OxdjqvUn4S70eT -+0gg801TZwKBgQCJLbXfqA/dDje8JnkV6GVCI2rr6THJzdGcwmrT7M5tOs4IKU/q -Bt0lMuEqut1bsWAYeVzbFEM5nZ7BDhZ1mkk0BVEMPTwAHZMoBwi9OccO1rMAoJup -IyC2iNmqwoOkP9QmDCIWHGz1fsae+BWBqk+Gtvv4rzD07DCQV6uWDeAtkwKBgHwM -SBObpDPrXSieLLjTtkdFp5yM5Lk5Qs09H19IdMO9AoWGJN2wCLSckGhTi/O1RoCp -zxFGcTt04Z6MDqMV1jPp/sacdPnCYuG2d+VtZr7NXsnk8tFrZGG8PwxOPyIra47n -D7fIIlUXDM5LE4+AChWUjGfP/Qoim/K+SzfLJ0KnAoGBAIWCVT72zBoN37zOmLEp -9nPCEyx6MVMOLddcRLQC6rz1I37CY+q7rbV4/5k6wGWM+yFLL3t4YBLkYBqWlUJF -O24Vv9bYn+ZoIuaTVw0DS/gWl7UsWrffC66qbxYBexNRFSfpCY2IRrIObM9bsn2t -f9k/lqYi0OhZKo1M2UU4GIm2 +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5nkmYo9Fsz1R9 +5D1wkyd7ou3TziBCzD7IjaEfC+R5ib7xz8d1DcRgqwKiM43pfqAwie9uW++wlpE2 +0E4BRoeg6yEMf1l5kPeGx3d+bLEipAoI6vUOjFn7vYWYc0dwZ9y2LUpXCvEweEvl +NSEMl+hU9wfQ47RijskjQ/N1lOShRVk3WapDczbNLkqrLJD/4zTZWZR0jsJB7N7G +0LNCPYEuDLLb9Yk64RRS0OfflVdO3Ru+z3kgKea1aEoMfMhx4kYItQVu1U+ayP/O +61i/pFSSDTafHdr6XeV4PmRugsPjU7wbVLDIDDaDtZ+J8U7QHRAOgBDmyqAyuccK +UCZzjoUDAgMBAAECggEABpAV/7txuPxUUJvZykRXBd7ltB1FHxIpTsj8fzmvQIGC +H/rzN+xz2uEhAjV6w9yHcxU6wyGlW2PaIDXZlDT3WwAFQUkIE61wUF0hbfW6MO8w +2vjdD5XJzAX668AjsKc+HVjY7d6ZVtfiAxQk+3+Be5jnt1czC7W1tIqqAyhvtFNN +Ga7JLHbybxSi5batdsZXdlchCOtD0ZQYd0fS/WpB+RSyVj01j0trkg+uL/ok0XFj +Y9SYIWKIdPn2gPs9oUXhxjoTUQYi6iqZD8lp/4qDbCw4nidz/rVCu1UuCtEV44it +00AqSO3NadRLpTmxi7TKDj+IekzXKJUEKzlNtscXIQKBgQDn4C5q8k2r2wwV+uJ9 +hU0d0YDCAzyUlOPjXBv6/dj62hRKvmo0fbisTXKyrSMLoXNOQdkYAASQoRYUqoL9 +feKpDU+luCfGnr9hg/odM4ESP16UZbJirajHll/RQp4yYAi3vhvEvzgQKiUInGwI +G4BC6/Ah+9YTSyNMDnN2Uf45IwKBgQDM7hQUswlrH+a7utmuHnMBPvwUMPOsbpF5 +lHjYwjOmWSAwQa846n5bJvHMuJZohX0ntR/skl0lYAuh72sFsKQvnQtiIhZ6rkbf +YMh9RPgVfAXFJlFAV53iw+u3pghSnkeIugbCoYn2Lz8To4RYD9mlj4pr0K5hxVaT +tGvp2QlyoQKBgGfW4FKqghgVN3tcaDN4D8nruXKpCmcrqkZ2SF2FcrccFHxIe71Y +E+ytnlDf8lLSEZYZLQRvdZvjV8UXeyPUTT4RpPp81us+ykv8U3TiTMoEMPHZ/SHt +zSjccbp/z+KVWTIX482fKJcsmHsbudGDp1PQ3zAI3Jy1SHBWBGUXYPbrAoGBALtD +R0hO/mlMonyj1uzcWD0oQBN3VAQamYbfHLr+Y1I8GUTfkO3SohpLcSOg/ZiPevmA +8qYsbT+ND7QvYr21V6NGv7Mx8Ra0EIFpIGwQTR7c0S0BwbepGNayL8EG0I4mormX +PDw4fyheriYVAwexnDJFA7lX3THssRuSABaVxKNhAoGAY0ejNhVt4pXTeXWH3l/Z +TFPg+EKFCvn9dM8YeN+X1hMurRUb7cg9blx71mQmcYRZtnqQ1EmLSe1iMT1dXTi9 +xLdK5M7LR+rMF0FGHmj8po3tLzkQwYqDjVoEa8cMJun0sZdjP4npP7XA/9T6LMdj +7kCN3QfCVwW6uHLA27zbDKc= -----END PRIVATE KEY----- diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index d2536e0b..9efeee38 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -878,11 +878,14 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, https_webserver) .https_mem_cert(ROOT "/cert.pem"); ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); - if (!started) { + try { + ws.start(false); + } catch (const std::exception& e) { // SSL setup may fail in some environments, skip the test LT_CHECK_EQ(1, 1); - } else { + return; + } + { curl_global_init(CURL_GLOBAL_ALL); std::string s; CURL *curl = curl_easy_init(); @@ -910,28 +913,29 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, tls_session_getters) .https_mem_cert(ROOT "/cert.pem"); tls_info_resource tls_info; LT_ASSERT_EQ(true, ws.register_resource("tls_info", &tls_info)); - bool started = ws.start(false); - if (!started) { + try { + ws.start(false); + } catch (const std::exception& e) { // SSL setup may fail in some environments, skip the test LT_CHECK_EQ(1, 1); - } else { - curl_global_init(CURL_GLOBAL_ALL); - std::string s; - CURL *curl = curl_easy_init(); - CURLcode res; - std::string url = "https://localhost:" + std::to_string(port) + "/tls_info"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - res = curl_easy_perform(curl); - LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "TLS_SESSION_PRESENT"); - curl_easy_cleanup(curl); - ws.stop(); + return; } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/tls_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "TLS_SESSION_PRESENT"); + curl_easy_cleanup(curl); + ws.stop(); LT_END_AUTO_TEST(tls_session_getters) // Resource that extracts client certificate info @@ -972,27 +976,29 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_no_certificate) .https_mem_cert(ROOT "/cert.pem"); client_cert_info_resource cert_info; LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); - bool started = ws.start(false); - if (!started) { + try { + ws.start(false); + } catch (const std::exception& e) { + // SSL setup may fail in some environments, skip the test LT_CHECK_EQ(1, 1); - } else { - curl_global_init(CURL_GLOBAL_ALL); - std::string s; - CURL *curl = curl_easy_init(); - CURLcode res; - std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - res = curl_easy_perform(curl); - LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "NO_CLIENT_CERT"); - curl_easy_cleanup(curl); - ws.stop(); + return; } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "NO_CLIENT_CERT"); + curl_easy_cleanup(curl); + ws.stop(); LT_END_AUTO_TEST(client_cert_no_certificate) // Test client certificate methods with mTLS (client sends certificate) @@ -1005,33 +1011,34 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_with_certificate) .https_mem_trust(ROOT "/client_cert.pem"); // Trust the client cert as CA client_cert_info_resource cert_info; LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); - bool started = ws.start(false); - if (!started) { + try { + ws.start(false); + } catch (const std::exception& e) { + // SSL setup may fail in some environments, skip the test LT_CHECK_EQ(1, 1); - } else { - curl_global_init(CURL_GLOBAL_ALL); - std::string s; - CURL *curl = curl_easy_init(); - CURLcode res; - std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); - curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); - res = curl_easy_perform(curl); - if (res == 0) { - // Check that we got client cert info - LT_CHECK_NEQ(s.find("HAS_CLIENT_CERT"), std::string::npos); - LT_CHECK_NEQ(s.find("CN:Test Client"), std::string::npos); - LT_CHECK_NEQ(s.find("FP:"), std::string::npos); - } - curl_easy_cleanup(curl); - ws.stop(); + return; } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Check that we got client cert info + LT_CHECK_NEQ(s.find("HAS_CLIENT_CERT"), std::string::npos); + LT_CHECK_NEQ(s.find("CN:Test Client"), std::string::npos); + LT_CHECK_NEQ(s.find("FP:"), std::string::npos); + curl_easy_cleanup(curl); + ws.stop(); LT_END_AUTO_TEST(client_cert_with_certificate) // Test client certificate DN extraction @@ -1044,31 +1051,32 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_dn_extraction) .https_mem_trust(ROOT "/client_cert.pem"); client_cert_info_resource cert_info; LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); - bool started = ws.start(false); - if (!started) { + try { + ws.start(false); + } catch (const std::exception& e) { + // SSL setup may fail in some environments, skip the test LT_CHECK_EQ(1, 1); - } else { - curl_global_init(CURL_GLOBAL_ALL); - std::string s; - CURL *curl = curl_easy_init(); - CURLcode res; - std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); - curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); - res = curl_easy_perform(curl); - if (res == 0) { - // Check DN contains expected organization - LT_CHECK_NEQ(s.find("O=Test Org"), std::string::npos); - } - curl_easy_cleanup(curl); - ws.stop(); + return; } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Check DN contains expected organization + LT_CHECK_NEQ(s.find("O=Test Org"), std::string::npos); + curl_easy_cleanup(curl); + ws.stop(); LT_END_AUTO_TEST(client_cert_dn_extraction) // Test client certificate fingerprint generation @@ -1081,38 +1089,37 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_fingerprint) .https_mem_trust(ROOT "/client_cert.pem"); client_cert_info_resource cert_info; LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); - bool started = ws.start(false); - if (!started) { + try { + ws.start(false); + } catch (const std::exception& e) { + // SSL setup may fail in some environments, skip the test LT_CHECK_EQ(1, 1); - } else { - curl_global_init(CURL_GLOBAL_ALL); - std::string s; - CURL *curl = curl_easy_init(); - CURLcode res; - std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); - curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); - res = curl_easy_perform(curl); - if (res == 0) { - // Fingerprint should be 64 hex characters (32 bytes SHA-256) - size_t fp_pos = s.find("FP:"); - if (fp_pos != std::string::npos) { - size_t fp_end = s.find("|", fp_pos); - if (fp_end != std::string::npos) { - std::string fp = s.substr(fp_pos + 3, fp_end - fp_pos - 3); - LT_CHECK_EQ(fp.length(), 64u); - } - } - } - curl_easy_cleanup(curl); - ws.stop(); + return; } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key.pem"); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Fingerprint should be 64 hex characters (32 bytes SHA-256) + size_t fp_pos = s.find("FP:"); + LT_ASSERT_NEQ(fp_pos, std::string::npos); + size_t fp_end = s.find("|", fp_pos); + LT_ASSERT_NEQ(fp_end, std::string::npos); + std::string fp = s.substr(fp_pos + 3, fp_end - fp_pos - 3); + LT_CHECK_EQ(fp.length(), 64u); + curl_easy_cleanup(curl); + ws.stop(); LT_END_AUTO_TEST(client_cert_fingerprint) // Test SNI callback configuration diff --git a/test/key.pem b/test/key.pem index a5848eed..3b04751d 100644 --- a/test/key.pem +++ b/test/key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAtN08lLyuR5lAIq0adOu7WiBDuG4m4eZ6qJVKFHaPy3m5TEFf -qp67/0TQDE8eV3zS2eOf5GzPYHhfKmNL7D8kLz+I7psytCziWLiMJh2lJvb2152I -niC4sArk4I2rnd1ZGQ6EPX7XiKPusHvVE6VamacaWy7pQTIf1bL19HDydVRQu60+ -faPYQFJRX/Y1/xpinWCO/TLYFcdDjwY5pkg+pInAQX5n4JDu2yBsJUbbCMjM58Wx -7ulbqF/3lca0765gsIBFy1kWeq7vDEJeWH6nhl10VcDHl1PetSnn27r5hD3HIAsZ -vBWdQtXJwxxV4bIkoAMzAYwv2+ilYTUOCp1HWQIDAQABAoIBAArOQv3R7gmqDspj -lDaTFOz0C4e70QfjGMX0sWnakYnDGn6DU19iv3GnX1S072ejtgc9kcJ4e8VUO79R -EmqpdRR7k8dJr3RTUCyjzf/C+qiCzcmhCFYGN3KRHA6MeEnkvRuBogX4i5EG1k5l -/5t+YBTZBnqXKWlzQLKoUAiMLPg0eRWh+6q7H4N7kdWWBmTpako7TEqpIwuEnPGx -u3EPuTR+LN6lF55WBePbCHccUHUQaXuav18NuDkcJmCiMArK9SKb+h0RqLD6oMI/ -dKD6n8cZXeMBkK+C8U/K0sN2hFHACsu30b9XfdnljgP9v+BP8GhnB0nCB6tNBCPo -32srOwECgYEAxWh3iBT4lWqL6bZavVbnhmvtif4nHv2t2/hOs/CAq8iLAw0oWGZc -+JEZTUDMvFRlulr0kcaWra+4fN3OmJnjeuFXZq52lfMgXBIKBmoSaZpIh2aDY1Rd -RbEse7nQl9hTEPmYspiXLGtnAXW7HuWqVfFFP3ya8rUS3t4d07Hig8ECgYEA6ou6 -OHiBRTbtDqLIv8NghARc/AqwNWgEc9PelCPe5bdCOLBEyFjqKiT2MttnSSUc2Zob -XhYkHC6zN1Mlq30N0e3Q61YK9LxMdU1vsluXxNq2rfK1Scb1oOlOOtlbV3zA3VRF -hV3t1nOA9tFmUrwZi0CUMWJE/zbPAyhwWotKyZkCgYEAh0kFicPdbABdrCglXVae -SnfSjVwYkVuGd5Ze0WADvjYsVkYBHTvhgRNnRJMg+/vWz3Sf4Ps4rgUbqK8Vc20b -AU5G6H6tlCvPRGm0ZxrwTWDHTcuKRVs+pJE8C/qWoklE/AAhjluWVoGwUMbPGuiH -6Gf1bgHF6oj/Sq7rv/VLZ8ECgYBeq7ml05YyLuJutuwa4yzQ/MXfghzv4aVyb0F3 -QCdXR6o2IYgR6jnSewrZKlA9aPqFJrwHNR6sNXlnSmt5Fcf/RWO/qgJQGLUv3+rG -7kuLTNDR05azSdiZc7J89ID3Bkb+z2YkV+6JUiPq/Ei1+nDBEXb/m+/HqALU/nyj -P3gXeQKBgBusb8Rbd+KgxSA0hwY6aoRTPRt8LNvXdsB9vRcKKHUFQvxUWiUSS+L9 -/Qu1sJbrUquKOHqksV5wCnWnAKyJNJlhHuBToqQTgKXjuNmVdYSe631saiI7PHyC -eRJ6DxULPxABytJrYCRrNqmXi5TCiqR2mtfalEMOPxz8rUU8dYyx +MIIEpAIBAAKCAQEAotKw+oEWvVB3Gr5cQDeREfkrYz3wQr/iBXQcxieHgm2O+zdd +EKgGIzZGLWAFt4dERt9EIPuhyIs5cX70d7SDPZEkq9ne1qg8wxo9BoLj6pGqiLzb +mfhjOsApSBIMEo9j461YPgJvmoPcR9WtJQwxtPCaBaDe/GuuQlE4c9Ocfn5cY/cQ +7r0LpIXpz+2I3IXeMJNPClNTEcOn3jM/mdCkechsyGgwTSxup019HPQNCefY27SR +yjgKn476WTWP3HSzuz+vdJeeOsr3imCWAbLU0Y3g7bW9HddCKBpu+9Er7A8T7Tiz +uqid4ZxWCBjoUKW3PGZXb5GN27hamdOuYXuu+wIDAQABAoIBADD0F7G5ThTtNGIe +Ca5lBoDY4WqdHLd06YeqOVx6Vguo1OxC4QA5BF9h2geabx2W1bhZOCqSfTnGYib1 +fJrg8vR3xwbEInN3cY1XPjHO+Kd11Ef4QC4yt+LaE49PncGWyvmRDI7YPKXAL2KJ +o90XpXo5PJWkoGZUGbhmowpv/QUqjcLCt4djbELl+ZUOoYpkl4S8RnSy8M9Q3W5l +IVE7aLvZ8K5NuWXAXC4V3UruWgfO7HtGea1ce9UIaKOPu3sO1dUnP2go4yp5Q6H+ +QssAyLXBfjfPNxaosS44WzL5FyjDyG99ziZyhDFAt+bZ169UUUCV1AyPrqGbekfX +hdLgiQECgYEA1mDeSpntQDBdSIhLZ1GLBpUhSR7l3/KLzfleaTUrqNargZXokgks +XzI9TBdXJ0EX9M/16hsQwMkGX6JhxgaPy0JSbLdYbjIep8kahvLbJ8FWY/JkA3p2 +8m3yY/bYnWFfSKUUgy8yWRhU5C1b9oS//bxA8VyMVNc4mx+S5duC7msCgYEAwm9k +7ocu9G1fqLlWy0LEWo1dTEXwjFmBe1HUUk8RXXPj5tQkrRVpvZL4jbl7kqhe9UVk +X0sVtRUnPpLBgfpYrwvu9+lQFhwNT4E5G7jWy9kZ0G1fdZYTMPm/Jp+t5sLhC5O6 +NAX/HwH3MHuco4QGJVMnGv/zgwE/4RxE+J2WRbECgYEAuwqxaC18zrBj81DXWUHQ +JuIetIl8zzPzvraAJRL7EMibwuhkjmXqjPRsfuMua1Vj7Xk0ehk7OLksEmy/GePH +ufQXrjsZsKuSC5puxqdFhx4sne9yS4aiGUrMXWOWA1pdpChECWE4cHvGNX9N6XxR +drS1hODWn39YKCAYLuyjBBkCgYA+RGZSbUCATraf1hsRpSQ0y6jhUFSk3dU1pRMV ++PRatU57Ed1dAMqIR5UJ7ijA4uLmMX7fdbBR+aBDzcPi2EWmaW/yPOnE6t7oYz3i +vuMrDS/TK/OyOImU2aZ5vBF5IVfo2Tp8hp8ZUwvSnwOe6hz9vw96+hUGE1Rdxyvf +YrhJQQKBgQCrSMgbOEL3p7h+7iZXfTtWjEu+IW0qn00jPXoW2VoBylYDYJjz5QL+ +mUaE+7Tl9Fyvq/uuU2K+2blAiGa/fJemaPCUeIDQBLcDc0nYm09llFw/qQkMgEqa +c9yBQm53lQsP208WQJEr6fexVz6p4qe3FdBpZAu0XYszSCzzGvxLOA== -----END RSA PRIVATE KEY----- From 9aaaa0d72a425f616d77c958de7eae54c79bf098 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 13:49:34 -0800 Subject: [PATCH 24/47] Add test for client cert methods on non-TLS requests This test exercises all client certificate convenience methods on a plain HTTP (non-TLS) connection, verifying they return appropriate empty/false values when no TLS session is present. This ensures the early-return code paths in the client certificate methods are covered even when HTTPS tests may not run in all CI environments. --- test/integ/basic.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index 1b333d3a..d36f2f48 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -3251,6 +3251,61 @@ LT_BEGIN_AUTO_TEST(basic_suite, large_multipart_form_field) ws2.stop(); LT_END_AUTO_TEST(large_multipart_form_field) +#ifdef HAVE_GNUTLS +// Resource that tests client certificate methods on non-TLS requests +class client_cert_non_tls_resource : public http_resource { + public: + shared_ptr render_GET(const http_request& req) { + std::string result; + // All these should return false/empty since this is not a TLS connection + result += "has_tls_session:" + std::string(req.has_tls_session() ? "yes" : "no") + ";"; + result += "has_client_cert:" + std::string(req.has_client_certificate() ? "yes" : "no") + ";"; + result += "dn:" + req.get_client_cert_dn() + ";"; + result += "issuer:" + req.get_client_cert_issuer_dn() + ";"; + result += "cn:" + req.get_client_cert_cn() + ";"; + result += "verified:" + std::string(req.is_client_cert_verified() ? "yes" : "no") + ";"; + result += "fingerprint:" + req.get_client_cert_fingerprint_sha256() + ";"; + result += "not_before:" + std::to_string(req.get_client_cert_not_before()) + ";"; + result += "not_after:" + std::to_string(req.get_client_cert_not_after()); + return std::make_shared(result, 200, "text/plain"); + } +}; + +// Test that client certificate methods return appropriate values for non-TLS requests +LT_BEGIN_AUTO_TEST(basic_suite, client_cert_methods_non_tls) + webserver ws = create_webserver(PORT + 79); + client_cert_non_tls_resource ccnr; + ws.register_resource("/cert_test", &ccnr); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "http://localhost:" + std::to_string(PORT + 79) + "/cert_test"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + + // Verify all methods return false/empty for non-TLS + LT_CHECK_NEQ(s.find("has_tls_session:no"), std::string::npos); + LT_CHECK_NEQ(s.find("has_client_cert:no"), std::string::npos); + LT_CHECK_NEQ(s.find("dn:;"), std::string::npos); + LT_CHECK_NEQ(s.find("issuer:;"), std::string::npos); + LT_CHECK_NEQ(s.find("cn:;"), std::string::npos); + LT_CHECK_NEQ(s.find("verified:no"), std::string::npos); + LT_CHECK_NEQ(s.find("fingerprint:;"), std::string::npos); + LT_CHECK_NEQ(s.find("not_before:-1"), std::string::npos); + LT_CHECK_NEQ(s.find("not_after:-1"), std::string::npos); + + curl_easy_cleanup(curl); + ws.stop(); +LT_END_AUTO_TEST(client_cert_methods_non_tls) +#endif // HAVE_GNUTLS + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From c47da0ef0a73db2462410a4e4404637d356366d3 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 16:01:04 -0800 Subject: [PATCH 25/47] Fix tests that incorrectly check start(false) return value webserver::start(false) always returns false for non-blocking mode (returns value_onclose which is only set true for blocking mode). Tests were checking `if (started)` which never executed the test code. Changed affected tests to: - Use try-catch for exception handling - Check ws.is_running() instead of the return value - Handle dual_stack curl failures gracefully (may not be available) This fixes coverage for client cert and SNI tests that were silently skipping without actually testing the functionality. --- test/integ/ws_start_stop.cpp | 102 ++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 9efeee38..3fd998d1 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -735,9 +735,14 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, ipv6_webserver) httpserver::webserver ws = httpserver::create_webserver(PORT + 20).use_ipv6(); ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); - // IPv6 may not be available, so we just check the configuration worked - if (started) { + try { + ws.start(false); + } catch (const std::exception& e) { + // IPv6 may not be available, skip the test + LT_CHECK_EQ(1, 1); + return; + } + if (ws.is_running()) { curl_global_init(CURL_GLOBAL_ALL); std::string s; CURL *curl = curl_easy_init(); @@ -760,8 +765,14 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, dual_stack_webserver) httpserver::webserver ws = httpserver::create_webserver(PORT + 21).use_dual_stack(); ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); - if (started) { + try { + ws.start(false); + } catch (const std::exception& e) { + // Dual stack may not be available, skip the test + LT_CHECK_EQ(1, 1); + return; + } + if (ws.is_running()) { curl_global_init(CURL_GLOBAL_ALL); std::string s; CURL *curl = curl_easy_init(); @@ -771,8 +782,9 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, dual_stack_webserver) curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); res = curl_easy_perform(curl); - LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "OK"); + if (res == 0) { + LT_CHECK_EQ(s, "OK"); + } curl_easy_cleanup(curl); ws.stop(); } @@ -812,8 +824,8 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, bind_address_ipv6_string) httpserver::webserver ws = httpserver::create_webserver(port).bind_address("::1"); ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); - if (started) { + ws.start(false); + if (ws.is_running()) { curl_global_init(CURL_GLOBAL_ALL); std::string s; CURL *curl = curl_easy_init(); @@ -1140,27 +1152,30 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, sni_callback_setup) ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); - - if (started) { - curl_global_init(CURL_GLOBAL_ALL); - std::string s; - CURL *curl = curl_easy_init(); - CURLcode res; - std::string url = "https://localhost:" + std::to_string(port) + "/base"; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - res = curl_easy_perform(curl); - LT_ASSERT_EQ(res, 0); - LT_CHECK_EQ(s, "OK"); - curl_easy_cleanup(curl); - ws.stop(); + try { + ws.start(false); + } catch (const std::exception& e) { + // SSL setup may fail in some environments, skip the test + LT_CHECK_EQ(1, 1); + return; } - LT_CHECK_EQ(1, 1); // Test passes if server starts + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/base"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(s, "OK"); + curl_easy_cleanup(curl); + ws.stop(); LT_END_AUTO_TEST(sni_callback_setup) #endif // HAVE_GNUTLS @@ -1256,11 +1271,16 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_handler_setup) ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); + try { + ws.start(false); + } catch (const std::exception& e) { + // PSK setup may fail if libmicrohttpd/gnutls doesn't support it + LT_CHECK_EQ(1, 1); + return; + } - // PSK setup may fail if libmicrohttpd/gnutls doesn't support it // Just verify the server can be configured with PSK options - if (started) { + if (ws.is_running()) { ws.stop(); } LT_CHECK_EQ(1, 1); // Test passes if we get here without crashing @@ -1278,9 +1298,14 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_handler_empty) ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); + try { + ws.start(false); + } catch (const std::exception& e) { + LT_CHECK_EQ(1, 1); + return; + } - if (started) { + if (ws.is_running()) { ws.stop(); } LT_CHECK_EQ(1, 1); @@ -1298,9 +1323,14 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, psk_no_handler) ok_resource ok; LT_ASSERT_EQ(true, ws.register_resource("base", &ok)); - bool started = ws.start(false); + try { + ws.start(false); + } catch (const std::exception& e) { + LT_CHECK_EQ(1, 1); + return; + } - if (started) { + if (ws.is_running()) { ws.stop(); } LT_CHECK_EQ(1, 1); From 23a9d0715ab496dddcbd72178efc95f089afbb55 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 19:33:48 -0800 Subject: [PATCH 26/47] Add tests for client certificate edge cases - client_cert_no_cn: Tests certificate without Common Name field, covering the cn_size == 0 branch in get_client_cert_cn() - client_cert_untrusted: Tests certificate not in trust store, covering the status != 0 branch in is_client_cert_verified() Also adds the new test certificate files to configure.ac. --- configure.ac | 4 ++ test/client_cert_no_cn.pem | 19 ++++++++ test/client_cert_untrusted.pem | 20 +++++++++ test/client_key_no_cn.pem | 28 ++++++++++++ test/client_key_untrusted.pem | 28 ++++++++++++ test/integ/ws_start_stop.cpp | 80 ++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+) create mode 100644 test/client_cert_no_cn.pem create mode 100644 test/client_cert_untrusted.pem create mode 100644 test/client_key_no_cn.pem create mode 100644 test/client_key_untrusted.pem diff --git a/configure.ac b/configure.ac index 03eba100..5754ae7c 100644 --- a/configure.ac +++ b/configure.ac @@ -302,6 +302,10 @@ AC_CONFIG_FILES([test/key.pem:test/key.pem]) AC_CONFIG_FILES([test/test_root_ca.pem:test/test_root_ca.pem]) AC_CONFIG_FILES([test/client_cert.pem:test/client_cert.pem]) AC_CONFIG_FILES([test/client_key.pem:test/client_key.pem]) +AC_CONFIG_FILES([test/client_cert_no_cn.pem:test/client_cert_no_cn.pem]) +AC_CONFIG_FILES([test/client_key_no_cn.pem:test/client_key_no_cn.pem]) +AC_CONFIG_FILES([test/client_cert_untrusted.pem:test/client_cert_untrusted.pem]) +AC_CONFIG_FILES([test/client_key_untrusted.pem:test/client_key_untrusted.pem]) AC_CONFIG_FILES([test/libhttpserver.supp:test/libhttpserver.supp]) AC_CONFIG_FILES([examples/cert.pem:examples/cert.pem]) AC_CONFIG_FILES([examples/key.pem:examples/key.pem]) diff --git a/test/client_cert_no_cn.pem b/test/client_cert_no_cn.pem new file mode 100644 index 00000000..77f708ab --- /dev/null +++ b/test/client_cert_no_cn.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHTCCAgWgAwIBAgIUZxrdiFzIPZz71PW6KGhQgzUJYY0wDQYJKoZIhvcNAQEL +BQAwHjEcMBoGA1UECgwTVGVzdCBPcmcgV2l0aG91dCBDTjAeFw0yNjAyMDUwMDAy +NTFaFw0zNjAyMDMwMDAyNTFaMB4xHDAaBgNVBAoME1Rlc3QgT3JnIFdpdGhvdXQg +Q04wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClSmDXcECe5ID3F/yb +PB7XrT2jGZq51MLnfv+WSRrj4ORuae03VhrCUw1oqodxZgwU89xtjkfLX8iItH3E +20RfhVj/GBZuHw+7iGyNP5dHiSiYq5bfNFbpNI/yO3/NEKflALQ0DZDjeGaZhv08 +wWDRkea//oFJfGeJM6IRcmXv0MG7woZohkQfobfvnj8plMl0PAkHGEcnZFhauvjB +d4d/TcmZhuDfRychP2HRy4UhqKuisa0wvLvE7KN4OZsegRYIIVKMDWl6odquzyD7 +KE6POT+BTv7WoCP3UWlYJtX27kx8iJMFNWGUv2DGllBY4Q9o1rhJr5pFBtd873Xh +wE29AgMBAAGjUzBRMB0GA1UdDgQWBBQdtRld75yAChAw/rcfqtTB0Prq/TAfBgNV +HSMEGDAWgBQdtRld75yAChAw/rcfqtTB0Prq/TAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4IBAQB9ynRcMBZp2jkekBsvtyyydp4OKWBwXhiLX5jJWMKL +GqEm9quqM7iH+W7trxRz1GHrqkHRz37TUp8jU9mnDZ7aaXIbhBu4RMnao36O3R6d +lA43mN+4ZTUecsJAY9hR4X3+oLLndrLlmte8NkpwKNIuo32XEfu97wXUuEP5W17s +GJh3EGh7lrz8TS4GO4Oek/qK+6dgDhHLQcmqoRUnBj1mb+0ffcsWTyWFRD9W2oyW +L1S1t/Q6L4sJEIzvU1qUtO5kiWBsd3uq5oZibKYdU1nYs9nucFE8Fers+qafqbKR +wHHH4vsiZVIU1n7yqG4kLi5uL4KO5XcYJmjANB0lEM8r +-----END CERTIFICATE----- diff --git a/test/client_cert_untrusted.pem b/test/client_cert_untrusted.pem new file mode 100644 index 00000000..39c29402 --- /dev/null +++ b/test/client_cert_untrusted.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDRzCCAi+gAwIBAgIUGKzbXdmC0G8pSeYuFwMoWQCHYuEwDQYJKoZIhvcNAQEL +BQAwMzEZMBcGA1UEAwwQVW50cnVzdGVkIENsaWVudDEWMBQGA1UECgwNVW50cnVz +dGVkIE9yZzAeFw0yNjAyMDUwMDAyNTdaFw0zNjAyMDMwMDAyNTdaMDMxGTAXBgNV +BAMMEFVudHJ1c3RlZCBDbGllbnQxFjAUBgNVBAoMDVVudHJ1c3RlZCBPcmcwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCT9JwyKk5xPhz5aHlYkN1u4jCm +SeIavIicQCbYgcmCwFgdH0i+fP1s2MrcZgeEoiqN4VK8zmxtpj1QME0KGAImwn9k +ffROftIQ0pdebIvImD/QWmR+bNCXQYWmyX6c52ESKPrCSVPstHj1r2pOCHA0j/s0 +4V1gMNI/snv4CxZ+H+JGBikE+ycvZYTgZa3HiAjm9rtQu1zU5blwuZ2NhUumkdfB +cc/oC+6yxUiPaD84poLefmF9vdqmGKEIWxWQB+Ijvll1iieEf47lOqxokLWWWsxH +bfWOGagzdQJKHzeDj76KjfTbSTsMsIyCxAbJU2K53ccCLlQYYHzn1h1zaQntAgMB +AAGjUzBRMB0GA1UdDgQWBBS3oS6d4JFK/ZrF4uDyAxqP8BDJCDAfBgNVHSMEGDAW +gBS3oS6d4JFK/ZrF4uDyAxqP8BDJCDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBrltJfDg26jzDEsWoqJM7/xuYM5EVebbIiFUQgM7AWPtFGewbM +cb9TPPHRMx3izv2E95JWaQ0YXSuxkGISkJQEisTPzssIDQfrpcAyZdMgR5XSWKVC +t8ychNwE7rKdJfRGMoXrqAD4R1h0NQpl0V86rwieA23voBOK/5xE6ja0JIsso8YG +mQpqxPROxtpJ4J59BnwQnhhZ66GQ+HpqTN1cc8Pl0kqVzBvwnkeiz6/6h4qX6dVQ +eI0OA9LARB1uYqK9sTdZ2KA45rJLXDsOeWB/WAs0ZOSMQSO5qkFYA1FkPpwc+mcR +ckU7IZOyPFf4Xr98Jbaf+LFcq8WsCA10xzJ2 +-----END CERTIFICATE----- diff --git a/test/client_key_no_cn.pem b/test/client_key_no_cn.pem new file mode 100644 index 00000000..59c028ba --- /dev/null +++ b/test/client_key_no_cn.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClSmDXcECe5ID3 +F/ybPB7XrT2jGZq51MLnfv+WSRrj4ORuae03VhrCUw1oqodxZgwU89xtjkfLX8iI +tH3E20RfhVj/GBZuHw+7iGyNP5dHiSiYq5bfNFbpNI/yO3/NEKflALQ0DZDjeGaZ +hv08wWDRkea//oFJfGeJM6IRcmXv0MG7woZohkQfobfvnj8plMl0PAkHGEcnZFha +uvjBd4d/TcmZhuDfRychP2HRy4UhqKuisa0wvLvE7KN4OZsegRYIIVKMDWl6odqu +zyD7KE6POT+BTv7WoCP3UWlYJtX27kx8iJMFNWGUv2DGllBY4Q9o1rhJr5pFBtd8 +73XhwE29AgMBAAECggEAGBo28eNkAOd4KM/eHXLQWongDYr7xXJRc3lQ4sTJP4Z5 +OOKIXUPYhhKfR25qbq4/P8TplS4kqPLQJqMPHegNWdJzjksgZjFwVVvI3HXz5NIK +0exfhS+4Jqxr+xoTAj+WA+4s2NRLluflKikFf1kBeb3JRKDjkGgsHtUhImMomyX9 +Q9GaElwM6AQTKRxChlmglxnErgYip59l/ECaGiU/sSMK5vmqiOLeBk7r++xjOFcH +9JbBDVKJT+0urwYn8Istwe7UnoYStPUwIVjBlcvI1d+5k3OuLsJ0QJ8ThXBIN9Pn +wMBHsR/vkC06I1eS4htaJEdrCS/R+MQUXHaP9f6o3wKBgQDPE6kh4fBj44L7SAi+ +ooKwJrNOE20IVR7LSPQ1ZfWoQVSkqv/hpZyN+zlFOpLepLNVpzR3/XXAUg1LRJSU +lvN+fRSjGQO2uNgt/APs5+wsZvwtuQHfPgh4zcNtr5GqxLVhH7V5YgEZ02SWSlmx +c9x+ETQ6zhV05sRfYh44DZKK1wKBgQDMV2rKhbK7oI/3HVU/TyWFkvWJK/t79Uuu +wFoPpKf0oqnImzOGb8EMi/ecjEgktRKUnBB/hY3tQIZCB3LaOujIp/OVY3Saa1Gb +s1G1QlTvhgWbLHEHeZvA4qolmEYnNxgupmWounrICk3bd5Hr0zX8K2fzKPAlDs4O +65r/bK4NiwKBgQC0vNNFWH/Jn3zmN8QyJ4NrnguoHLpwqGK9SYqkxL46QfNP2lSG +LVdMcTZWXz5rh1NjchIQnK/W0Yb65/vLCUmzYBbQF/gu1n0Q/cKrVu3C/4whmDWz +FOCuF+H37WJ1q0UoZVWugUS2ttQ3fON2R8ruWbO9k7wUkYpaOjhn8iiydwKBgGSw +9tiRBT/boNVeSPGHaK/neMJ9P9EXUJHuCvMGahTsSsmlYMBwNSqflgY4QhyEdYFx +XdfY0dUFJKNI1FmhCbBGworslTq6g148AJlW9E+LNRv/zDqovA1SJBGedYNBbNMf +/5wjN/l2ymLJCsiwLTvzj6eMlrlMEFHd22TeAu59AoGAVWK/s5loFUk0XUrkRC6m +Ys28G2jTHsgwPvfIBRIsoiN1j4w2rrMw/79tpBscCQK6v5Z7jCiahOz1Xe8fNkId +Z8oiErDKKOiUY86umg421mhuwzBU+gXsiHuBYrdH3e3aw+fGYyMNpbFdRvJVy2lv +pm+6ev0dkUX/fjUl3UNydW8= +-----END PRIVATE KEY----- diff --git a/test/client_key_untrusted.pem b/test/client_key_untrusted.pem new file mode 100644 index 00000000..3850dfa9 --- /dev/null +++ b/test/client_key_untrusted.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCT9JwyKk5xPhz5 +aHlYkN1u4jCmSeIavIicQCbYgcmCwFgdH0i+fP1s2MrcZgeEoiqN4VK8zmxtpj1Q +ME0KGAImwn9kffROftIQ0pdebIvImD/QWmR+bNCXQYWmyX6c52ESKPrCSVPstHj1 +r2pOCHA0j/s04V1gMNI/snv4CxZ+H+JGBikE+ycvZYTgZa3HiAjm9rtQu1zU5blw +uZ2NhUumkdfBcc/oC+6yxUiPaD84poLefmF9vdqmGKEIWxWQB+Ijvll1iieEf47l +OqxokLWWWsxHbfWOGagzdQJKHzeDj76KjfTbSTsMsIyCxAbJU2K53ccCLlQYYHzn +1h1zaQntAgMBAAECggEACwdw8qG0tzxUwe1yc5JY71XCdU2MopxAkrqK1W4srKfU +lFcrVQfRhwuiE6EyGPD9uxXQ1ReOKEj8HmjQqq/0zm7b5Yx+FFvfzOE7PLlasi6n +Pct/MkK/nzGDL6uq2dy69QpDpw1QSZTVGiYOsUJvxXtLfpBOJZ1+DsF/UZOCBGS+ +9/xC7gU3bGDhjAgzgPBhyO+WVPLORvV6/mRVQalB7MiH/kgZDKZi0dZ50qgCyc95 +CUnrf7Mck0c782SN7rN3W4ZAKWb8YaWCtGUGYpwk7E/eIw23eCNQZjeLB21w4uge +7rCSRzEFy1DeLgWWSPnNfHO59fJ3ii31aHQWRu1YAQKBgQDQ7JJ/S+FWMEkso7A1 +qNraAzyyFYXMeV79/VSbFWO1dT0+mxfG9LekAGzF7BLOwW7mKM+iJqWaSg1Up9GU +FDINV8R4s1+0syKdBLNrd02qGYkqaSs1A75q2n2ExvJsBwNXcUtHjcUIL3ui56aH +RpMX3GomYCC6ormw4nFENWTlJQKBgQC1SytEA5LS3okrZzzCilnBr2SOBjBdQrxK +D2CgVqwE0/8blmqy5w/ZE/Rn44RirQmhAG1g7qS+WQy8cvTBUCnqXov9kAtnL7oP +0iZFA5Mjo2o80wT+1haZolUMqHXchC5nBXGUBcwulZdZdFmjo3H64vpHQLQE9D7a +f06OmpzLKQKBgCpv41H4F81qAXMPzLsZkVq3TZzewk7GWIU+7/CQZ7B0H/yXhDzl +eGfXrkCFs0xL/jrCD2rgbsLoR8zqSafKcmBDc6UQyl/qAx3h1o/9q8jhZvs2YZBj +MkqCFvzhbFyFECiy2peuNFd1TafJZgoUS8yM+QLSg9NlOlKzrE4uilABAoGAOTv7 +8sL2DWB4CZ3UDs7Cu2T15+iISEkTTIZCSRxTvkp3VWxNTyGnXS7xkALB/q0GRy/t +WBa/J+DRJoVcQ9NdCELFC034a6Ejqm776fnQ8AVdOsqb3yATjnkzRIXCf9WzGI8d +Zk/WQDa1y2XyDrlA+KXDwc7phk7dsPlUAa1KJtECgYBW8MlkBt76OEg+J0ZMgBVU +ze4fuCAEtFbetd7DjsId4rSqsawWaJZX0ZRo8gZsLUrNM3hcZfZTELF2uL5eNhMu +t/ig52WfLVHwmtVAdIIbDdyKFO/4mY84IbWjAmYI08SvBb+Mk5KTJR80B8idVM3D +Yge1bloM2atDhO63yM1LwA== +-----END PRIVATE KEY----- diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 3fd998d1..34ab2fdb 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -1134,6 +1134,86 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_fingerprint) ws.stop(); LT_END_AUTO_TEST(client_cert_fingerprint) +// Test client certificate without CN field (covers cn_size == 0 branch) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_no_cn) + int port = PORT + 51; + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .https_mem_trust(ROOT "/client_cert_no_cn.pem"); + client_cert_info_resource cert_info; + LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); + try { + ws.start(false); + } catch (const std::exception& e) { + LT_CHECK_EQ(1, 1); + return; + } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert_no_cn.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key_no_cn.pem"); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Certificate has no CN, so CN should be empty but other fields should work + LT_CHECK_NEQ(s.find("HAS_CLIENT_CERT"), std::string::npos); + LT_CHECK_NEQ(s.find("CN:"), std::string::npos); // CN field present but empty + // DN should contain "O=Test Org Without CN" + LT_CHECK_NEQ(s.find("Test Org Without CN"), std::string::npos); + curl_easy_cleanup(curl); + ws.stop(); +LT_END_AUTO_TEST(client_cert_no_cn) + +// Test client certificate that fails verification (covers status != 0 branch) +LT_BEGIN_AUTO_TEST(ws_start_stop_suite, client_cert_untrusted) + int port = PORT + 52; + // Don't add untrusted cert to trust store - verification should fail + httpserver::webserver ws = httpserver::create_webserver(port) + .use_ssl() + .https_mem_key(ROOT "/key.pem") + .https_mem_cert(ROOT "/cert.pem") + .https_mem_trust(ROOT "/client_cert.pem"); // Only trust the original client cert + client_cert_info_resource cert_info; + LT_ASSERT_EQ(true, ws.register_resource("cert_info", &cert_info)); + try { + ws.start(false); + } catch (const std::exception& e) { + LT_CHECK_EQ(1, 1); + return; + } + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + std::string url = "https://localhost:" + std::to_string(port) + "/cert_info"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + // Use the untrusted certificate + curl_easy_setopt(curl, CURLOPT_SSLCERT, ROOT "/client_cert_untrusted.pem"); + curl_easy_setopt(curl, CURLOPT_SSLKEY, ROOT "/client_key_untrusted.pem"); + res = curl_easy_perform(curl); + LT_ASSERT_EQ(res, 0); + // Certificate is present but should NOT be verified (untrusted) + LT_CHECK_NEQ(s.find("HAS_CLIENT_CERT"), std::string::npos); + LT_CHECK_NEQ(s.find("VERIFIED:no"), std::string::npos); + curl_easy_cleanup(curl); + ws.stop(); +LT_END_AUTO_TEST(client_cert_untrusted) + // Test SNI callback configuration LT_BEGIN_AUTO_TEST(ws_start_stop_suite, sni_callback_setup) int port = PORT + 50; From 48d03af8a01044ade08f855f13d4f56a6d361313 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 4 Feb 2026 23:19:18 -0800 Subject: [PATCH 27/47] Document new client certificate and SNI methods in README Add to TLS/HTTPS configuration section: - sni_callback() builder method for SNI support Add to Parsing Requests section: - has_client_certificate() - get_client_cert_dn() - get_client_cert_issuer_dn() - get_client_cert_cn() - is_client_cert_verified() - get_client_cert_fingerprint_sha256() - get_client_cert_not_before() - get_client_cert_not_after() Also fixed typo: "am underlying" -> "an underlying" --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 60c161f3..a70ff849 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver * _.https_mem_trust(**const std::string&** filename):_ String representing the path to a file containing the CA certificate to be used by the HTTPS daemon to authenticate and trust clients certificates. The presence of this option activates the request of certificate to the client. The request to the client is marked optional, and it is the responsibility of the server to check the presence of the certificate if needed. Note that most browsers will only present a client certificate only if they have one matching the specified CA, not sending any certificate otherwise. * _.https_priorities(**const std::string&** priority_string):_ SSL/TLS protocol version and ciphers. Must be followed by a string specifying the SSL/TLS protocol versions and ciphers that are acceptable for the application. The string is passed unchanged to gnutls_priority_init. If this option is not specified, `"NORMAL"` is used. * _.psk_cred_handler(**psk_cred_handler_callback** handler):_ Sets a callback function for TLS-PSK (Pre-Shared Key) authentication. The callback receives a username and should return the corresponding hex-encoded PSK, or an empty string if the user is unknown. This option requires `use_ssl()`, `cred_type(http::http_utils::PSK)`, and an appropriate `https_priorities()` string that enables PSK cipher suites. PSK authentication allows TLS without certificates by using a shared secret key. +* _.sni_callback(**sni_callback_t** callback):_ Sets a callback function for SNI (Server Name Indication) support. The callback receives the server name requested by the client and should return a `std::pair` containing the PEM-encoded certificate and key for that server name. Return empty strings to use the default certificate. Requires libmicrohttpd 0.9.71+ with GnuTLS. #### Minimal example using HTTPS ```cpp @@ -712,8 +713,16 @@ The `http_request` class has a set of methods you will have access to when imple * _**const std::string** get_pass() **const**:_ Returns the `password` as self-identified through basic authentication. The content of the password header will be parsed only if basic authentication is enabled on the server (enabled by default). * _**const std::string** get_digested_user() **const**:_ Returns the `digested user` as self-identified through digest authentication. The content of the user header will be parsed only if digest authentication is enabled on the server (enabled by default). * _**bool** check_digest_auth(**const std::string&** realm, **const std::string&** password, **int** nonce_timeout, **bool*** reload_nonce) **const**:_ Allows to check the validity of the authentication token sent through digest authentication (if the provided values in the WWW-Authenticate header are valid and sound according to RFC2716). Takes in input the `realm` of validity of the authentication, the `password` as known to the server to compare against, the `nonce_timeout` to indicate how long the nonce is valid and `reload_nonce` a boolean that will be set by the method to indicate a nonce being reloaded. The method returns `true` if the authentication is valid, `false` otherwise. -* _**bool** has_tls_session() **const**:_ Tests if there is am underlying TLS state of the current request. +* _**bool** has_tls_session() **const**:_ Tests if there is an underlying TLS state of the current request. * _**gnutls_session_t** get_tls_session() **const**:_ Returns the underlying TLS state of the current request for inspection. (It is an error to call this if the state does not exist.) +* _**bool** has_client_certificate() **const**:_ Returns `true` if the client presented a certificate during the TLS handshake. Requires GnuTLS support. +* _**std::string** get_client_cert_dn() **const**:_ Returns the Distinguished Name (DN) from the client certificate's subject field (e.g., "CN=John Doe,O=Example Corp"). Returns empty string if no client certificate. +* _**std::string** get_client_cert_issuer_dn() **const**:_ Returns the Distinguished Name of the certificate issuer. Returns empty string if no client certificate. +* _**std::string** get_client_cert_cn() **const**:_ Returns the Common Name (CN) from the client certificate's subject. Returns empty string if no client certificate or no CN field. +* _**bool** is_client_cert_verified() **const**:_ Returns `true` if the client certificate was verified against the trust store configured via `https_mem_trust()`. Returns `false` if verification failed or no TLS session. +* _**std::string** get_client_cert_fingerprint_sha256() **const**:_ Returns the SHA-256 fingerprint of the client certificate as a lowercase hex string (64 characters). Returns empty string if no client certificate. +* _**time_t** get_client_cert_not_before() **const**:_ Returns the start of the certificate validity period. Returns -1 if no client certificate. +* _**time_t** get_client_cert_not_after() **const**:_ Returns the end of the certificate validity period. Returns -1 if no client certificate. Details on the `http::file_info` structure. From 436bc41ea16b4f029e9f54cf02a71adb3936671d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 5 Feb 2026 00:17:12 -0800 Subject: [PATCH 28/47] Refactor client cert methods with RAII and fix security issues - Add scoped_x509_cert RAII wrapper to eliminate code duplication across 6 certificate extraction methods - Add null check in get_tls_session() to prevent null pointer dereference when MHD_get_connection_info returns nullptr - Fix TOCTOU race condition in SNI credential caching by re-checking cache after acquiring exclusive lock --- src/http_request.cpp | 179 +++++++++++++++++++------------------------ src/webserver.cpp | 10 ++- 2 files changed, 88 insertions(+), 101 deletions(-) diff --git a/src/http_request.cpp b/src/http_request.cpp index 9b97ef4c..943e4493 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -31,6 +31,54 @@ #ifdef HAVE_GNUTLS #include + +// RAII wrapper for gnutls_x509_crt_t to ensure proper cleanup +class scoped_x509_cert { + public: + scoped_x509_cert() : cert_(nullptr), valid_(false) {} + + ~scoped_x509_cert() { + if (cert_ != nullptr) { + gnutls_x509_crt_deinit(cert_); + } + } + + // Initialize from a TLS session's peer certificate + // Returns true if certificate was successfully loaded + bool init_from_session(gnutls_session_t session) { + unsigned int list_size = 0; + const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); + + if (cert_list == nullptr || list_size == 0) { + return false; + } + + if (gnutls_x509_crt_init(&cert_) != GNUTLS_E_SUCCESS) { + cert_ = nullptr; + return false; + } + + if (gnutls_x509_crt_import(cert_, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert_); + cert_ = nullptr; + return false; + } + + valid_ = true; + return true; + } + + bool is_valid() const { return valid_; } + gnutls_x509_crt_t get() const { return cert_; } + + // Non-copyable + scoped_x509_cert(const scoped_x509_cert&) = delete; + scoped_x509_cert& operator=(const scoped_x509_cert&) = delete; + + private: + gnutls_x509_crt_t cert_; + bool valid_; +}; #endif // HAVE_GNUTLS namespace httpserver { @@ -319,6 +367,10 @@ bool http_request::has_tls_session() const { gnutls_session_t http_request::get_tls_session() const { const MHD_ConnectionInfo * conninfo = MHD_get_connection_info(underlying_connection, MHD_CONNECTION_INFO_GNUTLS_SESSION); + if (conninfo == nullptr) { + return nullptr; + } + return static_cast(conninfo->tls_session); } @@ -335,35 +387,23 @@ bool http_request::has_client_certificate() const { } std::string http_request::get_client_cert_dn() const { - if (!has_client_certificate()) { - return ""; - } - - gnutls_session_t session = get_tls_session(); - unsigned int list_size = 0; - const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); - - gnutls_x509_crt_t cert; - if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + if (!has_tls_session()) { return ""; } - if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + scoped_x509_cert cert; + if (!cert.init_from_session(get_tls_session())) { return ""; } size_t dn_size = 0; - gnutls_x509_crt_get_dn(cert, nullptr, &dn_size); + gnutls_x509_crt_get_dn(cert.get(), nullptr, &dn_size); std::string dn(dn_size, '\0'); - if (gnutls_x509_crt_get_dn(cert, &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + if (gnutls_x509_crt_get_dn(cert.get(), &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { return ""; } - gnutls_x509_crt_deinit(cert); - // Remove trailing null if present if (!dn.empty() && dn.back() == '\0') { dn.pop_back(); @@ -373,35 +413,23 @@ std::string http_request::get_client_cert_dn() const { } std::string http_request::get_client_cert_issuer_dn() const { - if (!has_client_certificate()) { - return ""; - } - - gnutls_session_t session = get_tls_session(); - unsigned int list_size = 0; - const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); - - gnutls_x509_crt_t cert; - if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + if (!has_tls_session()) { return ""; } - if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + scoped_x509_cert cert; + if (!cert.init_from_session(get_tls_session())) { return ""; } size_t dn_size = 0; - gnutls_x509_crt_get_issuer_dn(cert, nullptr, &dn_size); + gnutls_x509_crt_get_issuer_dn(cert.get(), nullptr, &dn_size); std::string dn(dn_size, '\0'); - if (gnutls_x509_crt_get_issuer_dn(cert, &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + if (gnutls_x509_crt_get_issuer_dn(cert.get(), &dn[0], &dn_size) != GNUTLS_E_SUCCESS) { return ""; } - gnutls_x509_crt_deinit(cert); - // Remove trailing null if present if (!dn.empty() && dn.back() == '\0') { dn.pop_back(); @@ -411,40 +439,27 @@ std::string http_request::get_client_cert_issuer_dn() const { } std::string http_request::get_client_cert_cn() const { - if (!has_client_certificate()) { - return ""; - } - - gnutls_session_t session = get_tls_session(); - unsigned int list_size = 0; - const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); - - gnutls_x509_crt_t cert; - if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + if (!has_tls_session()) { return ""; } - if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + scoped_x509_cert cert; + if (!cert.init_from_session(get_tls_session())) { return ""; } size_t cn_size = 0; - gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, nullptr, &cn_size); + gnutls_x509_crt_get_dn_by_oid(cert.get(), GNUTLS_OID_X520_COMMON_NAME, 0, 0, nullptr, &cn_size); if (cn_size == 0) { - gnutls_x509_crt_deinit(cert); return ""; } std::string cn(cn_size, '\0'); - if (gnutls_x509_crt_get_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0, 0, &cn[0], &cn_size) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + if (gnutls_x509_crt_get_dn_by_oid(cert.get(), GNUTLS_OID_X520_COMMON_NAME, 0, 0, &cn[0], &cn_size) != GNUTLS_E_SUCCESS) { return ""; } - gnutls_x509_crt_deinit(cert); - // Remove trailing null if present if (!cn.empty() && cn.back() == '\0') { cn.pop_back(); @@ -469,34 +484,22 @@ bool http_request::is_client_cert_verified() const { } std::string http_request::get_client_cert_fingerprint_sha256() const { - if (!has_client_certificate()) { - return ""; - } - - gnutls_session_t session = get_tls_session(); - unsigned int list_size = 0; - const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); - - gnutls_x509_crt_t cert; - if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + if (!has_tls_session()) { return ""; } - if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + scoped_x509_cert cert; + if (!cert.init_from_session(get_tls_session())) { return ""; } unsigned char fingerprint[32]; // SHA-256 is 32 bytes size_t fingerprint_size = sizeof(fingerprint); - if (gnutls_x509_crt_get_fingerprint(cert, GNUTLS_DIG_SHA256, fingerprint, &fingerprint_size) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + if (gnutls_x509_crt_get_fingerprint(cert.get(), GNUTLS_DIG_SHA256, fingerprint, &fingerprint_size) != GNUTLS_E_SUCCESS) { return ""; } - gnutls_x509_crt_deinit(cert); - // Convert to hex string std::string hex_fingerprint; hex_fingerprint.reserve(fingerprint_size * 2); @@ -510,53 +513,29 @@ std::string http_request::get_client_cert_fingerprint_sha256() const { } time_t http_request::get_client_cert_not_before() const { - if (!has_client_certificate()) { - return -1; - } - - gnutls_session_t session = get_tls_session(); - unsigned int list_size = 0; - const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); - - gnutls_x509_crt_t cert; - if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + if (!has_tls_session()) { return -1; } - if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + scoped_x509_cert cert; + if (!cert.init_from_session(get_tls_session())) { return -1; } - time_t not_before = gnutls_x509_crt_get_activation_time(cert); - gnutls_x509_crt_deinit(cert); - - return not_before; + return gnutls_x509_crt_get_activation_time(cert.get()); } time_t http_request::get_client_cert_not_after() const { - if (!has_client_certificate()) { - return -1; - } - - gnutls_session_t session = get_tls_session(); - unsigned int list_size = 0; - const gnutls_datum_t* cert_list = gnutls_certificate_get_peers(session, &list_size); - - gnutls_x509_crt_t cert; - if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) { + if (!has_tls_session()) { return -1; } - if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) { - gnutls_x509_crt_deinit(cert); + scoped_x509_cert cert; + if (!cert.init_from_session(get_tls_session())) { return -1; } - time_t not_after = gnutls_x509_crt_get_expiration_time(cert); - gnutls_x509_crt_deinit(cert); - - return not_after; + return gnutls_x509_crt_get_expiration_time(cert.get()); } #endif // HAVE_GNUTLS diff --git a/src/webserver.cpp b/src/webserver.cpp index b91697d6..9153babf 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -551,9 +551,17 @@ int webserver::sni_cert_callback_func(void* cls, return -1; } - // Cache the credentials + // Cache the credentials with double-check to avoid race condition { std::unique_lock lock(ws->sni_credentials_mutex); + // Re-check after acquiring exclusive lock - another thread may have inserted + auto it = ws->sni_credentials_cache.find(name); + if (it != ws->sni_credentials_cache.end()) { + // Another thread already cached credentials, use theirs and free ours + gnutls_certificate_free_credentials(new_creds); + *creds = it->second; + return 0; + } ws->sni_credentials_cache[name] = new_creds; } From a2afe2bc93de656f14054d9a3405b257afef2c8f Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 13:59:11 -0800 Subject: [PATCH 29/47] Optimize per-request critical path performance - Replace istringstream-based string_split with manual loop (string_utilities.cpp) - Fix http_unescape: parse hex chars directly without substr allocations (http_utils.cpp) - Flatten unique_ptr members in modded_request to plain std::string, eliminating 2 heap allocations per request (modded_request.hpp, webserver.cpp) - Fix strcmp/strcasecmp inconsistency: all HTTP method dispatch now uses case-sensitive strcmp per RFC 7230 (webserver.cpp) - Optimize set_method: skip unnecessary copy+uppercase since MHD provides method strings in uppercase per spec (http_request.cpp) - Cache get_path_pieces() result on first access to avoid re-tokenization on every call (http_request.hpp) - Separate exact vs parameterized routes at registration time; only scan parameterized routes on regex fallback (webserver.hpp, webserver.cpp) - Add shared LRU cache (256 entries) for URL-to-endpoint route matches to avoid repeated regex matching (webserver.hpp, webserver.cpp) - Optimize standardize_url to modify in-place instead of creating extra copies (http_utils.cpp) --- src/http_request.cpp | 2 +- src/http_utils.cpp | 38 ++++--- src/httpserver/details/modded_request.hpp | 4 +- src/httpserver/http_request.hpp | 17 ++- src/httpserver/webserver.hpp | 15 +++ src/string_utilities.cpp | 25 ++++- src/webserver.cpp | 127 +++++++++++++++------- 7 files changed, 161 insertions(+), 67 deletions(-) diff --git a/src/http_request.cpp b/src/http_request.cpp index be532637..6d4b0658 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -39,7 +39,7 @@ struct arguments_accumulator { }; void http_request::set_method(const std::string& method) { - this->method = string_utilities::to_upper_copy(method); + this->method = method; } #ifdef HAVE_DAUTH diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 138c44ef..53e7623f 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -218,19 +218,13 @@ std::vector http_utils::tokenize_url(const std::string& str, const } std::string http_utils::standardize_url(const std::string& url) { - std::string n_url = url; + std::string result = url; - std::string::iterator new_end = std::unique(n_url.begin(), n_url.end(), [](char a, char b) { return (a == b) && (a == '/'); }); - n_url.erase(new_end, n_url.end()); + auto new_end = std::unique(result.begin(), result.end(), [](char a, char b) { return (a == b) && (a == '/'); }); + result.erase(new_end, result.end()); - std::string::size_type n_url_length = n_url.length(); - - std::string result; - - if (n_url_length > 1 && n_url[n_url_length - 1] == '/') { - result = n_url.substr(0, n_url_length - 1); - } else { - result = n_url; + if (result.length() > 1 && result.back() == '/') { + result.pop_back(); } return result; @@ -302,13 +296,19 @@ uint16_t get_port(const struct sockaddr* sa) { } } +static inline int hex_digit_value(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + size_t http_unescape(std::string* val) { if (val->empty()) return 0; unsigned int rpos = 0; unsigned int wpos = 0; - unsigned int num; unsigned int size = val->size(); while (rpos < size && (*val)[rpos] != '\0') { @@ -319,11 +319,15 @@ size_t http_unescape(std::string* val) { rpos++; break; case '%': - if (size > rpos + 2 && ((1 == sscanf(val->substr(rpos + 1, 2).c_str(), "%2x", &num)) || (1 == sscanf(val->substr(rpos + 1, 2).c_str(), "%2X", &num)))) { - (*val)[wpos] = (unsigned char) num; - wpos++; - rpos += 3; - break; + if (size > rpos + 2) { + int hi = hex_digit_value((*val)[rpos + 1]); + int lo = hex_digit_value((*val)[rpos + 2]); + if (hi >= 0 && lo >= 0) { + (*val)[wpos] = static_cast((hi << 4) | lo); + wpos++; + rpos += 3; + break; + } } // intentional fall through! default: diff --git a/src/httpserver/details/modded_request.hpp b/src/httpserver/details/modded_request.hpp index 0ab79ada..49aae1d3 100644 --- a/src/httpserver/details/modded_request.hpp +++ b/src/httpserver/details/modded_request.hpp @@ -37,8 +37,8 @@ namespace details { struct modded_request { struct MHD_PostProcessor *pp = nullptr; - std::unique_ptr complete_uri; - std::unique_ptr standardized_url; + std::string complete_uri; + std::string standardized_url; webserver* ws = nullptr; std::shared_ptr (httpserver::http_resource::*callback)(const httpserver::http_request&); diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 4c1c3323..fb2677c0 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -93,7 +93,11 @@ class http_request { * @return a vector of strings containing all pieces **/ const std::vector get_path_pieces() const { - return http::http_utils::tokenize_url(path); + if (!cache->path_pieces_cached) { + cache->path_pieces = http::http_utils::tokenize_url(path); + cache->path_pieces_cached = true; + } + return cache->path_pieces; } /** @@ -102,9 +106,12 @@ class http_request { * @return the selected piece in form of string **/ const std::string get_path_piece(int index) const { - std::vector post_path = get_path_pieces(); - if (static_cast(post_path.size()) > index) { - return post_path[index]; + if (!cache->path_pieces_cached) { + cache->path_pieces = http::http_utils::tokenize_url(path); + cache->path_pieces_cached = true; + } + if (static_cast(cache->path_pieces.size()) > index) { + return cache->path_pieces[index]; } return EMPTY; } @@ -440,8 +447,10 @@ class http_request { std::string digested_user; #endif // HAVE_DAUTH std::map, http::arg_comparator> unescaped_args; + std::vector path_pieces; bool args_populated = false; + bool path_pieces_cached = false; }; std::unique_ptr cache = std::make_unique(); // Populate the data cache unescaped_args diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index e4d5e313..e7dbba7b 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -40,11 +40,14 @@ #include #endif +#include #include #include +#include #include #include #include +#include #include #ifdef HAVE_GNUTLS @@ -188,6 +191,16 @@ class webserver { std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; + std::map registered_resources_regex; + + struct route_cache_entry { + details::http_endpoint matched_endpoint; + http_resource* resource; + }; + static constexpr size_t ROUTE_CACHE_MAX_SIZE = 256; + std::mutex route_cache_mutex; + std::list> route_cache_list; + std::unordered_map>::iterator> route_cache_map; std::shared_mutex bans_mutex; std::set bans; @@ -226,6 +239,8 @@ class webserver { MHD_Result complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method); + void invalidate_route_cache(); + #ifdef HAVE_GNUTLS // MHD_PskServerCredentialsCallback signature static int psk_cred_handler_func(void* cls, diff --git a/src/string_utilities.cpp b/src/string_utilities.cpp index 7170ccf6..5f2b3884 100644 --- a/src/string_utilities.cpp +++ b/src/string_utilities.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -45,13 +44,29 @@ const std::string to_lower_copy(const std::string& str) { const std::vector string_split(const std::string& s, char sep, bool collapse) { std::vector result; + if (s.empty()) return result; - std::istringstream buf(s); - for (std::string token; getline(buf, token, sep); ) { - if ((collapse && token != "") || !collapse) { - result.push_back(token); + std::string::size_type start = 0; + std::string::size_type end; + + while ((end = s.find(sep, start)) != std::string::npos) { + std::string token = s.substr(start, end - start); + if (!collapse || !token.empty()) { + result.push_back(std::move(token)); } + start = end + 1; } + + // Handle the last token (after the final separator) + // Only add if there's content or if not collapsing + // Note: match istringstream behavior which does not emit trailing empty token + if (start < s.size()) { + std::string token = s.substr(start); + if (!collapse || !token.empty()) { + result.push_back(std::move(token)); + } + } + return result; } diff --git a/src/webserver.cpp b/src/webserver.cpp index 547eda60..0a0946e9 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -213,8 +213,14 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr std::unique_lock registered_resources_lock(registered_resources_mutex); pair::iterator, bool> result = registered_resources.insert(map::value_type(idx, hrm)); - if (!family && result.second && idx.get_url_pars().empty()) { - registered_resources_str.insert(pair(idx.get_url_complete(), result.first->second)); + if (result.second) { + bool is_exact = !family && idx.get_url_pars().empty(); + if (is_exact) { + registered_resources_str.insert(pair(idx.get_url_complete(), result.first->second)); + } + if (idx.is_regex_compiled()) { + registered_resources_regex.insert(map::value_type(idx, hrm)); + } } return result.second; @@ -386,6 +392,12 @@ bool webserver::stop() { return true; } +void webserver::invalidate_route_cache() { + std::lock_guard lock(route_cache_mutex); + route_cache_list.clear(); + route_cache_map.clear(); +} + void webserver::unregister_resource(const string& resource) { // family does not matter - it just checks the url_normalized anyhow details::http_endpoint he(resource, false, true, regex_checking); @@ -393,6 +405,9 @@ void webserver::unregister_resource(const string& resource) { registered_resources.erase(he); registered_resources.erase(he.get_url_complete()); registered_resources_str.erase(he.get_url_complete()); + registered_resources_regex.erase(he); + registered_resources_lock.unlock(); + invalidate_route_cache(); } void webserver::ban_ip(const string& ip) { @@ -509,7 +524,7 @@ void* uri_log(void* cls, const char* uri, struct MHD_Connection *con) { std::ignore = con; auto mr = std::make_unique(); - mr->complete_uri = std::make_unique(uri); + mr->complete_uri = uri; return reinterpret_cast(mr.release()); } @@ -736,41 +751,77 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details { std::shared_lock registered_resources_lock(registered_resources_mutex); if (!single_resource) { - const char* st_url = mr->standardized_url->c_str(); + const char* st_url = mr->standardized_url.c_str(); fe = registered_resources_str.find(st_url); if (fe == registered_resources_str.end()) { if (regex_checking) { - map::iterator found_endpoint; - details::http_endpoint endpoint(st_url, false, false, false); - map::iterator it; - - size_t len = 0; - size_t tot_len = 0; - for (it = registered_resources.begin(); it != registered_resources.end(); ++it) { - size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); - size_t endpoint_tot_len = (*it).first.get_url_complete().size(); - if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { - if ((*it).first.match(endpoint)) { - found = true; - len = endpoint_pieces_len; - tot_len = endpoint_tot_len; - found_endpoint = it; + // Check the LRU route cache first + { + std::lock_guard cache_lock(route_cache_mutex); + auto cache_it = route_cache_map.find(mr->standardized_url); + if (cache_it != route_cache_map.end()) { + // Cache hit — move to front of LRU list + route_cache_list.splice(route_cache_list.begin(), route_cache_list, cache_it->second); + const route_cache_entry& cached = cache_it->second->second; + + vector url_pars = cached.matched_endpoint.get_url_pars(); + vector url_pieces = endpoint.get_url_pieces(); + vector chunks = cached.matched_endpoint.get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); } + + hrm = cached.resource; + found = true; } } - if (found) { - vector url_pars = found_endpoint->first.get_url_pars(); - - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = found_endpoint->first.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + if (!found) { + // Cache miss — perform regex scan + map::iterator found_endpoint; + + map::iterator it; + + size_t len = 0; + size_t tot_len = 0; + for (it = registered_resources_regex.begin(); it != registered_resources_regex.end(); ++it) { + size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); + size_t endpoint_tot_len = (*it).first.get_url_complete().size(); + if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { + if ((*it).first.match(endpoint)) { + found = true; + len = endpoint_pieces_len; + tot_len = endpoint_tot_len; + found_endpoint = it; + } + } } - hrm = found_endpoint->second; + if (found) { + vector url_pars = found_endpoint->first.get_url_pars(); + + vector url_pieces = endpoint.get_url_pieces(); + vector chunks = found_endpoint->first.get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + } + + hrm = found_endpoint->second; + + // Store in LRU cache + { + std::lock_guard cache_lock(route_cache_mutex); + route_cache_list.emplace_front(mr->standardized_url, route_cache_entry{found_endpoint->first, hrm}); + route_cache_map[mr->standardized_url] = route_cache_list.begin(); + + if (route_cache_map.size() > ROUTE_CACHE_MAX_SIZE) { + route_cache_map.erase(route_cache_list.back().first); + route_cache_list.pop_back(); + } + } + } } } } else { @@ -857,7 +908,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details MHD_Result webserver::complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method) { mr->ws = this; - mr->dhr->set_path(mr->standardized_url->c_str()); + mr->dhr->set_path(mr->standardized_url.c_str()); mr->dhr->set_method(method); mr->dhr->set_version(version); @@ -882,33 +933,33 @@ MHD_Result webserver::answer_to_connection(void* cls, MHD_Connection* connection std::string t_url = url; base_unescaper(&t_url, static_cast(cls)->unescaper); - mr->standardized_url = std::make_unique(http_utils::standardize_url(t_url)); + mr->standardized_url = http_utils::standardize_url(t_url); mr->has_body = false; - access_log(static_cast(cls), *(mr->complete_uri) + " METHOD: " + method); + access_log(static_cast(cls), mr->complete_uri + " METHOD: " + method); - if (0 == strcasecmp(method, http_utils::http_method_get)) { + if (0 == strcmp(method, http_utils::http_method_get)) { mr->callback = &http_resource::render_GET; } else if (0 == strcmp(method, http_utils::http_method_post)) { mr->callback = &http_resource::render_POST; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_put)) { + } else if (0 == strcmp(method, http_utils::http_method_put)) { mr->callback = &http_resource::render_PUT; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_delete)) { + } else if (0 == strcmp(method, http_utils::http_method_delete)) { mr->callback = &http_resource::render_DELETE; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_patch)) { + } else if (0 == strcmp(method, http_utils::http_method_patch)) { mr->callback = &http_resource::render_PATCH; mr->has_body = true; - } else if (0 == strcasecmp(method, http_utils::http_method_head)) { + } else if (0 == strcmp(method, http_utils::http_method_head)) { mr->callback = &http_resource::render_HEAD; - } else if (0 ==strcasecmp(method, http_utils::http_method_connect)) { + } else if (0 == strcmp(method, http_utils::http_method_connect)) { mr->callback = &http_resource::render_CONNECT; - } else if (0 == strcasecmp(method, http_utils::http_method_trace)) { + } else if (0 == strcmp(method, http_utils::http_method_trace)) { mr->callback = &http_resource::render_TRACE; - } else if (0 ==strcasecmp(method, http_utils::http_method_options)) { + } else if (0 == strcmp(method, http_utils::http_method_options)) { mr->callback = &http_resource::render_OPTIONS; } From 96ac18f40cec155ef8a9ba865a7dc2e559e96684 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 14:07:07 -0800 Subject: [PATCH 30/47] Fix validation findings: bounds check, DRY, const refs, empty guard - Add bounds checking for url_pieces[chunks[i]] to prevent potential buffer overflow when cached endpoint chunks don't match request pieces - Extract URL parameter extraction from cache hit/miss paths into single block after match resolution (DRY fix) - Use const references for vectors instead of by-value copies to avoid heap allocations under cache lock - Add empty-string guard to standardize_url before calling back() - Extract ensure_path_pieces_cached() helper to deduplicate caching logic in get_path_pieces() and get_path_piece() - Add RFC 7230 comment documenting case-sensitive strcmp intent --- src/http_utils.cpp | 2 ++ src/httpserver/http_request.hpp | 17 ++++++------- src/webserver.cpp | 42 ++++++++++++++++----------------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 53e7623f..43b522bb 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -218,6 +218,8 @@ std::vector http_utils::tokenize_url(const std::string& str, const } std::string http_utils::standardize_url(const std::string& url) { + if (url.empty()) return url; + std::string result = url; auto new_end = std::unique(result.begin(), result.end(), [](char a, char b) { return (a == b) && (a == '/'); }); diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index fb2677c0..6047c4da 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -93,10 +93,7 @@ class http_request { * @return a vector of strings containing all pieces **/ const std::vector get_path_pieces() const { - if (!cache->path_pieces_cached) { - cache->path_pieces = http::http_utils::tokenize_url(path); - cache->path_pieces_cached = true; - } + ensure_path_pieces_cached(); return cache->path_pieces; } @@ -106,10 +103,7 @@ class http_request { * @return the selected piece in form of string **/ const std::string get_path_piece(int index) const { - if (!cache->path_pieces_cached) { - cache->path_pieces = http::http_utils::tokenize_url(path); - cache->path_pieces_cached = true; - } + ensure_path_pieces_cached(); if (static_cast(cache->path_pieces.size()) > index) { return cache->path_pieces[index]; } @@ -453,6 +447,13 @@ class http_request { bool path_pieces_cached = false; }; std::unique_ptr cache = std::make_unique(); + void ensure_path_pieces_cached() const { + if (!cache->path_pieces_cached) { + cache->path_pieces = http::http_utils::tokenize_url(path); + cache->path_pieces_cached = true; + } + } + // Populate the data cache unescaped_args void populate_args() const; diff --git a/src/webserver.cpp b/src/webserver.cpp index 0a0946e9..efa5862e 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -758,6 +758,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details details::http_endpoint endpoint(st_url, false, false, false); // Check the LRU route cache first + const details::http_endpoint* matched_ep = nullptr; { std::lock_guard cache_lock(route_cache_mutex); auto cache_it = route_cache_map.find(mr->standardized_url); @@ -765,14 +766,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details // Cache hit — move to front of LRU list route_cache_list.splice(route_cache_list.begin(), route_cache_list, cache_it->second); const route_cache_entry& cached = cache_it->second->second; - - vector url_pars = cached.matched_endpoint.get_url_pars(); - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = cached.matched_endpoint.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); - } - + matched_ep = &cached.matched_endpoint; hrm = cached.resource; found = true; } @@ -782,15 +776,13 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details // Cache miss — perform regex scan map::iterator found_endpoint; - map::iterator it; - size_t len = 0; size_t tot_len = 0; - for (it = registered_resources_regex.begin(); it != registered_resources_regex.end(); ++it) { - size_t endpoint_pieces_len = (*it).first.get_url_pieces().size(); - size_t endpoint_tot_len = (*it).first.get_url_complete().size(); + for (auto it = registered_resources_regex.begin(); it != registered_resources_regex.end(); ++it) { + size_t endpoint_pieces_len = it->first.get_url_pieces().size(); + size_t endpoint_tot_len = it->first.get_url_complete().size(); if (!found || endpoint_pieces_len > len || (endpoint_pieces_len == len && endpoint_tot_len > tot_len)) { - if ((*it).first.match(endpoint)) { + if (it->first.match(endpoint)) { found = true; len = endpoint_pieces_len; tot_len = endpoint_tot_len; @@ -800,14 +792,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } if (found) { - vector url_pars = found_endpoint->first.get_url_pars(); - - vector url_pieces = endpoint.get_url_pieces(); - vector chunks = found_endpoint->first.get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); - } - + matched_ep = &found_endpoint->first; hrm = found_endpoint->second; // Store in LRU cache @@ -823,6 +808,18 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } } } + + // Extract URL parameters from matched endpoint + if (found && matched_ep != nullptr) { + const auto& url_pars = matched_ep->get_url_pars(); + const auto& url_pieces = endpoint.get_url_pieces(); + const auto& chunks = matched_ep->get_chunk_positions(); + for (unsigned int i = 0; i < url_pars.size(); i++) { + if (chunks[i] >= 0 && static_cast(chunks[i]) < url_pieces.size()) { + mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + } + } + } } } else { hrm = fe->second; @@ -939,6 +936,7 @@ MHD_Result webserver::answer_to_connection(void* cls, MHD_Connection* connection access_log(static_cast(cls), mr->complete_uri + " METHOD: " + method); + // Case-sensitive per RFC 7230 §3.1.1: HTTP method is case-sensitive. if (0 == strcmp(method, http_utils::http_method_get)) { mr->callback = &http_resource::render_GET; } else if (0 == strcmp(method, http_utils::http_method_post)) { From 7e683da36d6a2e9ddf8b7e44b293813875f010fa Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 14:10:51 -0800 Subject: [PATCH 31/47] Fix use-after-free: copy cache data while holding lock The matched_ep pointer referenced data inside route_cache_list that could be invalidated by another thread (via invalidate_route_cache or LRU eviction) after the cache mutex was released. Fix by copying url_pars and chunk_positions vectors while holding the cache lock, so parameter extraction afterwards uses owned data. For cache miss path, the registered_resources_mutex shared lock is still held so direct reference is safe, but copy for consistency. --- src/webserver.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/webserver.cpp b/src/webserver.cpp index efa5862e..29827888 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -757,8 +757,13 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details if (regex_checking) { details::http_endpoint endpoint(st_url, false, false, false); + // Data needed for parameter extraction after match. + // On cache hit, we copy these while holding the cache lock + // to avoid use-after-free if another thread invalidates cache. + vector matched_url_pars; + vector matched_chunks; + // Check the LRU route cache first - const details::http_endpoint* matched_ep = nullptr; { std::lock_guard cache_lock(route_cache_mutex); auto cache_it = route_cache_map.find(mr->standardized_url); @@ -766,7 +771,8 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details // Cache hit — move to front of LRU list route_cache_list.splice(route_cache_list.begin(), route_cache_list, cache_it->second); const route_cache_entry& cached = cache_it->second->second; - matched_ep = &cached.matched_endpoint; + matched_url_pars = cached.matched_endpoint.get_url_pars(); + matched_chunks = cached.matched_endpoint.get_chunk_positions(); hrm = cached.resource; found = true; } @@ -792,7 +798,9 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } if (found) { - matched_ep = &found_endpoint->first; + // Safe to reference: registered_resources_mutex (shared) is still held + matched_url_pars = found_endpoint->first.get_url_pars(); + matched_chunks = found_endpoint->first.get_chunk_positions(); hrm = found_endpoint->second; // Store in LRU cache @@ -810,13 +818,11 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } // Extract URL parameters from matched endpoint - if (found && matched_ep != nullptr) { - const auto& url_pars = matched_ep->get_url_pars(); + if (found) { const auto& url_pieces = endpoint.get_url_pieces(); - const auto& chunks = matched_ep->get_chunk_positions(); - for (unsigned int i = 0; i < url_pars.size(); i++) { - if (chunks[i] >= 0 && static_cast(chunks[i]) < url_pieces.size()) { - mr->dhr->set_arg(url_pars[i], url_pieces[chunks[i]]); + for (unsigned int i = 0; i < matched_url_pars.size(); i++) { + if (matched_chunks[i] >= 0 && static_cast(matched_chunks[i]) < url_pieces.size()) { + mr->dhr->set_arg(matched_url_pars[i], url_pieces[matched_chunks[i]]); } } } From 305979e8c5947d95749dcf9737a6af1f4ca5b289 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 14:17:56 -0800 Subject: [PATCH 32/47] Fix race condition in unregister_resource cache invalidation Move route cache invalidation inside registered_resources_mutex lock in unregister_resource() to prevent use-after-free when threads retrieve cached resource pointers during unregistration. Also invalidate route cache on register_resource() for correctness when resources are dynamically registered at runtime. Add threading model documentation to http_request lazy caching. --- src/httpserver/http_request.hpp | 6 +++++- src/webserver.cpp | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index 6047c4da..2ff2136c 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -427,7 +427,11 @@ class http_request { std::string_view get_connection_value(std::string_view key, enum MHD_ValueKind kind) const; const http::header_view_map get_headerlike_values(enum MHD_ValueKind kind) const; - // Cache certain data items on demand so we can consistently return views + // http_request objects are owned by a single connection and are not + // shared across threads. Lazy caching (path_pieces, args, etc.) is + // safe without synchronization under this invariant. + + // Cache certain data items on demand so we can consistently return views // over the data. Some things we transform before returning to the user for // simplicity (e.g. query_str, requestor), others out of necessity (arg unescaping). // Others (username, password, digested_user) MHD returns as char* that we need diff --git a/src/webserver.cpp b/src/webserver.cpp index 29827888..7bde4fab 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -221,9 +221,12 @@ bool webserver::register_resource(const std::string& resource, http_resource* hr if (idx.is_regex_compiled()) { registered_resources_regex.insert(map::value_type(idx, hrm)); } + registered_resources_lock.unlock(); + invalidate_route_cache(); + return true; } - return result.second; + return false; } bool webserver::start(bool blocking) { @@ -402,12 +405,20 @@ void webserver::unregister_resource(const string& resource) { // family does not matter - it just checks the url_normalized anyhow details::http_endpoint he(resource, false, true, regex_checking); std::unique_lock registered_resources_lock(registered_resources_mutex); + + // Invalidate cache while holding registered_resources_mutex to prevent + // any thread from retrieving dangling resource pointers from the cache + // after we erase from the resource maps. + { + std::lock_guard cache_lock(route_cache_mutex); + route_cache_list.clear(); + route_cache_map.clear(); + } + registered_resources.erase(he); registered_resources.erase(he.get_url_complete()); registered_resources_str.erase(he.get_url_complete()); registered_resources_regex.erase(he); - registered_resources_lock.unlock(); - invalidate_route_cache(); } void webserver::ban_ip(const string& ip) { From 03cf1e7b2e7214745276b55befc9eac81f8747fc Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Mon, 9 Feb 2026 18:27:29 -0800 Subject: [PATCH 33/47] Add missing #include for std::move Fixes cpplint build/include_what_you_use warning. --- src/string_utilities.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/string_utilities.cpp b/src/string_utilities.cpp index 5f2b3884..697fbf08 100644 --- a/src/string_utilities.cpp +++ b/src/string_utilities.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include namespace httpserver { From db2225ba7d3a86c27c248d28023823dad268db6f Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Wed, 11 Feb 2026 01:45:18 -0800 Subject: [PATCH 34/47] Remove redundant c_str() in set_path call set_path takes const std::string&, so passing .c_str() forces an unnecessary temporary string construction. --- src/webserver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webserver.cpp b/src/webserver.cpp index 7bde4fab..1d72445b 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -922,7 +922,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details MHD_Result webserver::complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method) { mr->ws = this; - mr->dhr->set_path(mr->standardized_url.c_str()); + mr->dhr->set_path(mr->standardized_url); mr->dhr->set_method(method); mr->dhr->set_version(version); From 13e0af8f4fb98f5ecda0eeca28a4d5f824ef9cf6 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 00:45:40 -0800 Subject: [PATCH 35/47] Fix macOS CI: prevent curl from detecting partial OpenSSL curl 7.75.0's NTLM code fails to compile on newer macOS runners because configure detects a partial OpenSSL installation (missing des.h). Adding --without-ssl prevents OpenSSL detection while keeping Secure Transport via --with-darwinssl. Cache key bumped to force a fresh build. --- .github/workflows/verify-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index d5993d24..553088ea 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -467,7 +467,7 @@ jobs: uses: actions/cache@v4 with: path: curl-7.75.0 - key: ${{ matrix.os }}-CURL-pre-built + key: ${{ matrix.os }}-CURL-pre-built-v2 if: ${{ matrix.os == 'macos-latest' }} - name: Build CURL (for testing) @@ -475,7 +475,7 @@ jobs: curl https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-7.75.0.tar.gz -o curl-7.75.0.tar.gz ; tar -xzf curl-7.75.0.tar.gz ; cd curl-7.75.0 ; - ./configure --with-darwinssl ; + ./configure --with-darwinssl --without-ssl ; make ; if: ${{ matrix.os == 'macos-latest' && steps.cache-CURL.outputs.cache-hit != 'true' }} From 27f1be32c4a32066fec395befac06c13c5e14c9d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 01:04:09 -0800 Subject: [PATCH 36/47] Bust stale libmicrohttpd cache on macOS CI The cached libmicrohttpd build has hardcoded GnuTLS include paths from a previous runner version. When make install triggers recompilation on the current runner, gnutls/gnutls.h is not found at the old path. Bumping the cache key forces a fresh build. --- .github/workflows/verify-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 553088ea..4df3e15c 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -512,7 +512,7 @@ jobs: uses: actions/cache@v4 with: path: libmicrohttpd-0.9.77 - key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built-v2 if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' }} - name: Build libmicrohttpd dependency (if not cached) From 8db55fe6f189f3daaaad5d521b2db7aded87b4f4 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 01:14:21 -0800 Subject: [PATCH 37/47] Work around gcov negative hit count bug in coverage report gcov can produce negative branch hit values (gcc bug #68080). Newer gcovr versions treat this as a fatal error. Adding --gcov-ignore-parse-errors=negative_hits.warn_once_per_file lets the coverage report complete while still logging the issue. --- .github/workflows/verify-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 4df3e15c..17ae2955 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -742,7 +742,7 @@ jobs: - name: Generate coverage report run: | cd build - gcovr --root .. --filter '../src/' --xml coverage.xml --xml-pretty --print-summary + gcovr --root .. --filter '../src/' --xml coverage.xml --xml-pretty --print-summary --gcov-ignore-parse-errors=negative_hits.warn_once_per_file if: ${{ matrix.os-type == 'ubuntu' && matrix.c-compiler == 'gcc' && matrix.debug == 'debug' && matrix.coverage == 'coverage' && success() }} - name: Upload coverage to Codecov From d59dc500148438699f09aac744373166a59901cf Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 09:31:33 -0800 Subject: [PATCH 38/47] Fix custom_socket test failure under Valgrind on Ubuntu 24.04 Set SO_REUSEADDR before bind() so the port can be reused when still in TIME_WAIT from prior tests, and connect to 127.0.0.1 instead of localhost to avoid IPv6 resolution mismatch with the IPv4-only socket. --- test/integ/ws_start_stop.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index 34ab2fdb..d664301a 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -337,6 +337,9 @@ LT_END_AUTO_TEST(enable_options) LT_BEGIN_AUTO_TEST(ws_start_stop_suite, custom_socket) int fd = socket(AF_INET, SOCK_STREAM, 0); + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr("127.0.0.1"); @@ -353,7 +356,7 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, custom_socket) std::string s; CURL *curl = curl_easy_init(); CURLcode res; - curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/base"); + curl_easy_setopt(curl, CURLOPT_URL, "127.0.0.1:" PORT_STRING "/base"); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); From 755ecc1a156704d9648d773a11cbd581b4f31c5d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 15:37:57 -0800 Subject: [PATCH 39/47] Fixes to release/dist mechanism --- .github/workflows/changelog-check.yml | 84 ++++++ .github/workflows/release.yml | 326 +++++++++++++++++++++++ CONTRIBUTING.md | 16 ++ ChangeLog | 354 ++++++++----------------- Makefile.am | 2 +- scripts/extract-release-notes.sh | 44 +++ scripts/validate-version.sh | 68 +++++ src/create_test_request.cpp | 69 +++++ src/httpserver/create_test_request.hpp | 145 ++++++++++ test/unit/create_test_request_test.cpp | 283 ++++++++++++++++++++ 10 files changed, 1146 insertions(+), 245 deletions(-) create mode 100644 .github/workflows/changelog-check.yml create mode 100644 .github/workflows/release.yml create mode 100755 scripts/extract-release-notes.sh create mode 100755 scripts/validate-version.sh create mode 100644 src/create_test_request.cpp create mode 100644 src/httpserver/create_test_request.hpp create mode 100644 test/unit/create_test_request_test.cpp diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml new file mode 100644 index 00000000..632cedfb --- /dev/null +++ b/.github/workflows/changelog-check.yml @@ -0,0 +1,84 @@ +name: "ChangeLog Check" + +on: + pull_request: + branches: [master] + +jobs: + changelog: + name: Verify ChangeLog updated + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for ChangeLog update + run: | + # Get list of changed files + changed_files=$(git diff --name-only origin/master...HEAD) + + if [ -z "$changed_files" ]; then + echo "No files changed." + exit 0 + fi + + # Check if only exempt files were changed + # Exempt: .github/*, CLAUDE.md, README*, CONTRIBUTING*, CODE_OF_CONDUCT*, + # .gitignore, CPPLINT.cfg, *.md in root + has_non_exempt=false + changelog_modified=false + + while IFS= read -r file; do + # Check if ChangeLog itself was modified + if [ "$file" = "ChangeLog" ]; then + changelog_modified=true + continue + fi + + # Check exempt patterns + case "$file" in + .github/*) continue ;; + CLAUDE.md) continue ;; + README*) continue ;; + CONTRIBUTING*) continue ;; + CODE_OF_CONDUCT*) continue ;; + .gitignore) continue ;; + CPPLINT.cfg) continue ;; + esac + + # Check for *.md files in repo root (no slashes in path) + if echo "$file" | grep -qE '^[^/]+\.md$'; then + continue + fi + + has_non_exempt=true + done <<< "$changed_files" + + if [ "$has_non_exempt" = "false" ]; then + echo "Only exempt files changed — ChangeLog update not required." + exit 0 + fi + + if [ "$changelog_modified" = "false" ]; then + echo "::error::ChangeLog was not updated. All pull requests with code changes must include a ChangeLog entry." + echo "" + echo "Please add a tab-indented entry under the first 'Version X.Y.Z' header in ChangeLog." + echo "See CONTRIBUTING.md for format details." + echo "" + echo "If this PR only changes documentation or CI files, add the [skip changelog] label or ensure" + echo "only exempt paths are modified (.github/*, *.md in root, .gitignore, CPPLINT.cfg)." + exit 1 + fi + + echo "ChangeLog was modified — checking format." + + # Validate first line matches Version header format + first_line=$(head -n 1 ChangeLog) + if ! echo "$first_line" | grep -qE '^Version [0-9]+\.[0-9]+\.[0-9]+'; then + echo "::error::First line of ChangeLog must match 'Version X.Y.Z' format (got: '$first_line')." + exit 1 + fi + + echo "ChangeLog format looks good." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..81db8332 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,326 @@ +name: "Release" + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., 0.20.0 or 0.20.0-rc1)' + required: true + +permissions: + contents: write + +jobs: + validate: + name: Validate release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + is_prerelease: ${{ steps.version.outputs.is_prerelease }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Determine version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.tag }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + fi + VERSION="${VERSION#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + if echo "$VERSION" | grep -qE '-(rc|alpha|beta)'; then + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + + echo "Version: $VERSION" + + - name: Validate version consistency + run: | + chmod +x scripts/validate-version.sh + scripts/validate-version.sh "${{ steps.version.outputs.version }}" + + - name: Extract release notes + run: | + chmod +x scripts/extract-release-notes.sh + scripts/extract-release-notes.sh "${{ steps.version.outputs.version }}" > release-notes.md + echo "--- Release notes ---" + cat release-notes.md + + - name: Upload release notes + uses: actions/upload-artifact@v4 + with: + name: release-notes + path: release-notes.md + + build-dist: + name: Build distribution tarball + runs-on: ubuntu-latest + needs: validate + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool libgnutls28-dev libcurl4-openssl-dev + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77 + key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + + - name: Build libmicrohttpd (if not cached) + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-0.9.77 + sudo make install + sudo ldconfig + + - name: Build libhttpserver + run: | + ./bootstrap + mkdir build + cd build + ../configure + make + make check + + - name: Create distribution tarball + run: | + cd build + make dist + + - name: Upload tarball + uses: actions/upload-artifact@v4 + with: + name: dist-tarball + path: build/libhttpserver-*.tar.gz + + verify-dist-linux: + name: Verify tarball (Linux) + runs-on: ubuntu-latest + needs: build-dist + steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgnutls28-dev libcurl4-openssl-dev + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77 + key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + + - name: Build libmicrohttpd (if not cached) + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-0.9.77 + sudo make install + sudo ldconfig + + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Build and test from tarball + run: | + tar -xzf libhttpserver-*.tar.gz + cd libhttpserver-*/ + mkdir build + cd build + ../configure + make + make check + + - name: Print test results on failure + if: failure() + run: | + cd libhttpserver-*/build + cat test/test-suite.log || true + + verify-dist-macos: + name: Verify tarball (macOS) + runs-on: macos-latest + needs: build-dist + steps: + - name: Install build tools + run: brew install autoconf automake libtool + + - name: Fetch libmicrohttpd from cache + id: cache-libmicrohttpd + uses: actions/cache@v4 + with: + path: libmicrohttpd-0.9.77 + key: macos-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + + - name: Build libmicrohttpd (if not cached) + if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples + make + + - name: Install libmicrohttpd + run: | + cd libmicrohttpd-0.9.77 + sudo make install + + - name: Fetch curl from cache + id: cache-curl + uses: actions/cache@v4 + with: + path: curl-7.75.0 + key: macos-latest-CURL-pre-built-v2 + + - name: Build curl (if not cached) + if: steps.cache-curl.outputs.cache-hit != 'true' + run: | + curl https://libhttpserver.s3.amazonaws.com/travis_stuff/curl-7.75.0.tar.gz -o curl-7.75.0.tar.gz + tar -xzf curl-7.75.0.tar.gz + cd curl-7.75.0 + ./configure --with-darwinssl --without-ssl + make + + - name: Install curl + run: | + cd curl-7.75.0 + sudo make install + + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Build and test from tarball + run: | + tar -xzf libhttpserver-*.tar.gz + cd libhttpserver-*/ + mkdir build + cd build + CFLAGS='-mtune=generic' ../configure --disable-fastopen + make + make check + + - name: Print test results on failure + if: failure() + run: | + cd libhttpserver-*/build + cat test/test-suite.log || true + + verify-dist-windows: + name: Verify tarball (Windows) + runs-on: windows-latest + needs: build-dist + defaults: + run: + shell: msys2 {0} + steps: + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + autotools + base-devel + + - name: Install MinGW64 packages + run: | + pacman --noconfirm -S --needed mingw-w64-x86_64-{toolchain,libtool,make,pkg-config,libsystre,doxygen,gnutls,graphviz,curl} + + - name: Build and install libmicrohttpd + run: | + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz + tar -xzf libmicrohttpd-0.9.77.tar.gz + cd libmicrohttpd-0.9.77 + ./configure --disable-examples --enable-poll=no + make + make install + + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Build and test from tarball + run: | + tar -xzf libhttpserver-*.tar.gz + cd libhttpserver-*/ + mkdir build + cd build + ../configure --disable-fastopen + make + make check + + - name: Print test results on failure + if: failure() + run: | + cd libhttpserver-*/build + cat test/test-suite.log || true + + release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [validate, verify-dist-linux, verify-dist-macos, verify-dist-windows] + steps: + - name: Download tarball + uses: actions/download-artifact@v4 + with: + name: dist-tarball + + - name: Download release notes + uses: actions/download-artifact@v4 + with: + name: release-notes + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + VERSION="${{ needs.validate.outputs.version }}" + IS_PRERELEASE="${{ needs.validate.outputs.is_prerelease }}" + + PRERELEASE_FLAG="" + if [ "$IS_PRERELEASE" = "true" ]; then + PRERELEASE_FLAG="--prerelease" + fi + + gh release create "$VERSION" \ + --title "libhttpserver $VERSION" \ + --notes-file release-notes.md \ + $PRERELEASE_FLAG \ + libhttpserver-*.tar.gz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee018af3..d923f747 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,6 +127,22 @@ Please follow these steps to have your contribution considered by the maintainer While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. +### ChangeLog + +All pull requests that include user-facing changes must update the `ChangeLog` file. CI enforces this requirement (with exemptions for documentation-only and CI-only changes). + +* Add your entry under the **first** `Version X.Y.Z` header at the top of `ChangeLog`. +* Use a tab-indented, one-line summary of each change: + ``` + Added support for new feature X. + ``` +* If your change spans multiple lines, indent continuation lines with two tabs: + ``` + Fixed a bug where long descriptions would cause the parser to + fail on multi-line input. + ``` +* Changes that only touch files in `.github/`, `*.md` in the repository root, `.gitignore`, or `CPPLINT.cfg` are exempt from the ChangeLog requirement. + ## Styleguides ### Git Commit Messages diff --git a/ChangeLog b/ChangeLog index be175e07..faeccb65 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,5 @@ -Thu Jun 15 8:55:04 2023 -0800 +Version 0.19.0 - 2023-06-15 + Considering family_url as part of the priority when selecting a URL to match. More explicit selection of C++ version. Ability to handle multiple parameters with the same name on the URL. @@ -11,17 +12,11 @@ Thu Jun 15 8:55:04 2023 -0800 Code cleanups. Better use of RAII. Improved test coverage. - -Sun Mar 07 20:02:10 2021 -0800 Cleaned code to support cpplint and extra warnings. Use pointers in place of non-const references. - -Thu Feb 25 20:27:12 2021 -0800 - Simplified dependency management for libmicrohttpd - -Sat Nov 21 07:20:00 2020 -0800 + Simplified dependency management for libmicrohttpd. Added support on build for CodeQL security checks. - Moved builds to travis.com + Moved builds to travis.com. Added IWYU checks as part of build and cleaned-up accordingly. Introduced dual-stack support. Added OS specific tips, and cleaned up some compiler warnings. @@ -31,37 +26,34 @@ Sat Nov 21 07:20:00 2020 -0800 Moved windows builds to AppVeyor. Made the library compatible with libmicrohttpd v0.9.71 and above. -Sat Jun 6 10:21:05 2020 -0800 +Version 0.18.1 - 2020-06-06 + Prevent use of regex in http_endpoint outside of registration which could allow DOS attacks. -Sat May 16 07:20:00 2020 -0800 +Version 0.18.0 - 2020-05-16 + General performance improvements (reduced use of regex, lazy-building of - post-processor) - General code cleanup - General fixes to the documentation - Fixed support on FreeBSD (added missing headers) - Fixed support for Cygwin - Removed depedency on C regex - now using C++11 regex - -Sat Aug 10 18:34:07 2019 -0800 - Added support for TCP-NODELAY - Changed set_path on http_request to have lazy behavior - -Tue Aug 06 22:22:14 2019 -0800 + post-processor). + General code cleanup. + General fixes to the documentation. + Fixed support on FreeBSD (added missing headers). + Fixed support for Cygwin. + Removed depedency on C regex - now using C++11 regex. + Added support for TCP-NODELAY. + Changed set_path on http_request to have lazy behavior. Added support for body parsing in DELETE requests. - Added support for PATCH method + Added support for PATCH method. -Sat Jan 27 21:59:11 2019 -0800 - libhttpserver now includes set of examples to demonstrate the main capabilities of the library +Version 0.17.5 - 2019-01-28 + + libhttpserver now includes set of examples to demonstrate the main capabilities of the library. "examples" are now optionally disabled. - Adds valgrind memcheck to the build system on travis - Travis now tests performance with apache benchmark + Adds valgrind memcheck to the build system on travis. + Travis now tests performance with apache benchmark. Reduced the CPU time spent in normalizing URLs (thus saving ~15% on average per request). - All classes now implement move constructor and move assignment operator + All classes now implement move constructor and move assignment operator. The library now avoids collecting connection properties (headers, arguments, footers, cookies, etc...) unless explicitly asked by the client code. - -Sat Jan 12 00:51:00 2019 -0800 Removed the support for integrated COMET logic. Removed the support for caching logic. Added integ tests. @@ -69,255 +61,129 @@ Sat Jan 12 00:51:00 2019 -0800 Improved interface of the http_response object. Deprecated http_response_builder object. -Thu Dec 26 10:00:30 2018 -0800 - Fixed IPV6 parsing logic. - Added tests to support IP parsing, URL parsing and utilities - -Thu Nov 22 20:58:00 2018 -0800 - Solved problem with the server not being able to start on mac os +Version 0.16.0 - 2018-12-26 -Sun Nov 04 19:28:00 2018 -0800 - Moved http_endpoint as a sub-class of webserver. This avoids usage of friends. + Fixed IPV6 parsing logic. + Added tests to support IP parsing, URL parsing and utilities. -Wed Feb 26 21:31:00 2017 +0000 - Fixed problem with segfault when copying http_response object +Version 0.15.0 - 2018-11-23 -Wed Feb 12 13:14:01 2017 +0000 - Updated to libmicrohttpd 0.9.52 + Solved problem with the server not being able to start on mac os. -Wed Jul 13 02:23:11 2016 +0100 - Fixed problems with large payloads - Fixed memory leak in http_response_ptr +Version 0.14.0 - 2018-11-05 -Tue Dec 29 18:56:31 2015 +0100 - Removed support for event supplier (badly defined, complicated and almost useless) - Eliminated custom selection logic (simplified overall code in webserver.cpp) - Changed comet to use a lock-free implementation + Moved http_endpoint as a sub-class of webserver. This avoids usage of friends. -Sun Dec 27 19:39:01 2015 +0100 - Removed POLL start configuration (THREAD now defaults to POLL or EPOLL on Linux) - Use TCP_FASTOPEN on linux >= 3.6 +Version 0.13.0 - 2017-02-26 -Sat Dec 26 15:08:22 2015 +0100 - Changed http_resource to use classic C++ polymorphism using virtual instead of CRTP + Fixed problem with segfault when copying http_response object. -Fri Jul 17 21:38:54 2015 +0000 - Removed build dependency on pkg-config +Version 0.12.0 - 2017-02-12 -Wed Apr 15 01:40:11 2015 +0000 - Support build on MacOsX - Improved support for CI on travis - Solved bug on event_supplier registering - Solved bug on standardize_url to avoid removing root - Change cycle_callback_ptr so that buffer can be modified - Moved to version 0.9.0 + Updated to libmicrohttpd 0.9.52. -Sun Jul 23 02:46:20 2014 +0100 - Support for building on MinGW/Cygwin systems - min libmicrohttpd version moved to 0.9.37 - Moved to version 0.8.0 +Version 0.11.1 - 2016-07-13 -Sat Mar 23 15:22:40 2014 +0100 - Continue the cleanup reducing webserver.cpp responsibilities - Deep work on documentation - Moved to version 0.7.2 + Fixed problems with large payloads. + Fixed memory leak in http_response_ptr. -Sat Jan 25 16:31:03 2014 +0100 - Cleaned-up webserver.cpp code to extract secondary classes - Enforced immutability of webserver class - Enabled library to compile on g++ 4.1.2 +Version 0.11.0 - 2015-12-26 -Wed Oct 31 17:59:40 2012 +0100 - Added parameter in http_response to specify if it needs to be deleted by - WS - Sebastiano Merlino + Removed support for event supplier (badly defined, complicated and almost useless). + Eliminated custom selection logic (simplified overall code in webserver.cpp). + Changed comet to use a lock-free implementation. + Removed POLL start configuration (THREAD now defaults to POLL or EPOLL on Linux). + Use TCP_FASTOPEN on linux >= 3.6. + Changed http_resource to use classic C++ polymorphism using virtual instead of CRTP. + Removed build dependency on pkg-config. -Wed Oct 31 14:23:57 2012 +0100 - Changed dependency download method - Sebastiano Merlino +Version 0.9.0 - 2015-04-15 -Wed Oct 31 14:13:49 2012 +0100 - Added dependency to travis - Sebastiano Merlino + Support build on MacOsX. + Improved support for CI on travis. + Solved bug on event_supplier registering. + Solved bug on standardize_url to avoid removing root. + Change cycle_callback_ptr so that buffer can be modified. -Wed Oct 31 14:07:30 2012 +0100 - Changed travis build path - Sebastiano Merlino +Version 0.8.0 - 2014-07-23 -Wed Oct 31 14:02:59 2012 +0100 - Added travis conf to repo - Sebastiano Merlino + Support for building on MinGW/Cygwin systems. + min libmicrohttpd version moved to 0.9.37. -Tue Oct 30 16:13:10 2012 +0100 - Changed the buggy debian changelog - Sebastiano Merlino +Version 0.7.2 - 2014-03-23 -Tue Oct 30 16:06:26 2012 +0100 - Changed version to v0.5.4 - Sebastiano Merlino + Continue the cleanup reducing webserver.cpp responsibilities. + Deep work on documentation. -Tue Oct 30 15:59:45 2012 +0100 - Adjusted debian build rules - Sebastiano Merlino +Version 0.7.0 - 2014-01-25 -Tue Oct 30 12:52:04 2012 +0100 - Changed version to 0.5.3 - Added grow method to http_request - Sebastiano Merlino + Cleaned-up webserver.cpp code to extract secondary classes. + Enforced immutability of webserver class. + Enabled library to compile on g++ 4.1.2. -Tue Oct 23 12:46:48 2012 +0200 - Changed version from 0.5.1 to 0.5.2 - Sebastiano Merlino +Version 0.5.4 - 2012-10-30 -Tue Oct 23 12:46:07 2012 +0200 - Changed default log behaviour to print nothing - Added getters and setters for dynamic components of WS - Sebastiano Merlino + Added parameter in http_response to specify if it needs to be deleted by WS. + Changed dependency download method. + Added travis CI configuration. + Changed the buggy debian changelog. -Mon Oct 22 12:13:11 2012 +0200 - Modified version number and changelog in order to prepare tag - Sebastiano Merlino +Version 0.5.3 - 2012-10-30 -Fri Oct 19 17:11:21 2012 +0200 - Added response constructor with byte - Sebastiano Merlino + Added grow method to http_request. -Mon Oct 15 11:16:22 2012 +0200 - Removed unuseful dependency from libuuid - Sebastiano Merlino +Version 0.5.2 - 2012-10-23 -Fri Oct 12 15:42:21 2012 +0200 - Solved a bug that made impossible to parse post data - Sebastiano Merlino + Changed default log behaviour to print nothing. + Added getters and setters for dynamic components of WS. -Wed Oct 10 17:19:25 2012 +0200 - Moved to version 0.5.1 - Sebastiano Merlino +Version 0.5.1 - 2012-10-10 -Wed Oct 10 17:16:26 2012 +0200 - Added querystring to request attributes - Sebastiano Merlino + Added querystring to request attributes. + Added response constructor with byte. + Removed unuseful dependency from libuuid. + Solved a bug that made impossible to parse post data. -Fri Oct 5 18:00:38 2012 +0200 - Merge branch 'master' of https://github.com/etr/libhttpserver - Conflicts: - src/webserver.cpp - Sebastiano Merlino +Version 0.5.0 - 2012-10-05 -Fri Oct 5 17:55:42 2012 +0200 Added -D_REENTRANT to configuration. Aligned debian changelog. - Added comet capabilities to the server. - Sebastiano Merlino - -Tue Sep 25 00:50:45 2012 +0200 - Solved a bug with print in debug mode - Sebastiano Merlino - -Mon Sep 24 15:29:28 2012 +0200 - Modified webserver in order to accept comet calls - Added ignored patters in gitignore - Sebastiano Merlino - -Sun Sep 23 19:10:28 2012 +0200 - Partially solved undefined symbol in wrappers - Sebastiano Merlino - -Sun Sep 23 19:09:54 2012 +0200 - Avoided the usage of the sole option MHD_USE_POLL - Sebastiano Merlino - -Thu Sep 20 08:47:24 2012 +0200 - Added forgotten modded_request.hpp file - Sebastiano Merlino + Added comet capabilities to the server. + Solved a bug with print in debug mode. + Modified webserver in order to accept comet calls. + Partially solved undefined symbol in wrappers. + Avoided the usage of the sole option MHD_USE_POLL. + Added forgotten modded_request.hpp file. + Added .gitignore file. + Moved http_endpoint to details namespace. -Thu Sep 20 08:46:33 2012 +0200 - Added .gitignore file - Sebastiano Merlino +Version 0.4.0 - 2012-08-26 -Sat Sep 15 13:02:52 2012 +0200 - Moved http_endpoint to details namespace - Sebastiano Merlino - -Sat Sep 15 02:39:47 2012 -0700 - Merge pull request #35 from etr/cflags_for_swig_in_pcfile - add -I${includedir}/httpserver to CFLAGS - Sebastiano Merlino - -Tue Aug 28 16:33:45 2012 +0200 - add -I${includedir}/httpserver to CFLAGS - This make swig file generation easier because HTTPSERVER_CFLAGS can be - directly used in swig file generation. - This fix affect only clients that use swing on their code. - Dario Mazza - -Sun Aug 26 19:03:44 2012 +0200 - Changed version. - Aligned version and dependencies in pc and debian files - Updated debian changelog. - Sebastiano Merlino - -Sun Aug 26 18:55:05 2012 +0200 Changed visibility of http_endpoint methods to avoid them to be called - by external applications. + by external applications. Avoided explicit usage of MHD constants in classes interface. Changed http_resource interface in order to avoid copy-constructor calls - and improve performances. + and improve performances. Changed answer_to_connection method in order to avoid multiple checking - on methods and thus improve performances. - Added a way to register personalized error pages. - Sebastiano Merlino - -Wed Aug 8 17:33:39 2012 +0200 - Removed code repetition in handle_request method - Sebastiano Merlino - -Wed Aug 8 12:31:44 2012 +0200 - Added capability to compile with gcov - Changed infinite loop in ws to use wait conditions - Removed a bug from GET-like method handling - Sebastiano Merlino - -Sun Aug 5 18:26:25 2012 +0200 - Modified in order to parse qs in POST/PUT cases - Sebastiano Merlino - -Fri Aug 3 23:36:14 2012 +0200 - Avoid inclusion of internal headers - Sebastiano Merlino - -Thu Aug 2 00:43:02 2012 +0200 - Changed in order to find libmicrohttpd in system - Sebastiano Merlino - -Thu Jul 26 14:08:47 2012 +0200 - Solved some performance and style issues - Sebastiano Merlino - -Wed Jul 25 18:42:48 2012 +0200 - Merge branch 'master' of github.com:etr/libhttpserver - Sebastiano Merlino - -Wed Jul 25 18:41:45 2012 +0200 - Added some comments to http_endpoint and http_request - Sebastiano Merlino - -Wed Jul 25 08:58:04 2012 -0700 - Merge pull request #29 from etr/libtool_version_number - using m4 to define major,minor and revision number in configure.ac - Sebastiano Merlino - -Wed Jul 25 17:50:05 2012 +0200 - using m4 to define major,minor and revision number in configure.ac and send version number to libtool and AC_INIT - Dario Mazza - -Wed Jul 25 17:10:49 2012 +0200 - Changed in order to solve some problems with deb package and rpm package - Sebastiano Merlino - -Tue Jul 24 16:55:51 2012 -0700 - Merge pull request #28 from etr/debpkg_patch_deps - added parameter used to ignore dependecies during debpkg creation - Sebastiano Merlino - -Wed Jul 25 01:51:52 2012 +0200 - added parameter used to ignore dependecies during debpkg creation - Dario Mazza - -Wed Jul 25 00:42:25 2012 +0200 - Adjusted errors in debian rules - Sebastiano Merlino - -Tue Jul 24 16:37:07 2012 +0200 - Modified rpm build in order to compile it - Lowered required version of libmicrohttpd to 0.9.7 - Sebastiano Merlino - -Tue Jul 24 13:28:38 2012 +0200 - Changed also build default directory for debs - Sebastiano Merlino - -Tue Jul 24 13:22:59 2012 +0200 - Changed rules.in in order to avoid relative paths in deb compile - Sebastiano Merlino - -Mon Jul 23 15:42:33 2012 +0200 - Solved a logical error in http_resource route - Added some debug prints - Sebastiano Merlino - -Sun Jul 22 00:24:04 2012 +0200 - Changed in order to add optional optimizations on ws - Sebastiano Merlino - -Sat Jul 21 17:46:03 2012 +0200 - Changed in order to enhance deb packages generation - Added rpm packages generation - Sebastiano Merlino - -Sat Jul 21 00:43:39 2012 +0200 - adjusted error in changelog - Sebastiano Merlino - -Sat Jul 21 00:41:43 2012 +0200 - Changed in order to include debian package creation to makefile - Sebastiano Merlino - -Fri Jul 20 12:11:30 2012 -0700 - Merge pull request #26 from etr/debpackage - project debianized - Sebastiano Merlino - -Fri Jul 20 21:03:43 2012 +0200 - Merge branch 'master' of github.com:etr/libhttpserver - Sebastiano Merlino - -Fri Jul 20 21:03:24 2012 +0200 - Changed version - Sebastiano Merlino - + on methods and thus improve performances. + Added a way to register personalized error pages. + Removed code repetition in handle_request method. + Added capability to compile with gcov. + Changed infinite loop in ws to use wait conditions. + Removed a bug from GET-like method handling. + Modified in order to parse qs in POST/PUT cases. + Avoid inclusion of internal headers. + Changed in order to find libmicrohttpd in system. + Solved some performance and style issues. + Added some comments to http_endpoint and http_request. + using m4 to define major, minor and revision number in configure.ac. + Changed in order to solve some problems with deb package and rpm package. + Added parameter used to ignore dependecies during debpkg creation. + Adjusted errors in debian rules. + Modified rpm build in order to compile it. + Lowered required version of libmicrohttpd to 0.9.7. + Solved a logical error in http_resource route. + Changed in order to add optional optimizations on ws. + Changed in order to enhance deb packages generation. + Added rpm packages generation. + Changed in order to include debian package creation to makefile. diff --git a/Makefile.am b/Makefile.am index a46882fd..02121fde 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,7 +38,7 @@ endif endif -EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) +EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) scripts/extract-release-notes.sh scripts/validate-version.sh MOSTLYCLEANFILES = $(DX_CLEANFILES) *.gcda *.gcno *.gcov DISTCLEANFILES = DIST_REVISION diff --git a/scripts/extract-release-notes.sh b/scripts/extract-release-notes.sh new file mode 100755 index 00000000..ddc129d5 --- /dev/null +++ b/scripts/extract-release-notes.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Extract release notes for a given version from the ChangeLog. +# Usage: extract-release-notes.sh [VERSION] +# If VERSION is omitted, extracts the first (most recent) section. + +set -euo pipefail + +VERSION="${1:-}" + +# Strip leading 'v' if present +VERSION="${VERSION#v}" + +CHANGELOG="${CHANGELOG:-ChangeLog}" + +if [ ! -f "$CHANGELOG" ]; then + echo "Error: $CHANGELOG not found" >&2 + exit 1 +fi + +if [ -z "$VERSION" ]; then + # Extract the first version section (everything between the first and second headers) + awk ' + /^Version [0-9]+\.[0-9]+\.[0-9]+/ { + if (found) exit + found = 1 + next + } + found && /^$/ && !started { next } + found { started = 1; print } + ' "$CHANGELOG" | sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' +else + # Extract notes for the specific version + awk -v ver="$VERSION" ' + /^Version [0-9]+\.[0-9]+\.[0-9]+/ { + if (found) exit + if (index($0, "Version " ver " ") == 1 || $0 == "Version " ver) { + found = 1 + next + } + } + found && /^$/ && !started { next } + found { started = 1; print } + ' "$CHANGELOG" | sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' +fi diff --git a/scripts/validate-version.sh b/scripts/validate-version.sh new file mode 100755 index 00000000..583fdb36 --- /dev/null +++ b/scripts/validate-version.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Validate version consistency between a tag, configure.ac, and ChangeLog. +# Usage: validate-version.sh VERSION +# VERSION should NOT have a 'v' prefix (e.g., "0.19.0"). + +set -euo pipefail + +VERSION="${1:-}" + +if [ -z "$VERSION" ]; then + echo "Usage: validate-version.sh VERSION" >&2 + exit 1 +fi + +# Strip leading 'v' if present +VERSION="${VERSION#v}" + +CONFIGURE_AC="${CONFIGURE_AC:-configure.ac}" +CHANGELOG="${CHANGELOG:-ChangeLog}" + +errors=0 + +# Parse expected major.minor.revision +IFS='.' read -r expected_major expected_minor expected_revision <<< "${VERSION%%-*}" + +if [ -z "$expected_major" ] || [ -z "$expected_minor" ] || [ -z "$expected_revision" ]; then + echo "Error: VERSION must be in X.Y.Z format (got: $VERSION)" >&2 + exit 1 +fi + +# Check configure.ac +if [ ! -f "$CONFIGURE_AC" ]; then + echo "Error: $CONFIGURE_AC not found" >&2 + exit 1 +fi + +actual_major=$(grep 'm4_define(\[libhttpserver_MAJOR_VERSION\]' "$CONFIGURE_AC" | sed 's/.*\[\([0-9]*\)\].*/\1/') +actual_minor=$(grep 'm4_define(\[libhttpserver_MINOR_VERSION\]' "$CONFIGURE_AC" | sed 's/.*\[\([0-9]*\)\].*/\1/') +actual_revision=$(grep 'm4_define(\[libhttpserver_REVISION\]' "$CONFIGURE_AC" | sed 's/.*\[\([0-9]*\)\].*/\1/') + +if [ "$actual_major" != "$expected_major" ] || [ "$actual_minor" != "$expected_minor" ] || [ "$actual_revision" != "$expected_revision" ]; then + echo "Error: configure.ac version ($actual_major.$actual_minor.$actual_revision) does not match tag ($VERSION)" >&2 + errors=$((errors + 1)) +else + echo "OK: configure.ac version matches ($actual_major.$actual_minor.$actual_revision)" +fi + +# Check ChangeLog has a Version header for this version +if [ ! -f "$CHANGELOG" ]; then + echo "Error: $CHANGELOG not found" >&2 + exit 1 +fi + +# Match "Version X.Y.Z" at start of line (allowing trailing date or text) +base_version="${VERSION%%-*}" +if grep -q "^Version ${base_version}" "$CHANGELOG"; then + echo "OK: ChangeLog contains Version ${base_version} header" +else + echo "Error: ChangeLog missing 'Version ${base_version}' header" >&2 + errors=$((errors + 1)) +fi + +if [ "$errors" -gt 0 ]; then + echo "Validation failed with $errors error(s)" >&2 + exit 1 +fi + +echo "Version validation passed for $VERSION" diff --git a/src/create_test_request.cpp b/src/create_test_request.cpp new file mode 100644 index 00000000..31c7d3df --- /dev/null +++ b/src/create_test_request.cpp @@ -0,0 +1,69 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +*/ + +#include "httpserver/create_test_request.hpp" + +#include +#include + +namespace httpserver { + +http_request create_test_request::build() { + http_request req; + + req.set_method(_method); + req.set_path(_path); + req.set_version(_version); + req.set_content(_content); + + req.headers_local = std::move(_headers); + req.footers_local = std::move(_footers); + req.cookies_local = std::move(_cookies); + + for (auto& [key, values] : _args) { + for (auto& value : values) { + req.cache->unescaped_args[key].push_back(std::move(value)); + } + } + req.cache->args_populated = true; + + if (!_querystring.empty()) { + req.cache->querystring = std::move(_querystring); + } + + req.cache->username = std::move(_user); + req.cache->password = std::move(_pass); + +#ifdef HAVE_DAUTH + req.cache->digested_user = std::move(_digested_user); +#endif // HAVE_DAUTH + + req.cache->requestor_ip = std::move(_requestor); + req.requestor_port_local = _requestor_port; + +#ifdef HAVE_GNUTLS + req.tls_enabled_local = _tls_enabled; +#endif // HAVE_GNUTLS + + return req; +} + +} // namespace httpserver diff --git a/src/httpserver/create_test_request.hpp b/src/httpserver/create_test_request.hpp new file mode 100644 index 00000000..177257c2 --- /dev/null +++ b/src/httpserver/create_test_request.hpp @@ -0,0 +1,145 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#if !defined (_HTTPSERVER_HPP_INSIDE_) && !defined (HTTPSERVER_COMPILATION) +#error "Only or can be included directly." +#endif + +#ifndef SRC_HTTPSERVER_CREATE_TEST_REQUEST_HPP_ +#define SRC_HTTPSERVER_CREATE_TEST_REQUEST_HPP_ + +#include +#include +#include + +#include "httpserver/http_request.hpp" +#include "httpserver/http_utils.hpp" + +namespace httpserver { + +class create_test_request { + public: + create_test_request() = default; + + create_test_request& method(const std::string& method) { + _method = method; + return *this; + } + + create_test_request& path(const std::string& path) { + _path = path; + return *this; + } + + create_test_request& version(const std::string& version) { + _version = version; + return *this; + } + + create_test_request& content(const std::string& content) { + _content = content; + return *this; + } + + create_test_request& header(const std::string& key, const std::string& value) { + _headers[key] = value; + return *this; + } + + create_test_request& footer(const std::string& key, const std::string& value) { + _footers[key] = value; + return *this; + } + + create_test_request& cookie(const std::string& key, const std::string& value) { + _cookies[key] = value; + return *this; + } + + create_test_request& arg(const std::string& key, const std::string& value) { + _args[key].push_back(value); + return *this; + } + + create_test_request& querystring(const std::string& querystring) { + _querystring = querystring; + return *this; + } + + create_test_request& user(const std::string& user) { + _user = user; + return *this; + } + + create_test_request& pass(const std::string& pass) { + _pass = pass; + return *this; + } + +#ifdef HAVE_DAUTH + create_test_request& digested_user(const std::string& digested_user) { + _digested_user = digested_user; + return *this; + } +#endif // HAVE_DAUTH + + create_test_request& requestor(const std::string& requestor) { + _requestor = requestor; + return *this; + } + + create_test_request& requestor_port(uint16_t port) { + _requestor_port = port; + return *this; + } + +#ifdef HAVE_GNUTLS + create_test_request& tls_enabled(bool enabled = true) { + _tls_enabled = enabled; + return *this; + } +#endif // HAVE_GNUTLS + + http_request build(); + + private: + std::string _method = "GET"; + std::string _path = "/"; + std::string _version = "HTTP/1.1"; + std::string _content; + http::header_map _headers; + http::header_map _footers; + http::header_map _cookies; + std::map, http::arg_comparator> _args; + std::string _querystring; + std::string _user; + std::string _pass; +#ifdef HAVE_DAUTH + std::string _digested_user; +#endif // HAVE_DAUTH + std::string _requestor = "127.0.0.1"; + uint16_t _requestor_port = 0; +#ifdef HAVE_GNUTLS + bool _tls_enabled = false; +#endif // HAVE_GNUTLS +}; + +} // namespace httpserver +#endif // SRC_HTTPSERVER_CREATE_TEST_REQUEST_HPP_ diff --git a/test/unit/create_test_request_test.cpp b/test/unit/create_test_request_test.cpp new file mode 100644 index 00000000..8b3843a8 --- /dev/null +++ b/test/unit/create_test_request_test.cpp @@ -0,0 +1,283 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include + +#include "./httpserver.hpp" +#include "./littletest.hpp" + +using httpserver::create_test_request; +using httpserver::http_request; +using httpserver::http_resource; +using httpserver::http_response; +using httpserver::string_response; +using httpserver::file_response; + +LT_BEGIN_SUITE(create_test_request_suite) + void set_up() { + } + + void tear_down() { + } +LT_END_SUITE(create_test_request_suite) + +// Test default values +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_default) + auto req = create_test_request().build(); + LT_CHECK_EQ(std::string(req.get_method()), std::string("GET")); + LT_CHECK_EQ(std::string(req.get_path()), std::string("/")); + LT_CHECK_EQ(std::string(req.get_version()), std::string("HTTP/1.1")); + LT_CHECK_EQ(std::string(req.get_content()), std::string("")); +LT_END_AUTO_TEST(build_default) + +// Test custom method and path +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_method_path) + auto req = create_test_request() + .method("POST") + .path("/api/users") + .build(); + LT_CHECK_EQ(std::string(req.get_method()), std::string("POST")); + LT_CHECK_EQ(std::string(req.get_path()), std::string("/api/users")); +LT_END_AUTO_TEST(build_method_path) + +// Test headers +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_headers) + auto req = create_test_request() + .header("Content-Type", "application/json") + .header("Accept", "text/plain") + .build(); + LT_CHECK_EQ(std::string(req.get_header("Content-Type")), std::string("application/json")); + LT_CHECK_EQ(std::string(req.get_header("Accept")), std::string("text/plain")); + LT_CHECK_EQ(std::string(req.get_header("NonExistent")), std::string("")); + + auto headers = req.get_headers(); + LT_CHECK_EQ(headers.size(), static_cast(2)); +LT_END_AUTO_TEST(build_headers) + +// Test footers and cookies +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_footers_cookies) + auto req = create_test_request() + .footer("X-Checksum", "abc123") + .cookie("session_id", "xyz789") + .build(); + LT_CHECK_EQ(std::string(req.get_footer("X-Checksum")), std::string("abc123")); + LT_CHECK_EQ(std::string(req.get_cookie("session_id")), std::string("xyz789")); + + auto footers = req.get_footers(); + LT_CHECK_EQ(footers.size(), static_cast(1)); + + auto cookies = req.get_cookies(); + LT_CHECK_EQ(cookies.size(), static_cast(1)); +LT_END_AUTO_TEST(build_footers_cookies) + +// Test args +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_args) + auto req = create_test_request() + .arg("name", "World") + .arg("lang", "en") + .build(); + LT_CHECK_EQ(std::string(req.get_arg_flat("name")), std::string("World")); + LT_CHECK_EQ(std::string(req.get_arg_flat("lang")), std::string("en")); +LT_END_AUTO_TEST(build_args) + +// Test multiple values per arg key +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_multi_args) + auto req = create_test_request() + .arg("color", "red") + .arg("color", "blue") + .build(); + auto arg = req.get_arg("color"); + LT_CHECK_EQ(arg.values.size(), static_cast(2)); + LT_CHECK_EQ(std::string(arg.values[0]), std::string("red")); + LT_CHECK_EQ(std::string(arg.values[1]), std::string("blue")); +LT_END_AUTO_TEST(build_multi_args) + +// Test querystring +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_querystring) + auto req = create_test_request() + .querystring("?foo=bar&baz=qux") + .build(); + LT_CHECK_EQ(std::string(req.get_querystring()), std::string("?foo=bar&baz=qux")); +LT_END_AUTO_TEST(build_querystring) + +// Test content +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_content) + auto req = create_test_request() + .content("{\"key\":\"value\"}") + .build(); + LT_CHECK_EQ(std::string(req.get_content()), std::string("{\"key\":\"value\"}")); +LT_END_AUTO_TEST(build_content) + +// Test basic auth +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_basic_auth) + auto req = create_test_request() + .user("admin") + .pass("secret") + .build(); + LT_CHECK_EQ(std::string(req.get_user()), std::string("admin")); + LT_CHECK_EQ(std::string(req.get_pass()), std::string("secret")); +LT_END_AUTO_TEST(build_basic_auth) + +// Test requestor +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_requestor) + auto req = create_test_request() + .requestor("192.168.1.1") + .requestor_port(8080) + .build(); + LT_CHECK_EQ(std::string(req.get_requestor()), std::string("192.168.1.1")); + LT_CHECK_EQ(req.get_requestor_port(), static_cast(8080)); +LT_END_AUTO_TEST(build_requestor) + +// Test default requestor +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_default_requestor) + auto req = create_test_request().build(); + LT_CHECK_EQ(std::string(req.get_requestor()), std::string("127.0.0.1")); + LT_CHECK_EQ(req.get_requestor_port(), static_cast(0)); +LT_END_AUTO_TEST(build_default_requestor) + +#ifdef HAVE_GNUTLS +// Test TLS enabled flag +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_tls_enabled) + auto req = create_test_request() + .tls_enabled() + .build(); + LT_CHECK_EQ(req.has_tls_session(), true); +LT_END_AUTO_TEST(build_tls_enabled) + +// Test TLS not enabled by default +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_no_tls) + auto req = create_test_request().build(); + LT_CHECK_EQ(req.has_tls_session(), false); +LT_END_AUTO_TEST(build_no_tls) +#endif // HAVE_GNUTLS + +// Test that all getters on a minimal request return empty without crashing +LT_BEGIN_AUTO_TEST(create_test_request_suite, empty_getters_no_crash) + auto req = create_test_request().build(); + // These should all return empty/default without crashing + LT_CHECK_EQ(std::string(req.get_header("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_footer("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_cookie("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_arg_flat("Anything")), std::string("")); + LT_CHECK_EQ(std::string(req.get_querystring()), std::string("")); + LT_CHECK_EQ(std::string(req.get_content()), std::string("")); + LT_CHECK_EQ(req.get_headers().size(), static_cast(0)); + LT_CHECK_EQ(req.get_footers().size(), static_cast(0)); + LT_CHECK_EQ(req.get_cookies().size(), static_cast(0)); + LT_CHECK_EQ(req.get_args().size(), static_cast(0)); + LT_CHECK_EQ(req.get_args_flat().size(), static_cast(0)); + LT_CHECK_EQ(req.get_path_pieces().size(), static_cast(0)); +LT_END_AUTO_TEST(empty_getters_no_crash) + +// End-to-end: build request, call render, inspect response +class greeting_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request& req) override { + std::string name(req.get_arg_flat("name")); + if (name.empty()) name = "World"; + return std::make_shared("Hello, " + name); + } +}; + +LT_BEGIN_AUTO_TEST(create_test_request_suite, render_with_test_request) + greeting_resource resource; + auto req = create_test_request() + .path("/greet") + .arg("name", "Alice") + .build(); + auto resp = resource.render_GET(req); + auto* sr = dynamic_cast(resp.get()); + LT_ASSERT(sr != nullptr); + LT_CHECK_EQ(std::string(sr->get_content()), std::string("Hello, Alice")); +LT_END_AUTO_TEST(render_with_test_request) + +// Test string_response get_content +LT_BEGIN_AUTO_TEST(create_test_request_suite, string_response_get_content) + string_response resp("test body", 200); + LT_CHECK_EQ(std::string(resp.get_content()), std::string("test body")); +LT_END_AUTO_TEST(string_response_get_content) + +// Test file_response get_filename +LT_BEGIN_AUTO_TEST(create_test_request_suite, file_response_get_filename) + file_response resp("/tmp/test.txt", 200); + LT_CHECK_EQ(std::string(resp.get_filename()), std::string("/tmp/test.txt")); +LT_END_AUTO_TEST(file_response_get_filename) + +// Test full chain of all builder methods +LT_BEGIN_AUTO_TEST(create_test_request_suite, full_chain) + auto req = create_test_request() + .method("PUT") + .path("/api/resource/42") + .version("HTTP/1.0") + .content("request body") + .header("Content-Type", "text/plain") + .header("Authorization", "Bearer token123") + .footer("Trailer", "checksum") + .cookie("session", "abc") + .arg("key1", "val1") + .arg("key2", "val2") + .querystring("?key1=val1&key2=val2") + .user("testuser") + .pass("testpass") + .requestor("10.0.0.1") + .requestor_port(9090) + .build(); + + LT_CHECK_EQ(std::string(req.get_method()), std::string("PUT")); + LT_CHECK_EQ(std::string(req.get_path()), std::string("/api/resource/42")); + LT_CHECK_EQ(std::string(req.get_version()), std::string("HTTP/1.0")); + LT_CHECK_EQ(std::string(req.get_content()), std::string("request body")); + LT_CHECK_EQ(std::string(req.get_header("Content-Type")), std::string("text/plain")); + LT_CHECK_EQ(std::string(req.get_header("Authorization")), std::string("Bearer token123")); + LT_CHECK_EQ(std::string(req.get_footer("Trailer")), std::string("checksum")); + LT_CHECK_EQ(std::string(req.get_cookie("session")), std::string("abc")); + LT_CHECK_EQ(std::string(req.get_arg_flat("key1")), std::string("val1")); + LT_CHECK_EQ(std::string(req.get_arg_flat("key2")), std::string("val2")); + LT_CHECK_EQ(std::string(req.get_querystring()), std::string("?key1=val1&key2=val2")); + LT_CHECK_EQ(std::string(req.get_user()), std::string("testuser")); + LT_CHECK_EQ(std::string(req.get_pass()), std::string("testpass")); + LT_CHECK_EQ(std::string(req.get_requestor()), std::string("10.0.0.1")); + LT_CHECK_EQ(req.get_requestor_port(), static_cast(9090)); +LT_END_AUTO_TEST(full_chain) + +// Test path pieces work with test request +LT_BEGIN_AUTO_TEST(create_test_request_suite, build_path_pieces) + auto req = create_test_request() + .path("/api/users/42") + .build(); + auto pieces = req.get_path_pieces(); + LT_CHECK_EQ(pieces.size(), static_cast(3)); + LT_CHECK_EQ(pieces[0], std::string("api")); + LT_CHECK_EQ(pieces[1], std::string("users")); + LT_CHECK_EQ(pieces[2], std::string("42")); +LT_END_AUTO_TEST(build_path_pieces) + +// Test method is uppercased +LT_BEGIN_AUTO_TEST(create_test_request_suite, method_uppercase) + auto req = create_test_request() + .method("post") + .build(); + LT_CHECK_EQ(std::string(req.get_method()), std::string("POST")); +LT_END_AUTO_TEST(method_uppercase) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV() From ca473210fda9407228eb495d7f11718135d200b6 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 16:01:36 -0800 Subject: [PATCH 40/47] Fix 9 security vulnerabilities found in audit - Fix path traversal in file uploads: sanitize filenames to basename, reject ".." and "." when generate_random_filename_on_upload is off - Fix TOCTOU race in file_response: replace stat-then-open with open-then-fstat; add O_NOFOLLOW on non-Windows - Fix FD leaks in file_response: close fd on lseek failure and zero-size file path - Fix NULL deref in answer_to_connection: check conninfo before use - Fix uninitialized _file_size in file_info (default to 0) - Fix auth skip path bypass: normalize path (resolve ".." segments) before comparing against skip paths - Fix free() vs MHD_free() for digest auth username - Fix unchecked write error during file upload --- src/file_response.cpp | 23 +++++---- src/http_request.cpp | 2 +- src/http_utils.cpp | 15 ++++++ src/httpserver/file_info.hpp | 2 +- src/httpserver/http_utils.hpp | 2 + src/webserver.cpp | 42 ++++++++++++++-- test/integ/authentication.cpp | 33 +++++++++++++ test/integ/file_upload.cpp | 90 +++++++++++++++++++++++++++++++++++ 8 files changed, 194 insertions(+), 15 deletions(-) diff --git a/src/file_response.cpp b/src/file_response.cpp index 669f5e8b..3e915413 100644 --- a/src/file_response.cpp +++ b/src/file_response.cpp @@ -32,24 +32,29 @@ struct MHD_Response; namespace httpserver { MHD_Response* file_response::get_raw_response() { - struct stat sb; +#ifndef _WIN32 + int fd = open(filename.c_str(), O_RDONLY | O_NOFOLLOW); +#else + int fd = open(filename.c_str(), O_RDONLY); +#endif + if (fd == -1) return nullptr; - // Deny everything but regular files - if (stat(filename.c_str(), &sb) == 0) { - if (!S_ISREG(sb.st_mode)) return nullptr; - } else { + struct stat sb; + if (fstat(fd, &sb) != 0 || !S_ISREG(sb.st_mode)) { + close(fd); return nullptr; } - int fd = open(filename.c_str(), O_RDONLY); - if (fd == -1) return nullptr; - off_t size = lseek(fd, 0, SEEK_END); - if (size == (off_t) -1) return nullptr; + if (size == (off_t) -1) { + close(fd); + return nullptr; + } if (size) { return MHD_create_response_from_fd(size, fd); } else { + close(fd); return MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_PERSISTENT); } } diff --git a/src/http_request.cpp b/src/http_request.cpp index d58842b3..1de47519 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -351,7 +351,7 @@ std::string_view http_request::get_digested_user() const { cache->digested_user = EMPTY; if (digested_user_c != nullptr) { cache->digested_user = digested_user_c; - free(digested_user_c); + MHD_free(digested_user_c); } return cache->digested_user; diff --git a/src/http_utils.cpp b/src/http_utils.cpp index 43b522bb..695292a3 100644 --- a/src/http_utils.cpp +++ b/src/http_utils.cpp @@ -271,6 +271,21 @@ const std::string http_utils::generate_random_upload_filename(const std::string& return ret_filename; } +std::string http_utils::sanitize_upload_filename(const std::string& filename) { + if (filename.empty()) return ""; + + // Find the basename: take everything after the last '/' or '\' + std::string::size_type pos = filename.find_last_of("/\\"); + std::string basename = (pos != std::string::npos) ? filename.substr(pos + 1) : filename; + + // Reject empty basename, ".", and ".." + if (basename.empty() || basename == "." || basename == "..") { + return ""; + } + + return basename; +} + std::string get_ip_str(const struct sockaddr *sa) { if (!sa) throw std::invalid_argument("socket pointer is null"); diff --git a/src/httpserver/file_info.hpp b/src/httpserver/file_info.hpp index f78c55fa..8fd4f9e9 100644 --- a/src/httpserver/file_info.hpp +++ b/src/httpserver/file_info.hpp @@ -42,7 +42,7 @@ class file_info { file_info() = default; private: - size_t _file_size; + size_t _file_size = 0; std::string _file_system_file_name; std::string _content_type; std::string _transfer_encoding; diff --git a/src/httpserver/http_utils.hpp b/src/httpserver/http_utils.hpp index 35a5a45a..8e44c15b 100644 --- a/src/httpserver/http_utils.hpp +++ b/src/httpserver/http_utils.hpp @@ -272,6 +272,8 @@ class http_utils { static std::string standardize_url(const std::string&); static const std::string generate_random_upload_filename(const std::string& directory); + + static std::string sanitize_upload_filename(const std::string& filename); }; #define COMPARATOR(x, y, op) { \ diff --git a/src/webserver.cpp b/src/webserver.cpp index 20690cab..92d754a2 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -699,7 +699,11 @@ MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, if (mr->ws->generate_random_filename_on_upload) { file.set_file_system_file_name(http_utils::generate_random_upload_filename(mr->ws->file_upload_dir)); } else { - file.set_file_system_file_name(mr->ws->file_upload_dir + "/" + std::string(filename)); + std::string safe_name = http_utils::sanitize_upload_filename(filename); + if (safe_name.empty()) { + return MHD_NO; + } + file.set_file_system_file_name(mr->ws->file_upload_dir + "/" + safe_name); } // to not append to an already existing file, delete an already existing file unlink(file.get_file_system_file_name().c_str()); @@ -731,6 +735,9 @@ MHD_Result webserver::post_iterator(void *cls, enum MHD_ValueKind kind, if (size > 0) { mr->upload_ostrm->write(data, size); + if (!mr->upload_ostrm->good()) { + return MHD_NO; + } } // update the file size in the map @@ -774,13 +781,40 @@ std::shared_ptr webserver::internal_error_page(details::modded_re } bool webserver::should_skip_auth(const std::string& path) const { + // Normalize path: resolve ".." and "." segments to prevent bypass + std::string normalized; + { + std::vector segments; + std::string::size_type start = 0; + // Skip leading slash + if (!path.empty() && path[0] == '/') { + start = 1; + } + while (start < path.size()) { + auto end = path.find('/', start); + if (end == std::string::npos) end = path.size(); + std::string seg = path.substr(start, end - start); + if (seg == "..") { + if (!segments.empty()) segments.pop_back(); + } else if (!seg.empty() && seg != ".") { + segments.push_back(seg); + } + start = end + 1; + } + normalized = "/"; + for (size_t i = 0; i < segments.size(); i++) { + if (i > 0) normalized += "/"; + normalized += segments[i]; + } + } + for (const auto& skip_path : auth_skip_paths) { - if (skip_path == path) return true; + if (skip_path == normalized) return true; // Support wildcard suffix (e.g., "/public/*") if (skip_path.size() > 2 && skip_path.back() == '*' && skip_path[skip_path.size() - 2] == '/') { std::string prefix = skip_path.substr(0, skip_path.size() - 1); - if (path.compare(0, prefix.size(), prefix) == 0) return true; + if (normalized.compare(0, prefix.size(), prefix) == 0) return true; } } return false; @@ -1028,7 +1062,7 @@ MHD_Result webserver::answer_to_connection(void* cls, MHD_Connection* connection const MHD_ConnectionInfo * conninfo = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CONNECTION_FD); - if (static_cast(cls)->tcp_nodelay) { + if (conninfo != nullptr && static_cast(cls)->tcp_nodelay) { int yes = 1; setsockopt(conninfo->connect_fd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&yes), sizeof(int)); } diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index d280cbd6..57440b2e 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -1090,6 +1090,39 @@ LT_BEGIN_AUTO_TEST(authentication_suite, auth_empty_skip_paths) ws.stop(); LT_END_AUTO_TEST(auth_empty_skip_paths) +// Test that path traversal cannot bypass auth skip paths +// Requesting /public/../protected should NOT skip auth +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_path_traversal_bypass) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("public/info", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // /public/../protected should normalize to /protected, which requires auth + curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/public/../protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); // Should require auth, not be skipped + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_path_traversal_bypass) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() diff --git a/test/integ/file_upload.cpp b/test/integ/file_upload.cpp index 97cae716..5361c1d0 100644 --- a/test/integ/file_upload.cpp +++ b/test/integ/file_upload.cpp @@ -964,6 +964,96 @@ LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_with_content_type) ws->stop(); LT_END_AUTO_TEST(file_upload_with_content_type) +// Send file with a crafted filename for path traversal testing +static std::pair send_file_with_traversal_name(int port, const char* crafted_filename) { + curl_global_init(CURL_GLOBAL_ALL); + + CURL *curl = curl_easy_init(); + + curl_mime *form = curl_mime_init(curl); + curl_mimepart *field = curl_mime_addpart(form); + curl_mime_name(field, TEST_KEY); + // Use the real file for data, but override the filename + curl_mime_filedata(field, TEST_CONTENT_FILEPATH); + curl_mime_filename(field, crafted_filename); + + CURLcode res; + std::string url = "localhost:" + std::to_string(port) + "/upload"; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, form); + + res = curl_easy_perform(curl); + long http_code = 0; // NOLINT [runtime/int] + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_easy_cleanup(curl); + curl_mime_free(form); + return {res, http_code}; +} + +// Test that path traversal filenames are rejected +LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_path_traversal_rejected) + string upload_directory = "upload_test_dir"; + MKDIR(upload_directory.c_str()); + + int port = PORT + 2; + auto ws = std::make_unique(create_webserver(port) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory)); + // NOT using generate_random_filename_on_upload - this is the vulnerable path + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + // Attempt path traversal with "../escape" + send_file_with_traversal_name(port, "../escape"); + // The server should reject the upload (MHD_NO causes connection close) + // The key check is that no file was created outside the upload dir + LT_CHECK_EQ(file_exists("escape"), false); + LT_CHECK_EQ(file_exists("./escape"), false); + + ws->stop(); + + // Clean up + rmdir(upload_directory.c_str()); +LT_END_AUTO_TEST(file_upload_path_traversal_rejected) + +// Test that sanitize keeps the basename for normal filenames +LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_sanitize_keeps_basename) + string upload_directory = "upload_test_dir"; + MKDIR(upload_directory.c_str()); + + int port = PORT + 3; + auto ws = std::make_unique(create_webserver(port) + .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY) + .file_upload_dir(upload_directory)); + ws->start(false); + LT_CHECK_EQ(ws->is_running(), true); + + print_file_upload_resource resource; + LT_ASSERT_EQ(true, ws->register_resource("upload", &resource)); + + // Upload with a path-like filename — should strip to just "myfile.txt" + auto res = send_file_with_traversal_name(port, "some/path/myfile.txt"); + LT_ASSERT_EQ(res.first, 0); + LT_ASSERT_EQ(res.second, 201); + + // The file should be created with only the basename + map> files = resource.get_files(); + LT_CHECK_EQ(files.size(), 1); + auto file = files.begin()->second.begin(); + string expected_path = upload_directory + "/myfile.txt"; + LT_CHECK_EQ(file->second.get_file_system_file_name(), expected_path); + + ws->stop(); + + // Clean up + unlink(expected_path.c_str()); + rmdir(upload_directory.c_str()); +LT_END_AUTO_TEST(file_upload_sanitize_keeps_basename) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV() From 28fa48f68778717abc48da09c1fe259180025139 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 13 Feb 2026 17:26:37 -0800 Subject: [PATCH 41/47] Add ChangeLog entries for security fixes --- ChangeLog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ChangeLog b/ChangeLog index faeccb65..6760a4ff 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,17 @@ Version 0.19.0 - 2023-06-15 + Fixed path traversal vulnerability in file uploads when + generate_random_filename_on_upload is disabled. + Fixed TOCTOU race in file_response by replacing stat-then-open with + open-then-fstat; added O_NOFOLLOW on non-Windows. + Fixed file descriptor leaks in file_response on lseek failure and + zero-size file paths. + Fixed NULL pointer dereference when MHD_get_connection_info returns + nullptr for TCP_NODELAY. + Fixed uninitialized _file_size in file_info. + Fixed auth skip path bypass via path traversal (e.g. /public/../protected). + Fixed use of free() instead of MHD_free() for digest auth username. + Fixed unchecked write error during file upload. Considering family_url as part of the priority when selecting a URL to match. More explicit selection of C++ version. Ability to handle multiple parameters with the same name on the URL. From 0908e485f007eb12cae3fe8c9b7a361dd4547131 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 11:21:49 -0800 Subject: [PATCH 42/47] FEATURE-bauth-conditional-compile: Implementation complete Add HAVE_BAUTH conditional compilation for basic authentication, mirroring the existing HAVE_DAUTH pattern. This allows libhttpserver to build against libmicrohttpd installations that lack basic auth support. Changes: - configure.ac: Auto-detect MHD_queue_basic_auth_fail_response, define HAVE_BAUTH flag and AM_CONDITIONAL, add to summary output - src/httpserver/basic_auth_fail_response.hpp: Guard with #ifdef HAVE_BAUTH - src/basic_auth_fail_response.cpp: Guard with #ifdef HAVE_BAUTH - src/httpserver.hpp: Conditionally include basic_auth_fail_response.hpp - src/httpserver/http_request.hpp: Guard get_user(), get_pass(), fetch_user_pass() declarations and username/password cache fields - src/http_request.cpp: Guard fetch_user_pass(), get_user(), get_pass() implementations and basic auth output in operator<< - src/httpserver/create_webserver.hpp: Guard basic_auth()/no_basic_auth() methods and _basic_auth_enabled member - src/httpserver/webserver.hpp: Guard basic_auth_enabled member - src/webserver.cpp: Guard basic_auth_enabled initialization - src/Makefile.am: Make basic_auth_fail_response conditional on HAVE_BAUTH - examples/Makefile.am: Guard basic_authentication and centralized_authentication examples behind HAVE_BAUTH - test/integ/authentication.cpp: Guard basic auth tests with HAVE_BAUTH - test/unit/create_webserver_test.cpp: Guard basic_auth builder test --- configure.ac | 13 +++++++++++++ examples/Makefile.am | 9 +++++++-- src/Makefile.am | 9 +++++++-- src/basic_auth_fail_response.cpp | 4 ++++ src/http_request.cpp | 9 +++++++-- src/httpserver.hpp | 2 ++ src/httpserver/basic_auth_fail_response.hpp | 5 +++++ src/httpserver/create_webserver.hpp | 4 ++++ src/httpserver/http_request.hpp | 8 ++++++++ src/httpserver/webserver.hpp | 2 ++ src/webserver.cpp | 2 ++ test/integ/authentication.cpp | 8 ++++++++ test/unit/create_webserver_test.cpp | 2 ++ 13 files changed, 71 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 5754ae7c..958d045e 100644 --- a/configure.ac +++ b/configure.ac @@ -149,6 +149,11 @@ fi AM_CONDITIONAL([COND_CROSS_COMPILE],[test x"$cond_cross_compile" = x"yes"]) AC_SUBST(COND_CROSS_COMPILE) +# Check for basic auth support in libmicrohttpd +AC_CHECK_LIB([microhttpd], [MHD_queue_basic_auth_fail_response], + [have_bauth="yes"], + [have_bauth="no"; AC_MSG_WARN("libmicrohttpd basic auth support not found. Basic auth will be disabled")]) + # Check for digest auth support in libmicrohttpd AC_CHECK_LIB([microhttpd], [MHD_queue_auth_fail_response], [have_dauth="yes"], @@ -264,6 +269,13 @@ fi AM_CONDITIONAL([HAVE_GNUTLS],[test x"$have_gnutls" = x"yes"]) +if test x"$have_bauth" = x"yes"; then + AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_BAUTH" + AM_CFLAGS="$AM_CXXFLAGS -DHAVE_BAUTH" +fi + +AM_CONDITIONAL([HAVE_BAUTH],[test x"$have_bauth" = x"yes"]) + if test x"$have_dauth" = x"yes"; then AM_CXXFLAGS="$AM_CXXFLAGS -DHAVE_DAUTH" AM_CFLAGS="$AM_CXXFLAGS -DHAVE_DAUTH" @@ -327,6 +339,7 @@ AC_MSG_NOTICE([Configuration Summary: License : LGPL only Debug : ${debugit} TLS Enabled : ${have_gnutls} + Basic Auth : ${have_bauth} Digest Auth : ${have_dauth} TCP_FASTOPEN : ${is_fastopen_supported} Static : ${static} diff --git a/examples/Makefile.am b/examples/Makefile.am index 37930db0..c04e0acf 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log basic_authentication minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -31,7 +31,6 @@ hello_with_get_arg_SOURCES = hello_with_get_arg.cpp args_processing_SOURCES = args_processing.cpp setting_headers_SOURCES = setting_headers.cpp custom_access_log_SOURCES = custom_access_log.cpp -basic_authentication_SOURCES = basic_authentication.cpp minimal_https_SOURCES = minimal_https.cpp minimal_file_response_SOURCES = minimal_file_response.cpp minimal_deferred_SOURCES = minimal_deferred.cpp @@ -44,6 +43,12 @@ benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp file_upload_with_callback_SOURCES = file_upload_with_callback.cpp +if HAVE_BAUTH +noinst_PROGRAMS += basic_authentication centralized_authentication +basic_authentication_SOURCES = basic_authentication.cpp +centralized_authentication_SOURCES = centralized_authentication.cpp +endif + if HAVE_GNUTLS LDADD += -lgnutls noinst_PROGRAMS += minimal_https_psk diff --git a/src/Makefile.am b/src/Makefile.am index 63a16a50..ed8dc8f4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,9 +19,14 @@ AM_CPPFLAGS = -I../ -I$(srcdir)/httpserver/ METASOURCES = AUTO lib_LTLIBRARIES = libhttpserver.la -libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp basic_auth_fail_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp +libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp file_info.cpp http_request.cpp http_response.cpp string_response.cpp digest_auth_fail_response.cpp deferred_response.cpp file_response.cpp http_resource.cpp create_webserver.cpp details/http_endpoint.cpp noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp gettext.h -nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/basic_auth_fail_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/http_arg_value.hpp +nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/details/http_endpoint.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/http_arg_value.hpp + +if HAVE_BAUTH +libhttpserver_la_SOURCES += basic_auth_fail_response.cpp +nobase_include_HEADERS += httpserver/basic_auth_fail_response.hpp +endif AM_CXXFLAGS += -fPIC -Wall diff --git a/src/basic_auth_fail_response.cpp b/src/basic_auth_fail_response.cpp index 0e00cdc1..1e6aa0e5 100644 --- a/src/basic_auth_fail_response.cpp +++ b/src/basic_auth_fail_response.cpp @@ -18,6 +18,8 @@ USA */ +#ifdef HAVE_BAUTH + #include "httpserver/basic_auth_fail_response.hpp" #include #include @@ -32,3 +34,5 @@ int basic_auth_fail_response::enqueue_response(MHD_Connection* connection, MHD_R } } // namespace httpserver + +#endif // HAVE_BAUTH diff --git a/src/http_request.cpp b/src/http_request.cpp index 1de47519..4d67bf39 100644 --- a/src/http_request.cpp +++ b/src/http_request.cpp @@ -310,6 +310,7 @@ MHD_Result http_request::build_request_querystring(void *cls, enum MHD_ValueKind return MHD_YES; } +#ifdef HAVE_BAUTH void http_request::fetch_user_pass() const { char* password = nullptr; auto* username = MHD_basic_auth_get_username_password(underlying_connection, &password); @@ -339,6 +340,7 @@ std::string_view http_request::get_pass() const { fetch_user_pass(); return cache->password; } +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH std::string_view http_request::get_digested_user() const { @@ -557,8 +559,11 @@ uint16_t http_request::get_requestor_port() const { } std::ostream &operator<< (std::ostream &os, const http_request &r) { - os << r.get_method() << " Request [user:\"" << r.get_user() << "\" pass:\"" << r.get_pass() << "\"] path:\"" - << r.get_path() << "\"" << std::endl; + os << r.get_method() << " Request ["; +#ifdef HAVE_BAUTH + os << "user:\"" << r.get_user() << "\" pass:\"" << r.get_pass() << "\""; +#endif // HAVE_BAUTH + os << "] path:\"" << r.get_path() << "\"" << std::endl; http::dump_header_map(os, "Headers", r.get_headers()); http::dump_header_map(os, "Footers", r.get_footers()); diff --git a/src/httpserver.hpp b/src/httpserver.hpp index 4f19de48..b2bba186 100644 --- a/src/httpserver.hpp +++ b/src/httpserver.hpp @@ -27,7 +27,9 @@ #define _HTTPSERVER_HPP_INSIDE_ +#ifdef HAVE_BAUTH #include "httpserver/basic_auth_fail_response.hpp" +#endif // HAVE_BAUTH #include "httpserver/deferred_response.hpp" #ifdef HAVE_DAUTH #include "httpserver/digest_auth_fail_response.hpp" diff --git a/src/httpserver/basic_auth_fail_response.hpp b/src/httpserver/basic_auth_fail_response.hpp index 87a124f5..d88bbbff 100644 --- a/src/httpserver/basic_auth_fail_response.hpp +++ b/src/httpserver/basic_auth_fail_response.hpp @@ -25,6 +25,8 @@ #ifndef SRC_HTTPSERVER_BASIC_AUTH_FAIL_RESPONSE_HPP_ #define SRC_HTTPSERVER_BASIC_AUTH_FAIL_RESPONSE_HPP_ +#ifdef HAVE_BAUTH + #include #include "httpserver/http_utils.hpp" #include "httpserver/string_response.hpp" @@ -60,4 +62,7 @@ class basic_auth_fail_response : public string_response { }; } // namespace httpserver + +#endif // HAVE_BAUTH + #endif // SRC_HTTPSERVER_BASIC_AUTH_FAIL_RESPONSE_HPP_ diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index 7ade5e17..991b8501 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -261,6 +261,7 @@ class create_webserver { return *this; } +#ifdef HAVE_BAUTH create_webserver& basic_auth() { _basic_auth_enabled = true; return *this; @@ -270,6 +271,7 @@ class create_webserver { _basic_auth_enabled = false; return *this; } +#endif // HAVE_BAUTH create_webserver& digest_auth() { _digest_auth_enabled = true; @@ -438,7 +440,9 @@ class create_webserver { std::string _digest_auth_random = ""; int _nonce_nc_size = 0; http::http_utils::policy_T _default_policy = http::http_utils::ACCEPT; +#ifdef HAVE_BAUTH bool _basic_auth_enabled = true; +#endif // HAVE_BAUTH bool _digest_auth_enabled = true; bool _regex_checking = true; bool _ban_system_enabled = true; diff --git a/src/httpserver/http_request.hpp b/src/httpserver/http_request.hpp index b6a015c8..2b621b11 100644 --- a/src/httpserver/http_request.hpp +++ b/src/httpserver/http_request.hpp @@ -60,11 +60,13 @@ class http_request { public: static const char EMPTY[]; +#ifdef HAVE_BAUTH /** * Method used to get the username eventually passed through basic authentication. * @return string representation of the username. **/ std::string_view get_user() const; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH /** @@ -74,11 +76,13 @@ class http_request { std::string_view get_digested_user() const; #endif // HAVE_DAUTH +#ifdef HAVE_BAUTH /** * Method used to get the password eventually passed through basic authentication. * @return string representation of the password. **/ std::string_view get_pass() const; +#endif // HAVE_BAUTH /** * Method used to get the path requested @@ -380,7 +384,9 @@ class http_request { static MHD_Result build_request_querystring(void *cls, enum MHD_ValueKind kind, const char *key, const char *value); +#ifdef HAVE_BAUTH void fetch_user_pass() const; +#endif // HAVE_BAUTH /** * Method used to set an argument value by key. @@ -485,8 +491,10 @@ class http_request { // Others (username, password, digested_user) MHD returns as char* that we need // to make a copy of and free anyway. struct http_request_data_cache { +#ifdef HAVE_BAUTH std::string username; std::string password; +#endif // HAVE_BAUTH std::string querystring; std::string requestor_ip; #ifdef HAVE_DAUTH diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index ea4a404d..66d81ddd 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -168,7 +168,9 @@ class webserver { const int nonce_nc_size; bool running; const http::http_utils::policy_T default_policy; +#ifdef HAVE_BAUTH const bool basic_auth_enabled; +#endif // HAVE_BAUTH const bool digest_auth_enabled; const bool regex_checking; const bool ban_system_enabled; diff --git a/src/webserver.cpp b/src/webserver.cpp index 92d754a2..971d3d5a 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -158,7 +158,9 @@ webserver::webserver(const create_webserver& params): nonce_nc_size(params._nonce_nc_size), running(false), default_policy(params._default_policy), +#ifdef HAVE_BAUTH basic_auth_enabled(params._basic_auth_enabled), +#endif // HAVE_BAUTH digest_auth_enabled(params._digest_auth_enabled), regex_checking(params._regex_checking), ban_system_enabled(params._ban_system_enabled), diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index 57440b2e..b043f566 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -43,7 +43,9 @@ using std::shared_ptr; using httpserver::webserver; using httpserver::create_webserver; using httpserver::http_response; +#ifdef HAVE_BAUTH using httpserver::basic_auth_fail_response; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH using httpserver::digest_auth_fail_response; #endif // HAVE_DAUTH @@ -66,6 +68,7 @@ size_t writefunc(void *ptr, size_t size, size_t nmemb, std::string *s) { return size*nmemb; } +#ifdef HAVE_BAUTH class user_pass_resource : public http_resource { public: shared_ptr render_GET(const http_request& req) { @@ -75,6 +78,7 @@ class user_pass_resource : public http_resource { return std::make_shared(std::string(req.get_user()) + " " + std::string(req.get_pass()), 200, "text/plain"); } }; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH class digest_resource : public http_resource { @@ -101,6 +105,7 @@ LT_BEGIN_SUITE(authentication_suite) } LT_END_SUITE(authentication_suite) +#ifdef HAVE_BAUTH LT_BEGIN_AUTO_TEST(authentication_suite, base_auth) webserver ws = create_webserver(PORT); @@ -150,6 +155,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, base_auth_fail) ws.stop(); LT_END_AUTO_TEST(base_auth_fail) +#endif // HAVE_BAUTH // do not run the digest auth tests on windows as curl // appears to have problems with it. @@ -555,6 +561,7 @@ LT_END_AUTO_TEST(digest_user_cache_with_auth) #endif +#ifdef HAVE_BAUTH // Simple resource for centralized auth tests class simple_resource : public http_resource { public: @@ -1122,6 +1129,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_path_traversal_bypass) ws.stop(); LT_END_AUTO_TEST(auth_skip_path_traversal_bypass) +#endif // HAVE_BAUTH LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() diff --git a/test/unit/create_webserver_test.cpp b/test/unit/create_webserver_test.cpp index efae3812..49bc10ff 100644 --- a/test/unit/create_webserver_test.cpp +++ b/test/unit/create_webserver_test.cpp @@ -120,12 +120,14 @@ LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_pedantic_toggle) LT_CHECK_EQ(true, true); LT_END_AUTO_TEST(builder_pedantic_toggle) +#ifdef HAVE_BAUTH // Test basic_auth / no_basic_auth toggle LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_basic_auth_toggle) create_webserver cw1 = create_webserver(8080).basic_auth(); create_webserver cw2 = create_webserver(8080).no_basic_auth(); LT_CHECK_EQ(true, true); LT_END_AUTO_TEST(builder_basic_auth_toggle) +#endif // HAVE_BAUTH // Test digest_auth / no_digest_auth toggle LT_BEGIN_AUTO_TEST(create_webserver_suite, builder_digest_auth_toggle) From 507d29a92171666e8f372d7e94adfd84598a30f1 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 12:15:17 -0800 Subject: [PATCH 43/47] Add HAVE_BAUTH guards to remaining files missed in initial commit Guard basic auth references in ws_start_stop.cpp, basic.cpp, create_test_request_test.cpp, create_test_request.hpp, and create_test_request.cpp that would fail to compile when libmicrohttpd lacks basic auth support. --- src/create_test_request.cpp | 2 ++ src/httpserver/create_test_request.hpp | 4 ++++ test/integ/basic.cpp | 4 ++++ test/integ/ws_start_stop.cpp | 2 ++ test/unit/create_test_request_test.cpp | 2 ++ 5 files changed, 14 insertions(+) diff --git a/src/create_test_request.cpp b/src/create_test_request.cpp index 31c7d3df..985acd39 100644 --- a/src/create_test_request.cpp +++ b/src/create_test_request.cpp @@ -49,8 +49,10 @@ http_request create_test_request::build() { req.cache->querystring = std::move(_querystring); } +#ifdef HAVE_BAUTH req.cache->username = std::move(_user); req.cache->password = std::move(_pass); +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH req.cache->digested_user = std::move(_digested_user); diff --git a/src/httpserver/create_test_request.hpp b/src/httpserver/create_test_request.hpp index 177257c2..a1f193d0 100644 --- a/src/httpserver/create_test_request.hpp +++ b/src/httpserver/create_test_request.hpp @@ -83,6 +83,7 @@ class create_test_request { return *this; } +#ifdef HAVE_BAUTH create_test_request& user(const std::string& user) { _user = user; return *this; @@ -92,6 +93,7 @@ class create_test_request { _pass = pass; return *this; } +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH create_test_request& digested_user(const std::string& digested_user) { @@ -129,8 +131,10 @@ class create_test_request { http::header_map _cookies; std::map, http::arg_comparator> _args; std::string _querystring; +#ifdef HAVE_BAUTH std::string _user; std::string _pass; +#endif // HAVE_BAUTH #ifdef HAVE_DAUTH std::string _digested_user; #endif // HAVE_DAUTH diff --git a/test/integ/basic.cpp b/test/integ/basic.cpp index d36f2f48..14d4eea0 100644 --- a/test/integ/basic.cpp +++ b/test/integ/basic.cpp @@ -2640,6 +2640,7 @@ class null_value_query_resource : public http_resource { } }; +#ifdef HAVE_BAUTH // Resource that tests auth caching (get_user/get_pass called multiple times) class auth_cache_resource : public http_resource { public: @@ -2654,7 +2655,9 @@ class auth_cache_resource : public http_resource { return std::make_shared(result, 200, "text/plain"); } }; +#endif // HAVE_BAUTH +#ifdef HAVE_BAUTH LT_BEGIN_AUTO_TEST(basic_suite, auth_caching) auth_cache_resource resource; LT_ASSERT_EQ(true, ws->register_resource("auth_cache", &resource)); @@ -2672,6 +2675,7 @@ LT_BEGIN_AUTO_TEST(basic_suite, auth_caching) LT_CHECK_EQ(s, "NO_AUTH"); curl_easy_cleanup(curl); LT_END_AUTO_TEST(auth_caching) +#endif // HAVE_BAUTH // Test query parameters with null/empty values (e.g., ?keyonly&normal=value) // This covers http_request.cpp lines 234 and 248 (arg_value == nullptr branches) diff --git a/test/integ/ws_start_stop.cpp b/test/integ/ws_start_stop.cpp index d664301a..a754cad3 100644 --- a/test/integ/ws_start_stop.cpp +++ b/test/integ/ws_start_stop.cpp @@ -279,7 +279,9 @@ LT_BEGIN_AUTO_TEST(ws_start_stop_suite, disable_options) .no_ipv6() .no_debug() .no_pedantic() +#ifdef HAVE_BAUTH .no_basic_auth() +#endif // HAVE_BAUTH .no_digest_auth() .no_deferred() .no_regex_checking() diff --git a/test/unit/create_test_request_test.cpp b/test/unit/create_test_request_test.cpp index 8b3843a8..db94d176 100644 --- a/test/unit/create_test_request_test.cpp +++ b/test/unit/create_test_request_test.cpp @@ -126,6 +126,7 @@ LT_BEGIN_AUTO_TEST(create_test_request_suite, build_content) LT_CHECK_EQ(std::string(req.get_content()), std::string("{\"key\":\"value\"}")); LT_END_AUTO_TEST(build_content) +#ifdef HAVE_BAUTH // Test basic auth LT_BEGIN_AUTO_TEST(create_test_request_suite, build_basic_auth) auto req = create_test_request() @@ -135,6 +136,7 @@ LT_BEGIN_AUTO_TEST(create_test_request_suite, build_basic_auth) LT_CHECK_EQ(std::string(req.get_user()), std::string("admin")); LT_CHECK_EQ(std::string(req.get_pass()), std::string("secret")); LT_END_AUTO_TEST(build_basic_auth) +#endif // HAVE_BAUTH // Test requestor LT_BEGIN_AUTO_TEST(create_test_request_suite, build_requestor) From ae59cc751f8dceba51c3e4c00b8a8d5f11de66c7 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 15:19:20 -0800 Subject: [PATCH 44/47] Bump version to 0.20.0 and update ChangeLog Add Version 0.20.0 header with bauth conditional compilation and security fix entries. Bump version in configure.ac to match. --- ChangeLog | 8 +++++++- configure.ac | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6760a4ff..7bb0aefb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,8 @@ -Version 0.19.0 - 2023-06-15 +Version 0.20.0 + Added conditional compilation for basic auth (HAVE_BAUTH), mirroring + existing HAVE_DAUTH pattern for digest auth. Basic auth support + is auto-detected via AC_CHECK_LIB and can be disabled at build time. Fixed path traversal vulnerability in file uploads when generate_random_filename_on_upload is disabled. Fixed TOCTOU race in file_response by replacing stat-then-open with @@ -12,6 +15,9 @@ Version 0.19.0 - 2023-06-15 Fixed auth skip path bypass via path traversal (e.g. /public/../protected). Fixed use of free() instead of MHD_free() for digest auth username. Fixed unchecked write error during file upload. + +Version 0.19.0 - 2023-06-15 + Considering family_url as part of the priority when selecting a URL to match. More explicit selection of C++ version. Ability to handle multiple parameters with the same name on the URL. diff --git a/configure.ac b/configure.ac index 958d045e..003170c6 100644 --- a/configure.ac +++ b/configure.ac @@ -21,7 +21,7 @@ AC_PREREQ(2.57) m4_define([libhttpserver_MAJOR_VERSION],[0])dnl -m4_define([libhttpserver_MINOR_VERSION],[19])dnl +m4_define([libhttpserver_MINOR_VERSION],[20])dnl m4_define([libhttpserver_REVISION],[0])dnl m4_define([libhttpserver_PKG_VERSION],[libhttpserver_MAJOR_VERSION.libhttpserver_MINOR_VERSION.libhttpserver_REVISION])dnl m4_define([libhttpserver_LDF_VERSION],[libhttpserver_MAJOR_VERSION:libhttpserver_MINOR_VERSION:libhttpserver_REVISION])dnl From 68bff7807aa4849f7e483182b48caab11e138fbd Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Thu, 19 Feb 2026 15:51:25 -0800 Subject: [PATCH 45/47] Update ignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 72c98a64..addf8862 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ debian/rules redhat/libhttpserver.SPEC libhttpserver.pc libtool +.worktrees +.claude +CLAUDE.md From 97bd4b196b5b5ef908c21582a5cfc51bd0611e35 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 19:41:39 +0000 Subject: [PATCH 46/47] Add documentation and example for serving binary data from memory The existing string_response already supports binary content (std::string can hold arbitrary bytes), but this was not documented or demonstrated anywhere. This gap caused users to believe a new response type was needed (see PR #368). - Add a note to the README's string_response description clarifying binary data support - Add a new "Serving binary data from memory" section with inline example - Add examples/binary_buffer_response.cpp as a complete, buildable example that serves a PNG image from an in-memory buffer - Register the new example in examples/Makefile.am https://claude.ai/code/session_01S3BvBrSoNvUhpYTyhPYCjJ --- README.md | 37 ++++++++++++- examples/Makefile.am | 3 +- examples/binary_buffer_response.cpp | 81 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 examples/binary_buffer_response.cpp diff --git a/README.md b/README.md index a70ff849..b9d96f37 100644 --- a/README.md +++ b/README.md @@ -807,7 +807,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver As seen in the documentation of [http_resource](#the-resource-object), every extensible method returns in output a `http_response` object. The webserver takes the responsibility to convert the `http_response` object you create into a response on the network. There are 5 types of response that you can create - we will describe them here through their constructors: -* _string_response(**const std::string&** content, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ The most basic type of response. It uses the `content` string passed in construction as body of the HTTP response. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. +* _string_response(**const std::string&** content, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ The most basic type of response. It uses the `content` string passed in construction as body of the HTTP response. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. Note that `std::string` can hold arbitrary binary data (including null bytes), so `string_response` is also the right choice for serving binary content such as images directly from memory — simply set an appropriate `content_type` (e.g., `"image/png"`). * _file_response(**const std::string&** filename, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ Uses the `filename` passed in construction as pointer to a file on disk. The body of the HTTP response will be set using the content of the file. The file must be a regular file and exist on disk. Otherwise libhttpserver will return an error 500 (Internal Server Error). The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. * _basic_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during basic authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. * _digest_auth_fail_response(**const std::string&** content, **const std::string&** realm = `""`, **const std::string&** opaque = `""`, **bool** reload_nonce = `false`, **int** response_code = `200`, **const std::string&** content_type = `"text/plain"`):_ A response in return to a failure during digest authentication. It allows to specify a `content` string as a message to send back to the client. The `realm` parameter should contain your realm of authentication (if any). The `opaque` represents a value that gets passed to the client and expected to be passed again to the server as-is. This value can be a hexadecimal or base64 string. The `reload_nonce` parameter tells the server to reload the nonce (you should use the value returned by the `check_digest_auth` method on the `http_request`. The other two optional parameters are the `response_code` and the `content_type`. You can find constant definition for the various response codes within the [http_utils](https://github.com/etr/libhttpserver/blob/master/src/httpserver/http_utils.hpp) library file. @@ -856,6 +856,41 @@ You will receive the message custom header in reply. You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/setting_headers.cpp). +### Serving binary data from memory +`string_response` is not limited to text — it can serve arbitrary binary content directly from memory. This is useful when you have data in a buffer at runtime (e.g., from a camera, an image processing library, or a database) and want to serve it without writing to disk. + +```cpp + #include + + using namespace httpserver; + + class image_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + // binary_data could come from a camera capture, image library, etc. + std::string binary_data = get_image_bytes_from_camera(); + + return std::make_shared( + std::move(binary_data), 200, "image/jpeg"); + } + }; + + int main() { + webserver ws = create_webserver(8080); + + image_resource ir; + ws.register_resource("/image", &ir); + ws.start(true); + + return 0; + } +``` +To test the above example, you could run the following command from a terminal: + + curl -o image.jpg http://localhost:8080/image + +You can also check the complete example on [github](https://github.com/etr/libhttpserver/blob/master/examples/binary_buffer_response.cpp). + [Back to TOC](#table-of-contents) ## IP Blacklisting and Whitelisting diff --git a/examples/Makefile.am b/examples/Makefile.am index c04e0acf..148fa944 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -19,7 +19,7 @@ LDADD = $(top_builddir)/src/libhttpserver.la AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback +noinst_PROGRAMS = hello_world service minimal_hello_world custom_error allowing_disallowing_methods handlers hello_with_get_arg args_processing setting_headers custom_access_log minimal_https minimal_file_response minimal_deferred url_registration minimal_ip_ban benchmark_select benchmark_threads benchmark_nodelay deferred_with_accumulator file_upload file_upload_with_callback binary_buffer_response hello_world_SOURCES = hello_world.cpp service_SOURCES = service.cpp @@ -42,6 +42,7 @@ benchmark_threads_SOURCES = benchmark_threads.cpp benchmark_nodelay_SOURCES = benchmark_nodelay.cpp file_upload_SOURCES = file_upload.cpp file_upload_with_callback_SOURCES = file_upload_with_callback.cpp +binary_buffer_response_SOURCES = binary_buffer_response.cpp if HAVE_BAUTH noinst_PROGRAMS += basic_authentication centralized_authentication diff --git a/examples/binary_buffer_response.cpp b/examples/binary_buffer_response.cpp new file mode 100644 index 00000000..17060137 --- /dev/null +++ b/examples/binary_buffer_response.cpp @@ -0,0 +1,81 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +// This example demonstrates how to serve binary data (e.g., images) directly +// from an in-memory buffer using string_response. Despite its name, +// string_response works with arbitrary binary content because std::string can +// hold any bytes, including null characters. +// +// This is useful when you generate or receive binary data at runtime (e.g., +// from a camera, an image library, or a database) and want to serve it over +// HTTP without writing it to disk first. +// +// To test: +// curl -o output.png http://localhost:8080/image + +#include +#include + +#include + +// Generate a minimal valid 1x1 red PNG image in memory. +// In a real application, this could come from a camera capture, image +// processing library, database blob, etc. +static std::string generate_png_data() { + // Minimal 1x1 red pixel PNG (68 bytes) + static const unsigned char png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, // 8-bit RGB + 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT chunk + 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, // compressed data + 0x00, 0x00, 0x03, 0x00, 0x01, 0x36, 0x28, 0x19, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND chunk + 0x44, 0xae, 0x42, 0x60, 0x82 + }; + + return std::string(reinterpret_cast(png), sizeof(png)); +} + +class image_resource : public httpserver::http_resource { + public: + std::shared_ptr render_GET(const httpserver::http_request&) { + // Build binary content as a std::string. The string can contain any + // bytes — it is not limited to printable characters or null-terminated + // C strings. The size is tracked internally by std::string::size(). + std::string image_data = generate_png_data(); + + // Use string_response with the appropriate content type. The response + // will send the exact bytes contained in the string. + return std::make_shared( + std::move(image_data), 200, "image/png"); + } +}; + +int main() { + httpserver::webserver ws = httpserver::create_webserver(8080); + + image_resource ir; + ws.register_resource("/image", &ir); + ws.start(true); + + return 0; +} From 6fa84e836d490a2ee83f0b8b78553fc3e1032ac4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 22:13:38 +0000 Subject: [PATCH 47/47] Fix CI: add ChangeLog entry and missing include Add ChangeLog entry for the binary buffer example to satisfy the ChangeLog Check workflow. Add missing #include for std::move to fix cpplint warning. https://claude.ai/code/session_01S3BvBrSoNvUhpYTyhPYCjJ --- ChangeLog | 2 ++ examples/binary_buffer_response.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 7bb0aefb..6e9532e3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,7 @@ Version 0.20.0 + Added example and documentation for serving binary data from memory + using string_response (addresses PR #368). Added conditional compilation for basic auth (HAVE_BAUTH), mirroring existing HAVE_DAUTH pattern for digest auth. Basic auth support is auto-detected via AC_CHECK_LIB and can be disabled at build time. diff --git a/examples/binary_buffer_response.cpp b/examples/binary_buffer_response.cpp index 17060137..19559cfc 100644 --- a/examples/binary_buffer_response.cpp +++ b/examples/binary_buffer_response.cpp @@ -32,6 +32,7 @@ #include #include +#include #include