diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 00000000..6a451d66 --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,8 @@ +--- +# Codacy project configuration. +# +# specs/ holds internal groundwork artifacts (product specs, architecture +# notes, task records, review notes). They are not user-facing docs and are +# not subject to the same markdown style as README/ChangeLog/CONTRIBUTING. +exclude_paths: + - 'specs/**' diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index db762069..f512c151 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -108,26 +108,7 @@ jobs: debug: debug coverage: nocoverage shell: bash - - test-group: extra - os: ubuntu-latest - os-type: ubuntu - build-type: none - compiler-family: gcc - c-compiler: gcc-9 - cc-compiler: g++-9 - debug: nodebug - coverage: nocoverage - shell: bash - - test-group: extra - os: ubuntu-latest - os-type: ubuntu - build-type: none - compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 - debug: nodebug - coverage: nocoverage - shell: bash + # gcc-9 and gcc-10 dropped: lack full C++20 support (no concepts library, no std::span, no features). - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -168,26 +149,8 @@ jobs: debug: nodebug coverage: nocoverage shell: bash - - test-group: extra - os: ubuntu-22.04 - os-type: ubuntu - build-type: none - compiler-family: clang - c-compiler: clang-11 - cc-compiler: clang++-11 - debug: nodebug - coverage: nocoverage - shell: bash - - test-group: extra - os: ubuntu-22.04 - os-type: ubuntu - build-type: none - compiler-family: clang - c-compiler: clang-12 - cc-compiler: clang++-12 - debug: nodebug - coverage: nocoverage - shell: bash + # clang-11, clang-12, clang-14, and clang-15 dropped: incomplete C++20 support (concepts// gaps). + # clang-13 retained: passes the autoconf C++20 feature check on ubuntu-22.04. - test-group: extra os: ubuntu-22.04 os-type: ubuntu @@ -198,26 +161,6 @@ jobs: debug: nodebug coverage: nocoverage shell: bash - - test-group: extra - os: ubuntu-latest - os-type: ubuntu - build-type: none - compiler-family: clang - c-compiler: clang-14 - cc-compiler: clang++-14 - debug: nodebug - coverage: nocoverage - shell: bash - - test-group: extra - os: ubuntu-latest - os-type: ubuntu - build-type: none - compiler-family: clang - c-compiler: clang-15 - cc-compiler: clang++-15 - debug: nodebug - coverage: nocoverage - shell: bash - test-group: extra os: ubuntu-latest os-type: ubuntu @@ -275,8 +218,8 @@ jobs: os-type: ubuntu build-type: select compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 + c-compiler: gcc-14 + cc-compiler: g++-14 debug: nodebug coverage: nocoverage shell: bash @@ -285,8 +228,8 @@ jobs: os-type: ubuntu build-type: nodelay compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 + c-compiler: gcc-14 + cc-compiler: g++-14 debug: nodebug coverage: nocoverage shell: bash @@ -295,8 +238,8 @@ jobs: os-type: ubuntu build-type: threads compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 + c-compiler: gcc-14 + cc-compiler: g++-14 debug: nodebug coverage: nocoverage shell: bash @@ -305,11 +248,29 @@ jobs: os-type: ubuntu build-type: lint compiler-family: gcc - c-compiler: gcc-10 - cc-compiler: g++-10 + c-compiler: gcc-14 + cc-compiler: g++-14 debug: debug coverage: nocoverage shell: bash + # TASK-007: dedicated header-hygiene gate. Runs `make check-hygiene` + # (preprocesses against the staged install and greps + # for forbidden backend headers). Surfaces this gate as its own named + # GitHub Actions check so reviewers see header-hygiene status + # independently of the broader `make check` log. Until M5 lands the + # check is informational (HEADER_HYGIENE_STRICT defaults to "no"); + # TASK-020 flips it to strict. + - test-group: extra + os: ubuntu-latest + os-type: ubuntu + build-type: header-hygiene + compiler-family: gcc + c-compiler: gcc-14 + cc-compiler: g++-14 + debug: nodebug + coverage: nocoverage + linking: dynamic + shell: bash - test-group: basic os: windows-latest os-type: windows @@ -393,8 +354,12 @@ jobs: pacman --noconfirm -S --needed msys2-devel gcc make libcurl-devel libgnutls-devel - name: Install Ubuntu test sources + # ppa:ubuntu-toolchain-r/test was historically used to backport newer + # gcc onto older Ubuntu LTS. With the C++20 floor (TASK-001), our matrix + # only retains compilers that ship in stock ubuntu-22.04 / 24.04 repos + # (gcc-11..14, clang-13/16/17/18), so the PPA is no longer needed -- and + # add-apt-repository talks to launchpad, which is a flaky dependency. run: | - sudo add-apt-repository ppa:ubuntu-toolchain-r/test ; sudo apt-get update ; if: ${{ matrix.os-type == 'ubuntu' }} @@ -662,7 +627,7 @@ jobs: # IWYU always return an error code. If it returns "2" it indicates a success so we manage this within the function below. function safe_make_iwyu() { { - make -k CXX='/usr/local/bin/include-what-you-use -Xiwyu --mapping_file=${top_builddir}/../custom_iwyu.imp' CXXFLAGS="-std=c++11 -DHTTPSERVER_COMPILATION -D_REENTRANT $CXXFLAGS" ; + make -k CXX='/usr/local/bin/include-what-you-use -Xiwyu --mapping_file=${top_builddir}/../custom_iwyu.imp' CXXFLAGS="-std=c++20 -DHTTPSERVER_COMPILATION -D_REENTRANT $CXXFLAGS" ; } || { if [ $? -ne 2 ]; then return 1; @@ -685,7 +650,18 @@ jobs: run: | cd build ; make check; - if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' }} + if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'header-hygiene' }} + + - name: Run header-hygiene check + # TASK-007: dedicated public-header hygiene gate. Runs the + # preprocessor-grep target (Layer 2) against a staged install and + # reports any forbidden backend headers reaching . + # Currently informational (HEADER_HYGIENE_STRICT=no) -- TASK-020 + # flips this to strict when M5 closes the umbrella. + run: | + cd build + make check-hygiene + if: ${{ matrix.build-type == 'header-hygiene' }} - name: Print tests results shell: bash diff --git a/.gitignore b/.gitignore index addf8862..40430a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ libtool .worktrees .claude CLAUDE.md +.groundwork-plans/ +.DS_Store diff --git a/ChangeLog b/ChangeLog index ea6c2045..531da1a7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,10 @@ Version 0.20.0 + Raised minimum C++ standard to C++20. Build now requires gcc >= 10 + or clang >= 13 (Apple Clang from Xcode 15+). Updated + AX_CXX_COMPILE_STDCXX macro (m4/ax_cxx_compile_stdcxx.m4) to + serial 25 to support C++20 detection. Pruned CI matrix rows + (gcc-9, clang-11, clang-12) that lack full C++20 support. Raised minimum libmicrohttpd requirement to 1.0.0. Migrated Basic Auth to v3 API (MHD_basic_auth_get_username_password3, MHD_queue_basic_auth_required_response3) with UTF-8 support. diff --git a/Makefile.am b/Makefile.am index 02121fde..f74ad8b0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,11 +38,272 @@ endif endif -EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) scripts/extract-release-notes.sh scripts/validate-version.sh +EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) scripts/extract-release-notes.sh scripts/validate-version.sh \ + test/headers/consumer_direct.cpp test/headers/consumer_detail.cpp test/headers/consumer_umbrella.cpp \ + test/headers/consumer_post_umbrella.cpp \ + test/headers/consumer_umbrella_no_backend.cpp + +# --------------------------------------------------------------------------- +# Header-hygiene checks (TASK-002) +# +# check-headers verifies that the public/private header gates are wired up +# correctly: +# A.1 a consumer including a public header WITHOUT the umbrella must hit the +# inclusion-gate #error. +# A.2 a consumer including a detail header WITHOUT HTTPSERVER_COMPILATION +# must hit the gate. +# A.3 a consumer including only the umbrella, WITHOUT HTTPSERVER_COMPILATION, +# must compile cleanly. +# +# The CXX invocations below override CXXFLAGS to '' so that +# -DHTTPSERVER_COMPILATION (injected by configure.ac into CXXFLAGS for the +# library and test build) does NOT leak into the consumer-style compile. We +# still pass -std=c++20 explicitly because libhttpserver requires C++20. +# --------------------------------------------------------------------------- + +# Compose CXX with: explicit -std, the source/build include search paths used by +# the library, and $(CPPFLAGS) (e.g., -I/opt/homebrew/include from configure). +# Deliberately omit $(CXXFLAGS), $(AM_CPPFLAGS), and any per-target CPPFLAGS so +# that -DHTTPSERVER_COMPILATION (set in src/ and test/ AM_CPPFLAGS) cannot +# leak into the consumer-style compile. A true consumer never has that macro. +CHECK_HEADERS_CXX = $(CXX) -std=c++20 -I$(top_builddir) -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver $(CPPFLAGS) +CHECK_HEADERS_GATE_MSG = Only or can be included directly + +check-headers: + @echo "=== check-headers A.1: direct public-header include must fail ===" + @if $(CHECK_HEADERS_CXX) -c $(top_srcdir)/test/headers/consumer_direct.cpp -o /dev/null 2>check-headers-A1.log; then \ + echo "FAIL: consumer_direct.cpp compiled but should have errored"; \ + cat check-headers-A1.log; \ + rm -f check-headers-A1.log; \ + exit 1; \ + fi + @if ! grep -q "$(CHECK_HEADERS_GATE_MSG)" check-headers-A1.log; then \ + echo "FAIL: consumer_direct.cpp failed but not for the gate reason"; \ + cat check-headers-A1.log; \ + rm -f check-headers-A1.log; \ + exit 1; \ + fi + @rm -f check-headers-A1.log + @echo " PASS: A.1 gate fired as expected" + @echo "=== check-headers A.2: direct detail-header include must fail ===" + @if $(CHECK_HEADERS_CXX) -c $(top_srcdir)/test/headers/consumer_detail.cpp -o /dev/null 2>check-headers-A2.log; then \ + echo "FAIL: consumer_detail.cpp compiled but should have errored"; \ + cat check-headers-A2.log; \ + rm -f check-headers-A2.log; \ + exit 1; \ + fi + @if ! grep -q "$(CHECK_HEADERS_GATE_MSG)" check-headers-A2.log; then \ + echo "FAIL: consumer_detail.cpp failed but not for the gate reason"; \ + cat check-headers-A2.log; \ + rm -f check-headers-A2.log; \ + exit 1; \ + fi + @rm -f check-headers-A2.log + @echo " PASS: A.2 gate fired as expected" + @echo "=== check-headers A.3: umbrella include must succeed ===" + @if ! $(CHECK_HEADERS_CXX) -c $(top_srcdir)/test/headers/consumer_umbrella.cpp -o consumer_umbrella.check.o 2>check-headers-A3.log; then \ + echo "FAIL: consumer_umbrella.cpp did not compile"; \ + cat check-headers-A3.log; \ + rm -f check-headers-A3.log consumer_umbrella.check.o; \ + exit 1; \ + fi + @rm -f check-headers-A3.log consumer_umbrella.check.o + @echo " PASS: A.3 umbrella compiled cleanly" + @echo "=== check-headers A.4: post-umbrella direct include must fail ===" + @if $(CHECK_HEADERS_CXX) -c $(top_srcdir)/test/headers/consumer_post_umbrella.cpp -o /dev/null 2>check-headers-A4.log; then \ + echo "FAIL: consumer_post_umbrella.cpp compiled but should have errored"; \ + cat check-headers-A4.log; \ + rm -f check-headers-A4.log; \ + exit 1; \ + fi + @if ! grep -q "$(CHECK_HEADERS_GATE_MSG)" check-headers-A4.log; then \ + echo "FAIL: consumer_post_umbrella.cpp failed but not for the gate reason"; \ + cat check-headers-A4.log; \ + rm -f check-headers-A4.log; \ + exit 1; \ + fi + @rm -f check-headers-A4.log + @echo " PASS: A.4 umbrella does not leak _HTTPSERVER_HPP_INSIDE_" + +# check-install-layout asserts that `make install DESTDIR=$(STAGE)` produces +# a public include tree with NO `detail/` directory and NO `*_impl.hpp` files. +# This protects the public/private split as described in TASK-002 / DR-002. +CHECK_INSTALL_STAGE = $(abs_top_builddir)/.install-stage + +check-install-layout: + @echo "=== check-install-layout: staged install must hide detail/ and *_impl.hpp ===" + @if test "$(CHECK_INSTALL_SHARED)" != "yes"; then \ + rm -rf $(CHECK_INSTALL_STAGE); \ + $(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(CHECK_INSTALL_STAGE) >check-install.log 2>&1 || { \ + echo "FAIL: staged install failed"; \ + cat check-install.log; \ + rm -f check-install.log; \ + rm -rf $(CHECK_INSTALL_STAGE); \ + exit 1; \ + }; \ + rm -f check-install.log; \ + fi + @leaked_detail=`find $(CHECK_INSTALL_STAGE) -type d -name detail 2>/dev/null`; \ + if test -n "$$leaked_detail"; then \ + echo "FAIL: detail/ directory leaked into install:"; \ + echo "$$leaked_detail"; \ + if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi; \ + exit 1; \ + fi + @leaked_impl=`find $(CHECK_INSTALL_STAGE) -name '*_impl.hpp' 2>/dev/null`; \ + if test -n "$$leaked_impl"; then \ + echo "FAIL: *_impl.hpp file leaked into install:"; \ + echo "$$leaked_impl"; \ + if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi; \ + exit 1; \ + fi + @umbrella_count=`find $(CHECK_INSTALL_STAGE) -name 'httpserver.hpp' | wc -l | tr -d ' '`; \ + if test "$$umbrella_count" != "1"; then \ + echo "FAIL: expected exactly 1 installed httpserver.hpp, got $$umbrella_count"; \ + if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi; \ + exit 1; \ + fi + @if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi + @echo " PASS: staged install layout is clean" + +# --------------------------------------------------------------------------- +# Header-hygiene preprocessor gate (TASK-007). +# +# This is the preprocessor-grep half of the TASK-007 enforcement (the +# compile-time half lives as `header_hygiene` in test/Makefile.am). +# +# Procedure: +# 1. Stage `make install DESTDIR=$(CHECK_HYGIENE_STAGE)` to get a +# pristine public include tree -- exactly what packagers and +# downstream consumers see. +# 2. Preprocess test/headers/consumer_umbrella_no_backend.cpp using +# ONLY -I$(CHECK_HYGIENE_STAGE)$(includedir) plus $(CPPFLAGS) (so +# e.g. /opt/homebrew/include is on the search path -- the grep +# below NEEDS to resolve if the umbrella pulls it +# in, otherwise we couldn't detect the leak). +# 3. Grep the cpp output for `# ""` line markers that +# name any forbidden backend header. The line-marker filter +# avoids false positives from substrings in code or comments. +# +# HEADER_HYGIENE_STRICT controls whether a leak is fatal: +# - "yes" (default since TASK-020): leaks fail the build. The umbrella +# is now clean and any regression should break CI loudly. +# - "no" (legacy): leaks were reported as EXPECTED-FAIL and exit 0 +# while M2-M5 were in flight. Override from the command line +# (`make check-hygiene HEADER_HYGIENE_STRICT=no`) only if you +# are deliberately running against an in-flight umbrella. +# +# Cross-reference: keep HEADER_HYGIENE_FORBIDDEN in sync with the +# #ifdef ladder in test/unit/header_hygiene_test.cpp. +# +# TASK-020 caveat (libc++ AND libstdc++ in thread mode): +# is intentionally absent from the forbidden list below. Both +# mainstream STLs unconditionally pull in from any STL +# container header (, , , etc.) when threading +# is enabled: +# - libc++ (Apple's default STL on macOS) routes through +# <__thread/support/pthread.h>. +# - libstdc++ in thread-enabled mode (which is the default whenever +# -D_REENTRANT is set, as configure.ac does) routes through +# , which #include directly. +# The resulting `# N "...pthread.h"` line markers therefore appear in +# the preprocessed output even though libhttpserver itself does not +# include . The runtime sentinel +# test/unit/header_hygiene_test.cpp keeps the pthread guards but skips +# them on both libc++ (_LIBCPP_VERSION) and libstdc++ in thread mode +# (_GLIBCXX_HAS_GTHREADS), so the guards still fire on STLs that don't +# route std::thread through pthread (e.g. MSVC's Microsoft STL). +# --------------------------------------------------------------------------- + +HEADER_HYGIENE_FORBIDDEN = microhttpd\.h|gnutls/gnutls\.h|sys/socket\.h|sys/uio\.h +CHECK_HYGIENE_STAGE = $(abs_top_builddir)/.hygiene-stage +CHECK_HYGIENE_CXX = $(CXX) -std=c++20 -E -I$(CHECK_HYGIENE_STAGE)$(includedir) $(CPPFLAGS) +HEADER_HYGIENE_STRICT ?= yes + +# Sentinel file: only re-run the staged install when headers have changed. +# This is an mtime gate used exclusively for standalone `make check-hygiene` +# invocations — it avoids paying a full `make install` cost on every +# repeated standalone run. When check-local drives check-hygiene it sets +# CHECK_HYGIENE_SHARED=yes and passes CHECK_HYGIENE_STAGE pointing at its +# own pre-built shared stage, so this stamp target is bypassed entirely. +HYGIENE_STAMP = $(CHECK_HYGIENE_STAGE)/.hygiene-stamp + +$(HYGIENE_STAMP): $(wildcard $(top_srcdir)/src/httpserver/*.hpp) + @rm -rf $(CHECK_HYGIENE_STAGE) + @$(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(CHECK_HYGIENE_STAGE) >check-hygiene-install.log 2>&1 || { \ + echo "FAIL: staged install failed"; cat check-hygiene-install.log; \ + rm -f check-hygiene-install.log; rm -rf $(CHECK_HYGIENE_STAGE); exit 1; } + @rm -f check-hygiene-install.log + @touch $(HYGIENE_STAMP) + +check-hygiene: + @echo "=== check-hygiene: must not transitively include backend headers ===" + @if test "$(CHECK_HYGIENE_SHARED)" != "yes"; then \ + $(MAKE) $(AM_MAKEFLAGS) $(HYGIENE_STAMP); \ + else \ + if ! test -d "$(CHECK_HYGIENE_STAGE)"; then \ + echo "FAIL: CHECK_HYGIENE_SHARED=yes but stage dir '$(CHECK_HYGIENE_STAGE)' does not exist."; \ + echo " Always pair CHECK_HYGIENE_SHARED=yes with CHECK_HYGIENE_STAGE=."; \ + exit 1; \ + fi; \ + fi + @status=0; \ + if ! $(CHECK_HYGIENE_CXX) $(top_srcdir)/test/headers/consumer_umbrella_no_backend.cpp >check-hygiene.i 2>check-hygiene.err; then \ + if test "$(HEADER_HYGIENE_STRICT)" = "yes"; then \ + echo "FAIL: preprocessor failed"; cat check-hygiene.err; \ + status=1; \ + else \ + echo "EXPECTED-FAIL (informational until M5): preprocessor failed against staged install."; \ + echo " This is expected while M2-M5 are in flight (e.g. webserver.hpp still"; \ + echo " references private detail headers that aren't shipped)."; \ + echo " Tail of preprocessor diagnostics:"; \ + sed 's/^/ /' check-hygiene.err | tail -10; \ + fi; \ + else \ + leaks=`grep -hE '^# [0-9]+ "[^"]*/($(HEADER_HYGIENE_FORBIDDEN))"' check-hygiene.i | awk '{print $$3}' | sort -u`; \ + if test -n "$$leaks"; then \ + if test "$(HEADER_HYGIENE_STRICT)" = "yes"; then \ + echo "FAIL: forbidden headers leaked through :"; \ + echo "$$leaks"; \ + status=1; \ + else \ + echo "EXPECTED-FAIL (informational until M5): forbidden headers currently leak through :"; \ + echo "$$leaks"; \ + fi; \ + else \ + echo " PASS: no forbidden headers reached the consumer TU"; \ + fi; \ + fi; \ + rm -f check-hygiene.i check-hygiene.err; \ + exit $$status + +# check-local runs check-install-layout and check-hygiene against a single +# shared staged install to avoid paying two full `make install` costs on +# every `make check`. Both sub-checks can still be invoked standalone (they +# will do their own install when CHECK_*_SHARED is not set). +check-local: check-headers + @echo "=== Shared staged install for check-install-layout and check-hygiene ===" + @rm -rf $(abs_top_builddir)/.shared-check-stage + @$(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(abs_top_builddir)/.shared-check-stage >check-shared-install.log 2>&1 || { \ + echo "FAIL: shared staged install failed"; cat check-shared-install.log; \ + rm -f check-shared-install.log; rm -rf $(abs_top_builddir)/.shared-check-stage; exit 1; } + @rm -f check-shared-install.log + @$(MAKE) $(AM_MAKEFLAGS) check-install-layout \ + CHECK_INSTALL_STAGE=$(abs_top_builddir)/.shared-check-stage \ + CHECK_INSTALL_SHARED=yes + @$(MAKE) $(AM_MAKEFLAGS) check-hygiene \ + CHECK_HYGIENE_STAGE=$(abs_top_builddir)/.shared-check-stage \ + CHECK_HYGIENE_SHARED=yes + @rm -rf $(abs_top_builddir)/.shared-check-stage + +.PHONY: check-headers check-install-layout check-hygiene MOSTLYCLEANFILES = $(DX_CLEANFILES) *.gcda *.gcno *.gcov DISTCLEANFILES = DIST_REVISION +clean-local: + rm -rf $(CHECK_HYGIENE_STAGE) $(abs_top_builddir)/.shared-check-stage $(CHECK_INSTALL_STAGE) + pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = libhttpserver.pc diff --git a/README.CentOS-7 b/README.CentOS-7 index 1dfaaa70..4cbaf071 100644 --- a/README.CentOS-7 +++ b/README.CentOS-7 @@ -1,7 +1,8 @@ ## Cent OS 7 / RHEL 7 -CentOS 7 has a lower version of gcc (4.8.7) that is barely C++11 capable and this library -needs a better compiler. We recommend at least gcc 5+ +CentOS 7's stock gcc (4.8.7) is far too old: this library requires a C++20 compiler +(gcc >= 10 or clang >= 13). -We recommend installing devtoolset-8 -https://www.softwarecollections.org/en/scls/rhscl/devtoolset-8/ +Install gcc-toolset-14 (or newer) from the RHEL/CentOS Software Collections and +`source /opt/rh/gcc-toolset-14/enable` before configuring. The same workaround applies +to RHEL 9 systems whose stock gcc-11 lacks some C++20 library features. diff --git a/README.md b/README.md index 7933a235..3b90fd39 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,14 @@ Additionally, clients can specify resource limits on the overall number of conne libhttpserver can be used without any dependencies aside from libmicrohttpd. The minimum versions required are: -* g++ >= 5.5.0 or clang-3.6 -* C++17 or newer +* g++ >= 10 or clang >= 13 (Apple Clang from Xcode 15+) +* C++20 or newer * libmicrohttpd >= 1.0.0 * [Optionally]: for TLS (HTTPS) support, you'll need [libgnutls](http://www.gnutls.org/). * [Optionally]: to compile the code-reference, you'll need [doxygen](http://www.doxygen.nl/). +On RHEL 9 (and derivatives), the stock GCC 11 is too old for some C++20 library features the build relies on; install the `gcc-toolset-14` package and `source /opt/rh/gcc-toolset-14/enable` before configuring. + Additionally, for MinGW on windows you will need: * libwinpthread (For MinGW-w64, if you use thread model posix then you have this) @@ -559,14 +561,14 @@ Once a webserver is created, you can manage its execution through the following The `http_resource` class represents a logical collection of HTTP methods that will be associated to a URL when registered on the webserver. The class is **designed for extension** and it is where most of your code should ideally live. When the webserver matches a request against a resource (see: [resource registration](#registering-resources)), the method correspondent to the one in the request (GET, POST, etc..) (see below) is called on the resource. Given this, the `http_resource` class contains the following extensible methods (also called `handlers` or `render methods`): -* _**std::shared_ptr** http_resource::render_GET(**const http_request&** req):_ Invoked on an HTTP GET request. -* _**std::shared_ptr** http_resource::render_POST(**const http_request&** req):_ Invoked on an HTTP POST request. -* _**std::shared_ptr** http_resource::render_PUT(**const http_request&** req):_ Invoked on an HTTP PUT request. -* _**std::shared_ptr** http_resource::render_HEAD(**const http_request&** req):_ Invoked on an HTTP HEAD request. -* _**std::shared_ptr** http_resource::render_DELETE(**const http_request&** req):_ Invoked on an HTTP DELETE request. -* _**std::shared_ptr** http_resource::render_TRACE(**const http_request&** req):_ Invoked on an HTTP TRACE request. -* _**std::shared_ptr** http_resource::render_OPTIONS(**const http_request&** req):_ Invoked on an HTTP OPTIONS request. -* _**std::shared_ptr** http_resource::render_CONNECT(**const http_request&** req):_ Invoked on an HTTP CONNECT request. +* _**std::shared_ptr** http_resource::render_get(**const http_request&** req):_ Invoked on an HTTP GET request. +* _**std::shared_ptr** http_resource::render_post(**const http_request&** req):_ Invoked on an HTTP POST request. +* _**std::shared_ptr** http_resource::render_put(**const http_request&** req):_ Invoked on an HTTP PUT request. +* _**std::shared_ptr** http_resource::render_head(**const http_request&** req):_ Invoked on an HTTP HEAD request. +* _**std::shared_ptr** http_resource::render_delete(**const http_request&** req):_ Invoked on an HTTP DELETE request. +* _**std::shared_ptr** http_resource::render_trace(**const http_request&** req):_ Invoked on an HTTP TRACE request. +* _**std::shared_ptr** http_resource::render_options(**const http_request&** req):_ Invoked on an HTTP OPTIONS request. +* _**std::shared_ptr** http_resource::render_connect(**const http_request&** req):_ Invoked on an HTTP CONNECT request. * _**std::shared_ptr** http_resource::render(**const http_request&** req):_ Invoked as a backup method if the matching method is not implemented. It can be used whenever you want all the invocations on a URL to activate the same behavior regardless of the HTTP method requested. The default implementation of the `render` method returns an empty response with a `404`. #### Example of implementation of render methods @@ -577,7 +579,7 @@ Given this, the `http_resource` class contains the following extensible methods class hello_world_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + std::shared_ptr render_get(const http_request&) { return std::shared_ptr(new string_response("GET: Hello, World!")); } @@ -911,7 +913,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class image_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + 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(); @@ -1012,7 +1014,7 @@ Client certificate authentication uses a X.509 certificate from the client. This class user_pass_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const http_request& req) { + std::shared_ptr render_get(const http_request& req) { if (req.get_user() != "myuser" || req.get_pass() != "mypass") { return std::shared_ptr(new basic_auth_fail_response("FAIL", "test@example.com")); } @@ -1058,7 +1060,7 @@ You can also use `check_digest_auth_digest` to verify against a pre-computed HA1 class digest_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const http_request& req) { + std::shared_ptr render_get(const http_request& req) { if (req.get_digested_user() == "") { return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); @@ -1110,14 +1112,14 @@ libhttpserver provides a centralized authentication mechanism that runs a single // Resources no longer need authentication logic class hello_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + 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&) { + std::shared_ptr render_get(const http_request&) { return std::make_shared("OK", 200, "text/plain"); } }; @@ -1183,7 +1185,7 @@ To enable client certificate authentication, configure your webserver with: class secure_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request& req) { + std::shared_ptr render_get(const http_request& req) { // Check if client provided a certificate if (!req.has_client_certificate()) { return std::make_shared( @@ -1407,7 +1409,7 @@ Additionally, the following utility methods are available: class file_response_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request& req) { + std::shared_ptr render_get(const http_request& req) { return std::shared_ptr(new file_response("test_content", 200, "text/plain")); } }; @@ -1450,7 +1452,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class deferred_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request& req) { + std::shared_ptr render_get(const http_request& req) { return std::shared_ptr >(new deferred_response(test_callback, nullptr, "cycle callback response")); } }; @@ -1507,7 +1509,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class deferred_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request& req) { + std::shared_ptr render_get(const http_request& req) { std::shared_ptr > closure_data(new std::atomic(counter++)); return std::shared_ptr > >(new deferred_response >(test_callback, closure_data, "cycle callback response")); } @@ -1537,13 +1539,13 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class no_content_resource : public http_resource { public: - std::shared_ptr render_DELETE(const http_request&) { + std::shared_ptr render_delete(const http_request&) { // Return a 204 No Content response with no body return std::make_shared( http::http_utils::http_no_content); } - std::shared_ptr render_HEAD(const http_request&) { + std::shared_ptr render_head(const http_request&) { // Return a HEAD-only response with headers but no body auto response = std::make_shared( http::http_utils::http_ok, @@ -1578,7 +1580,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class iovec_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + std::shared_ptr render_get(const http_request&) { // Build a response from multiple separate buffers without copying std::vector parts; parts.push_back("{\"header\": \"value\", "); @@ -1617,7 +1619,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class pipe_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + std::shared_ptr render_get(const http_request&) { int pipefd[2]; if (pipe(pipefd) == -1) { return std::make_shared("pipe failed", 500); @@ -1702,7 +1704,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class hello_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + std::shared_ptr render_get(const http_request&) { return std::make_shared("Hello, World!"); } }; @@ -1746,7 +1748,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class hello_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + std::shared_ptr render_get(const http_request&) { return std::make_shared("Hello from external event loop!"); } }; @@ -1791,7 +1793,7 @@ You can also check this example on [github](https://github.com/etr/libhttpserver class hello_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { + std::shared_ptr render_get(const http_request&) { return std::make_shared("Hello, turbo world!"); } }; diff --git a/configure.ac b/configure.ac index 4069589d..5fad0371 100644 --- a/configure.ac +++ b/configure.ac @@ -44,7 +44,7 @@ AC_LANG([C++]) AC_SYS_LARGEFILE # Minimal feature-set required -AX_CXX_COMPILE_STDCXX([17]) +AX_CXX_COMPILE_STDCXX([20], [noext], [mandatory]) native_srcdir=$srcdir @@ -80,10 +80,19 @@ For native Windows binaries, use the MinGW64 shell instead. ADDITIONAL_LIBS="-lpthread -no-undefined" NETWORK_LIBS="-lws2_32" native_srcdir=$(cd $srcdir; pwd -W) + # libmicrohttpd's asserts _SYS_TYPES_FD_SET on Cygwin/MSYS. + # newlib defines that macro via , included from + # only when __BSD_VISIBLE -- i.e. when _DEFAULT_SOURCE is set. Strict ANSI + # C++ (-std=c++NN, AX_CXX_COMPILE_STDCXX noext) suppresses newlib's + # auto-define, so expose it explicitly here. + CPPFLAGS="-D_DEFAULT_SOURCE $CPPFLAGS" ;; *-cygwin*) NETWORK_HEADER="arpa/inet.h" ADDITIONAL_LIBS="-lpthread -no-undefined" + # See *-msys* note: libmicrohttpd's fd_set check needs _DEFAULT_SOURCE + # under -std=c++NN strict mode. + CPPFLAGS="-D_DEFAULT_SOURCE $CPPFLAGS" ;; *) NETWORK_HEADER="arpa/inet.h" @@ -127,7 +136,11 @@ if test x"$host" = x"$build"; then [AC_MSG_ERROR(["microhttpd.h not found"])] ) - CXXFLAGS="-DHTTPSERVER_COMPILATION -D_REENTRANT $LIBMICROHTTPD_CFLAGS $CXXFLAGS" + # -DHTTPSERVER_COMPILATION is intentionally NOT injected globally into + # CXXFLAGS. It is added per-target via AM_CPPFLAGS in src/Makefile.am and + # test/Makefile.am so that examples (and any other consumer-style TUs) + # build through the umbrella header without seeing the internal macro. + CXXFLAGS="-D_REENTRANT $LIBMICROHTTPD_CFLAGS $CXXFLAGS" LDFLAGS="$LIBMICROHTTPD_LIBS $NETWORK_LIBS $ADDITIONAL_LIBS $LDFLAGS" cond_cross_compile="no" @@ -140,7 +153,9 @@ else [AC_MSG_ERROR(["microhttpd.h not found"])] ) - CXXFLAGS="-DHTTPSERVER_COMPILATION -D_REENTRANT $CXXFLAGS" + # See note above: HTTPSERVER_COMPILATION is scoped to lib + tests via + # per-directory AM_CPPFLAGS, not injected globally into CXXFLAGS. + CXXFLAGS="-D_REENTRANT $CXXFLAGS" LDFLAGS="$NETWORK_LIBS $ADDITIONAL_LIBS $LDFLAGS" cond_cross_compile="yes" @@ -221,7 +236,7 @@ AM_LDFLAGS="-lstdc++" if test x"$debugit" = x"yes"; then AC_DEFINE([DEBUG],[],[Debug Mode]) - AM_CXXFLAGS="$AM_CXXFLAGS -DDEBUG -g -Wall -Wextra -Werror -pedantic -std=c++17 -Wno-unused-command-line-argument -O0" + AM_CXXFLAGS="$AM_CXXFLAGS -DDEBUG -g -Wall -Wextra -Werror -pedantic -Wno-unused-command-line-argument -O0" AM_CFLAGS="$AM_CXXFLAGS -DDEBUG -g -Wall -Wextra -Werror -pedantic -Wno-unused-command-line-argument -O0" else AC_DEFINE([NDEBUG],[],[No-debug Mode]) diff --git a/examples/allowing_disallowing_methods.cpp b/examples/allowing_disallowing_methods.cpp index 50efa4fd..28af4f72 100644 --- a/examples/allowing_disallowing_methods.cpp +++ b/examples/allowing_disallowing_methods.cpp @@ -25,17 +25,17 @@ 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!")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string("Hello, World!"))); } }; int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - hello_world_resource hwr; - hwr.disallow_all(); - hwr.set_allowing("GET", true); - ws.register_resource("/hello", &hwr); + auto hwr = std::make_shared(); + hwr->disallow_all(); + hwr->set_allowing(httpserver::http_method::get, true); + ws.register_path("/hello", hwr); ws.start(true); return 0; diff --git a/examples/args_processing.cpp b/examples/args_processing.cpp index ddf41c4e..cd3bc890 100644 --- a/examples/args_processing.cpp +++ b/examples/args_processing.cpp @@ -40,9 +40,11 @@ class args_resource : public httpserver::http_resource { 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(); + // get_args() returns a const reference to 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". The reference + // remains valid for the duration of this handler call. + const 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 @@ -80,15 +82,15 @@ class args_resource : public httpserver::http_resource { response_body << "name (via get_arg_flat): " << name_flat << "\n"; } - return std::make_shared(response_body.str(), 200, "text/plain"); + return std::make_shared(httpserver::http_response::string(response_body.str())); } }; int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - args_resource ar; - ws.register_resource("/args", &ar); + auto ar = std::make_shared(); + ws.register_path("/args", ar); std::cout << "Server running on http://localhost:8080/args\n"; std::cout << "Try: http://localhost:8080/args?name=john&age=30\n"; diff --git a/examples/basic_authentication.cpp b/examples/basic_authentication.cpp index 661bbb3c..4b769956 100644 --- a/examples/basic_authentication.cpp +++ b/examples/basic_authentication.cpp @@ -25,20 +25,21 @@ class user_pass_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request& req) { + std::shared_ptr render_get(const httpserver::http_request& req) { if (req.get_user() != "myuser" || req.get_pass() != "mypass") { - return std::shared_ptr(new httpserver::basic_auth_fail_response("FAIL", "test@example.com")); + return std::make_shared( + httpserver::http_response::unauthorized("Basic", "test@example.com", "FAIL")); } - return std::shared_ptr(new httpserver::string_response(std::string(req.get_user()) + " " + std::string(req.get_pass()), 200, "text/plain")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string(std::string(req.get_user()) + " " + std::string(req.get_pass())))); } }; int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - user_pass_resource hwr; - ws.register_resource("/hello", &hwr); + auto hwr = std::make_shared(); + ws.register_path("/hello", hwr); ws.start(true); return 0; diff --git a/examples/benchmark_nodelay.cpp b/examples/benchmark_nodelay.cpp index 96c2f570..553f05e1 100755 --- a/examples/benchmark_nodelay.cpp +++ b/examples/benchmark_nodelay.cpp @@ -48,11 +48,11 @@ int main(int argc, char** argv) { .tcp_nodelay() .max_threads(atoi(argv[2])); - std::shared_ptr hello = std::shared_ptr(new httpserver::string_response(BODY, 200)); + std::shared_ptr hello = std::shared_ptr(new httpserver::http_response(httpserver::http_response::string(BODY))); hello->with_header("Server", "libhttpserver"); - hello_world_resource hwr(hello); - ws.register_resource(PATH, &hwr, false); + auto hwr = std::make_shared(hello); + ws.register_path(PATH, hwr); ws.start(true); diff --git a/examples/benchmark_select.cpp b/examples/benchmark_select.cpp index ef5cd089..6eb154ac 100755 --- a/examples/benchmark_select.cpp +++ b/examples/benchmark_select.cpp @@ -47,11 +47,11 @@ int main(int argc, char** argv) { .start_method(httpserver::http::http_utils::INTERNAL_SELECT) .max_threads(atoi(argv[2])); - std::shared_ptr hello = std::shared_ptr(new httpserver::string_response(BODY, 200)); + std::shared_ptr hello = std::shared_ptr(new httpserver::http_response(httpserver::http_response::string(BODY))); hello->with_header("Server", "libhttpserver"); - hello_world_resource hwr(hello); - ws.register_resource(PATH, &hwr, false); + auto hwr = std::make_shared(hello); + ws.register_path(PATH, hwr); ws.start(true); diff --git a/examples/benchmark_threads.cpp b/examples/benchmark_threads.cpp index db376168..8c0008c5 100755 --- a/examples/benchmark_threads.cpp +++ b/examples/benchmark_threads.cpp @@ -46,11 +46,11 @@ int main(int argc, char** argv) { httpserver::webserver ws = httpserver::create_webserver(atoi(argv[1])) .start_method(httpserver::http::http_utils::THREAD_PER_CONNECTION); - std::shared_ptr hello = std::shared_ptr(new httpserver::string_response(BODY, 200)); + std::shared_ptr hello = std::shared_ptr(new httpserver::http_response(httpserver::http_response::string(BODY))); hello->with_header("Server", "libhttpserver"); - hello_world_resource hwr(hello); - ws.register_resource(PATH, &hwr, false); + auto hwr = std::make_shared(hello); + ws.register_path(PATH, hwr); ws.start(true); diff --git a/examples/binary_buffer_response.cpp b/examples/binary_buffer_response.cpp index 19559cfc..72e508d8 100644 --- a/examples/binary_buffer_response.cpp +++ b/examples/binary_buffer_response.cpp @@ -58,7 +58,7 @@ static std::string generate_png_data() { class image_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request&) { + 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(). @@ -66,16 +66,15 @@ class image_resource : public httpserver::http_resource { // 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"); + return std::make_shared(httpserver::http_response::string(std::move(image_data), "image/png")); } }; int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - image_resource ir; - ws.register_resource("/image", &ir); + auto ir = std::make_shared(); + ws.register_path("/image", ir); ws.start(true); return 0; diff --git a/examples/centralized_authentication.cpp b/examples/centralized_authentication.cpp index 0f965af6..ee82cffc 100644 --- a/examples/centralized_authentication.cpp +++ b/examples/centralized_authentication.cpp @@ -28,21 +28,18 @@ 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"); + std::shared_ptr render_get(const http_request&) { + return std::make_shared(http_response::string("Hello, authenticated user!")); } }; class health_resource : public http_resource { public: - std::shared_ptr render_GET(const http_request&) { - return std::make_shared("OK", 200, "text/plain"); + std::shared_ptr render_get(const http_request&) { + return std::make_shared(http_response::string("OK")); } }; @@ -50,7 +47,7 @@ class health_resource : public http_resource { // 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 std::make_shared(http_response::unauthorized("Basic", "MyRealm", "Unauthorized")); } return nullptr; // Allow request } @@ -63,11 +60,11 @@ int main() { .auth_handler(auth_handler) .auth_skip_paths({"/health", "/public/*"}); - hello_resource hello; - health_resource health; + auto hello = std::make_shared(); + auto health = std::make_shared(); - ws.register_resource("/api", &hello); - ws.register_resource("/health", &health); + ws.register_path("/api", hello); + ws.register_path("/health", health); ws.start(true); diff --git a/examples/client_cert_auth.cpp b/examples/client_cert_auth.cpp index 90a3ba84..a0618238 100644 --- a/examples/client_cert_auth.cpp +++ b/examples/client_cert_auth.cpp @@ -52,6 +52,7 @@ * curl -k https://localhost:8443/secure */ +#include #include #include #include @@ -66,51 +67,45 @@ 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) { + 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"); + return std::make_shared(httpserver::http_response::string("Client certificate required").with_status(httpserver::http::http_utils::http_unauthorized)); } - // 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(); + // Get certificate information. TASK-019: the four string-typed + // accessors return string_view aliasing per-request storage; we + // copy into std::string here so the locals survive the rest of + // this method (and so the `+` chains below compile). + 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"); + return std::make_shared(httpserver::http_response::string("Certificate not verified by trusted CA").with_status(httpserver::http::http_utils::http_forbidden)); } // 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"); + return std::make_shared(httpserver::http_response::string("Certificate not in allowlist").with_status(httpserver::http::http_utils::http_forbidden)); } - // Check certificate validity times + // Check certificate validity times. TASK-019 narrows the + // accessor return type to std::int64_t. 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(); + std::int64_t not_before = req.get_client_cert_not_before(); + std::int64_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"); + return std::make_shared(httpserver::http_response::string("Certificate not yet valid").with_status(httpserver::http::http_utils::http_forbidden)); } if (now > not_after) { - return std::make_shared( - "Certificate has expired", - httpserver::http::http_utils::http_forbidden, "text/plain"); + return std::make_shared(httpserver::http_response::string("Certificate has expired").with_status(httpserver::http::http_utils::http_forbidden)); } // Build response with certificate info @@ -121,26 +116,28 @@ class secure_resource : public httpserver::http_resource { response += " Fingerprint (SHA-256): " + fingerprint + "\n"; response += " Verified: " + std::string(verified ? "Yes" : "No") + "\n"; - return std::make_shared(response, 200, "text/plain"); + return std::make_shared(httpserver::http_response::string(response)); } }; // 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::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"; + // TASK-019: get_client_cert_cn() returns string_view; copy + // into std::string for the `+` chain. + response += " Common Name: " + std::string(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"); + return std::make_shared(httpserver::http_response::string(response)); } }; @@ -157,11 +154,11 @@ int main() { .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; + auto secure = std::make_shared(); + auto info = std::make_shared(); - ws.register_resource("/secure", &secure); - ws.register_resource("/info", &info); + ws.register_path("/secure", secure); + ws.register_path("/info", info); std::cout << "Server started. Press Ctrl+C to stop.\n\n"; std::cout << "Test commands:\n"; diff --git a/examples/custom_access_log.cpp b/examples/custom_access_log.cpp index 8f596c90..09293499 100644 --- a/examples/custom_access_log.cpp +++ b/examples/custom_access_log.cpp @@ -31,7 +31,7 @@ void custom_access_log(const std::string& url) { 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!")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string("Hello, World!"))); } }; @@ -39,8 +39,8 @@ int main() { httpserver::webserver ws = httpserver::create_webserver(8080) .log_access(custom_access_log); - hello_world_resource hwr; - ws.register_resource("/hello", &hwr); + auto hwr = std::make_shared(); + ws.register_path("/hello", hwr); ws.start(true); return 0; diff --git a/examples/custom_error.cpp b/examples/custom_error.cpp index c38fb169..e0d2b4d8 100644 --- a/examples/custom_error.cpp +++ b/examples/custom_error.cpp @@ -23,17 +23,17 @@ #include std::shared_ptr not_found_custom(const httpserver::http_request&) { - return std::shared_ptr(new httpserver::string_response("Not found custom", 404, "text/plain")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string("Not found custom").with_status(404))); } std::shared_ptr not_allowed_custom(const httpserver::http_request&) { - return std::shared_ptr(new httpserver::string_response("Not allowed custom", 405, "text/plain")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string("Not allowed custom").with_status(405))); } 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!")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string("Hello, World!"))); } }; @@ -42,10 +42,10 @@ int main() { .not_found_resource(not_found_custom) .method_not_allowed_resource(not_allowed_custom); - hello_world_resource hwr; - hwr.disallow_all(); - hwr.set_allowing("GET", true); - ws.register_resource("/hello", &hwr); + auto hwr = std::make_shared(); + hwr->disallow_all(); + hwr->set_allowing(httpserver::http_method::get, true); + ws.register_path("/hello", hwr); ws.start(true); return 0; diff --git a/examples/daemon_info.cpp b/examples/daemon_info.cpp index c854bbac..87957010 100644 --- a/examples/daemon_info.cpp +++ b/examples/daemon_info.cpp @@ -25,8 +25,8 @@ class hello_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request&) { - return std::make_shared("Hello, World!"); + std::shared_ptr render_get(const httpserver::http_request&) { + return std::make_shared(httpserver::http_response::string("Hello, World!")); } }; @@ -34,8 +34,8 @@ int main() { // Use port 0 to let the OS assign an ephemeral port httpserver::webserver ws = httpserver::create_webserver(0); - hello_resource hr; - ws.register_resource("/hello", &hr); + auto hr = std::make_shared(); + ws.register_path("/hello", hr); ws.start(false); // Query daemon information diff --git a/examples/deferred_with_accumulator.cpp b/examples/deferred_with_accumulator.cpp index a4367773..f6258c32 100644 --- a/examples/deferred_with_accumulator.cpp +++ b/examples/deferred_with_accumulator.cpp @@ -20,6 +20,7 @@ #include #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. @@ -60,17 +61,30 @@ ssize_t test_callback(std::shared_ptr > closure_data, char* buf class deferred_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request&) { + std::shared_ptr render_get(const httpserver::http_request&) { std::shared_ptr > closure_data(new std::atomic(counter++)); - return std::shared_ptr > >(new httpserver::deferred_response >(test_callback, closure_data, "cycle callback response")); + std::string initial = "cycle callback response"; + return std::make_shared( + httpserver::http_response::deferred( + [closure_data, initial, + served = false](std::uint64_t, char* buf, + std::size_t max) mutable -> ssize_t { + if (!served) { + served = true; + std::size_t n = std::min(initial.size(), max); + memcpy(buf, initial.data(), n); + return n; + } + return test_callback(closure_data, buf, max); + })); } }; int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - deferred_resource hwr; - ws.register_resource("/hello", &hwr); + auto hwr = std::make_shared(); + ws.register_path("/hello", hwr); ws.start(true); return 0; diff --git a/examples/digest_authentication.cpp b/examples/digest_authentication.cpp index ddf0be77..f3d8649f 100644 --- a/examples/digest_authentication.cpp +++ b/examples/digest_authentication.cpp @@ -26,30 +26,27 @@ class digest_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request& req) { + std::shared_ptr render_get(const httpserver::http_request& req) { using httpserver::http::http_utils; if (req.get_digested_user() == "") { - return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, - http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + return std::make_shared(httpserver::http_response::unauthorized("Digest", "test@example.com", "FAIL")); } else { auto result = req.check_digest_auth("test@example.com", "mypass", 300, 0, http_utils::digest_algorithm::MD5); if (result == http_utils::digest_auth_result::NONCE_STALE) { - return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, true, - http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + return std::make_shared(httpserver::http_response::unauthorized("Digest", "test@example.com", "FAIL")); } else if (result != http_utils::digest_auth_result::OK) { - return std::make_shared("FAIL", "test@example.com", MY_OPAQUE, false, - http_utils::http_ok, http_utils::text_plain, http_utils::digest_algorithm::MD5); + return std::make_shared(httpserver::http_response::unauthorized("Digest", "test@example.com", "FAIL")); } } - return std::make_shared("SUCCESS", 200, "text/plain"); + return std::make_shared(httpserver::http_response::string("SUCCESS")); } }; int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - digest_resource hwr; - ws.register_resource("/hello", &hwr); + auto hwr = std::make_shared(); + ws.register_path("/hello", hwr); ws.start(true); return 0; diff --git a/examples/empty_response_example.cpp b/examples/empty_response_example.cpp index 17a4a443..82678ea6 100644 --- a/examples/empty_response_example.cpp +++ b/examples/empty_response_example.cpp @@ -18,23 +18,25 @@ USA */ +#include + #include #include class no_content_resource : public httpserver::http_resource { public: - std::shared_ptr render_DELETE(const httpserver::http_request&) { + std::shared_ptr render_delete(const httpserver::http_request&) { // Return a 204 No Content response with no body - return std::make_shared( - httpserver::http::http_utils::http_no_content); + return std::make_shared( + httpserver::http_response::empty()); } - std::shared_ptr render_HEAD(const httpserver::http_request&) { + std::shared_ptr render_head(const httpserver::http_request&) { // Return a HEAD-only response with headers but no body - auto response = std::make_shared( - httpserver::http::http_utils::http_ok, - httpserver::empty_response::HEAD_ONLY); + auto response = std::make_shared( + httpserver::http_response::empty(MHD_RF_HEAD_ONLY_RESPONSE) + .with_status(httpserver::http::http_utils::http_ok)); response->with_header("X-Total-Count", "42"); return response; } @@ -43,8 +45,8 @@ class no_content_resource : public httpserver::http_resource { int main() { httpserver::webserver ws = httpserver::create_webserver(8080); - no_content_resource ncr; - ws.register_resource("/items", &ncr); + auto ncr = std::make_shared(); + ws.register_path("/items", ncr); ws.start(true); return 0; diff --git a/examples/external_event_loop.cpp b/examples/external_event_loop.cpp index df6d9749..e755ccf0 100644 --- a/examples/external_event_loop.cpp +++ b/examples/external_event_loop.cpp @@ -33,8 +33,8 @@ void signal_handler(int) { class hello_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request&) { - return std::make_shared("Hello from external event loop!"); + std::shared_ptr render_get(const httpserver::http_request&) { + return std::make_shared(httpserver::http_response::string("Hello from external event loop!")); } }; @@ -49,8 +49,8 @@ int main() { httpserver::webserver ws = httpserver::create_webserver(8080) .start_method(httpserver::http::http_utils::EXTERNAL_SELECT); - hello_resource hr; - ws.register_resource("/hello", &hr); + auto hr = std::make_shared(); + ws.register_path("/hello", hr); ws.start(false); std::cout << "Server running on port " << ws.get_bound_port() << std::endl; diff --git a/examples/file_upload.cpp b/examples/file_upload.cpp index 0916a4fc..db1d270e 100644 --- a/examples/file_upload.cpp +++ b/examples/file_upload.cpp @@ -26,7 +26,7 @@ class file_upload_resource : public httpserver::http_resource { public: - std::shared_ptr render_GET(const httpserver::http_request&) { + std::shared_ptr render_get(const httpserver::http_request&) { std::string get_response = "\n"; get_response += " \n"; get_response += "
\n"; @@ -40,10 +40,10 @@ class file_upload_resource : public httpserver::http_resource { get_response += " \n"; get_response += "\n"; - return std::shared_ptr(new httpserver::string_response(get_response, 200, "text/html")); + return std::shared_ptr(new httpserver::http_response(httpserver::http_response::string(get_response, "text/html"))); } - std::shared_ptr render_POST(const httpserver::http_request& req) { + std::shared_ptr render_post(const httpserver::http_request& req) { std::string post_response = "\n"; post_response += "\n"; post_response += "